Bagaimana Node.js secara inheren lebih cepat ketika masih bergantung pada Threads secara internal?

281

Saya baru saja menonton video berikut: Pengantar Node.js dan masih tidak mengerti bagaimana Anda mendapatkan manfaat kecepatan.

Terutama, pada satu titik Ryan Dahl (pencipta Node.js ') mengatakan bahwa Node.js adalah event-loop berdasarkan bukan berbasis thread. Utas mahal dan hanya boleh diserahkan kepada para ahli pemrograman konkuren untuk dimanfaatkan.

Kemudian, ia kemudian menunjukkan tumpukan arsitektur Node.js yang memiliki implementasi C yang mendasarinya yang memiliki kumpulan Thread sendiri secara internal. Jadi jelas pengembang Node.js tidak akan pernah memulai utas mereka sendiri atau menggunakan kumpulan utas secara langsung ... mereka menggunakan panggilan balik async. Sejauh itu saya mengerti.

Apa yang saya tidak mengerti adalah titik bahwa Node.js masih menggunakan utas ... itu hanya menyembunyikan implementasi jadi bagaimana ini lebih cepat jika 50 orang meminta 50 file (tidak saat ini dalam memori) baik maka tidak diperlukan 50 utas ?

Satu-satunya perbedaan adalah bahwa karena dikelola secara internal, pengembang Node.js tidak harus mengkodekan detail berulir tetapi di bawahnya masih menggunakan utas untuk memproses permintaan file IO (pemblokiran).

Jadi bukankah Anda benar-benar hanya mengambil satu masalah (threading) dan menyembunyikannya sementara masalah itu masih ada: terutama beberapa utas, pengalihan konteks, kunci-mati ... dll?

Pasti ada beberapa detail yang masih belum saya mengerti di sini.

Ralph Caraveo
sumber
14
Saya cenderung setuju dengan Anda bahwa klaimnya agak terlalu disederhanakan. Saya percaya keunggulan kinerja node bermuara pada dua hal: 1) utas sebenarnya semua terkandung pada tingkat yang cukup rendah, dan dengan demikian tetap terkendala dalam ukuran dan jumlah, dan sinkronisasi utas dengan demikian disederhanakan; 2) "switching" melalui OS select()lebih cepat daripada pertukaran konteks thread.
Runcing

Jawaban:

140

Sebenarnya ada beberapa hal berbeda yang digabungkan di sini. Tetapi itu dimulai dengan meme bahwa utas sangat sulit. Jadi jika mereka sulit, Anda lebih mungkin, ketika menggunakan utas untuk 1) rusak karena bug dan 2) tidak menggunakannya seefisien mungkin. (2) adalah yang Anda tanyakan.

Pikirkan salah satu contoh yang dia berikan, di mana permintaan masuk dan Anda menjalankan beberapa permintaan, dan kemudian melakukan sesuatu dengan hasil itu. Jika Anda menulisnya dengan cara prosedural standar, kode tersebut mungkin terlihat seperti ini:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Jika permintaan masuk menyebabkan Anda membuat utas baru yang menjalankan kode di atas, Anda akan memiliki utas duduk di sana, tidak melakukan apa-apa saat query()menjalankan. (Apache, menurut Ryan, menggunakan satu utas untuk memenuhi permintaan asli sedangkan nginx mengungguli itu dalam kasus yang dia bicarakan karena tidak.)

Sekarang, jika Anda benar-benar pintar, Anda akan mengekspresikan kode di atas dengan cara di mana lingkungan bisa mati dan melakukan sesuatu yang lain saat Anda menjalankan kueri:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

Ini pada dasarnya adalah apa yang dilakukan node.js. Anda pada dasarnya dekorasi - dengan cara yang nyaman karena bahasa dan lingkungan, maka poin tentang penutupan - kode Anda sedemikian rupa sehingga lingkungan dapat menjadi pintar tentang apa yang berjalan, dan kapan. Dengan cara itu, node.js bukanlah hal baru dalam arti bahwa ia menciptakan I / O yang tidak sinkron (tidak ada orang yang mengklaim sesuatu seperti ini), tetapi itu baru dalam cara yang diungkapkannya sedikit berbeda.

