Catch Me If You Can — Memory Leaks

Dec 01 2022
Sebuah retrospektif pada kebocoran memori
Pendahuluan Kebocoran memori adalah salah satu hal yang, jika terjadi, dapat benar-benar membuat Anda terpuruk. Mendiagnosis mereka tampaknya seperti tugas yang menantang pada awalnya.
Insinyur Elli vs. kebocoran memori (ilustrasi oleh Jane Kim)

pengantar

Kebocoran memori adalah salah satu hal yang, ketika terjadi, benar-benar dapat membuat Anda terpuruk. Mendiagnosis mereka tampaknya seperti tugas yang menantang pada awalnya. Mereka memerlukan pemahaman mendalam tentang alat dan komponen yang diandalkan oleh layanan Anda. Pemeriksaan close-up ini tidak hanya memperdalam pemahaman Anda tentang lanskap layanan Anda, tetapi juga memberikan wawasan tentang bagaimana berbagai hal berjalan di bawah tenda. Meski menakutkan pada pandangan pertama, kebocoran memori pada dasarnya adalah berkah tersembunyi.

Di Elli, kami melakukan yang terbaik untuk meminimalkan utang teknis seminimal mungkin. Namun, insiden masih terjadi, dan pendekatan kami adalah belajar dan berbagi pengetahuan dengan menyelesaikan masalah tersebut.

Jadi, artikel ini bertujuan untuk melakukan hal ini. Dalam posting ini, kami memandu Anda melalui pendekatan kami untuk mengidentifikasi kebocoran memori dan membagikan pembelajaran kami selama ini.

Konteks

Sebelum kita mulai memperbaiki kebocoran memori, kita memerlukan beberapa konteks tentang infrastruktur Elli dan di mana kebocoran memori terjadi.

Elli, antara lain, adalah Charging Point Operator. Kami bertanggung jawab untuk menghubungkan stasiun pengisian daya (CS) ke backend kami dan mengontrolnya melalui protokol OCPP . Ergo, pelanggan kami dapat mengisi daya EV mereka di stasiun swasta atau publik. CS terhubung ke sistem kami melalui WebSockets. Dalam hal autentikasi, kami mendukung koneksi melalui TLS atau mutual TLS (mTLS). Selama TLS, CS akan memverifikasi sertifikat server kami dan memastikannya tersambung ke backend Elli. Dengan mTLS, kami juga memverifikasi bahwa CS memiliki sertifikat klien yang dikeluarkan oleh kami.

Di sisi konektivitas, server yang ditulis dalam Node.js, bertanggung jawab untuk menjaga logika UPGRADE dari HTTP ke WebSockets dan menjaga status koneksi. Ini diterapkan di kluster Kubernetes dan dikelola oleh Horizontal Pod Autoscaler (HPA). Idealnya, HPA mengikuti beban lalu lintas dan menskalakan pod ke atas atau ke bawah.

Kami mempertahankan puluhan ribu koneksi TCP yang bertahan lama dan bertahan lama dari stasiun pengisian daya secara bersamaan . Ini memperkenalkan kompleksitas dan secara signifikan berbeda dari layanan RESTful pada umumnya. Metrik proxy yang melacak beban adalah pemanfaatan memori karena mencerminkan jumlah koneksi yang dibuat, dan logika aplikasi tidak memerlukan banyak perhitungan. Pod kami berumur panjang, dan penskalaan melalui memori membawa kami pada pengamatan bahwa jumlah pod perlahan meningkat untuk jumlah koneksi yang konstan. Singkat cerita, kami melihat kebocoran memori.

Penilaian dampak

Saat dihadapkan pada masalah produksi apa pun, tim teknik Elli segera menilai implikasi insiden ini terhadap pelanggan dan bisnis kami. Jadi setelah menemukan kebocoran memori ini, kami membuat penilaian berikut:

Aplikasi ini membocorkan memori dalam hitungan hari. Artinya, tanpa menerima lalu lintas tambahan, infrastruktur kami terus berkembang.

Saat sebuah pod tidak dapat menangani lalu lintas tambahan, berkat pemeriksaan kesiapan Kubernetes, pod tersebut berhenti menerima lalu lintas tambahan tetapi tetap melayani koneksi yang telah dibuat. Sebuah pod yang akan melayani koneksi X hanya dapat melayani sebagian kecil dari kemampuannya karena kebocoran, tanpa menyebabkan gangguan apa pun di sisi pelanggan. Artinya, kami dapat dengan mudah menyerap dampaknya hanya dengan memutar lebih banyak pod.

Investigasi

Sekarang untuk penyelaman teknis yang sebenarnya ke dalam kebocoran memori.

