Debugging kebocoran memori tersembunyi di Ruby


Pada 2015, saya menulis tentang alat yang disediakan Ruby untuk mendeteksi kebocoran memori yang dikelola . Sebagian besar artikel berbicara tentang kebocoran yang mudah dikelola. Kali ini saya akan berbicara tentang alat dan trik yang dapat Anda gunakan untuk menghilangkan kebocoran yang tidak mudah dianalisis di Ruby. Secara khusus, saya akan berbicara tentang mwrap, heaptrack, iseq_collector dan chap.


Kebocoran memori yang tidak dikelola


Program kecil ini memicu kebocoran dengan panggilan langsung ke malloc. Dimulai dengan konsumsi 16 MB RSS, dan berakhir dengan 118 MB. Kode tersebut menempatkan dalam memori 100 ribu blok 1024 byte dan menghapus 50 ribu di antaranya.

require 'fiddle' require 'objspace' def usage rss = `ps -p #{Process.pid} -o rss -h`.strip.to_i * 1024 puts "RSS: #{rss / 1024} ObjectSpace size #{ObjectSpace.memsize_of_all / 1024}" end def leak_memory pointers = [] 100_000.times do i = Fiddle.malloc(1024) pointers << i end 50_000.times do Fiddle.free(pointers.pop) end end usage # RSS: 16044 ObjectSpace size 2817 leak_memory usage # RSS: 118296 ObjectSpace size 3374 

Meskipun RSS adalah 118 MB, objek Ruby kami hanya mengetahui tiga megabita. Dalam analisis, kami hanya melihat sebagian kecil dari kebocoran memori yang sangat besar ini.

Contoh nyata dari kebocoran tersebut dijelaskan oleh Oleg Dashevsky , saya sarankan membaca artikel yang luar biasa ini.

Terapkan Mwrap


Mwrap adalah profiler memori untuk Ruby yang memonitor semua alokasi data dalam memori dengan mencegat malloc dan fungsi-fungsi lain dari keluarga ini. Itu memotong panggilan tempat itu dan membebaskan memori menggunakan LD_PRELOAD . Ini menggunakan liburcu untuk menghitung dan dapat melacak penghitung alokasi dan penghapusan untuk setiap titik panggilan, dalam kode C dan Ruby. Mwrap berukuran kecil, sekitar dua kali lebih besar dari RSS untuk program yang diprofilkan, dan sekitar dua kali lebih lambat.

Ini berbeda dari banyak perpustakaan lain dalam ukurannya yang sangat kecil dan dukungan Ruby. Ini melacak lokasi dalam file Ruby dan tidak terbatas pada valgrind + masif backtrack C-level dan profiler serupa. Ini sangat menyederhanakan mengisolasi sumber masalah.

Untuk menggunakan profiler, Anda harus menjalankan aplikasi melalui shell Mwrap, itu akan menerapkan lingkungan LD_PRELOAD dan menjalankan biner Ruby.

Mari tambahkan Mwrap ke skrip kami:

 require 'mwrap' def report_leaks results = [] Mwrap.each do |location, total, allocations, frees, age_total, max_lifespan| results << [location, ((total / allocations.to_f) * (allocations - frees)), allocations, frees] end results.sort! do |(_, growth_a), (_, growth_b)| growth_b <=> growth_a end results[0..20].each do |location, growth, allocations, frees| next if growth == 0 puts "#{location} growth: #{growth.to_i} allocs/frees (#{allocations}/#{frees})" end end GC.start Mwrap.clear leak_memory GC.start # Don't track allocations for this block Mwrap.quiet do report_leaks end 

Sekarang jalankan skrip dengan pembungkus Mwrap:

 % gem install mwrap % mwrap ruby leak.rb leak.rb:12 growth: 51200000 allocs/frees (100000/50000) leak.rb:51 growth: 4008 allocs/frees (1/0) 

Mwrap mendeteksi kebocoran dalam skrip dengan benar (50.000 * 1024). Dan tidak hanya ditentukan, tetapi juga mengisolasi garis tertentu ( i = Fiddle.malloc(1024) ), yang menyebabkan kebocoran. Profiler dengan benar mengikatnya ke panggilan ke Fiddle.free .

Penting untuk dicatat bahwa kita sedang berhadapan dengan penilaian. Mwrap memonitor memori bersama yang dialokasikan oleh rekan panggilan, dan kemudian memantau pembebasan memori. Tetapi jika Anda memiliki satu titik panggilan yang mengalokasikan blok memori dengan ukuran yang berbeda, hasilnya akan tidak akurat. Kami memiliki akses ke evaluasi: ((total / allocations) * (allocations - frees))

