Optimalisasi aplikasi node.js

Diberikan: aplikasi http node.js lama dan peningkatan beban di atasnya.

Solusi standar untuk masalah ini: keluar dari server, tulis ulang semuanya dari 0, optimalkan apa yang sudah ditulis.

Mari kita coba melalui optimasi dan mencari tahu bagaimana menemukan dan meningkatkan kelemahan aplikasi. Dan mungkin mempercepat tanpa menyentuh satu baris kode :)

Semua yang tertarik selamat datang di bawah kucing!

Pertama, mari kita putuskan teknik pengujian kinerja. Kami akan tertarik dengan jumlah permintaan yang dilayani dalam 1 detik: rps.

Kami akan menjalankan aplikasi dalam mode 1 pekerja (1 proses), mengukur kinerja kode lama dan kode dengan optimisasi - kinerja absolut tidak penting, kinerja komparatif penting.

Dalam aplikasi tipikal dengan banyak rute berbeda, masuk akal untuk pertama-tama menemukan permintaan yang paling banyak dimuat, yang prosesnya membutuhkan sebagian besar waktu. Utilitas seperti request-log-analizer atau banyak yang serupa akan memungkinkan Anda untuk mengekstrak informasi ini dari log.

Di sisi lain, Anda dapat mengambil daftar permintaan yang sebenarnya dan memenuhinya semua (misalnya, menggunakan tangki yandex) - kami mendapatkan profil beban yang andal.

Tetapi dengan melakukan banyak iterasi optimasi kode, akan jauh lebih nyaman untuk menggunakan alat yang lebih sederhana dan lebih cepat dan satu jenis permintaan tertentu (dan setelah mengoptimalkan satu permintaan, pelajari yang berikutnya, dll.). Pilihan saya adalah wrk . Selain itu, dalam kasus saya jumlah rute tidak besar - tidak sulit untuk memeriksa semuanya satu per satu.

Harus segera dicatat bahwa dalam hal memblokir kueri, harapan basis data, dll. aplikasi sudah dioptimalkan, semuanya tergantung pada CPU: selama tes, pekerja mengkonsumsi CPU 100%.

Server yang dijual menggunakan node.js versi 6 - mari kita mulai dengan itu:

Permintaan / dtk: 1210

Kami mencoba pada simpul ke-8:
Permintaan / dtk: 2308
Catatan 10:
Permintaan / dtk: 2590

Perbedaannya jelas. Peran kunci di sini dimainkan dengan memperbarui versi v8 - banyak kode v8 yang kurang optimal di masa lalu. Dan agar tidak berurusan dengan kincir angin yang menghilang di node.js v8, lebih baik segera memutakhirkan, dan kemudian melakukan optimasi kode.

Kami beralih ke pencarian aktual untuk kemacetan: menurut pendapat saya, alat terbaik untuk ini adalah flamegraph. Dan dengan munculnya proyek 0x , mendapatkan flamegraph sangat sederhana - mulai 0x alih-alih simpul: 0x -o yourscript.js, lakukan tes, hentikan skrip, lihat hasilnya di browser.

Flamegraph dari kode yang diuji terlihat seperti ini sebelum optimasi:


Di bawah filter, tinggalkan aplikasi, deps - hanya kode aplikasi dan modul pihak ketiga.

Semakin lebar strip, semakin banyak waktu yang dihabiskan untuk melakukan fungsi ini (termasuk panggilan bersarang).

Kami akan berurusan dengan pusat, bagian terbesar.

Pertama-tama, kami menyoroti fungsi yang tidak dioptimalkan. Saya menemukan beberapa di aplikasi ini.

Lebih jauh, fungsi-fungsi teratas adalah kandidat khas untuk optimasi. Fungsi yang tersisa berbaris dengan langkah-langkah yang relatif sama - masing-masing fungsi menyumbang sebagian kecil dari penundaan, tidak ada pemimpin yang jelas.

Kemudian algoritma tindakan sederhana dimungkinkan: untuk mengoptimalkan fungsi terluas, bergerak dari satu ke yang lain. Tapi saya memilih pendekatan yang berbeda: untuk mengoptimalkan mulai dari titik masuk ke aplikasi (request handler di http.createServer). Pada akhir fungsi yang sedang dipelajari, alih-alih memanggil fungsi-fungsi berikut, saya menyelesaikan pemrosesan permintaan dengan tanggapan tiruan dan mempelajari kinerja fungsi khusus ini. Setelah optimalisasi, jawaban dummy bergerak lebih jauh di sepanjang tumpukan panggilan ke fungsi berikutnya, dll.

