Penanganan memori yang efisien di Node.js

Program, dalam pekerjaan, menggunakan memori akses acak komputer. Dalam JavaScript, di lingkungan Node.js, Anda dapat menulis proyek server dari berbagai skala. Pengaturan kerja dengan ingatan selalu merupakan tugas yang sulit dan bertanggung jawab. Pada saat yang sama, jika dalam bahasa seperti C dan C ++, programmer cukup terlibat dalam manajemen memori, JS memiliki mekanisme otomatis yang, sepertinya, sepenuhnya menghilangkan tanggung jawab programmer untuk bekerja secara efisien dengan memori. Namun, ini sebenarnya bukan masalahnya. Kode yang ditulis dengan buruk untuk Node.js dapat mengganggu operasi normal seluruh server yang digunakannya.



Bahannya, terjemahan yang kami terbitkan hari ini, akan fokus pada kerja efektif dengan memori di lingkungan Node.js. Secara khusus, konsep seperti aliran, buffer, dan metode aliran pipe() akan dibahas di sini. Node.js v8.12.0 akan digunakan dalam percobaan. Repositori dengan kode contoh dapat ditemukan di sini .

Tugas: menyalin file besar


Jika seseorang diminta membuat program untuk menyalin file di Node.js, maka kemungkinan besar ia akan segera menulis tentang apa yang ditunjukkan di bawah ini. Kami memberi nama file yang berisi kode ini basic_copy.js .

 const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; fs.readFile(fileName, (err, data) => {   if (err) throw err;   fs.writeFile(destPath || 'output', data, (err) => {       if (err) throw err;   });     console.log('New file has been created!'); }); 

Program ini membuat penangan untuk membaca dan menulis file dengan nama yang diberikan dan mencoba untuk menulis data file setelah membacanya. Untuk file kecil, pendekatan ini berfungsi.

Misalkan aplikasi kita perlu menyalin file besar (kami akan mempertimbangkan file "besar" lebih besar dari 4 GB) selama proses backup data. Sebagai contoh, saya memiliki file video berukuran 7,4 GB, yang saya, menggunakan program yang dijelaskan di atas, akan mencoba untuk menyalin dari direktori saya saat ini ke direktori Documents . Inilah perintah untuk mulai menyalin:

 $ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv 

Di Ubuntu, setelah menjalankan perintah ini, pesan kesalahan ditampilkan terkait dengan buffer overflow:

 /home/shobarani/Workspace/basic_copy.js:7   if (err) throw err;            ^ RangeError: File size is greater than possible Buffer: 0x7fffffff bytes   at FSReqWrap.readFileAfterStat [as oncomplete] (fs.js:453:11) 

Seperti yang Anda lihat, operasi pembacaan file gagal karena fakta bahwa Node.js memungkinkan hanya 2 GB data untuk dibaca ke dalam buffer. Bagaimana cara mengatasi batasan ini? Ketika melakukan operasi yang secara intensif menggunakan subsistem I / O (menyalin file, memproses, mengompresnya), perlu untuk mempertimbangkan kemampuan sistem dan keterbatasan yang terkait dengan memori.

Streaming dan Buffer di Node.js


Untuk mengatasi masalah yang dijelaskan di atas, kita memerlukan mekanisme yang dengannya kita dapat memecah sejumlah besar data menjadi fragmen kecil. Kami juga akan membutuhkan struktur data untuk menyimpan fragmen-fragmen ini dan bekerja dengannya. Buffer adalah struktur data yang memungkinkan Anda untuk menyimpan data biner. Selanjutnya, kita harus bisa membaca data dari disk dan menuliskannya ke disk. Peluang ini bisa memberi kita arus. Mari kita bicara tentang buffer dan utas.

UffBuffers


Buffer dapat dibuat dengan menginisialisasi objek Buffer .

 let buffer = new Buffer(10); // 10 -    console.log(buffer); //  <Buffer 00 00 00 00 00 00 00 00 00 00> 

Dalam versi Node.js yang lebih baru dari yang ke-8, yang terbaik adalah menggunakan konstruksi berikut untuk membuat buffer:

 let buffer = new Buffer.alloc(10); console.log(buffer); //  <Buffer 00 00 00 00 00 00 00 00 00 00> 

Jika kami sudah memiliki beberapa data, seperti array atau yang serupa, buffer dapat dibuat berdasarkan data ini.

 let name = 'Node JS DEV'; let buffer = Buffer.from(name); console.log(buffer) //  <Buffer 4e 6f 64 65 20 4a 53 20 44 45 5> 

