Tiga jenis kebocoran memori

Halo kolega.

Pencarian panjang kami untuk buku terlaris abadi tentang optimasi kode sejauh ini hanya menghasilkan hasil pertama, tetapi kami siap untuk menyenangkan Anda bahwa terjemahan buku legendaris Ben Watson " Menulis Kinerja Tinggi. Kode NET " baru saja selesai. Di toko - sementara pada bulan April, perhatikan iklan.

Dan hari ini kami menawarkan Anda untuk membaca artikel yang sepenuhnya praktis tentang jenis kebocoran memori yang paling mendesak, yang ditulis oleh Nelson Ilheidzhe (Strike).

Jadi, Anda memiliki program yang membutuhkan waktu lebih lama untuk diselesaikan, semakin lama dibutuhkan. Anda mungkin tidak akan mengalami kesulitan memahami bahwa ini adalah tanda pasti kebocoran memori.
Namun, apa sebenarnya yang dimaksud dengan "kebocoran memori"? Dalam pengalaman saya, kebocoran memori eksplisit dibagi menjadi tiga kategori utama, yang masing-masing ditandai dengan perilaku khusus, dan untuk debugging masing-masing kategori diperlukan alat dan teknik khusus. Pada artikel ini saya ingin menjelaskan ketiga kelas dan menyarankan cara mengenali dengan benar
di kelas mana Anda berurusan, dan bagaimana menemukan kebocoran.

Jenis (1): Fragmen memori yang tidak terjangkau dialokasikan

Ini adalah kebocoran memori klasik di C / C ++. Seseorang mengalokasikan memori menggunakan new atau malloc , dan tidak menelepon free atau delete untuk membebaskan memori setelah selesai bekerja dengannya.

 void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); /* ,   free() */ } 

Cara menentukan apakah kebocoran termasuk dalam kategori ini

  • Jika Anda menulis dalam C atau C ++, terutama dalam C ++ tanpa penggunaan smart pointer secara luas untuk mengontrol masa pakai segmen memori, maka ini adalah opsi yang kami pertimbangkan terlebih dahulu.
  • Jika program berjalan di lingkungan dengan pengumpulan sampah, ada kemungkinan kebocoran tipe ini dipicu oleh ekstensi kode asli , namun, kebocoran tipe (2) dan (3) harus terlebih dahulu dihilangkan.

Cara menemukan kebocoran seperti itu

  • Gunakan ASAN . Gunakan ASAN. Gunakan ASAN.
  • Gunakan detektor yang berbeda. Saya mencoba alat Valgrind atau tcmalloc untuk bekerja dengan banyak, ada juga alat lain di lingkungan lain.
  • Beberapa pengalokasi memori memungkinkan untuk membuang profil heap, yang akan menampilkan semua area memori yang tidak terisi. Jika Anda memiliki kebocoran, maka setelah beberapa waktu, hampir semua debit aktif akan mengalir darinya, jadi menemukan itu mungkin tidak sulit.
  • Jika semuanya gagal, buang dump memori dan memeriksanya seteliti mungkin . Tapi yang pasti jangan mulai dengan ini.

Jenis (2): alokasi memori berumur panjang yang tidak direncanakan

Situasi seperti itu tidak "bocor" dalam arti kata klasik, karena tautan dari suatu tempat ke bagian memori ini tetap dipertahankan, sehingga pada akhirnya dapat dilepaskan (jika program berhasil sampai ke sana tanpa menghabiskan semua memori).
Situasi dalam kategori ini dapat muncul karena berbagai alasan spesifik. Yang paling umum adalah:

  • Akumulasi negara yang tidak disengaja dalam struktur global; misalnya, server HTTP menulis ke daftar global setiap objek Request diterima.
  • Tembolok tanpa kebijakan usang yang dipikirkan dengan matang. Misalnya, cache ORM yang cache setiap objek tunggal dimuat, aktif selama migrasi, di mana semua catatan yang ada dalam tabel dimuat tanpa terkecuali.
  • Keadaan terlalu tebal ditangkap di sirkuit. Kasus ini sangat umum di Java Script, tetapi dapat juga terjadi di lingkungan lain.
  • Dalam arti yang lebih luas, retensi yang tidak disengaja dari setiap elemen array atau stream, sementara itu diasumsikan bahwa elemen-elemen ini akan diproses streaming online.