Catatan: ketika saya mengatakan bahwa lingkungan dapat menjadi pintar tentang apa yang berjalan dan kapan, khususnya yang saya maksud adalah bahwa utas yang digunakan untuk memulai beberapa I / O sekarang dapat digunakan untuk menangani beberapa permintaan lain, atau beberapa perhitungan yang dapat dilakukan secara paralel, atau memulai beberapa I / O paralel lainnya. (Saya tidak yakin simpul cukup canggih untuk memulai lebih banyak pekerjaan untuk permintaan yang sama, tetapi Anda mendapatkan idenya.)

Jrtipton
sumber
6
Oke, saya pasti bisa melihat bagaimana ini dapat meningkatkan kinerja karena bagi saya sepertinya Anda dapat memaksimalkan CPU Anda karena tidak ada utas atau tumpukan eksekusi hanya menunggu IO untuk kembali sehingga apa yang telah dilakukan Ryan secara efektif ditemukan cara untuk menutup semua celah.
Ralph Caraveo
34
Ya, satu hal yang saya katakan adalah bahwa dia tidak menemukan cara untuk menutup celah: itu bukan pola baru. Apa yang berbeda adalah bahwa ia menggunakan Javascript untuk membiarkan programmer mengekspresikan program mereka dengan cara yang jauh lebih nyaman untuk jenis sinkronisasi ini. Mungkin detail yang
rewel
16
Penting juga untuk menunjukkan bahwa untuk banyak tugas I / O, Node menggunakan api I / O async tingkat kernel apa pun yang tersedia (epoll, kqueue, / dev / polling, apa pun)
Paul
7
Saya masih tidak yakin bahwa saya sepenuhnya memahaminya. Jika kami menganggap bahwa di dalam permintaan IO operasi web adalah yang mengambil sebagian besar waktu yang diperlukan untuk memproses permintaan dan jika untuk setiap operasi IO dibuat utas baru, maka untuk 50 permintaan yang datang dalam suksesi yang sangat cepat, kami akan mungkin memiliki 50 utas yang berjalan secara paralel dan mengeksekusi bagian IO mereka. Perbedaan dari server web standar adalah bahwa di sana seluruh permintaan dieksekusi di utas, sedangkan di node.js hanya bagian IO-nya, tetapi itu adalah bagian yang mengambil sebagian besar waktu dan membuat utas menunggu.
Florin Dumitrescu
13
@ SystemParadox terima kasih telah menunjukkannya. Saya benar-benar membuat beberapa penelitian tentang topik belakangan ini dan memang yang menarik adalah bahwa Asynchronous I / O, ketika diimplementasikan dengan benar di tingkat kernel, tidak menggunakan utas saat melakukan operasi I / O async. Alih-alih utas panggilan dilepaskan segera setelah operasi I / O dimulai dan callback dijalankan ketika operasi I / O selesai dan utas tersedia untuk itu. Jadi node.js dapat menjalankan 50 permintaan bersamaan dengan 50 operasi I / O di (hampir) paralel menggunakan hanya satu utas jika dukungan async untuk operasi I / O diimplementasikan dengan benar.
Florin Dumitrescu
32

Catatan! Ini jawaban lama. Meskipun masih benar dalam garis besar, beberapa detail mungkin telah berubah karena perkembangan pesat Node dalam beberapa tahun terakhir.

Itu menggunakan utas karena:

  1. The pilihan O_NONBLOCK open () tidak bekerja pada file .
  2. Ada perpustakaan pihak ketiga yang tidak menawarkan IO non-pemblokiran.

Untuk memalsukan IO non-pemblokiran, utas diperlukan: jangan memblokir IO dalam utas terpisah. Ini adalah solusi yang jelek dan menyebabkan banyak overhead.

Lebih buruk lagi di tingkat perangkat keras:

  • Dengan DMA , CPU secara asinkron membongkar IO.
  • Data ditransfer langsung antara perangkat IO dan memori.
  • Kernel membungkus ini dalam panggilan sistem yang sinkron dan memblokir.
  • Node.js membungkus panggilan sistem pemblokiran dalam utas.

Ini benar-benar bodoh dan tidak efisien. Tapi setidaknya berhasil! Kita dapat menikmati Node.js karena menyembunyikan detail yang jelek dan rumit di balik arsitektur asinkron yang digerakkan oleh peristiwa.

