Advertisement

Pembuatan Profil Aplikasi Node.js


Pembuatan profil aplikasi Node.js melibatkan pengukuran kinerjanya dengan menganalisis CPU, memori, dan metrik runtime lainnya saat aplikasi sedang berjalan. Ini membantu mengidentifikasi hambatan, penggunaan CPU yang tinggi, kebocoran memori, atau panggilan fungsi yang lambat yang dapat memengaruhi efisiensi, responsivitas, dan skalabilitas aplikasi.

Ada banyak alat pihak ketiga yang tersedia untuk membuat profil aplikasi Node.js, tetapi, dalam banyak kasus, opsi termudah adalah menggunakan profiler bawaan Node.js. Profiler bawaan menggunakan profiler di dalam V8 yang mengambil sampel tumpukan secara berkala selama eksekusi program. Profiler mencatat hasil sampel ini, bersama dengan peristiwa pengoptimalan penting seperti kompilasi jit, sebagai serangkaian tanda centang:

code-creation,LazyCompile,0,0x2d5000a337a0,396,"bp native array.js:1153:16",0x289f644df68,~
code-creation,LazyCompile,0,0x2d5000a33940,716,"hasOwnProperty native v8natives.js:198:30",0x289f64438d0,~
code-creation,LazyCompile,0,0x2d5000a33c20,284,"ToName native runtime.js:549:16",0x289f643bb28,~
code-creation,Stub,2,0x2d5000a33d40,182,"DoubleToIStub"
code-creation,Stub,2,0x2d5000a33e00,507,"NumberToStringStub"

Di masa lalu, Anda memerlukan kode sumber V8 untuk dapat menginterpretasikan tanda centang. Untungnya, berbagai alat telah diperkenalkan sejak Node.js 4.4.0 yang memudahkan penggunaan informasi ini tanpa harus membangun V8 dari sumber secara terpisah. Mari kita lihat bagaimana profiler bawaan dapat membantu memberikan wawasan tentang kinerja aplikasi.

Untuk mengilustrasikan penggunaan profiler tanda centang, kita akan bekerja dengan aplikasi Express sederhana. Aplikasi kita akan memiliki dua pengendali, satu untuk menambahkan pengguna baru ke sistem kita:

 app.get('/newUser', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || users[username]) {
    return res.sendStatus(400);
  }

  const salt = crypto.randomBytes(128).toString('base64');
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  users[username] = { salt, hash };

  res.sendStatus(200);
});

 dan satu lagi untuk memvalidasi upaya otentikasi pengguna:

 app.get('/auth', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || !users[username]) {
    return res.sendStatus(400);
  }

  const { salt, hash } = users[username];
  const encryptHash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  if (crypto.timingSafeEqual(hash, encryptHash)) {
    res.sendStatus(200);
  } else {
    res.sendStatus(401);
  }
});

 Harap perhatikan bahwa ini BUKAN pengendali yang direkomendasikan untuk mengautentikasi pengguna di aplikasi Node.js Anda dan digunakan murni untuk tujuan ilustrasi. Anda tidak boleh mencoba merancang mekanisme autentikasi kriptografi Anda sendiri secara umum. Jauh lebih baik menggunakan solusi autentikasi yang sudah ada dan terbukti.

Sekarang asumsikan bahwa kita telah menerapkan aplikasi kita dan pengguna mengeluh tentang latensi yang tinggi pada permintaan. Kita dapat dengan mudah menjalankan aplikasi dengan profiler bawaan:

 NODE_ENV=production node --prof app.js

 dan memberi beberapa beban pada server menggunakan ab (ApacheBench):

 curl -X GET "http://localhost:8080/newUser?username=matt&password=password"

ab -k -c 20 -n 250 "http://localhost:8080/auth?username=matt&password=password"

 dan dapatkan keluaran ab:

Concurrency Level:      20
Time taken for tests:   46.932 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      50250 bytes
HTML transferred:       500 bytes
Requests per second:    5.33 [#/sec] (mean)
Time per request:       3754.556 [ms] (mean)
Time per request:       187.728 [ms] (mean, across all concurrent requests)
Transfer rate:          1.05 [Kbytes/sec] received

...

Percentage of the requests served within a certain time (ms)
  50%   3755
  66%   3804
  75%   3818
  80%   3825
  90%   3845
  95%   3858
  98%   3874
  99%   3875
 100%   4225 (longest request)

 

Dari keluaran ini, kita melihat bahwa kita hanya mampu melayani sekitar 5 permintaan per detik dan bahwa permintaan rata-rata memerlukan waktu kurang dari 4 detik untuk bolak-balik. Dalam contoh dunia nyata, kita dapat melakukan banyak pekerjaan dalam banyak fungsi atas nama permintaan pengguna, tetapi bahkan dalam contoh sederhana kita, waktu dapat terbuang untuk mengompilasi ekspresi reguler, menghasilkan garam acak, menghasilkan hash unik dari kata sandi pengguna, atau di dalam kerangka Express itu sendiri.

Karena kita menjalankan aplikasi kita menggunakan opsi --prof, file tick dihasilkan di direktori yang sama dengan aplikasi yang Anda jalankan secara lokal. File tersebut harus memiliki bentuk isolate-0xnnnnnnnnnnnn-v8.log (di mana n adalah digit).

Untuk memahami file ini, kita perlu menggunakan prosesor tick yang dibundel dengan biner Node.js. Untuk menjalankan prosesor, gunakan tanda --prof-process:

node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt

 

 Membuka file .txt yang diproses di editor teks favorit Anda akan memberikan beberapa jenis informasi yang berbeda. File tersebut dipecah menjadi beberapa bagian yang kemudian dipecah lagi berdasarkan bahasa. Pertama, kita lihat bagian ringkasan dan lihat:

  [Summary]:

   ticks  total  nonlib   name
     79    0.2%    0.2%  JavaScript
  36703   97.2%   99.2%  C++
      7    0.0%    0.0%  GC
    767    2.0%          Shared libraries
    215    0.6%          Unaccounted

 Ini memberi tahu kita bahwa 97% dari semua sampel yang dikumpulkan terjadi dalam kode C++ dan bahwa saat melihat bagian lain dari keluaran yang diproses, kita harus lebih memperhatikan pekerjaan yang dilakukan dalam C++ (bukan JavaScript). Dengan mengingat hal ini, selanjutnya kita menemukan bagian [C++] yang berisi informasi tentang fungsi C++ mana yang menghabiskan waktu CPU paling banyak dan melihat:

  [C++]:

   ticks  total  nonlib   name
  19557   51.8%   52.9%  node::crypto::PBKDF2(v8::FunctionCallbackInfo<v8::Value> const&)
   4510   11.9%   12.2%  _sha1_block_data_order
   3165    8.4%    8.6%  _malloc_zone_malloc

 Kita melihat bahwa 3 entri teratas menghabiskan 72,1% waktu CPU yang digunakan oleh program. Dari keluaran ini, kita langsung melihat bahwa sedikitnya 51,8% waktu CPU digunakan oleh fungsi yang disebut PBKDF2 yang sesuai dengan pembuatan hash dari kata sandi pengguna. Namun, mungkin tidak langsung jelas bagaimana dua entri yang lebih rendah berperan dalam aplikasi kita (atau jika memang demikian, kita akan berpura-pura sebaliknya demi contoh). Untuk lebih memahami hubungan antara fungsi-fungsi ini, selanjutnya kita akan melihat bagian [Bottom up (heavy) profile] yang menyediakan informasi tentang pemanggil utama setiap fungsi. Dengan memeriksa bagian ini, kita menemukan:

  ticks parent  name
  19557   51.8%  node::crypto::PBKDF2(v8::FunctionCallbackInfo<v8::Value> const&)
  19557  100.0%    v8::internal::Builtins::~Builtins()
  19557  100.0%      LazyCompile: ~pbkdf2 crypto.js:557:16

   4510   11.9%  _sha1_block_data_order
   4510  100.0%    LazyCompile: *pbkdf2 crypto.js:557:16
   4510  100.0%      LazyCompile: *exports.pbkdf2Sync crypto.js:552:30

   3165    8.4%  _malloc_zone_malloc
   3161   99.9%    LazyCompile: *pbkdf2 crypto.js:557:16
   3161  100.0%      LazyCompile: *exports.pbkdf2Sync crypto.js:552:30

 

 Menguraikan bagian ini memerlukan sedikit lebih banyak pekerjaan daripada hitungan tanda centang mentah di atas. Dalam setiap "tumpukan panggilan" di atas, persentase di kolom induk memberi tahu Anda persentase sampel yang fungsi di baris di atas dipanggil oleh fungsi di baris saat ini. Misalnya, di tengah "tumpukan panggilan" di atas untuk _sha1_block_data_order, kita melihat bahwa _sha1_block_data_order muncul di 11,9% sampel, yang kita ketahui dari hitungan mentah di atas. Namun, di sini, kita juga dapat mengetahui bahwa itu selalu dipanggil oleh fungsi pbkdf2 di dalam modul kripto Node.js. Kita melihat bahwa dengan cara yang sama, _malloc_zone_malloc dipanggil hampir secara eksklusif oleh fungsi pbkdf2 yang sama. Dengan demikian, dengan menggunakan informasi dalam tampilan ini, kita dapat mengetahui bahwa perhitungan hash kita dari kata sandi pengguna tidak hanya mencakup 51,8% dari atas tetapi juga untuk semua waktu CPU dalam 3 fungsi teratas yang paling banyak diambil sampelnya karena panggilan ke _sha1_block_data_order dan _malloc_zone_malloc dilakukan atas nama fungsi pbkdf2.

Pada titik ini, sangat jelas bahwa pembuatan hash berbasis kata sandi harus menjadi target pengoptimalan kita. Untungnya, Anda telah sepenuhnya memahami manfaat pemrograman asinkron dan Anda menyadari bahwa pekerjaan untuk membuat hash dari kata sandi pengguna dilakukan secara sinkron dan dengan demikian mengikat loop peristiwa. Hal ini mencegah kita mengerjakan permintaan masuk lainnya saat menghitung hash.

Untuk mengatasi masalah ini, Anda membuat sedikit modifikasi pada pengendali di atas untuk menggunakan versi asinkron dari fungsi pbkdf2:

 app.get('/auth', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || !users[username]) {
    return res.sendStatus(400);
  }

  crypto.pbkdf2(
    password,
    users[username].salt,
    10000,
    512,
    'sha512',
    (err, hash) => {
      if (users[username].hash.toString() === hash.toString()) {
        res.sendStatus(200);
      } else {
        res.sendStatus(401);
      }
    }
  );
});

 Pengujian baru ab benchmark di atas dengan versi asinkron aplikasi Anda menghasilkan:

 Concurrency Level: 20

Time taken for tests:   12.846 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      50250 bytes
HTML transferred:       500 bytes
Requests per second:    19.46 [#/sec] (mean)
Time per request:       1027.689 [ms] (mean)
Time per request:       51.384 [ms] (mean, across all concurrent requests)
Transfer rate:          3.82 [Kbytes/sec] received

...

Percentage of the requests served within a certain time (ms)
  50%   1018
  66%   1035
  75%   1041
  80%   1043
  90%   1049
  95%   1063
  98%   1070
  99%   1071
 100%   1079 (longest request)

 Hore! Aplikasi Anda sekarang melayani sekitar 20 permintaan per detik, kira-kira 4 kali lebih banyak daripada saat menggunakan pembuatan hash sinkron. Selain itu, latensi rata-rata turun dari 4 detik sebelumnya menjadi lebih dari 1 detik.

Semoga, melalui penyelidikan kinerja dari contoh (yang memang dibuat-buat) ini, Anda telah melihat bagaimana prosesor tick V8 dapat membantu Anda memperoleh pemahaman yang lebih baik tentang kinerja aplikasi Node.js Anda.

Anda mungkin juga menemukan cara membuat grafik api yang bermanfaat.

 


Post a Comment

0 Comments