Di sini kami menjelaskan alat dan metode yang kami gunakan untuk mengungkap sumber di balik kebocoran memori, apa yang kami harapkan dari eksperimen kami, dan apa yang sebenarnya kami amati. Kami menyertakan tautan ke sumber daya yang kami gunakan dalam penyelidikan kami untuk referensi Anda.

Primer cepat ke memori JS

Variabel dalam JavaScript (dan sebagian besar bahasa pemrograman lainnya) disimpan di dua tempat: tumpukan dan tumpukan. Tumpukan biasanya merupakan wilayah berkelanjutan dari memori yang mengalokasikan konteks lokal untuk setiap fungsi yang dijalankan. Heap adalah wilayah yang jauh lebih besar yang menyimpan semua yang dialokasikan secara dinamis. Pemisahan ini berguna untuk membuat eksekusi lebih aman terhadap korupsi (tumpukan lebih terlindungi) dan lebih cepat (tidak perlu pengumpulan sampah dinamis dari bingkai tumpukan, alokasi bingkai baru yang cepat).

Hanya tipe primitif yang diteruskan oleh nilai (Angka, Boolean, referensi ke objek) yang disimpan di tumpukan. Segala sesuatu yang lain dialokasikan secara dinamis dari kumpulan memori bersama yang disebut heap. Dalam JavaScript, Anda tidak perlu khawatir tentang membatalkan alokasi objek di dalam heap. Pengumpul sampah membebaskan mereka setiap kali tidak ada yang mereferensikan mereka. Tentu saja, membuat objek dalam jumlah besar membutuhkan kinerja yang buruk (seseorang perlu menyimpan semua pembukuan), plus menyebabkan fragmentasi memori.

Sumber:https://glebbahmutov.com/blog/javascript-stack-size/

Mengambil snapshot heap dari pod produksi | Heap Snapshot & Pembuatan Profil

Harapan

Kami mengumpulkan snapshot heap reguler dari aplikasi kami untuk melihat akumulasi objek dari waktu ke waktu. Karena sifat aplikasinya, sebagian besar memegang koneksi WebSocket, kami berharap objek TLSSocket cocok dengan jumlah koneksi dalam aplikasi. Kami berhipotesis bahwa ketika sebuah stasiun terputus, objek tersebut entah bagaimana masih direferensikan. Pengumpulan sampah bekerja dengan membersihkan benda-benda yang tidak terjangkau, sehingga dalam hal ini benda-benda tersebut dibiarkan utuh.

Hasil

Mendapatkan heap dump dari pod yang digunakan 90% menghasilkan kisaran 100MB. Setiap pod meminta sekitar 1,5 GB RAM, dan tumpukannya kurang dari 10% dari memori yang dialokasikan. Ini tampak mencurigakan …

Di mana sisa memori dialokasikan? Meskipun demikian, kami melanjutkan analisis. Mengambil tiga snapshot dalam interval dan mengamati perubahan memori dari waktu ke waktu tidak mengungkapkan apa pun. Kami tidak melihat akumulasi objek juga tidak ada masalah dengan pengumpulan sampah. Tumpukan sampah tampak agak sehat.

Gambar 1: Mengambil snapshot heap melalui alat dev chrome dari pod produksi. Jumlah objek TLSSocket selaras dengan koneksi pod saat ini bertentangan dengan hasil yang diharapkan.

Objek TLSSocket cocok dengan status aplikasi. Kembali ke pengamatan pertama, heapdump adalah urutan besarnya kurang dari penggunaan memori. Kami berpikir: Ini tidak mungkin benar. Kami mencari di tempat yang salah. Kita perlu mengambil langkah mundur.”

Selain itu, kami membuat profil aplikasi melalui Cloud Profiler yang ditawarkan oleh GCP. Kami tertarik melihat bagaimana objek dialokasikan dengan berlalunya waktu dan berpotensi mengidentifikasi kebocoran memori.

Mendapatkan heap dump memblokir utas utama dan berpotensi mematikan aplikasi, berlawanan dengan ini, profiler dapat disimpan dalam produksi dengan sedikit overhead.

Cloud Profiler adalah fitur pembuatan profil berkelanjutan yang didesain untuk aplikasi yang berjalan di Google Cloud. Ini adalah profiler statistik atau pengambilan sampel dengan overhead rendah dan cocok untuk lingkungan produksi.

Meskipun profiler berkontribusi pada pemahaman kami tentang penyewa tumpukan, itu tetap tidak memberi kami petunjuk apa pun tentang penyelidikan. Sebaliknya, itu mendorong kami menjauh dari arah yang benar.

Peringatan spoiler: profiler, bagaimanapun, memberi kami informasi yang cukup berharga selama insiden dalam produksi di mana kami mengidentifikasi dan memperbaiki kebocoran memori yang agresif, tetapi itu adalah cerita untuk lain waktu.

