Panduan praktis untuk menangani kebocoran memori di Node.js

Kebocoran memori mirip dengan entitas parasit pada aplikasi. Mereka diam-diam menembus ke dalam sistem, pada awalnya tanpa menimbulkan bahaya. Tetapi jika kebocorannya ternyata cukup kuat, itu dapat membawa aplikasi ke bencana. Misalnya - untuk memperlambatnya dengan kuat atau hanya untuk "membunuh" itu. Penulis artikel, terjemahan yang kami terbitkan hari ini, menyarankan untuk membicarakan kebocoran memori dalam JavaScript. Secara khusus, kita akan berbicara tentang manajemen memori dalam JavaScript, bagaimana mengidentifikasi kebocoran memori dalam aplikasi nyata, dan bagaimana menangani kebocoran memori.





Apa itu kebocoran memori?


Kebocoran memori, dalam arti luas, adalah sepotong memori yang dialokasikan untuk aplikasi yang tidak lagi dibutuhkan aplikasi ini, tetapi tidak dapat dikembalikan ke sistem operasi untuk digunakan di masa mendatang. Dengan kata lain, itu adalah blok memori yang ditangkap oleh aplikasi tanpa niat menggunakan memori ini di masa depan.

Manajemen memori


Manajemen memori adalah mekanisme untuk mengalokasikan memori sistem ke aplikasi yang membutuhkannya, dan mekanisme untuk mengembalikan memori yang tidak perlu ke sistem operasi. Ada banyak pendekatan untuk manajemen memori. Pendekatan mana yang digunakan tergantung pada bahasa pemrograman yang digunakan. Berikut ini adalah ikhtisar dari beberapa pendekatan umum untuk manajemen memori:

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . β€” JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • Penerapan konsep kepemilikan memori. Dengan pendekatan ini, setiap variabel harus memiliki pemiliknya sendiri. Segera setelah pemilik berada di luar ruang lingkup, nilai dalam variabel dihancurkan, membebaskan memori. Ide ini digunakan di Rust.

Ada pendekatan lain untuk manajemen memori yang digunakan dalam berbagai bahasa pemrograman. Misalnya, C ++ 11 menggunakan idiom RAII , sementara Swift menggunakan mekanisme ARC . Tetapi membicarakannya adalah di luar cakupan artikel ini. Untuk membandingkan metode manajemen memori di atas, untuk memahami pro dan kontra mereka, kita memerlukan artikel terpisah.

JavaScript, sebuah bahasa yang tanpa itu pemrogram web tidak dapat membayangkan pekerjaan mereka, menggunakan ide pengumpulan sampah. Karena itu, kita akan berbicara lebih banyak tentang bagaimana mekanisme ini bekerja.

Pengumpulan sampah JavaScript


Seperti yang telah disebutkan, JavaScript adalah bahasa yang menggunakan konsep pengumpulan sampah. Selama pengoperasian program JS, mekanisme yang disebut pengumpul sampah diluncurkan secara berkala. Dia mencari tahu bagian mana dari memori yang dialokasikan dapat diakses dari kode aplikasi. Yaitu, variabel mana yang dirujuk. Jika pengumpul sampah mengetahui bahwa sepotong memori tidak lagi diakses dari kode aplikasi, itu membebaskan memori ini. Pendekatan di atas dapat diimplementasikan menggunakan dua algoritma utama. Yang pertama adalah yang disebut algoritma Mark and Sweep. Ini digunakan dalam JavaScript. Yang kedua adalah Menghitung Referensi. Ini digunakan dalam Python dan PHP.


Tanda Fase (penandaan) dan Sapu (pembersihan) dari

algoritma Mark dan Sapu Saat menerapkan algoritme penandaan, daftar simpul akar yang diwakili oleh variabel lingkungan global (ini adalah objek di browserwindow) dibuat terlebih dahulu, dan kemudian pohon yang dihasilkan dirayapi dari akar ke simpul daun yang ditandai dengan semua bertemu di jalan benda. Memori pada heap yang ditempati oleh objek yang tidak berlabel dibebaskan.

Memori bocor di aplikasi Node.js


Sampai saat ini, kami telah menganalisis konsep teoritis yang cukup terkait dengan kebocoran memori dan pengumpulan sampah. Jadi - kami siap untuk melihat bagaimana semuanya terlihat dalam aplikasi nyata. Di bagian ini, kita akan menulis server Node.js yang memiliki kebocoran memori. Kami akan mencoba mengidentifikasi kebocoran ini menggunakan berbagai alat, dan kemudian kami akan menghilangkannya.

