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:
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 RVALUE
s) 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.
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.
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
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.