[RUBY] [Rails] Find_each schleift endlos und verbraucht Speicher in der Produktion

Praktische Methode find_each in ActiveRecord Ich werde über die Geschichte schreiben, weil die Implementierungsmethode nicht gut war und eine Endlosschleife aufwies und das Phänomen auftrat, dass sie vom OOM-Killer in der Produktionsumgebung gewaltsam gestoppt wurde.

Was ist find_each?

Find_each ruft nicht eine große Datenmenge auf einmal ab und schleift sie, sondern in festen Einheiten (1.000 Standardeinstellungen) und schleift sie. Wenn Sie mit einer großen Datenmenge arbeiten und alle Daten gleichzeitig erfassen, verwenden Sie eine große Speichermenge. Sie können sie jedoch mit einer kleinen Speichermenge verarbeiten, indem Sie sie mit find_each teilen.

Es ist schwer zu verstehen, selbst wenn Sie es in Worten schreiben. Das Folgende ist ein Ausführungsbeispiel.

#Wenn es 10.000 Benutzer gibt
pry(main)> User.all.count
   (1.1ms)  SELECT COUNT(*) FROM `users`
=> 10000

#Wenn Sie jeweils verwenden, werden 10.000 Gegenstände gleichzeitig erworben.
pry(main)> User.all.each {|user| p user.id}
  User Load (4.5ms)  SELECT `users`.* FROM `users`
1
2
3
...
10000