Statistik penggunaan memori

Kami membutuhkan wawasan yang lebih besar tentang penggunaan memori. Kami membuat dasbor untuk semua metrik yang ditawarkan oleh process.memoryUsage() .

HeapTotal dan heapUsed mengacu pada penggunaan memori V8.

Eksternal mengacu pada penggunaan memori objek C++ yang terikat ke objek JavaScript yang dikelola oleh V8 .

Rss , Resident Set Size , adalah jumlah ruang yang ditempati di perangkat memori utama (yaitu, subset dari total memori yang dialokasikan) untuk proses tersebut, termasuk semua objek dan kode C++ dan JavaScript.

arrayBuffers mengacu pada memori yang dialokasikan untuk ArrayBuffers dan SharedArrayBuffers , termasuk semua Node.js Buffers . Ini juga termasuk dalam nilai eksternal. Saat Node.js digunakan sebagai pustaka tersemat, nilai ini mungkin 0 karena alokasi untuk ArrayBuffer mungkin tidak dilacak dalam kasus tersebut.

Gambar 2: Visualisasi konten RSS. Tidak ada model resmi terbaru dari memori V8 karena cukup sering berubah. Ini adalah upaya terbaik kami untuk menggambarkan apa yang hidup di bawah RSS sehingga kami dapat memiliki gambaran yang lebih jelas tentang komponen memori potensial yang membocorkan memori. Jika Anda ingin mempelajari lebih lanjut tentang pengumpul sampah, kami sarankan https://v8.dev/blog/trash-talk. Terima kasih kepada @mlippautz untuk klarifikasinya.

Seperti yang kita lihat sebelumnya, kita mendapatkan snapshot heap ~100MB dari wadah yang memiliki penggunaan memori lebih dari 1 GB. Di mana sisa memori dialokasikan? Mari kita lihat.

Gambar 3: Penggunaan memori per pod (persentil ke-95). Itu tumbuh seiring waktu. Tidak ada yang baru di sini, kami menyadari kebocoran memori.
Gambar 4: Jumlah koneksi per pod dari waktu ke waktu (persentil ke-95); pod menangani koneksi yang semakin sedikit.
Gambar 5: Tumpukan memori yang digunakan (persentil ke-95). Heap diselaraskan dengan ukuran snapshot yang kami kumpulkan dan stabil dari waktu ke waktu.
Gambar 6: Memori eksternal (persentil ke-95): berukuran kecil dan stabil.
Gambar 7: Pemanfaatan memori dan Resident Set Size (RSS) (persentil ke-95). Ada korelasi — RSS mengikuti pola.

Apa yang kita ketahui sejauh ini? RSS berkembang, heap dan eksternal stabil yang membawa kita ke stack. Ini bisa berarti metode yang dipanggil dan tidak pernah keluar, sehingga mengarah ke stack overflow. Namun, tumpukannya tidak boleh ratusan MB. Pada titik ini, kami telah menguji di lingkungan non-produksi dengan ribuan stasiun tetapi tidak mendapatkan hasil apa pun.

pengalokasi memori

Saat melakukan brainstorming, kami mempertimbangkan fragmentasi memori: bongkahan memori dialokasikan secara tidak berurutan yang mengarah ke bongkahan kecil memori yang tidak dapat digunakan untuk alokasi baru. Dalam kasus kami, aplikasi berjalan lama dan melakukan banyak alokasi dan pembebasan. Fragmentasi memori merupakan perhatian yang valid dalam kasus ini. Googling yang ekstensif membawa kami ke masalah GitHub di mana orang-orang menghadapi situasi yang sama dengan kami. Pola yang sama dari kebocoran memori diamati, dan sejalan dengan hipotesis kami.

Kami memutuskan untuk menguji pengalokasi memori yang berbeda, dan kami beralih dari musl ke jemalloc . Kami tidak menemukan hasil yang berarti. Pada titik ini, kami tahu kami perlu istirahat. Kami harus memikirkan kembali pendekatan itu sepenuhnya.

Mungkinkah bocoran itu hanya muncul di koneksi mTLS?

Selama pengujian pertama kami, kami mencoba mereproduksi masalah di lingkungan non-produksi tetapi tidak berhasil. Kami menjalankan uji beban dengan ribuan stasiun yang mensimulasikan berbagai skenario, menghubungkan/memutuskan stasiun selama berhari-hari, tetapi hasilnya tidak berarti. Namun, kami mulai curiga bahwa ada sesuatu yang kami lewatkan saat menjalankan tes ini.