Selain itu, untuk menyederhanakan pelacakan kebocoran, Mwrap melacak age_total , yang merupakan jumlah umur dari setiap item yang dibebaskan, dan juga melacak max_lifespan , umur dari item tertua di titik panggilan. Jika age_total / frees besar, maka konsumsi memori meningkat meskipun banyak pengumpulan sampah.

Mwrap memiliki beberapa pembantu untuk mengurangi kebisingan. Mwrap.clear akan menghapus semua penyimpanan internal. Mwrap.quiet {} akan memaksa Mwrap untuk melacak blok kode.

Fitur lain yang membedakan Mwrap adalah pelacakan jumlah byte yang dialokasikan dan dibebaskan. Hapus yang clear dari skrip dan jalankan:

 usage puts "Tracked size: #{(Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed) / 1024}" # RSS: 130804 ObjectSpace size 3032 # Tracked size: 91691 

Hasilnya sangat menarik, karena meskipun ukuran RSS 130 MB, Mwrap hanya melihat 91 MB. Ini menunjukkan bahwa kami telah meningkatkan proses kami. Eksekusi tanpa Mwrap menunjukkan bahwa dalam situasi normal prosesnya membutuhkan 118 MB, dan dalam kasus sederhana ini perbedaannya adalah 12 MB. Pola alokasi / pembebasan menyebabkan fragmentasi. Pengetahuan ini bisa sangat berguna, dalam beberapa kasus, malloc glibc yang tidak terkonfigurasi memproses fragmen sedemikian rupa sehingga jumlah memori yang sangat besar yang digunakan dalam RSS sebenarnya gratis.

Bisakah Mwrap mengisolasi kebocoran redcarpet lama?


Dalam artikelnya, Oleg membahas cara yang sangat menyeluruh untuk mengisolasi kebocoran yang sangat tipis pada redcarpet. Ada banyak detail. Sangat penting untuk melakukan pengukuran. Jika Anda tidak membuat garis waktu untuk proses RSS, maka Anda tidak mungkin dapat menghilangkan kebocoran.

Mari masuk ke mesin waktu dan tunjukkan betapa jauh lebih mudah menggunakan Mwrap untuk kebocoran semacam itu.

 def red_carpet_leak 100_000.times do markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, extensions = {}) markdown.render("hi") end end GC.start Mwrap.clear red_carpet_leak GC.start # Don't track allocations for this block Mwrap.quiet do report_leaks end 

Redcarpet 3.3.2:

 redcarpet.rb:51 growth: 22724224 allocs/frees (500048/400028) redcarpet.rb:62 growth: 4008 allocs/frees (1/0) redcarpet.rb:52 growth: 634 allocs/frees (600007/600000) 

Redcarpet 3.5.0:

 redcarpet.rb:51 growth: 4433 allocs/frees (600045/600022) redcarpet.rb:52 growth: 453 allocs/frees (600005/600000) 

Jika Anda mampu menjalankan proses dengan kecepatan setengah dengan hanya me-restart di Mwrap prod dengan mencatat hasil ke file, maka Anda dapat mengidentifikasi berbagai kebocoran memori.

Kebocoran misterius


Baru-baru ini, Rails diperbarui ke versi 6. Secara umum, pengalamannya sangat positif, kinerjanya tetap kurang lebih sama. Rails 6 memiliki beberapa fitur yang sangat bagus yang akan kami gunakan (mis. Zeitwerk ). Rails mengubah cara templat diberikan, yang memerlukan beberapa perubahan untuk kompatibilitas. Beberapa hari setelah pembaruan, kami melihat peningkatan dalam RSS untuk pelaksana tugas Sidekiq.

Mwrap melaporkan peningkatan tajam dalam konsumsi memori karena alokasi ( tautan ):

  source.encode! # Now, validate that the source we got back from the template # handler is valid in the default_internal. This is for handlers # that handle encoding but screw up unless source.valid_encoding? raise WrongEncodingError.new(source, Encoding.default_internal) end begin mod.module_eval(source, identifier, 0) rescue SyntaxError # Account for when code in the template is not syntactically valid; eg if we're using # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate # the result into the template, but missing an end parenthesis. raise SyntaxErrorInTemplate.new(self, original_source) end end def handle_render_error(view, e) if e.is_a?(Template::Error) 

Awalnya kami sangat bingung. Kami mencoba memahami mengapa tidak puas dengan Mwrap? Mungkin dia bangkrut? Saat konsumsi memori meningkat, tumpukan di Ruby tetap tidak berubah.