Mungkin seseorang akan menerapkan O_NONBLOCK untuk file di masa mendatang? ...

Sunting: Saya membahas ini dengan seorang teman dan dia mengatakan kepada saya bahwa sebuah alternatif untuk utas adalah polling dengan select : tentukan batas waktu 0 dan lakukan IO pada deskriptor file yang dikembalikan (sekarang mereka dijamin tidak akan memblokir).

nekat
sumber
Bagaimana dengan Windows?
Pacerier
Maaf, tidak tahu. Saya hanya tahu bahwa libuv adalah lapisan platform-netral untuk melakukan pekerjaan asinkron. Di awal Node tidak ada libuv. Kemudian diputuskan untuk memisahkan libuv dan ini membuat kode khusus platform lebih mudah. Dengan kata lain, Windows memiliki kisah asinkronnya sendiri yang mungkin sangat berbeda dari Linux, tetapi bagi kami itu tidak masalah karena libuv melakukan kerja keras untuk kami.
nalply
28

Saya khawatir saya "melakukan hal yang salah" di sini, jika demikian hapus saya dan saya minta maaf. Secara khusus, saya gagal melihat bagaimana saya membuat anotasi kecil yang rapi yang dibuat beberapa orang. Namun, saya memiliki banyak kekhawatiran / pengamatan untuk dilakukan pada utas ini.

1) Elemen yang dikomentari dalam pseudo-code di salah satu jawaban populer

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

pada dasarnya palsu. Jika utasnya adalah komputasi, maka itu bukan jempol memutar, itu melakukan pekerjaan yang diperlukan. Jika, di sisi lain, itu hanya menunggu penyelesaian IO, maka itu tidak menggunakan waktu CPU, inti dari infrastruktur kontrol thread di kernel adalah bahwa CPU akan menemukan sesuatu yang berguna untuk dilakukan. Satu-satunya cara untuk "memutar-mutar ibu jari Anda" seperti yang disarankan di sini adalah dengan membuat loop polling, dan tidak ada orang yang memiliki kode server web yang sebenarnya tidak cukup cakap untuk melakukan itu.

2) "Utas itu sulit", hanya masuk akal dalam konteks berbagi data. Jika pada dasarnya Anda memiliki utas independen seperti halnya saat menangani permintaan web independen, maka threading sederhana, Anda hanya perlu mengkode alur linear cara menangani satu pekerjaan, dan duduk dengan cukup mengetahui bahwa itu akan menangani beberapa permintaan, dan masing-masing akan mandiri secara efektif. Secara pribadi, saya berani mengatakan bahwa bagi sebagian besar programmer, mempelajari mekanisme penutupan / panggilan balik lebih kompleks daripada hanya mengkode versi thread top-to-bottom. (Tapi ya, jika Anda harus berkomunikasi di antara utas-utas, hidup menjadi sangat sulit dengan sangat cepat, tapi kemudian saya tidak yakin bahwa mekanisme penutupan / panggilan balik benar-benar mengubah itu, itu hanya membatasi pilihan Anda, karena pendekatan ini masih dapat dicapai dengan utas Bagaimanapun, itu

3) Sejauh ini, tidak ada yang memberikan bukti nyata mengapa satu jenis konteks tertentu akan lebih atau kurang memakan waktu daripada jenis lainnya. Pengalaman saya dalam membuat kernel multi-tasking (dalam skala kecil untuk embedded controller, tidak ada yang semewah OS "nyata") menunjukkan bahwa ini tidak akan terjadi.