Cara menentukan apakah kebocoran termasuk dalam kategori ini

  • Jika program berjalan di lingkungan dengan pengumpulan sampah, maka ini adalah opsi yang kami pertimbangkan terlebih dahulu.
  • Bandingkan ukuran tumpukan yang ditampilkan dalam statistik pengumpul sampah dengan ukuran memori bebas yang dihasilkan oleh sistem operasi. Jika kebocoran masuk ke dalam kategori ini, maka jumlahnya akan sebanding dan, yang paling penting, akan mengikuti satu sama lain dari waktu ke waktu.

Cara menemukan kebocoran seperti itu

Gunakan profiler atau heap dump tools yang tersedia di lingkungan Anda. Saya tahu ada guppy di Python atau memory_profiler di Ruby, dan saya juga menulis ObjectSpace langsung di Ruby.

Jenis (3): memori bebas tetapi tidak digunakan atau tidak dapat digunakan

Kategori ini paling sulit dikarakterisasi, tetapi justru yang paling penting untuk dipahami dan diperhitungkan.

Kebocoran tipe ini terjadi di zona abu-abu, di antara memori, yang dianggap "bebas" dari sudut pandang pengalokasi di dalam VM atau lingkungan runtime, dan memori, yang "bebas" dari sudut pandang sistem operasi. Alasan paling umum (tapi bukan satu-satunya) untuk fenomena ini adalah tumpukan tumpukan . Beberapa pengalokasi hanya mengambil dan tidak mengembalikan memori ke sistem operasi setelah dialokasikan.

Kasus semacam ini dapat dipertimbangkan dengan contoh program singkat yang ditulis dengan Python:

 import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) 

Kami mengalokasikan 200.000 buffer 1-kb, dan kemudian menyimpan setiap berikutnya. Setiap detik, kami menampilkan status memori dari sudut pandang sistem operasi dan dari sudut pandang pengumpul sampah Python kami sendiri.

Di laptop saya, saya mendapatkan sesuatu seperti ini:

start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520


Kita dapat memastikan bahwa Python benar-benar membebaskan setengah dari buffer, karena tingkat gcsize turun hampir setengah dari nilai puncak, tetapi tidak dapat mengembalikan satu byte memori ini ke sistem operasi. Memori yang dibebaskan tetap dapat diakses oleh proses Python yang sama, tetapi tidak untuk proses lain pada mesin ini.

Fragmen memori yang bebas tetapi tidak digunakan seperti itu bisa bermasalah dan tidak berbahaya. Jika program Python bertindak seperti ini dan kemudian mengalokasikan beberapa fragmen 1kb, maka ruang ini hanya digunakan kembali, dan semuanya baik-baik saja.

Tetapi, jika kita melakukan ini selama pengaturan awal, dan kemudian mengalokasikan memori seminimal mungkin, atau jika semua fragmen yang dialokasikan selanjutnya masing-masing adalah 1,5 kb dan tidak sesuai dengan buffer yang tersisa sebelumnya, maka semua memori yang dialokasikan dengan cara ini akan selalu berdiri diam akan sia-sia.

Masalah semacam ini sangat relevan dalam lingkungan tertentu, yaitu, dalam sistem server multiproses untuk bekerja dengan bahasa seperti Ruby atau Python.

Katakanlah kita membuat sistem di mana:

  • Di setiap server, N pekerja single-threaded digunakan untuk melayani permintaan secara kompeten. Mari kita ambil N = 10 untuk akurasi.
  • Sebagai aturan, setiap karyawan memiliki jumlah memori yang hampir konstan. Untuk akurasi, mari ambil 500MB.
  • Dengan frekuensi rendah, kami menerima permintaan yang membutuhkan lebih banyak memori daripada permintaan median. Untuk akurasi, mari kita asumsikan bahwa sekali semenit kita mendapatkan permintaan, waktu eksekusi yang tambahannya membutuhkan memori 1GB tambahan, dan ketika permintaan diproses, memori ini dibebaskan.

Sekali semenit, permintaan "cetacean" semacam itu tiba, proses yang kami percayakan kepada salah satu dari 10 pekerja, misalnya, secara acak: ~random . Idealnya, selama pemrosesan permintaan ini, karyawan ini harus mengalokasikan 1GB RAM, dan setelah kerja selesai, kembalikan memori ini ke sistem operasi sehingga dapat digunakan kembali nanti. Untuk memproses permintaan tanpa batas dengan prinsip ini, server hanya perlu 10 * 500MB + 1GB = 6GB RAM.

Namun, mari kita asumsikan bahwa karena fragmentasi atau karena alasan lain, mesin virtual tidak akan pernah dapat mengembalikan memori ini ke sistem operasi. Artinya, jumlah RAM yang dibutuhkan dari OS sama dengan jumlah memori terbesar yang harus Anda alokasikan sekaligus. Dalam hal ini, ketika seorang karyawan tertentu melayani permintaan intensif sumber daya seperti itu, area yang ditempati oleh proses dalam memori akan selamanya membengkak oleh seluruh gigabyte.