Dua juta slot di heap hanya mengonsumsi 78 MB (40 byte per slot). Garis dan array dapat memakan lebih banyak ruang, tetapi masih tidak menjelaskan konsumsi memori abnormal yang kami amati. Ini dikonfirmasi ketika saya rbtrace -p SIDEKIQ_PID -e ObjectSpace.memsize_of_all .

Kemana memori itu pergi?

Heaptrack


Heaptrack adalah profiler memori tumpukan untuk Linux.

Milian Wolff dengan sempurna menjelaskan bagaimana profiler bekerja dan membicarakannya dalam beberapa pidato ( 1 , 2 , 3 ). Sebenarnya, ini adalah profiler tumpukan asli yang sangat efisien yang, dengan bantuan libunwind, mengumpulkan jejak balik dari aplikasi yang diprofilkan. Ini bekerja lebih cepat daripada Valgrind / Massif dan memiliki kemampuan untuk membuatnya jauh lebih nyaman untuk pembuatan profil sementara di prod. Itu bisa dilampirkan ke proses yang sudah berjalan!

Seperti kebanyakan profiler heap, saat memanggil setiap fungsi dalam keluarga malloc, Heaptrack harus menghitung. Prosedur ini sedikit memperlambat proses.

Menurut pendapat saya, arsitektur di sini adalah yang terbaik dari semua yang mungkin. Intersepsi dilakukan menggunakan LD_PRELOAD atau GDB untuk memuat profiler. Menggunakan file FIFO khusus, ia mentransfer data dari proses yang diprofilkan secepat mungkin. Heaptrack wrapper adalah skrip shell sederhana yang mempermudah pencarian masalah. Proses kedua membaca informasi dari FIFO dan on-the-fly kompres pelacakan data. Karena Heaptrack beroperasi dengan "chunk," Anda dapat menganalisis profil hanya beberapa detik setelah dimulainya pembuatan profil, tepat di tengah sesi. Cukup salin file profil ke lokasi lain dan luncurkan Heaptrack GUI.

Tiket GitLab ini memberi tahu saya tentang kemungkinan peluncuran Heaptrack. Jika mereka bisa menjalankannya, maka saya bisa.

Aplikasi kami berjalan dalam wadah, dan saya harus memulai kembali dengan --cap-add=SYS_PTRACE , ini memungkinkan GDB untuk menggunakan ptrace , yang diperlukan untuk Heaptrack untuk menyuntikkan dirinya sendiri. Saya juga memerlukan hack kecil untuk file shell untuk menerapkan root ke profil proses non- root (kami meluncurkan aplikasi Wacana kami dalam wadah di bawah akun terbatas).

Setelah semuanya selesai, tetap hanya menjalankan heaptrack -p PID dan menunggu hasilnya muncul. Heaptrack ternyata menjadi alat yang sangat baik, sangat mudah untuk melacak semua yang terjadi dengan kebocoran memori.



Pada grafik, Anda melihat dua lompatan, satu karena cppjieba , yang lain karena objspace_xmalloc0 di Ruby.

Saya tahu tentang cppjieba . Mengelompokkan bahasa Cina mahal, Anda perlu kamus besar, jadi ini bukan kebocoran. Tapi bagaimana dengan mengalokasikan memori di Ruby, yang masih belum memberitahu saya itu?



Gain utama terkait dengan iseq_set_sequence di iseq_set_sequence . Ternyata kebocoran itu karena urutan instruksi. Ini membersihkan kebocoran yang ditemukan oleh Mwrap. Penyebabnya adalah mod.module_eval(source, identifier, 0) , yang membuat urutan instruksi yang tidak dihapus dari memori.

Jika, dalam analisis retrospektif, saya dengan hati-hati mempertimbangkan dump heap dari Ruby, maka saya akan memperhatikan semua IMEMO ini, karena mereka termasuk dalam dump ini. Mereka tidak terlihat selama diagnosa dalam proses.

Sejak saat ini, debugging cukup sederhana. Saya melacak semua panggilan ke modul eval dan membuang apa yang dievaluasi. Saya menemukan bahwa kami menambahkan metode ke kelas besar berulang kali. Berikut ini adalah tampilan sederhana dari bug yang kami temui:

 require 'securerandom' module BigModule; end def leak_methods 10_000.times do method = "def _#{SecureRandom.hex}; #{"sleep;" * 100}; end" BigModule.module_eval(method) end end usage # RSS: 16164 ObjectSpace size 2869 leak_methods usage # RSS: 123096 ObjectSpace size 5583 