Buffer memiliki metode yang memungkinkan Anda untuk "melihat" ke dalamnya dan mencari tahu data apa yang ada - ini adalah metode toString() dan toJSON() .

Kami, dalam proses mengoptimalkan kode, tidak akan membuat buffer sendiri. Node.js membuat struktur data ini secara otomatis saat bekerja dengan stream atau soket jaringan.

โ– Streaming


Streaming, jika kita beralih ke bahasa fiksi ilmiah, dapat dibandingkan dengan portal ke dunia lain. Ada empat jenis aliran:

  • Aliran untuk membaca (data dapat dibaca dari itu).
  • Streaming untuk merekam (data dapat dikirim ke sana).
  • Duplex stream (terbuka untuk membaca data darinya dan untuk mengirim data ke sana).
  • Transforming stream (aliran dupleks khusus yang memungkinkan Anda memproses data, misalnya, kompres atau periksa kebenarannya).

Kita memerlukan stream karena tujuan vital API stream di Node.js, dan khususnya metode stream.pipe() , adalah untuk membatasi buffering data ke level yang dapat diterima. Ini dilakukan agar bekerja dengan sumber dan penerima data yang berbeda dalam kecepatan pemrosesan yang berbeda tidak akan membanjiri memori yang tersedia.

Dengan kata lain, untuk menyelesaikan masalah menyalin file besar, kita memerlukan semacam mekanisme yang memungkinkan kita untuk tidak membebani sistem.


Streaming dan buffer (berdasarkan dokumentasi Node.js)

Diagram sebelumnya menunjukkan dua jenis aliran - Aliran yang Dapat Dibaca dan Aliran yang Dapat Ditulis. Metode pipe() adalah mekanisme yang sangat sederhana yang memungkinkan Anda untuk melampirkan utas untuk dibaca ke utas untuk menulis. Jika skema di atas tidak terlalu jelas bagi Anda, maka itu tidak masalah. Setelah menganalisis contoh-contoh berikut, Anda dapat dengan mudah mengatasinya. Khususnya, sekarang kita akan mempertimbangkan contoh pemrosesan data menggunakan metode pipe() .

Solusi 1. Menyalin file menggunakan stream


Pertimbangkan solusi untuk masalah menyalin file besar, yang kita bicarakan di atas. Solusi ini dapat didasarkan pada dua utas dan akan terlihat seperti ini:

  • Kami berharap potongan data berikutnya muncul dalam aliran untuk dibaca.
  • Kami menulis data yang diterima ke dalam aliran untuk direkam.
  • Kami memantau perkembangan operasi penyalinan.