# find_1 mit jedem,Holen Sie sich 000 Artikel gleichzeitig
[8] pry(main)> User.all.find_each {|user| p user.id}
  User Load (3.9ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000
1
2
3
...
1000
  User Load (0.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
1001
1002
1003
...
2000
  User Load (0.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` > 2000 ORDER BY `users`.`id` ASC LIMIT 1000
2001
...
10000

Weitere Informationen finden Sie im Rails Guide. https://railsguides.jp/active_record_querying.html#find-each

find_each ist eine Endlosschleife! !!

Es ist eine bequeme Methode find_each, aber wie ich am Anfang schrieb, habe ich einen Fehler in der Implementierung gemacht und eine Endlosschleife durchgeführt. Bevor wir die Endlosschleifenimplementierung erläutern, wollen wir zunächst sehen, wie find_each funktioniert.

Wie find_each funktioniert

Lassen Sie uns anhand des am Anfang gezeigten Ausführungsbeispiels überprüfen, wie es funktioniert.

Werfen wir einen Blick auf die zuerst ausgegebene SQL.

SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000

1000 Gegenstände werden durch Angabe des Limits 1000 erworben. Hierbei ist zu beachten, dass sie in aufsteigender Reihenfolge des PRIMARY KEY (id) angeordnet sind.

Wie bekommen Sie die nächsten 1000 Fälle?

SELECT `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000

Beim Abrufen der nächsten 1000 Elemente mithilfe von SQL werden häufig LIMIT und OFFSET verwendet, OFFSET wird in diesem SQL jedoch nicht verwendet. Stattdessen können Sie sehen, dass die where-Klausel eine zusätzliche Anforderung von "users.id> 1000" enthält.

Für 1000 in users.id> 1000 wird die letzte ID der ersten erhaltenen 1000 angegeben. Da die Daten dieses Mal in aufsteigender Reihenfolge der ID angeordnet sind, können die nächsten 1000 Elemente ohne Verwendung von OFFSET erfasst werden, indem "users.id> 1000" angegeben wird. Dies bedeutet, dass Daten erfasst werden, die größer als die letzte ID sind. tun.

Implementierung, die zu einer Endlosschleife wird

Find_each, wo die Endlosschleife aufgetreten ist, wurde wie folgt implementiert. Was wird passieren?

# users.Ausweis und Bücher.Da nur der Titel verwendet wird, werden nur die erforderlichen Daten durch Auswahl erfasst.
Book.joins(:user).select('users.id, books.title').find_each do |book|
  p "user_id: #{book.id}, title: #{book.title}"
end

Zunächst wird die folgende SQL ausgegeben.

SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` ORDER BY `books`.`id` ASC LIMIT 1000

Es gibt kein besonderes Problem mit dem ersten SQL. Was ist also mit dem SQL, das die nächsten 1000 bekommt?

SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` WHERE `books`.`id` > 1000 ORDER BY `books`.`id` ASC LIMIT 1000

Die Bedingung books.id> 1000 wurde hinzugefügt. Die Bedingung 1000 ist die ID der letzten 1000 zuerst erhaltenen Daten. Es ist schwer zu bemerken, wenn Sie sich nur die SQL ansehen, aber die ID, die Sie mit dieser SQL erhalten, lautet "users.id" anstelle von "books.id". Daher gibt 1000, das auf "books.id> 1000" eingestellt ist, die users.id der letzten Daten an.

In dieser SQL steigt die Reihenfolge von books.id an und die Reihenfolge von users.id wird nicht besonders gesteuert. Daher ist es möglich, dass die letzten Daten des nächsten 1000. Elements "books.id: 2000, users.id: 1" sind. In diesem Fall lautet die als nächstes auszugebende SQL wie folgt.

SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` WHERE `books`.`id` > 1 ORDER BY `books`.`id` ASC LIMIT 1000

Die Bedingung ist "books.id> 1", und die Daten vor dem vorherigen SQL ("books.id> 1000") werden abgerufen. Durch die Einbeziehung von users.id, deren Reihenfolge im Zustand von books.id nicht auf diese Weise gesteuert wird, werden die zu erfassenden Daten verwechselt, und im schlimmsten Fall werden dieselben Daten viele Male erfasst, und es tritt eine Endlosschleife auf. Ich werde.

Der problematische Teil dieses Problems ist nicht immer eine Endlosschleife, und abhängig von den Daten kann "books.id> # {last users.id}" zufällig so schön und vollständig angegeben werden. Es gibt. In diesem Fall handelt es sich nicht um einen Fehler, sondern um einen Fehler, bei dem es schwer zu bemerken ist, dass die Daten etwas seltsam sind. Daher ist es möglicherweise besser, eine Endlosschleife zu haben.

Wie repariert man

Wenn Sie im obigen Beispiel die Erfassungsspalte nicht mit select eingrenzen, wird auch books.id erfasst, sodass es ordnungsgemäß funktioniert. Selbst wenn Sie die Erfassungsspalte mit select eingrenzen, funktioniert dies ordnungsgemäß, wenn Sie books.id wie unten gezeigt ordnungsgemäß erfassen.

Book.joins(:user).select('books.id AS id, users.id AS user_id, books.title').find_each do |book|
  p "user_id: #{book.user_id}, title: #{book.title}"
end

Wenn Sie es wie oben beheben, ist das Update abgeschlossen, aber ich denke, das Problem war diesmal, dass es keinen automatisierten Test gab. Es gab einen Test, der den entsprechenden Prozess bestanden hat, aber ich habe keinen Test geschrieben, der find_each mehr als zweimal wiederholt. Wenn Sie einen Test haben, sind Sie sich des Fehlers wahrscheinlich bewusst, da er sich auf unbestimmte Zeit wiederholt oder seltsame Ergebnisse liefert. Mit diesem Auslöser habe ich auch einen Test hinzugefügt, bei dem find_each mehr als zweimal wiederholt wird.

Zusammenfassung

Selbst wenn Sie den Mechanismus von find_each richtig verstehen, ist es schwierig, diesen Fehler zu bemerken, indem Sie ihn auf dem Schreibtisch überprüfen, z. B. die Codeüberprüfung. Darüber hinaus ist es ein seltener Prozess, dass die Anzahl der Fälle 1000 überschreitet und die Einheit 1000 nur eine Frage des Programms ist. Daher wurde sie für eine Weile als potenzieller Fehler versteckt, ohne dass dies selbst im Black-Box-Betriebstest bemerkt wurde.

Als ich darüber nachdachte, wie ich das im Voraus hätte bemerken sollen, dachte ich, dass es keine andere Wahl gab, als einen Test durchzuführen, bei dem find_each im White-Box-Test 2 schleift. Es ist eine Verschwendung, den White-Box-Test einmal manuell auszuführen. Daher ist es eine gute Idee, einen automatisierten Test zu schreiben, damit er kontinuierlich überprüft werden kann.

Recommended Posts

[Rails] Find_each schleift endlos und verbraucht Speicher in der Produktion
Entfernen Sie "Assets" und "Turbolinks" in "Rails6".
CRUD-Funktion und MVC in Rails
[Ruby] Schleifenunterscheidung und Verwendung in Ruby
[Rails] Setzen Sie die Datenbank in der Produktionsumgebung zurück