Ruby memiliki kelas untuk menyimpan urutan instruksi yang RubyVM::InstructionSequence : RubyVM::InstructionSequence . Namun, Ruby terlalu malas untuk membuat objek pembungkus ini, karena menyimpannya secara tidak perlu tidak efisien. Koichi Sasada membuat ketergantungan iseq_collector . Jika kita menambahkan kode ini, kita dapat menemukan memori tersembunyi kita:

 require 'iseq_collector' puts "#{ObjectSpace.memsize_of_all_iseq / 1024}" # 98747 ObjectSpace.memsize_of_all_iseq 

mematerialisasikan setiap urutan instruksi, yang dapat sedikit meningkatkan konsumsi memori dari proses dan memberikan pengumpul sampah sedikit lebih banyak pekerjaan.

Jika, misalnya, kami menghitung jumlah ISEQ sebelum dan setelah memulai kolektor, kami akan melihat bahwa setelah memulai ObjectSpace.memsize_of_all_iseq penghitung kami dari kelas RubyVM::InstructionSequence meningkat dari 0 hingga 11128 (dalam contoh ini):

 def count_iseqs ObjectSpace.each_object(RubyVM::InstructionSequence).count end 

Pembungkus ini akan tetap sepanjang umur metode, mereka akan perlu dikunjungi dengan menjalankan sepenuhnya pengumpul sampah. Masalah kami diselesaikan dengan menggunakan kembali kelas yang bertanggung jawab untuk merender templat email ( perbaikan terbaru 1 , perbaikan terbaru 2 ).

chap


Selama debugging, saya menggunakan alat yang sangat menarik. Beberapa tahun yang lalu, Tim Boddy mengeluarkan alat internal yang digunakan oleh VMWare untuk menganalisis kebocoran memori dan membuat kodenya terbuka. Ini adalah satu-satunya video tentang ini yang berhasil saya temukan: https://www.youtube.com/watch?v=EZ2n3kGtVDk . Tidak seperti kebanyakan alat serupa, yang ini tidak berpengaruh pada proses yang dapat dieksekusi. Ini hanya dapat diterapkan ke file-file dump utama, sementara glibc digunakan sebagai pengalokasi (tidak ada dukungan untuk jemalloc / tcmalloc, dll.).

Dengan chap, sangat mudah untuk mendeteksi kebocoran yang saya miliki. Beberapa distribusi memiliki binary chap, tetapi Anda dapat dengan mudah mengompilasinya dari kode sumber . Dia sangat aktif didukung.

 # 444098 is the `Process.pid` of the leaking process I had sudo gcore -p 444098 chap core.444098 chap> summarize leaked Unsigned allocations have 49974 instances taking 0x312f1b0(51,573,168) bytes. Unsigned allocations of size 0x408 have 49974 instances taking 0x312f1b0(51,573,168) bytes. 49974 allocations use 0x312f1b0 (51,573,168) bytes. chap> list leaked ... Used allocation at 562ca267cdb0 of size 408 Used allocation at 562ca267d1c0 of size 408 Used allocation at 562ca267d5d0 of size 408 ... chap> summarize anchored .... Signature 7fbe5caa0500 has 1 instances taking 0xc8(200) bytes. 23916 allocations use 0x2ad7500 (44,922,112) bytes. 

Chap dapat menggunakan tanda tangan untuk mencari lokasi memori yang berbeda, dan dapat melengkapi GDB. Saat debugging di Ruby, itu bisa sangat membantu dalam menentukan memori mana yang digunakan proses. Ini menunjukkan total memori yang digunakan, kadang-kadang glibc malloc dapat memecah-mecah sehingga volume yang digunakan bisa sangat berbeda dari RSS yang sebenarnya. Anda dapat membaca diskusi: Fitur # 14759: [PATCH] atur M_ARENA_MAX untuk glibc malloc - Ruby master - Ruby Tracking System System . Chap mampu menghitung dengan benar semua memori yang digunakan dan memberikan analisis alokasi yang mendalam.

Selain itu, chap dapat diintegrasikan ke dalam alur kerja untuk secara otomatis mendeteksi kebocoran dan menandai rakitan tersebut.

Tindak lanjut kerja


Putaran debugging ini membuat saya mengajukan beberapa pertanyaan terkait dengan toolkit pembantu kami:


Ringkasan


Toolkit kami hari ini untuk men-debug kebocoran memori yang sangat kompleks jauh lebih baik daripada 4 tahun yang lalu! Mwrap, Heaptrack, dan chap adalah alat yang sangat kuat untuk menyelesaikan masalah memori yang muncul selama pengembangan dan operasi.

Jika Anda mencari kebocoran memori sederhana di Ruby, saya sarankan membaca artikel 2015 saya , untuk sebagian besar itu relevan.

Saya harap Anda merasa lebih mudah saat berikutnya Anda mulai men-debug kebocoran memori asli yang rumit.

Source: https://habr.com/ru/post/id473408/


All Articles