Kami tidak memperhitungkan bahwa stasiun kami dapat terhubung melalui TLS atau mTLS. Tes pertama kami menyertakan stasiun TLS, tetapi bukan mTLS, dan alasannya sederhana: kami tidak dapat dengan mudah membuat stasiun mTLS dan sertifikat klien masing-masing. Insiden baru-baru ini memotivasi kami untuk meminimalkan radius ledakan dan membagi tanggung jawab aplikasi sehingga setiap penerapan akan menangani lalu lintas TLS dan mTLS secara terpisah. Eureka! Kebocoran memori hanya muncul di pod mTLS kami, sedangkan di TLS memori stabil.

Kemana kita pergi dari sini?

Kami memutuskan bahwa ada dua opsi: (1) Beralih ke tersangka berikutnya — perpustakaan yang menangani semua tugas Infrastruktur Kunci Publik serta potensi rekursi di suatu tempat di jalur kode tersebut, (2) atau tetap menggunakannya hingga kami mengerjakan ulang layanan kami sepenuhnya.

Selama investigasi kebocoran memori, banyak topik tak terduga yang menjadi perhatian kami terkait dengan layanan yang terpengaruh. Mempertimbangkan kebocoran memori dan semua hal lain yang kami temukan, kami memutuskan untuk meningkatkan lanskap layanan kami dan membagi tanggung jawab layanan . Aliran otentikasi dan otorisasi CS, antara lain, akan didelegasikan ke layanan baru dan kami akan menggunakan alat yang tepat untuk menangani tugas-tugas PKI.

Ringkasan

Meningkatkan penskalaan kami mengungkapkan bahwa kami memiliki kebocoran memori yang dapat dibiarkan tanpa diketahui untuk jangka waktu yang tidak terbatas. Memprioritaskan pelanggan dan menilai dampak kebocoran adalah yang pertama dan terpenting. Baru setelah itu kami dapat mengatur kecepatan penyelidikan kami karena kami menyadari bahwa tidak ada dampak terhadap pelanggan. Kami mulai dengan tempat yang paling jelas untuk dicari saat mendiagnosis kebocoran memori — heap. Menganalisis tumpukan, bagaimanapun, menunjukkan kepada kita bahwa kita melihat di tempat yang salah. Petunjuk lebih lanjut diperlukan dan API proses V8 memberi kami hal itu. Pada hasil pertama yang kami dapatkan, kebocoran memori muncul di RSS. Akhirnya, dengan menganalisis semua informasi yang dikumpulkan, kami mencurigai adanya fragmentasi memori.

Mengubah pengalokasi memori tidak memperbaiki situasi. Sebaliknya, mengubah pendekatan kami dan membagi beban kerja antara TLS dan mTLS, membantu kami mempersempit jalur kode yang terpengaruh.

Apa hasil akhir dari penyelidikan kami?

Rencana kami untuk meningkatkan skalabilitas bersamaan dengan mengatasi kebocoran memori, membuat kami memutuskan untuk membagi layanan dan menulis layanan baru untuk menangani aliran konektivitas CS secara terpisah dari spesifikasi CS lainnya.

Apakah kita memperbaiki kebocoran memori kita?

Waktu akan memberi tahu, tetapi menurut saya penyelidikannya jauh lebih dari itu. Pengalaman menyelidiki kebocoran membantu kami tumbuh sebagai pengembang dan layanan kami untuk mengadopsi arsitektur yang lebih tangguh dan dapat diskalakan.

Poin-poin penting dan pembelajaran

  • Τough tantangan teknik menyatukan orang; kami bermain ping-pong tentang ide dengan insinyur di luar tim kami.
  • Memberi kami motivasi untuk memikirkan kembali layanan, yang menghasilkan arsitektur yang lebih terukur.
  • Jika sakit, itu membutuhkan perhatian Anda; jangan abaikan itu.
  • https://nodejs.org/en/docs/guides/diagnostics/memory/using-heap-snapshot/
  • Aktifkan debugging jarak jauh ke pod melalui port forwarding:https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/
  • https://developers.google.com/cast/docs/debugging/remote_debugger
Ikuti kami di Media! (ilustrasi oleh Jane Kim)

Jika Anda tertarik untuk mengetahui lebih lanjut tentang cara kerja kami, silakan berlangganan blog Elli Medium dan kunjungi situs web perusahaan kami di elli.eco ! Sampai jumpa lain waktu!

Tentang Penulis

Thanos Amoutzias adalah Insinyur Perangkat Lunak, dia mengembangkan Sistem Manajemen Stasiun Pengisian Daya Elli dan mendorong topik SRE. Dia bersemangat membangun layanan yang andal dan memberikan produk yang berdampak. Anda dapat menemukannya di LinkedIn dan di ️.

Kredit: Terima kasih kepada semua rekan saya yang telah meninjau dan memberi umpan balik pada artikel ini!