4) Semua ilustrasi yang saya lihat sampai saat ini dimaksudkan untuk menunjukkan seberapa cepat Node dibandingkan webserer lain memiliki cacat yang sangat buruk, namun, mereka cacat dengan cara yang secara tidak langsung menggambarkan satu keuntungan yang pasti akan saya terima untuk Node (dan ini tidak berarti tidak berarti). Node tidak tampak seperti itu perlu (atau bahkan izin, sebenarnya) penyetelan. Jika Anda memiliki model ulir, Anda perlu membuat utas yang memadai untuk menangani beban yang diharapkan. Lakukan ini dengan buruk, dan Anda akan berakhir dengan kinerja yang buruk. Jika ada terlalu sedikit utas, maka CPU idle, tetapi tidak dapat menerima lebih banyak permintaan, membuat terlalu banyak utas, dan Anda akan membuang-buang memori kernel, dan dalam kasus lingkungan Java, Anda juga akan membuang-buang memori tumpukan utama . Sekarang, untuk Java, membuang heap adalah cara pertama, terbaik, untuk mengacaukan kinerja sistem, karena pengumpulan sampah yang efisien (saat ini, ini mungkin berubah dengan G1, tetapi tampaknya juri masih keluar pada titik itu setidaknya pada awal 2013) tergantung pada memiliki banyak tumpukan cadangan. Jadi, ada masalah, selaras dengan terlalu sedikit utas, Anda memiliki CPU yang menganggur dan throughput yang buruk, selaras dengan terlalu banyak, dan rusak dengan cara lain.

5) Ada cara lain di mana saya menerima logika klaim bahwa pendekatan Node "lebih cepat dengan desain", dan ini dia. Sebagian besar model utas menggunakan model sakelar konteks irisan waktu, berlapis di atas model preemptive yang lebih tepat (peringatan penilaian nilai :) dan lebih efisien (bukan penilaian nilai). Ini terjadi karena dua alasan, pertama, sebagian besar programmer tampaknya tidak memahami preemption prioritas, dan kedua, jika Anda belajar threading di lingkungan windows, ada rentang waktu apakah Anda suka atau tidak (tentu saja, ini memperkuat poin pertama terutama versi pertama Java menggunakan preemption prioritas pada implementasi Solaris, dan timeslicing di Windows. Karena kebanyakan programmer tidak mengerti dan mengeluh bahwa "threading tidak berfungsi di Solaris" mereka mengubah model menjadi kutu waktu di mana-mana). Bagaimanapun, intinya adalah bahwa penetapan waktu membuat switch konteks tambahan (dan mungkin tidak perlu). Setiap saklar konteks membutuhkan waktu CPU, dan waktu itu secara efektif dihilangkan dari pekerjaan yang dapat dilakukan pada pekerjaan nyata yang ada. Namun, jumlah waktu yang diinvestasikan dalam pengalihan konteks karena penentuan waktu tidak boleh lebih dari persentase yang sangat kecil dari keseluruhan waktu, kecuali jika sesuatu yang sangat aneh terjadi, dan tidak ada alasan saya dapat melihat untuk mengharapkan hal itu terjadi dalam suatu server web sederhana). Jadi, ya, saklar konteks berlebih yang terlibat dalam pengaturan waktu tidak efisien (dan ini tidak terjadi dan waktu itu secara efektif dihilangkan dari pekerjaan yang dapat dilakukan pada pekerjaan nyata yang ada. Namun, jumlah waktu yang diinvestasikan dalam pengalihan konteks karena penentuan waktu tidak boleh lebih dari persentase yang sangat kecil dari keseluruhan waktu, kecuali jika sesuatu yang sangat aneh terjadi, dan tidak ada alasan saya dapat melihat untuk mengharapkan hal itu terjadi dalam suatu server web sederhana). Jadi, ya, saklar konteks berlebih yang terlibat dalam pengaturan waktu tidak efisien (dan ini tidak terjadi dan waktu itu secara efektif dihilangkan dari pekerjaan yang dapat dilakukan pada pekerjaan nyata yang ada. Namun, jumlah waktu yang diinvestasikan dalam pengalihan konteks karena penentuan waktu tidak boleh lebih dari persentase yang sangat kecil dari keseluruhan waktu, kecuali jika sesuatu yang sangat aneh terjadi, dan tidak ada alasan saya dapat melihat untuk mengharapkan hal itu terjadi dalam suatu server web sederhana). Jadi, ya, saklar konteks berlebih yang terlibat dalam pengaturan waktu tidak efisien (dan ini tidak terjadikernel threads sebagai aturan, btw) tetapi perbedaannya adalah beberapa persen dari throughput, bukan jenis faktor bilangan bulat yang tersirat dalam klaim kinerja yang sering tersirat untuk Node.