▍ Keakraban dengan kode yang memiliki kebocoran memori


Untuk tujuan demonstrasi, saya menulis server Express yang memiliki rute kebocoran memori. Kami akan men-debug server ini.

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Ada larik leaksyang berada di luar cakupan kode pemrosesan permintaan API. Akibatnya, setiap kali kode yang sesuai dijalankan, elemen baru ditambahkan ke array. Array tidak pernah dihapus. Karena tautan ke larik ini tidak hilang setelah keluar dari penangan permintaan, pemulung tidak pernah membebaskan memori yang digunakannya.

▍Panggil kebocoran memori


Di sini kita sampai pada yang paling menarik. Banyak artikel telah ditulis tentang bagaimana, menggunakan node --inspect, untuk men-debug kebocoran memori server, setelah mengisi server dengan permintaan menggunakan sesuatu seperti artileri . Tetapi pendekatan ini memiliki satu kelemahan penting. Bayangkan Anda memiliki server API yang memiliki ribuan titik akhir. Masing-masing dari mereka mengambil banyak parameter, kode tertentu yang akan dipanggil tergantung pada fitur-fiturnya. Akibatnya, dalam kondisi nyata, jika pengembang tidak tahu di mana letak kebocoran memori, ia harus mengakses setiap API berkali-kali menggunakan semua kombinasi parameter yang mungkin untuk mengisi memori. Bagi saya, tidak mudah untuk melakukannya. Solusi untuk masalah ini, bagaimanapun, difasilitasi dengan menggunakan sesuatu sepertigoreplay - sistem yang memungkinkan Anda merekam dan "memainkan" lalu lintas nyata.

Untuk mengatasi masalah kami, kami akan melakukan debugging dalam produksi. Artinya, kami akan memungkinkan server kami untuk meluap memori selama penggunaannya yang sebenarnya (karena menerima berbagai permintaan API). Dan setelah kami menemukan peningkatan mencurigakan dalam jumlah memori yang dialokasikan untuk itu, kami akan melakukan debugging.

▍ Heap Dump


Untuk memahami apa heap dump, pertama-tama kita perlu mencari tahu arti konsep heap. Jika Anda menggambarkan konsep ini sesederhana mungkin, ternyata tumpukan adalah tempat di mana segala sesuatu yang dialokasikan memori jatuh ke dalamnya. Semua ini ada di tumpukan sampai pengumpul sampah menghapus semua yang dianggap tidak perlu. Tumpukan tumpukan adalah sedikit gambaran dari kondisi tumpukan saat ini. Dump berisi semua variabel internal dan variabel yang dideklarasikan oleh programmer. Ini mewakili semua memori yang dialokasikan pada heap pada saat dump diterima.

Akibatnya, jika kita dapat membandingkan tumpukan timbunan server yang baru saja dimulai dengan timbunan tumpukan server, yang telah berjalan untuk waktu yang lama dan memori yang meluap, maka kita dapat mengidentifikasi objek mencurigakan yang tidak diperlukan aplikasi, tetapi tidak dihapus oleh pengumpul sampah.

Sebelum melanjutkan percakapan, mari kita bicara tentang cara membuat heap dumps. Untuk mengatasi masalah ini, kami akan menggunakan heapdump paket npm , yang memungkinkan Anda untuk mendapatkan dump dari tumpukan server.

Instal paket:

npm i heapdump

Kami akan membuat beberapa perubahan pada kode server yang memungkinkan kami menggunakan paket ini:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

Di sini kami menggunakan paket ini untuk membuang server yang baru diluncurkan. Kami juga membuat API yang /heapdumpdirancang untuk membuat heap saat mengaksesnya. Kami akan beralih ke API ini pada saat kami menyadari bahwa server mulai mengkonsumsi terlalu banyak memori.

Jika server Anda berjalan di kluster Kubernetes, maka Anda tidak akan dapat, tanpa usaha ekstra, untuk beralih ke pod yang servernya sedang berjalan di mana menghabiskan terlalu banyak memori. Untuk melakukan ini, Anda dapat menggunakan penerusan porta . Selain itu, karena Anda tidak akan memiliki akses ke sistem file yang Anda butuhkan untuk mengunduh file dump, akan lebih baik untuk mengunggah file-file ini ke penyimpanan cloud eksternal (seperti S3).

