web-dev-qa-db-fra.com

Trouver la cause d'une fuite de mémoire dans Ruby

J'ai découvert une fuite de mémoire dans mon Rails - c'est-à-dire que j'ai trouvé quoi des fuites de code mais pas pourquoi il fuit. Je l'ai réduit à un cas de test qui ne nécessite pas de Rails:

require 'csspool'
require 'Ruby-mass'

def report
    puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)[1].to_s + 'KB'
    Mass.print
end

report

# note I do not store the return value here
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))

ObjectSpace.garbage_collect
sleep 1

report

Ruby-mass me permet censément de voir tous les objets en mémoire. CSSPool est un analyseur CSS basé sur racc . /home/jason/big.css est n fichier CSS de 1,5 Mo .

Cela produit:

Memory 9264KB

==================================================
 Objects within [] namespace
==================================================
  String: 7261
  RubyVM::InstructionSequence: 1151
  Array: 562
  Class: 313
  Regexp: 181
  Proc: 111
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 29
  Gem::Requirement: 25
  RubyVM::Env: 11
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Memory 258860KB

==================================================
 Objects within [] namespace
==================================================
  String: 7456
  RubyVM::InstructionSequence: 1151
  Array: 564
  Class: 313
  Regexp: 181
  Proc: 113
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 30
  Gem::Requirement: 25
  RubyVM::Env: 13
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Vous pouvez voir la mémoire monter dans le sens . Certains compteurs montent, mais aucun objet spécifique à CSSPool n'est présent. J'ai utilisé la méthode "index" de Ruby-mass pour inspecter les objets qui ont des références comme ceci:

Mass.index.each do |k,v|
    v.each do |id|
        refs = Mass.references(Mass[id])
        puts refs if !refs.empty?
    end
end

Mais encore une fois, cela ne me donne rien en rapport avec CSSPool, juste des informations sur les gemmes et autres.

J'ai aussi essayé de sortir "GC.stat" ...

puts GC.stat
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))
ObjectSpace.garbage_collect
sleep 1
puts GC.stat

Résultat:

{:count=>4, :heap_used=>126, :heap_length=>138, :heap_increment=>12, :heap_live_num=>50924, :heap_free_num=>24595, :heap_final_num=>0, :total_allocated_object=>86030, :total_freed_object=>35106}
{:count=>16, :heap_used=>6039, :heap_length=>12933, :heap_increment=>3841, :heap_live_num=>13369, :heap_free_num=>2443302, :heap_final_num=>0, :total_allocated_object=>3771675, :total_freed_object=>3758306}

Si je comprends bien, si un objet n'est pas référencé et que la récupération de place se produit, cet objet doit être effacé de la mémoire. Mais cela ne semble pas être ce qui se passe ici.

J'ai également lu des informations sur les fuites de mémoire au niveau C, et puisque CSSPool utilise Racc qui utilise du code C, je pense que c'est une possibilité. J'ai exécuté mon code via Valgrind:

valgrind --partial-loads-ok=yes --undef-value-errors=no --leak-check=full --fullpath-after= Ruby leak.rb 2> valgrind.txt

Les résultats sont ici . Je ne sais pas si cela confirme une fuite de niveau C, car j'ai également lu que Ruby fait des choses avec de la mémoire que Valgrind ne comprend pas.

