[RUBY] Une histoire sur la réduction de la consommation de mémoire à 1/100 avec find_in_batches

Comment gérer de grandes quantités de données avec une consommation de mémoire réduite

Écrivez-vous du code dans Rails en pensant à la mémoire?

Rails utilise le ramasse-miettes de Ruby ** * 1 **, vous pouvez donc écrire du code sans vous soucier de libérer de la mémoire.

* 1 Ruby collecte les objets qui ne sont plus utilisés et libère automatiquement la mémoire.

Par conséquent, il existe des cas où le serveur de production tombe soudainement en panne (en raison d'une erreur de mémoire) sans le remarquer, même si l'implémentation est telle qu'il consomme de la mémoire sans le savoir.

La raison pour laquelle je peux dire cela est que ce phénomène s'est produit sur le site où je travaille actuellement.

J'étais en charge de la modification de l'implémentation, mais j'ai beaucoup appris de cette expérience, je vais donc laisser une note pour ne pas l'oublier.

Recherche de cause

Tout d'abord, vous devez rechercher où se produit l'erreur de mémoire.

J'ai utilisé ʻObjectSpace.memsize_of_all` pour étudier l'utilisation de la mémoire dans Rails.

En utilisant cette méthode, vous pouvez étudier l'utilisation de la mémoire consommée par tous les objets vivants en octets.

Nous installerons cette méthode comme point de contrôle à l'endroit où le processus d'exécution est susceptible de chuter, et étudierons régulièrement où il consomme une grande quantité de mémoire.

■ Exemple d'utilisation pour vérifier l'utilisation de la mémoire

class Hoge
  def self.hoge
    puts 'Nombre de mémoires d'objets avant l'extension de la mémoire par carte'
    puts '↓'
    puts ObjectSpace.memsize_of_all <====Point de contrôle
    array = ('a'..'z').to_a
    array.map do |item|             <==== ①
      puts "#{item}Nombre de mémoires d'objets de"
      puts '↓'
      puts ObjectSpace.memsize_of_all <====Point de contrôle
      item.upcase
    end
  end
end

■ Résultat d'exécution

irb(main):001:0> Hoge.hoge
Nombre de mémoires d'objets avant l'extension de la mémoire par carte
↓
137789340561

Nombre de mémoires d'objets d'un
↓
137789342473

Nombre de mémoires d'objets en b
↓
137789342761

Nombre de mémoires d'objets en c
↓
137789343049

Nombre de mémoires d'objets en d
↓
137789343337

Nombre de mémoires d'objets de e
↓
137789343625

.
.
.

Nombre de mémoires d'objets de x
↓
137789349097
Nombre de mémoires d'objets de y
↓
137789349385
Nombre de mémoires d'objets en z
↓
137789349673
=> ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]

À partir de ce résultat d'exécution, vous pouvez voir que les données passées par la carte sont d'abord développées en mémoire à la fois et que la consommation de mémoire y a augmenté. (Partie ①)

Vous pouvez également voir que la consommation de mémoire augmente avec chaque processus de boucle.

Il n'y a aucun problème si le processus est simple comme cet exemple de code.

Si la quantité de données transmises est importante et que la mise en œuvre effectuée par le traitement en boucle est compliquée, la consommation de mémoire sera réduite.

** J'obtiens une erreur de mémoire (une erreur qui se produit lorsque le traitement de la mémoire ne peut pas suivre). ** **

Cette enquête a également été étudiée par la procédure ci-dessus et, par conséquent, il a été conclu qu'une erreur de mémoire s'était produite car la quantité de données transmises était importante et le traitement intensif des requêtes de crachat avec la carte a été mis en œuvre.

Contre-mesure

Je comprends la cause.

Pensons ensuite aux contre-mesures.

Les premières mesures que j'ai proposées sont les trois suivantes.

1.Augmentez la mémoire avec le pouvoir de l'argent
2. Thread(fil)Effectuer un traitement parallèle avec
3.Le traitement par lots

1. Augmentez la mémoire avec le pouvoir de l'argent

Pour être honnête, c'est le plus rapide, et vous n'avez qu'à augmenter les spécifications de mémoire du serveur avec la puissance de l'argent, alors faisons-le!

J'ai pensé.

Il n'y a pas d'implémentation gourmande en mémoire autre que ce processus, donc j'ai pensé qu'il serait insensé de dépenser de l'argent juste pour cette partie, alors j'ai arrêté cette idée.

2.Faites un traitement parallèle avec Thread

J'ai proposé le traitement parallèle de Ruby comme prochaine contre-mesure, mais si le goulot d'étranglement est le temps de traitement (timeout), il est correct car il sera plus rapide si plusieurs threads sont configurés et calculés en parallèle et fusionnés, mais cette fois Étant donné que le goulot d'étranglement est la pression de la mémoire due à une erreur de mémoire, la quantité de données gérées par plusieurs threads ne change pas, donc on s'attend à ce qu'une erreur de mémoire se produise à la fin, j'ai donc arrêté cette idée.

3. Traitement par lots

La principale cause de cette erreur de mémoire est une erreur de mémoire qui se produit lorsqu'une grande quantité de données est développée à la fois et que le traitement à charge élevée est répété dans une boucle.

Par conséquent, j'ai pensé qu'il serait bon qu'une grande quantité de données puisse être implémentée tout en économisant de la mémoire en la divisant en unités telles que 1000 par traitement par lots sans augmenter la mémoire à la fois.

