Memori Elixir – Tidak Cukup Gratis
Memori Elixir – Tidak Cukup Gratis – Saya telah mengerjakan pengiriman layanan Elixir di SalesLoft untuk menggantikan fungsionalitas yang ada di sistem kami dengan versi yang lebih baik. Salah satu perubahan inti adalah bahwa komunikasi websocket dari sistem ini akan dikelola oleh Elixir daripada oleh Pusher (goto Rails kami). Posting ini akan mengeksplorasi beberapa kejutan dan pelajaran berharga yang saya peroleh saat men-debug kebocoran memori di layanan.
Memori Elixir – Tidak Cukup Gratis
elixir-memory – Saya melakukan check-in biasa pada layanan dan memperhatikan bahwa memori untuk hanya ~200 soket web yang terhubung memuncak pada lebih dari 550MB. Ini sepertinya tidak aktif dan berarti menghubungkan semua pengguna ke layanan akan memakan banyak GB memori. Saya bingung tentang apa masalahnya, tetapi menemukan bantuan dalam alat diagnostik yang berguna.
Baca Juga : Elixir dalam Manajemen Memori
Mendiagnosis Pelaku Memori
Saat melakukan pengembangan lokal, sangat mudah untuk membuka :observer.startdan mendapatkan antarmuka yang bagus untuk menyaring proses dan konsumsi memorinya. Namun, mengekspos ini dalam produksi jauh lebih sulit (terlalu sulit dalam pengaturan saya untuk repot). Saya menemukan alat observer_cli dan harus mengatakan bahwa itu adalah salah satu alat terhebat di kotak alat saya saat ini.
Ini didasarkan pada perpustakaan terkenal recon, tetapi menyediakan antarmuka baris perintah yang bagus untuk melihat secara visual dan menyortir daftar proses. Saya harus memberikan alat peraga kepada pengelola perpustakaan untuk mengimplementasikan fitur yang saya minta dalam hitungan hari.
Saat saya melihat output observer_cli, saya perhatikan bahwa Phoenix Channel Servers muncul di seluruh pengguna memori teratas. Beberapa akan memakan waktu hingga 4MB, tetapi rata-rata sekitar 1 MB. Untuk 1000 soket, ini akan menjadi antara 1GB – 4GB memori!
Memahami Memori Erlang
Salah satu posting favorit saya di Erlang adalah oleh Hamidreza Soleimani, Erlang Garbage Collection Details . Posting ini membahas detail penting tentang cara kerja Erlang GC 2 bagian, tumpukan generasi muda dan tua. Intinya adalah bahwa operasi GC utama dapat mengumpulkan tumpukan muda dan tua, tetapi jarang dipanggil karena ini adalah jenis GC “hentikan proses”. Operasi GC minor hanya dapat mengumpulkan item heap muda, dan akan menandai item sebagai tua jika mereka selamat dari GC pass. Apa artinya ini?
Dalam konteks permintaan, operasi dapat mengambil beberapa sapuan kecil GC dan masih merujuk semua binari yang dialokasikan (data). Ketika ini terjadi, binari tersebut akan ditandai sebagai tumpukan lama dan mengharuskan sapuan penuh terjadi pada GC mereka. Jika full sweep hanya terjadi pada situasi tertentu, kemungkinan situasi tersebut tidak terjadi dan full sweep tidak terjadi. Dalam hal ini, kami memiliki kebocoran memori. Inilah yang terjadi pada soket web saya.
Dimungkinkan untuk memicu GC di seluruh node untuk menguji apakah ada kemungkinan kebocoran memori. Perhatikan bahwa Anda tidak ingin melakukan ini secara teratur dan menjalankan GC utama dapat membuat sesi debug Anda kurang berharga hingga waktu berlalu.
Mengatasi Kebocoran Memori ini
Permintaan websocket yang telah saya diskusikan baru dan sedikit berbeda dari apa yang telah saya lakukan di masa lalu. Ini adalah antarmuka ke API kami dan memungkinkan permintaan dibuat pada koneksi terbuka. Karena itu adalah permintaan API, itu akan mengembalikan hingga 100 item sekaligus dan memerlukan beberapa bagian data yang bersumber dari layanan lain untuk beroperasi. Ini setara dengan 2 hal: memori (1-4 MB) dan waktu (beberapa sapuan kecil).
Erlang di Anger 7.2.2 menyebutkan 5 cara berbeda untuk memperbaiki kebocoran memori:
- panggil pengumpulan sampah secara manual pada interval tertentu (menjijikkan, tetapi agak efisien)
- berhenti menggunakan binari (seringkali tidak diinginkan)
- gunakan binary:copy/1-2 jika hanya menyimpan fragmen kecil (biasanya kurang dari 64 byte) dari biner yang lebih besar
- pindahkan pekerjaan yang melibatkan binari yang lebih besar ke proses satu kali sementara yang akan mati ketika selesai (bentuk manual GC yang lebih rendah!)
- atau tambahkan panggilan hibernasi bila perlu (mungkin solusi terbersih untuk proses yang tidak aktif)
Di atas disalin dari Erlang di Anger verbatim.
Untuk pekerjaan yang dilakukan oleh permintaan soket web ini, sangat masuk akal untuk menggunakan proses yang berlangsung singkat untuk mengeksekusi dan menanggapi permintaan tersebut.
Pass awal pada perbaikan melibatkan penggunaan Task.asyncdan menunggu respons. Namun, ini terbukti lebih buruk pada memori karena mengirim respons melalui penghalang proses menyebabkan kebocoran yang sama. Solusi di sini akhirnya adalah menggunakan Phoenix.Channel socket_ref/1 dan menanggapi permintaan soket dalam Task.startproses d menggunakan Phoenix.Channel.reply(ref, reply). socket_refFungsinya sangat berguna dan memiliki beberapa efek samping yang bagus . Karena serialisasi proses, pendekatan asli akan memblokir akses ke soket selama permintaan. Dengan pendekatan baru, soket dapat menangani beberapa permintaan secara bersamaan.
Hasil dari perubahan kode ini langsung terlihat. Proses Phoenix.Channel.Server yang dari 1MB – 4MB sekarang menjadi 30KB – 60KB. Hal ini menyebabkan penurunan besar dalam keseluruhan memori seperti yang terlihat di atas.
Bilas dan Ulangi
Karena semakin banyak soket yang terhubung ke sistem, menjadi jelas bahwa masih ada kebocoran memori. Dengan menggunakan observer_clialat ini, dimungkinkan untuk melihat bahwa proses soket web koboi masing-masing melayang di 1-2MB. Setelah diskusi di komunitas Slack, ternyata pengkodean muatan besar mengalami jenis kebocoran memori yang sama yang disebutkan sebelumnya. Namun, perbaikannya kurang optimal karena kode tersebut tidak ditulis oleh kami.
Tampaknya memicu GC utama adalah pilihan terbaik. Phoenix bahkan mengakomodasi ini dengan :garbage_collect penangan pesan khusus, yang ditandai sebagai solusi untuk digunakan setelah memproses pesan besar. Kami akhirnya memicu 5s ini setelah respons muatan besar kami.
Penggunaan memori ini berasal dari soket yang jauh lebih terhubung daripada langkah 1, dan kita dapat dengan jelas melihat seberapa besar dampak yang dimiliki GC manual. Memori sekarang dapat diprediksi dan stabil untuk soket yang terhubung.
Pikiran Akhir
Ini adalah kasus penggunaan yang sangat terbatas, meskipun mungkin umum, untuk kebocoran memori soket web Phoenix. Namun, prinsip yang sama berlaku untuk semua proses yang kami lakukan di Elixir. Ketika kita memiliki sebuah proses, dan terutama dengan sejumlah besar proses, penting untuk memikirkan siklus hidupnya dan bagaimana proses itu akan berperan dalam pengumpulan sampah. Saat proses kami menjadi lebih lama, ini menjadi lebih penting karena sistem kami akan membocorkan memori dalam jangka waktu yang lebih lama.
Tampaknya paling mudah untuk hanya menampar GC sistem penuh ke dalam setiap proses untuk menjaga penggunaan memori tetap rendah (dan kita dapat melakukannya jika diinginkan), tetapi ada teknik lain yang terkait dengan siklus hidup proses dan konsumsi memori yang mungkin lebih efektif pada akhirnya.