Kami akan memanggil program yang mengimplementasikan ide ini streams_copy_basic.js . Ini kodenya:

 /*         . : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => {   this.fileSize = stats.size;   this.counter = 1;   this.fileArray = fileName.split('.');     try {       this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];   } catch(e) {       console.exception('File name is invalid! please pass the proper one');   }     process.stdout.write(`File: ${this.duplicate} is being created:`);     readable.on('data', (chunk)=> {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       writeable.write(chunk);       this.counter += 1;   });     readable.on('end', (e) => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully finished the operation");       return;   });     readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       console.log("Successfully created the file copy!");   });  }); 

Kami berharap pengguna menjalankan program ini untuk memberinya dua nama file. Yang pertama adalah file sumber, yang kedua adalah nama salinan yang akan datang. Kami membuat dua aliran - aliran untuk membaca dan aliran untuk menulis, mentransfer potongan data dari yang pertama ke yang kedua. Ada juga beberapa mekanisme tambahan. Mereka digunakan untuk memantau proses penyalinan dan untuk menampilkan informasi yang sesuai ke konsol.

Kami menggunakan mekanisme acara di sini, khususnya, kami berbicara tentang berlangganan acara-acara berikut:

  • data - dipanggil saat membaca sepotong data.
  • dipanggil saat data dibaca dari aliran baca.
  • error - dipanggil jika terjadi kesalahan saat membaca data.

Menggunakan program ini, file 7,4 GB disalin tanpa pesan kesalahan.

 $ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv 

Namun, ada satu masalah. Ini dapat diidentifikasi dengan melihat data tentang penggunaan sumber daya sistem dengan berbagai proses.


Data Penggunaan Sumber Daya Sistem

Perhatikan bahwa proses node , setelah menyalin 88% file, membutuhkan memori 4,6 GB. Ini banyak, penanganan memori seperti itu dapat mengganggu pekerjaan program lain.

โ– Alasan pemakaian memori yang berlebihan


Perhatikan kecepatan membaca data dari disk dan menulis data ke disk dari ilustrasi sebelumnya (kolom Disk Read dan Disk Write ). Yaitu, di sini Anda dapat melihat indikator berikut:

 Disk Read: 53.4 MiB/s Disk Write: 14.8 MiB/s 

Perbedaan kecepatan baca dari catatan data seperti itu berarti bahwa sumber data menghasilkan lebih cepat daripada yang dapat diterima dan diproses oleh penerima. Komputer harus menyimpan dalam memori fragmen data yang sudah dibaca sampai ditulis ke disk. Hasilnya, kami melihat indikator penggunaan memori tersebut.

Di komputer saya, program ini berjalan selama 3 menit 16 detik. Berikut ini informasi tentang perkembangan implementasinya:

 17.16s user 25.06s system 21% cpu 3:16.61 total 

Solusi 2. Menyalin file menggunakan stream dan dengan penyetelan otomatis kecepatan membaca dan menulis data


Untuk mengatasi masalah di atas, kita dapat memodifikasi program sehingga selama menyalin, membaca dan menulis kecepatan secara otomatis dikonfigurasi. Mekanisme ini disebut tekanan balik. Untuk menggunakannya, kita tidak perlu melakukan sesuatu yang istimewa. Cukup, menggunakan metode pipe() , untuk menghubungkan stream read ke stream write, dan Node.js akan secara otomatis menyesuaikan kecepatan transfer data.

Sebut program ini streams_copy_efficient.js . Ini kodenya:

 /*          pipe(). : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => {   this.fileSize = stats.size;   this.counter = 1;   this.fileArray = fileName.split('.');     try {       this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];   } catch(e) {       console.exception('File name is invalid! please pass the proper one');   }     process.stdout.write(`File: ${this.duplicate} is being created:`);     readable.on('data', (chunk) => {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       this.counter += 1;   });   readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully created the file copy!");   });     readable.pipe(writeable); //  !  }); 

Perbedaan utama antara program ini dan yang sebelumnya adalah bahwa kode untuk menyalin fragmen data diganti dengan baris berikut:

 readable.pipe(writeable); //  ! 

Inti dari semua yang terjadi di sini adalah metode pipe() . Ini mengontrol kecepatan baca dan tulis, yang mengarah pada fakta bahwa memori tidak lagi kelebihan beban.

Jalankan programnya.

 $ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv 

Kami sedang menyalin file besar yang sama. Sekarang mari kita lihat bagaimana cara kerjanya dengan memori dan dengan tampilan disk.


Dengan menggunakan pipa (), kecepatan baca dan tulis dikonfigurasikan secara otomatis

Sekarang kita melihat bahwa proses node hanya mengkonsumsi 61,9 MB memori. Jika Anda melihat data tentang penggunaan disk, Anda dapat melihat yang berikut:

 Disk Read: 35.5 MiB/s Disk Write: 35.5 MiB/s 

Berkat mekanisme tekanan balik, kecepatan baca dan tulis sekarang selalu sama satu sama lain. Selain itu, program baru berjalan 13 detik lebih cepat dari yang lama.

 12.13s user 28.50s system 22% cpu 3:03.35 total 

Dengan menggunakan metode pipe() , kami dapat mengurangi waktu eksekusi program dan mengurangi konsumsi memori sebesar 98,68%.

Dalam hal ini, 61,9 MB adalah ukuran buffer yang dibuat oleh data read stream. Kita dapat mengatur sendiri ukuran ini dengan menggunakan metode read() dari stream untuk membaca data:

 const readable = fs.createReadStream(fileName); readable.read(no_of_bytes_size); 

Di sini kami menyalin file dalam sistem file lokal, namun, pendekatan yang sama dapat digunakan untuk mengoptimalkan banyak tugas input-output data lainnya. Misalnya, ini bekerja dengan aliran data, sumbernya adalah Kafka, dan penerima adalah basis data. Menurut skema yang sama, dimungkinkan untuk mengatur pembacaan data dari disk, mengompresnya, seperti yang mereka katakan, "on the fly", dan menulisnya kembali ke disk yang sudah dalam bentuk terkompresi. Bahkan, ada banyak kegunaan lain untuk teknologi yang dijelaskan di sini.

Ringkasan


Salah satu tujuan dari artikel ini adalah untuk menunjukkan betapa mudahnya menulis program yang buruk di Node.js, meskipun platform ini menyediakan API yang bagus untuk pengembang. Dengan sedikit perhatian pada API ini, Anda dapat meningkatkan kualitas proyek perangkat lunak sisi-server.

Pembaca yang budiman! Bagaimana Anda bekerja dengan buffer dan utas di Node.js?

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


All Articles