Ngomong-ngomong, permintaan maaf untuk semua itu panjang dan kasar, tapi aku benar-benar merasa sejauh ini, diskusi belum membuktikan apa-apa, dan aku akan senang mendengar dari seseorang dalam salah satu situasi ini:

a) penjelasan nyata mengapa Node harus lebih baik (di luar dua skenario yang telah saya uraikan di atas, yang pertama (tuning yang buruk) Saya percaya adalah penjelasan nyata untuk semua tes yang saya lihat sejauh ini. ([edit ], sebenarnya, semakin saya memikirkannya, semakin saya bertanya-tanya apakah memori yang digunakan oleh sejumlah besar tumpukan mungkin signifikan di sini. Ukuran tumpukan standar untuk utas modern cenderung cukup besar, tetapi memori dialokasikan oleh sistem acara berbasis penutupan hanya akan menjadi apa yang dibutuhkan)

b) tolok ukur nyata yang benar-benar memberikan peluang yang adil ke server pilihan yang diulir. Setidaknya dengan cara itu, saya harus berhenti percaya bahwa klaim pada dasarnya salah;> (sunting) yang mungkin agak lebih kuat dari yang saya maksudkan, tetapi saya merasa bahwa penjelasan yang diberikan untuk manfaat kinerja tidak lengkap di terbaik, dan tolok ukur yang ditunjukkan tidak masuk akal).

Ceria, Toby

Toby Eggitt
sumber
2
Masalah dengan utas: mereka membutuhkan RAM. Server yang sangat sibuk dapat menjalankan hingga beberapa ribu utas. Node.js menghindari utas dan karenanya lebih efisien. Efisiensi bukan dengan menjalankan kode lebih cepat. Tidak masalah jika kode dijalankan di utas atau dalam suatu loop peristiwa. Untuk CPU itu sama. Tetapi dengan menghapus utas, kami menghemat RAM: hanya satu tumpukan, bukan beberapa ribu tumpukan. Dan kami juga menyimpan sakelar konteks.
nalply
3
Tetapi node tidak melakukan jauh dengan utas. Itu masih menggunakannya secara internal untuk tugas-tugas IO, yang merupakan apa yang paling dibutuhkan permintaan web.
levi
1
Node juga menyimpan penutupan callback dalam RAM, jadi saya tidak bisa melihat di mana ia menang.
Oleksandr Papchenko
@ Levi Tapi nodejs tidak menggunakan jenis “satu utas per permintaan”. Ia menggunakan IO threadpool, mungkin untuk menghindari komplikasi dengan menggunakan API IO asinkron (dan mungkin POSIX open()tidak dapat dibuat non-pemblokiran?). Dengan cara ini, ini mengamortisasi setiap hit kinerja di mana model tradisional fork()/ pthread_create()-on-permintaan harus membuat dan menghancurkan utas. Dan, seperti yang disebutkan dalam postscript a), ini juga mengamortisasi masalah ruang stack. Anda mungkin dapat melayani ribuan permintaan dengan, katakanlah, 16 utas baik-baik saja.
binki
"Ukuran stack default untuk utas modern cenderung sangat besar, tetapi memori yang dialokasikan oleh sistem acara berbasis penutupan hanya akan menjadi apa yang dibutuhkan" Saya mendapatkan kesan ini harus dari urutan yang sama. Penutupan tidak murah, runtime harus menyimpan seluruh pohon panggilan dari aplikasi single-threaded di memori ("emulasi tumpukan" begitu dikatakan) dan akan dapat membersihkan ketika daun pohon dilepaskan sebagai penutupan terkait akan "diselesaikan". Ini akan mencakup banyak referensi untuk hal-hal di-tumpukan yang tidak dapat dikumpulkan sampah dan akan mencapai kinerja pada saat pembersihan.
David Tonhofer
14

Apa yang saya tidak mengerti adalah titik bahwa Node.js masih menggunakan utas.

Ryan menggunakan utas untuk bagian-bagian yang memblokir (Sebagian besar node.js menggunakan non-blocking IO) karena beberapa bagian gila sulit untuk menulis yang tidak memblokir. Tapi saya yakin keinginan Ryan adalah agar semuanya tidak menghalangi. Pada slide 63 (desain internal) Anda melihat Ryan menggunakan libev (perpustakaan yang mengabstraksi pemberitahuan kejadian asinkron) untuk penguncian acara yang tidak memblokir . Karena event-loop node.js membutuhkan thread yang lebih kecil yang mengurangi switching konteks, konsumsi memori dll.