▍ Deteksi kebocoran memori


Dan sekarang, server dikerahkan. Dia telah bekerja selama beberapa hari. Ia menerima banyak permintaan (dalam kasus kami, hanya permintaan dari jenis yang sama) dan kami memperhatikan peningkatan jumlah memori yang dikonsumsi oleh server. Kebocoran memori dapat dideteksi menggunakan alat pemantauan seperti Express Status Monitor , Clinic , Prometheus . Setelah itu, kami memanggil API untuk membuang heap. Tempat sampah ini akan berisi semua objek yang tidak bisa dihapus oleh pemulung.

Inilah yang tampak seperti kueri untuk membuat dump:

curl --location --request GET 'http://localhost:3000/heapdump'

Ketika tumpukan sampah dibuat, pengumpul sampah terpaksa dijalankan. Akibatnya, kita tidak perlu khawatir tentang benda-benda yang mungkin dibuang oleh pengumpul sampah di masa depan, tetapi masih di tumpukan. Yaitu - tentang objek ketika bekerja dengan yang kebocoran memori tidak terjadi.

Setelah kami memiliki keduanya dump (dump dari server yang baru diluncurkan dan dump server yang telah bekerja selama beberapa waktu), kami dapat mulai membandingkannya.

Mendapatkan dump memori adalah operasi pemblokiran yang membutuhkan banyak memori untuk menyelesaikannya. Karena itu, harus dilakukan dengan hati-hati. Anda dapat membaca lebih lanjut tentang kemungkinan masalah yang terjadi selama operasi ini di sini .

Luncurkan Chrome dan tekan tombol.F12. Ini akan mengarah pada penemuan alat pengembang. Di sini Anda perlu pergi ke tab Memorydan memuat kedua snapshot memori.


Download dump memori pada tab Memory alat pengembang Chrome

Setelah men-download kedua snapshot, Anda perlu perubahanperspectiveuntukComparisondan klik pada snapshot dari memori server yang bekerja untuk beberapa waktu.


Mulai membandingkan foto-foto

Di sini kita dapat menganalisis kolomConstructordan mencari objek yang tidak dapat dihapus oleh pengumpul sampah. Sebagian besar objek ini akan diwakili oleh tautan internal yang digunakan node. Di sini berguna untuk menggunakan satu trik, yang terdiri dari pengurutan daftar berdasarkan bidangAlloc. Size. Ini akan dengan cepat menemukan objek yang menggunakan sebagian besar memori. Jika Anda memperluas blok(array), dan kemudian -(object elements), Anda dapat melihat array yangleaksberisi sejumlah besar objek yang tidak dapat dihapus menggunakan pengumpul sampah.


Analisis array yang mencurigakan

Teknik ini akan memungkinkan kita untuk pergi ke arrayleaksdan memahami bahwa itu adalah operasi yang salah dengan itu yang menyebabkan kebocoran memori.

▍Fiks memori bocor


Sekarang kita tahu bahwa "pelakunya" adalah sebuah array leaks, kita dapat menganalisis kode dan menemukan bahwa masalahnya adalah bahwa array tersebut dinyatakan di luar request handler. Akibatnya, ternyata tautan ke sana tidak pernah dihapus. Untuk memperbaiki masalah ini cukup sederhana - cukup transfer deklarasi array ke handler:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

Untuk memverifikasi keefektifan tindakan yang diambil, cukup untuk mengulangi langkah-langkah di atas dan membandingkan gambar tumpukan lagi.

Ringkasan


Kebocoran memori terjadi dalam berbagai bahasa. Secara khusus, mereka yang menggunakan mekanisme pengumpulan sampah. Misalnya, dalam JavaScript. Biasanya tidak sulit untuk memperbaiki kebocoran - kesulitan sebenarnya timbul hanya ketika Anda mencarinya.

Pada artikel ini, Anda membiasakan diri dengan dasar-dasar manajemen memori, dan bagaimana manajemen memori diatur dalam berbagai bahasa. Di sini kami mereproduksi skenario nyata kebocoran memori dan menjelaskan metode untuk pemecahan masalah.

Pembaca yang budiman! Pernahkah Anda mengalami kebocoran memori di proyek web Anda?


All Articles