Ketika Anda memulai server, Anda akan melihat bahwa jumlah memori yang digunakan adalah 10 * 500MB = 5GB. Segera setelah permintaan besar pertama tiba, pekerja pertama membutuhkan memori 1GB, dan kemudian tidak mengembalikannya. Jumlah total memori yang digunakan akan melonjak menjadi 6GB. Permintaan masuk berikut kadang-kadang dapat dialihkan ke proses yang sebelumnya memproses "paus", dalam hal ini jumlah memori yang digunakan tidak akan berubah. Tetapi kadang-kadang permintaan sebesar itu akan dikirimkan ke karyawan lain, karena itu ingatannya akan meningkat sebesar 1GB, dan seterusnya hingga setiap karyawan dapat memproses permintaan sebesar itu setidaknya satu kali. Dalam hal ini, Anda akan membutuhkan hingga 10 * (500MB + 1GB) = 15GB RAM dengan operasi ini, yang jauh lebih dari 6GB ideal! Selain itu, jika Anda melihat bagaimana armada server digunakan dari waktu ke waktu, Anda dapat melihat bagaimana jumlah memori yang digunakan secara bertahap tumbuh dari 5GB menjadi 15GB, yang akan sangat mengingatkan pada kebocoran "nyata".

Cara menentukan apakah kebocoran termasuk dalam kategori ini

  • Bandingkan ukuran tumpukan yang ditampilkan dalam statistik pengumpul sampah dengan ukuran memori bebas yang dihasilkan oleh sistem operasi. Jika kebocoran termasuk dalam kategori (ketiga) ini, maka jumlahnya akan berbeda dari waktu ke waktu.
  • Saya ingin mengonfigurasi server aplikasi saya sehingga kedua angka ini secara berkala menangkis dalam infrastruktur deret waktu saya, jadi nyaman untuk menampilkan grafik pada mereka.
  • Di Linux, lihat status sistem operasi di bidang 24 dari /proc/self/stat , dan lihat pengalokasi memori melalui bahasa atau API khusus mesin virtual.

Cara menemukan kebocoran seperti itu

Seperti yang telah disebutkan, kategori ini sedikit lebih berbahaya daripada yang sebelumnya, karena masalah sering muncul bahkan ketika semua komponen bekerja "sebagaimana dimaksud". Namun, ada sejumlah trik yang berguna untuk membantu mengurangi atau mengurangi dampak dari "kebocoran virtual" tersebut:

  • Mulai ulang proses Anda lebih sering. Jika masalah tumbuh lambat, maka mungkin me-restart semua proses aplikasi sekali setiap 15 menit atau sekali per jam mungkin tidak sulit.
  • Pendekatan yang bahkan lebih radikal: Anda dapat mengajarkan semua proses untuk memulai kembali secara mandiri, segera setelah ruang yang mereka tempati dalam memori melebihi nilai ambang tertentu atau tumbuh dengan nilai yang telah ditentukan. Namun, cobalah untuk meramalkan bahwa seluruh armada server Anda tidak dapat memulai restart sinkron spontan.
  • Ubah pengalokasi memori. Dalam jangka panjang, tcmalloc dan jemalloc biasanya menangani fragmentasi jauh lebih baik daripada pengalokasi default, dan bereksperimen dengannya sangat nyaman menggunakan variabel LD_PRELOAD .
  • Cari tahu apakah Anda memiliki kueri individual yang menghabiskan lebih banyak memori daripada yang lain. Di Stripe, server API kami mengukur RSS (konsumsi memori konstan) sebelum dan setelah melayani setiap permintaan API dan mencatat delta. Kemudian, kami dengan mudah meminta sistem agregasi log kami untuk menentukan apakah ada terminal dan pengguna (dan pola) yang dapat digunakan untuk menghapus semburan konsumsi memori.
  • Sesuaikan pengumpul sampah / pengalokasi memori. Banyak dari mereka memiliki parameter yang dapat disesuaikan yang memungkinkan Anda menentukan seberapa aktif mekanisme tersebut akan mengembalikan memori ke sistem operasi, seberapa optimalnya untuk menghilangkan fragmentasi; ada opsi berguna lainnya. Semuanya di sini juga cukup rumit: pastikan Anda memahami dengan tepat apa yang Anda ukur dan optimalkan, dan cobalah untuk menemukan ahli di mesin virtual yang sesuai dan berkonsultasi dengannya.

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


All Articles