Alfred
sumber
11

Utas hanya digunakan untuk menangani fungsi yang tidak memiliki fasilitas asinkron, seperti stat().

The stat()Fungsi selalu menghalangi, sehingga Node.js kebutuhan untuk menggunakan thread untuk melakukan panggilan sebenarnya tanpa menghalangi thread utama (event loop). Secara potensial, tidak ada utas dari kumpulan utas yang akan pernah digunakan jika Anda tidak perlu memanggil fungsi semacam itu.

gawi
sumber
7

Saya tidak tahu apa-apa tentang cara kerja internal node.js, tapi saya bisa melihat bagaimana menggunakan sebuah event loop dapat mengungguli penanganan I / O berulir. Bayangkan permintaan disk, beri saya staticFile.x, buatlah 100 permintaan untuk file itu. Setiap permintaan biasanya membutuhkan utas untuk mengambil kembali file itu, yaitu 100 utas.

Sekarang bayangkan permintaan pertama membuat satu utas yang menjadi objek penerbit, ke-99 permintaan lainnya pertama-tama melihat apakah ada objek penerbit untuk staticFile.x, jika demikian, dengarkan ketika sedang melakukan pekerjaannya, jika tidak, mulailah utas baru dan dengan demikian objek penerbit baru.

Setelah utas tunggal selesai, ia meneruskan staticFile.x ke 100 pendengar dan menghancurkan dirinya sendiri, sehingga permintaan berikutnya membuat utas baru dan objek penerbit.

Jadi 100 thread vs 1 thread dalam contoh di atas, tetapi juga 1 disk pencarian, bukan 100 disk pencarian, keuntungannya bisa sangat phenominal. Ryan adalah pria yang cerdas!

Cara lain untuk melihatnya adalah salah satu contohnya di awal film. Dari pada:

pseudo code:
result = query('select * from ...');

Sekali lagi, 100 pertanyaan yang terpisah untuk database versus ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Jika kueri sudah berjalan, kueri yang sama lainnya hanya akan ikut-ikutan, sehingga Anda dapat memiliki 100 kueri dalam satu perjalanan pulang-pergi database tunggal.

BGerrissen
sumber
3
Basis data lebih merupakan pertanyaan tentang tidak menunggu jawaban sambil memegang permintaan lain (yang mungkin atau mungkin tidak menggunakan basis data), melainkan meminta sesuatu dan kemudian membiarkannya memanggil Anda ketika kembali. Saya tidak berpikir itu menghubungkan mereka bersama, karena itu akan cukup sulit untuk melacak tanggapan. Juga saya tidak berpikir ada antarmuka MySQL yang memungkinkan Anda memegang beberapa tanggapan unbuffered pada satu koneksi (??)
Tor Valamo
Ini hanya contoh abstrak untuk menjelaskan bagaimana loop acara dapat menawarkan efisiensi lebih, nodejs tidak melakukan apa pun dengan DB tanpa modul tambahan;)
BGerrissen
1
Ya komentar saya lebih mengarah ke 100 pertanyaan dalam satu perjalanan ulang database. : p
Tor Valamo
2
Hai BGerrissen: postingan yang bagus. Jadi, ketika sebuah query dieksekusi, query serupa lainnya akan "mendengarkan" seperti contoh staticFile.X di atas? misalnya, 100 pengguna mengambil kueri yang sama, hanya satu kueri yang akan dieksekusi dan 99 lainnya akan mendengarkan yang pertama? terima kasih!
CHAPa
1
Anda membuatnya terdengar seperti nodejs secara otomatis memoizes panggilan fungsi atau sesuatu. Sekarang, karena Anda tidak perlu khawatir tentang sinkronisasi memori bersama dalam model loop acara JavaScript, lebih mudah untuk menyimpan cache di memori dengan aman. Tetapi itu tidak berarti nodejs secara ajaib melakukan itu untuk Anda atau bahwa ini adalah jenis peningkatan kinerja yang ditanyakan.
binki