Rails fournit une méthode appelée find_in_batches, qui peut être utilisée pour traiter 1000 éléments à la fois par défaut.

Exemple) 10,1 pour 000,Divisez en 000 processus et divisez en 10 processus par lots.
find_in_Une image qui utilise moins de mémoire en limitant le traitement avec des lots.

Conclusion

** Traitement par lots à l'aide de find_in_batches **

la mise en oeuvre

Une fois que vous savez comment y faire face, il ne vous reste plus qu'à le mettre en œuvre.

Mettons-le en œuvre. (Comme il n'est pas possible d'afficher réellement le code de l'entreprise, seule l'image est affichée)

■ Image de mise en œuvre

User.find_in_batches(batch_size: 1000) do |users|
  #Quelque chose de traitement
end

Même si 10 000 données utilisateur sont acquises, 1 000 seront traitées à l'aide de find_in_batches.

En d'autres termes, c'est une image qui se divise en 10 000/1 000 = 10 processus.

résultat

La consommation de mémoire a été réduite à 1/100.

Des idées pour mieux

** Cependant, le plus gros inconvénient de cette implémentation est qu'elle prend trop de temps de traitement. ** **

Si vous utilisez heroku etc., cette implémentation entraînera une erreur ** RequestTimeOut * 1 **.

* 1 Dans heroku, le traitement qui prend 30 secondes ou plus entraînera une erreur Request Time Out.

Par conséquent, je pense qu'il est préférable de déplacer cette implémentation de traitement à charge élevée vers le traitement en arrière-plan.

Si vous utilisez Rails, vous pouvez le faire en utilisant Sidekiq.

Je pense que vous devriez travailler avec la procédure suivante.

STEP1. find_in_Utilisez des lots pour réduire la consommation de mémoire

STEP2.Lorsque STEP1 est terminé, cela prendra un certain temps, mais il devrait être dans un état de fonctionnement sans erreur de mémoire.
Cependant, le traitement prend du temps, alors déplacez ce processus en arrière-plan.

Résumé

Au début, je pensais que c'était une tâche ennuyeuse.

J'ai beaucoup appris et je suis content de l'avoir mis en œuvre maintenant.

référence

https://techblog.lclco.com/entry/2019/07/31/180000 https://qiita.com/kinushu/items/a2ec4078410284b9856d

Recommended Posts

Une histoire sur la réduction de la consommation de mémoire à 1/100 avec find_in_batches
Histoire d'essayer de faire fonctionner le fichier JAVA
[PHP] Histoire de la sortie de PDF avec TCPDF + FPDI
Une histoire sur l'effort de décompiler les fichiers JAR
Une histoire sur le développement de ROS appelé rosjava avec java
Une histoire sur la création de chemin PKIX a échoué lors de la tentative de déploiement sur Tomcat avec Jenkins
Une histoire bloquée avec NotSerializableException
Une histoire à laquelle j'étais accro avec toString () d'Interface qui était proxy avec JdkDynamicAopProxy
Une histoire confuse sur un opérateur ternaire avec plusieurs expressions conditionnelles
Une histoire de malentendu sur l'utilisation du scanner Java (mémo)
Une histoire que j'ai eu du mal à défier le pro de la concurrence avec Java
[Note] Une histoire sur la modification des outils de compilation Java avec VS Code
Une histoire sur la connexion à un serveur CentOS 8 avec un ancien Ansible
Une histoire sur l'utilisation de l'API League Of Legends avec JAVA
Une histoire sur la difficulté à aligner un cadre de test avec Java 6
Histoire de changer d'emploi d'un pasteur chrétien (apprenti) à un ingénieur web
Une histoire sur la conversion des codes de caractères de UTF-8 en Shift-jis en Ruby
Une histoire sur l'envoi d'une pull request à MinGW pour mettre à jour la version libgr
Une histoire accro aux espaces réservés des modèles JDBC
Une petite histoire addictive avec def initialize
L'histoire selon laquelle traiter d'anciennes dates est agaçante
Notez que Junit 4 a été ajouté à Android Studio
Une histoire accro à EntityNotFoundException de getOne de JpaRepository
Une histoire sur la prise en charge de Java 11 pour les services Web
[Rails] rails nouveau pour créer une base de données avec PostgreSQL
Une histoire qui a mis du temps à établir une connexion
Convertissez une chaîne en un tableau caractère par caractère avec Swift
Une histoire très utile sur la classe Struct de Ruby
Une histoire sur la création d'un Builder qui hérite du Builder
Transition vers un contrôleur de vue avec Swift WebKit
Une histoire emballée avec le scanner d'entrée standard de Java
Ripper un CD en MP3 avec Ubuntu 18.04 LTS
L'histoire d'un nouvel ingénieur lisant un programmeur passionné
J'ai essayé de casser le bloc avec java (1)
[Jackson] Une histoire sur la conversion de la valeur de retour du type BigDecimal avec un sérialiseur personnalisé.
Une histoire sur la création d'un service qui propose des améliorations à un site Web à l'aide d'une API d'apprentissage automatique
L'histoire de la participation à la session d'étude Docker + k8s [JAZUG Women's Club x Java Women's Club]
Une histoire sur l'exécution de Sprint-boot avec kubernetes (GKE) et l'échec de la connexion à CloudSQL
Lorsque j'ai installé npm sur Laravel Homestead, une erreur s'est produite lors de la correspondance avec la valeur de la somme de contrôle.