Versions utilisées:

  • Ruby 2.0.0-p247 (c'est ce que mon Rails exécute)
  • Ruby 1.9.3-p392-ref (pour les tests avec Ruby-mass)
  • Masse rubis 0.1.3
  • CSSPool 4.0.0 de ici
  • CentOS 6.4 et Ubuntu 13.10
56
Jason Barnabe

Il semble que vous entrez Le monde perdu ici. Je ne pense pas non plus que le problème soit avec les liaisons c dans racc.

La gestion de la mémoire Ruby est à la fois élégante et encombrante. Il stocke des objets (nommés RVALUEs) dans ce que l'on appelle des tas d'une taille d'environ 16 Ko. À un niveau bas, RVALUE est une structure c, contenant un union de différentes représentations d'objets standard Ruby.

Ainsi, les tas stockent des objets RVALUE, dont la taille ne dépasse pas 40 octets. Pour des objets tels que String, Array, Hash etc., cela signifie que les petits objets peuvent tenir dans le tas, mais dès qu'ils atteignent un seuil, une mémoire supplémentaire à l'extérieur des Ruby tas seront alloués.

Cette mémoire supplémentaire est flexible; elle sera libérée dès qu'un objet deviendra GC. C'est pourquoi votre testcase avec big_string montre le comportement de la mémoire de haut en bas:

def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end
report
big_var = " " * 10000000
report
big_var = nil 
report
ObjectSpace.garbage_collect
sleep 1
report
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

Mais les tas (voir GC[:heap_length]) eux-mêmes ne sont pas publiés retour au système d'exploitation, une fois acquis. Écoutez, je vais changer complètement votre testcase:

- big_var = " " * 10000000
+ big_var = 1_000_000.times.map(&:to_s)

Et voilá:

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

La mémoire n'est plus libérée dans le système d'exploitation, car chaque élément du tableau que j'ai introduit convient à la taille RVALUE et est stocké dans le Ruby tas).

Si vous examinez la sortie de GC.stat après l'exécution du GC, vous constaterez que GC[:heap_used] la valeur est diminuée comme prévu. Ruby a maintenant beaucoup de tas vides, prêts.

Le résumé: Je ne pense pas, le code c fuit. Je pense que le problème est dans la représentation base64 d'une image énorme dans votre css. Je n'ai aucune idée de ce qui se passe à l'intérieur de l'analyseur, mais il semble que l'énorme chaîne force le nombre de tas Ruby à augmenter.

J'espère que cela aide.

38
Aleksei Matiushkin

D'accord, j'ai trouvé la réponse. Je laisse mon autre réponse en place car cette information était très difficile à recueillir, elle est liée et elle pourrait aider quelqu'un d'autre à rechercher un problème connexe.

Cependant, votre problème semble être dû au fait que Ruby ne fait pas libérer de la mémoire sur le système d'exploitation) Système une fois qu'il l'a acquis.

Allocation de mémoire

Alors que les programmeurs Ruby ne se préoccupent pas souvent de l'allocation de mémoire, la question suivante se pose parfois:

Pourquoi mon processus Ruby est-il resté si gros même après avoir effacé toutes les références aux gros objets? Je suis/sûr/GC a exécuté plusieurs fois et libéré mes gros objets et je ne le suis pas fuite de mémoire.

Un programmeur C pourrait poser la même question:

J'ai libéré () - beaucoup de mémoire, pourquoi mon processus est-il si important?

L'allocation de mémoire à l'espace utilisateur à partir du noyau est moins chère dans les gros morceaux, donc l'espace utilisateur évite l'interaction avec le noyau en faisant plus de travail lui-même.

Les bibliothèques/runtimes de l'espace utilisateur implémentent un allocateur de mémoire (par exemple: malloc (3) dans libc) qui prend de gros morceaux de mémoire du noyau2 et les divise en morceaux plus petits pour les applications de l'espace utilisateur à utiliser.

Ainsi, plusieurs allocations de mémoire d'espace utilisateur peuvent se produire avant que l'espace utilisateur n'ait besoin de demander plus de mémoire au noyau. Ainsi, si vous avez obtenu un gros morceau de mémoire du noyau et que vous n'en utilisez qu'une petite partie, ce gros morceau de mémoire reste alloué.

Libérer de la mémoire dans le noyau a également un coût. Les allocateurs de mémoire de l'espace utilisateur peuvent conserver cette mémoire (en privé) dans l'espoir qu'elle puisse être réutilisée dans le même processus et ne pas la restituer au noyau pour une utilisation dans d'autres processus. (Meilleures pratiques Ruby)

Ainsi, vos objets peuvent très bien avoir été récupérés et libérés dans la mémoire disponible de Ruby, mais parce que Ruby ne restitue jamais de mémoire inutilisée au système d'exploitation, la valeur rss du processus reste la même, même après la collecte des ordures. Ceci est en fait par conception. Selon Mike Perham :

... Et comme l'IRM ne restitue jamais de mémoire inutilisée, notre démon peut facilement prendre 300 à 400 Mo lorsqu'il n'utilise que 100 à 200.

Il est important de noter que ceci est essentiellement de conception. L'histoire de Ruby est principalement un outil en ligne de commande pour le traitement de texte et, par conséquent, elle valorise un démarrage rapide et une petite empreinte mémoire. Il n'a pas été conçu pour les processus démon/serveur de longue durée. Java fait un compromis similaire dans ses VM client et serveur.

15
Joe Edgar

En m'appuyant sur l'explication de @ mudasobwa, j'ai finalement trouvé la cause. Le code dans CSSPool vérifiait l'URI de données très long pour les séquences d'échappement. Il appellerait scan sur l'URI avec une expression rationnelle qui correspondait à une séquence d'échappement ou à un seul caractère, map ces résultats à unescape, puis join à nouveau dans une chaîne. C'était effectivement allouer une chaîne pour chaque caractère de l'URI. je l'ai modifié à gsub les séquences d'échappement, ce qui semble avoir les mêmes résultats (tous les tests réussissent) et réduit considérablement la mémoire de fin utilisée.

En utilisant le même scénario de test que celui publié initialement (moins le Mass.print sortie) voici le résultat avant le changement:

Memory 12404KB
Memory 292516KB

et voici le résultat après le changement:

Memory 12236KB
Memory 19584KB
9
Jason Barnabe

Cela pourrait être dû à la fonctionnalité "Balayage paresseux" dans Ruby 1.9.3 et supérieur.

Le balayage paresseux signifie essentiellement que, lors de la récupération de place, Ruby ne "balaye" que suffisamment d'objets pour créer de l'espace pour les nouveaux objets qu'il doit créer. Il le fait parce que, tandis que le Ruby garbage collector s'exécute, rien d'autre ne le fait. Ceci est connu sous le nom de "Stop the world" garbage collection.

Essentiellement, le balayage paresseux réduit le temps nécessaire à Ruby pour "arrêter le monde". Vous pouvez en savoir plus sur le balayage paresseux ici .

Que fait votre Ruby_GC_MALLOC_LIMIT la variable d'environnement ressemble?

Voici un extrait de le blog de Sam Saffron concernant le balayage paresseux et le Ruby_GC_MALLOC_LIMIT:

Le GC dans Ruby 2.0 est disponible en 2 versions différentes. Nous avons un GC "complet" qui s'exécute après avoir alloué plus que notre malloc_limit et un balayage paresseux (GC partiel) qui s'exécutera si jamais nous manquer de créneaux libres dans nos tas.

Le balayage paresseux prend moins de temps qu'un GC complet, mais n'effectue qu'un GC partiel. Son objectif est d'effectuer un GC court plus fréquemment, augmentant ainsi le débit global. Le monde s'arrête, mais pour moins de temps.

Le malloc_limit est réglé sur 8 Mo hors de la boîte, vous pouvez l'augmenter en définissant le Ruby_GC_MALLOC_LIMIT plus haut.

Est ton Ruby_GC_MALLOC_LIMIT Très haut? Le mien est réglé sur 100000000 (100 Mo). La valeur par défaut est d'environ 8 Mo, mais pour Rails applications, ils recommandent qu'elle soit un peu plus élevée. Si la vôtre est trop élevée, elle pourrait empêcher Ruby de supprimer) objets poubelles, car il pense qu'il a beaucoup d'espace pour grandir.

9
Joe Edgar