Artikel ini adalah yang pertama dalam satu siri artikel Musim Percutian A Node.JS yang dibawakan kepada anda oleh pasukan Identity Mozilla, yang mengeluarkan versi beta pertama Persona bulan lepas. Apabila membangunkan Persona, kami membina satu siri alatan, daripada penyahpepijatan, penyetempatan, kepada pengurusan pergantungan dan banyak lagi. Dalam siri artikel ini kami akan berkongsi pengalaman kami dan alatan ini dengan komuniti, yang akan berguna kepada sesiapa sahaja yang ingin membina perkhidmatan ketersediaan tinggi dengan Node.js. Kami berharap anda menikmati artikel ini dan berharap untuk melihat pemikiran dan sumbangan anda.
Kami akan mulakan dengan artikel topikal tentang masalah substantif dalam Node.js: kebocoran memori. Kami akan memperkenalkan node-memwatch — perpustakaan yang membantu mencari dan mengasingkan kebocoran memori dalam Node.
Kenapa awak minta masalah?
Soalan yang paling kerap ditanya tentang menjejaki kebocoran memori ialah, "Mengapa menyusahkan diri sendiri?". Tidakkah ada lagi isu mendesak yang perlu ditangani terlebih dahulu? Mengapa tidak memilih untuk memulakan semula perkhidmatan dari semasa ke semasa, atau memperuntukkan lebih banyak RAM kepadanya? Untuk menjawab soalan ini, kami mencadangkan tiga cadangan berikut:
1. Mungkin anda tidak mengambil berat tentang jejak memori yang semakin meningkat, tetapi V8 tidak (V8 ialah enjin masa jalan Node). Apabila kebocoran memori berkembang, V8 menjadi lebih agresif dengan pengumpul sampah, yang boleh menjadikan aplikasi anda berjalan lebih perlahan. Oleh itu, pada Node, kebocoran memori akan merosakkan prestasi program.
2. Kebocoran memori boleh mencetuskan jenis kegagalan lain. Kod yang membocorkan memori boleh terus merujuk sumber terhad. Anda mungkin kehabisan deskriptor fail anda mungkin juga tiba-tiba tidak dapat membuat sambungan pangkalan data baharu. Masalah jenis ini mungkin timbul lama sebelum apl anda kehabisan memori, tetapi ia masih boleh menyebabkan anda menghadapi masalah.
3. Akhirnya, apl anda akan ranap lambat laun, dan ia pasti akan berlaku apabila apl anda semakin popular. Semua orang akan mentertawakan anda dan mengejek anda di Berita Hacker, yang akan menjadikan anda tragedi.
Di manakah sarang semut yang memecahkan benteng beribu batu?
Apabila membina aplikasi yang kompleks, kebocoran memori mungkin berlaku di banyak tempat. Penutupan mungkin yang paling terkenal dan terkenal. Oleh kerana penutupan mengekalkan rujukan kepada perkara dalam skopnya, di sinilah kebocoran memori biasanya datang.
Kebocoran penutupan selalunya ditemui hanya apabila seseorang mencarinya. Tetapi dalam dunia tak segerak Node, kami sentiasa menjana penutupan melalui fungsi panggil balik pada bila-bila masa dan di mana-mana sahaja. Jika fungsi panggil balik ini tidak digunakan serta-merta selepas penciptaan, memori yang diperuntukkan akan terus berkembang dan kod yang kelihatan tidak mempunyai kebocoran memori akan bocor. Dan masalah seperti ini lebih sukar dicari.
Aplikasi anda juga boleh menyebabkan kebocoran memori disebabkan masalah dalam kod huluan. Mungkin anda boleh mencari kod yang membocorkan memori, tetapi anda mungkin hanya melihat kod sempurna anda dan tertanya-tanya bagaimana ia bocor!
Kebocoran memori yang sukar dikesan inilah yang membuatkan kita mahukan alat seperti nod-memwatch. Legenda mengatakan bahawa beberapa bulan yang lalu, Lloyd Hilaiel kami sendiri mengunci dirinya di dalam bilik kecil selama dua hari, cuba menjejaki kebocoran ingatan yang menjadi jelas di bawah ujian tekanan. (By the way, nantikan artikel Lloyd yang akan datang tentang ujian beban)
Selepas dua hari bekerja keras, dia akhirnya menemui punca dalam kernel Node: pendengar acara dalam http.ClientRequest tidak dikeluarkan. (Tampalan yang akhirnya membetulkan masalah itu hanya dua tetapi surat penting). Pengalaman yang menyakitkan inilah yang mendorong Lloyd menulis alat yang boleh membantu mencari kebocoran ingatan.
Alat mengesan kebocoran memori
Terdapat banyak alat yang berguna dan sentiasa menambah baik untuk mengesan kebocoran memori dalam aplikasi Node.js. Berikut adalah sebahagian daripadanya:
Kami semua menyukai alatan di atas, tetapi tiada satu pun daripada mereka yang sesuai dengan senario kami. Pemeriksa Web bagus untuk membangunkan aplikasi, tetapi sukar digunakan dalam senario penggunaan panas, terutamanya apabila berbilang pelayan dan sub-proses terlibat. Begitu juga, kebocoran memori yang berlaku semasa operasi beban tinggi jangka panjang juga sukar untuk dihasilkan semula. Alat seperti dtrace dan libumem, walaupun mengagumkan, tidak tersedia pada semua sistem pengendalian.
Enternode-memwatch
Kami memerlukan perpustakaan penyahpepijatan merentas platform yang tidak memerlukan peranti memberitahu kami bila program kami mungkin mengalami kebocoran memori dan akan membantu kami mencari tempat kebocoran itu wujud. Jadi kami melaksanakan nod-memwatch.
Ia memberikan kita tiga perkara:
Pemancar peristiwa 'kebocoran'
memwatch.on('leak', function(info) { // look at info to find out about what might be leaking });
'pemancar peristiwa status
var memwatch = require('memwatch'); memwatch.on('stats', function(stats) { // do something with post-gc memory usage stats });
Klasifikasi kawasan ingatan timbunan
var hd = new memwatch.HeapDiff(); // your code here ... var diff = hd.end();
Dan terdapat juga fungsi yang boleh mencetuskan pemungut sampah yang sangat berguna semasa ujian. Okay, empat semuanya.
var stats = memwatch.gc();
memwatch.on('stats', ...): Statistik timbunan pasca GC
nod-memwatch boleh mengeluarkan sampel penggunaan memori berikutan pengumpulan sampah yang lengkap dan pemadatan memori sebelum sebarang objek JS diperuntukkan. (Ia menggunakan cangkuk post-gc V8, V8::AddGCEpilogueCallback, untuk mengumpul maklumat penggunaan timbunan setiap kali kutipan sampah dicetuskan)
Statistik termasuk:
Berikut ialah contoh rupa data untuk aplikasi dengan kebocoran memori. Carta di bawah menjejaki penggunaan memori dari semasa ke semasa. Garis hijau gila menunjukkan proses.memoryUsage() yang dilaporkan. Garis merah menunjukkan current_base yang dilaporkan oleh node_memwatch. Kotak di sebelah kiri bawah menunjukkan maklumat tambahan.
Ambil perhatian bahawa Incr GC adalah sangat tinggi. Ini bermakna V8 sedang berusaha keras untuk mengosongkan ingatan.
memwatch.on('leak', ...): Aliran peruntukan timbunan
Kami telah menentukan algoritma pengesanan mudah untuk memaklumkan anda bahawa aplikasi anda mungkin mengalami kebocoran memori. Iaitu, jika selepas lima GC berturut-turut, memori masih diperuntukkan tetapi tidak dikeluarkan, nod-memwatch akan mengeluarkan peristiwa kebocoran. Format maklumat khusus acara itu jelas dan mudah dibaca, seperti ini:
{ start: Fri, 29 Jun 2012 14:12:13 GMT, end: Fri, 29 Jun 2012 14:12:33 GMT, growth: 67984, reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr' }
memwatch.HeapDiff(): 查找泄漏元凶
最后,node-memwatch能比较堆上对象的名称和分配数量的快照,其对比前后的差异可以帮助找出导致内存泄漏的元凶。
var hd = new memwatch.HeapDiff(); // Your code here ... var diff = hd.end();
对比产生的内容就像这样:
{ "before": { "nodes": 11625, "size_bytes": 1869904, "size": "1.78 mb" }, "after": { "nodes": 21435, "size_bytes": 2119136, "size": "2.02 mb" }, "change": { "size_bytes": 249232, "size": "243.39 kb", "freed_nodes": 197, "allocated_nodes": 10007, "details": [ { "what": "Array", "size_bytes": 66688, "size": "65.13 kb", "+": 4, "-": 78 }, { "what": "Code", "size_bytes": -55296, "size": "-54 kb", "+": 1, "-": 57 }, { "what": "LeakingClass", "size_bytes": 239952, "size": "234.33 kb", "+": 9998, "-": 0 }, { "what": "String", "size_bytes": -2120, "size": "-2.07 kb", "+": 3, "-": 62 } ] } }
HeapDiff方法在进行数据采样前会先进行一次完整的垃圾回收,以使得到的数据不会充满太多无用的信息。memwatch的事件处理会忽略掉由HeapDiff触发的垃圾回收事件,所以在stats事件的监听回调函数中你可以安全地调用HeapDiff方法。