Konsekuensi yang nyaman dari pendekatan ini: Anda dapat melihat rps dalam kondisi ideal (dengan hanya satu fungsi awal, rps dekat dengan rps maksimum dari aplikasi hellow world node.js), dan dengan pergerakan respons rintisan lebih jauh ke dalam aplikasi, amati kontribusi fungsi yang dipelajari ke penurunan kinerja. rps-ah.

Jadi, kita hanya menyisakan fungsi start, kita dapatkan:

Permintaan / dtk: 16176



Dengan menghubungkan inti, filter v8, Anda dapat melihat bahwa hampir seluruh fungsi yang diselidiki terdiri dari mengirim jawaban, masuk, dan hal-hal lain yang kurang dioptimalkan - kami melangkah lebih jauh.

Kami beralih ke fungsi berikut:

Permintaan / dtk: 16111
Tidak ada yang berubah - terjun lebih jauh:
Permintaan / dtk: 13330


Pelanggan kami! Dapat dilihat bahwa fungsi getByUrl yang terlibat menempati sebagian besar fungsi start - yang berkorelasi baik dengan penurunan rps.

Kami melihat dengan cermat apa yang terjadi di dalamnya (nyalakan inti, v8):

Banyak hal yang terjadi ... kami merokok kodenya, optimalkan:

for (var i in this.data) { if (this[i]._options.regexp_obj.test(url)) return this[i]; } return null; 

berubah menjadi

 let result = null; for (let i=0; i<this.length && !result; i++) { if (this[i]._options.regexp_obj.test(url)) result = this[i]; } 

Dalam hal ini, sederhana untuk jauh lebih cepat daripada untuk..dalam

Dapatkan Permintaan / dtk: 16015



Secara visual, fungsi "kempes" dan menempati sebagian kecil dari fungsi awal.
Dalam informasi terperinci tentang fungsi tersebut, semuanya juga sangat disederhanakan:

Kami beralih ke fungsi berikutnya.

Permintaan / dtk: 13316



Fungsi ini memiliki banyak fungsi array dan, meskipun ada percepatan yang signifikan dalam versi terbaru dari node.js, mereka masih lebih lambat dari loop sederhana: ubah [] .map dan filter. untuk reguler untuk dan dapatkan

Permintaan / dtk: 15067



Dan dari waktu ke waktu, untuk setiap fungsi selanjutnya.

Beberapa optimasi yang lebih bermanfaat: untuk hash dengan serangkaian kunci yang berubah secara dinamis, Peta baru () dapat 40% lebih cepat daripada {} biasa;

Math.round (el * 100) / 100 2 kali lebih cepat daripada toFixed (2).

Dalam flamegraph untuk fungsi inti dan v8, Anda dapat melihat entri yang tidak jelas dan cukup StringPrototypeSplit atau v8 :: internal :: Runtime_StringToNumber, dan, jika ini merupakan bagian penting dari eksekusi kode, cobalah untuk mengoptimalkan, misalnya, cukup menulis ulang kode yang tidak menjalankan ini operasi.

Misalnya, mengganti split dengan beberapa indexOf dan panggilan substring dapat memberikan keuntungan kinerja ganda.

Topik besar dan kompleks yang terpisah adalah optimasi jit, atau lebih tepatnya, fungsi yang dioptimalkan.
Jika ada sebagian besar dari fungsi-fungsi tersebut, akan perlu untuk menghadapinya.

Sebuah studi yang cermat tentang output dari node --trace_file_names --trace_opt_verbose --trace-deopt --trace_opt dapat membantu di sini.

Misalnya, garis-garis formulir

deoptimizing (DEOPT soft): begin 0x2bcf38b2d079 <Fungsi JS getTime ... Umpan balik tipe tidak mencukupi untuk operasi biner mengarah ke saluran

return val> = 10? val: '0' + val;

Penggantian untuk

return (val> = 10? '': '0') + val;

memperbaiki situasi.

Ada banyak informasi untuk mesin v8 lama karena alasan dan cara untuk memerangi deoptimisasi fungsi:

github.com/P0lip/v8-deoptimize-reason - daftar,
www.netguru.co/blog/tracing-patterns-hinder-performance - analisis penyebab khas,
www.html5rocks.com/en/tutorials/speed/v8 - tentang optimasi untuk v8, saya pikir hal yang sama berlaku untuk mesin v8 saat ini.

Tetapi banyak masalah tidak lagi relevan untuk v8 baru.

Bagaimanapun, setelah semua optimasi, saya berhasil mendapatkan Permintaan / detik: 9971 , yaitu itu akan mempercepat sekitar 2 kali karena transisi ke versi terbaru dari node.js, dan 4 kali lagi karena optimasi kode.

Semoga pengalaman ini bermanfaat bagi orang lain.

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


All Articles