Proses Node.JS hanya akan berjalan pada satu teras fizikal Disebabkan ini, perhatian khusus perlu diberikan semasa membangunkan pelayan berskala.
Oleh kerana terdapat set API yang stabil dan pembangunan sambungan asli untuk mengurus proses, terdapat banyak cara berbeza untuk mereka bentuk aplikasi Node.JS yang boleh diselaraskan. Dalam catatan blog ini, kami membandingkan kemungkinan seni bina ini.
Artikel ini juga memperkenalkan modul kluster pengiraan: perpustakaan Node.JS kecil yang boleh digunakan untuk mengurus proses dengan mudah dan melaksanakan pengkomputeran teragih baris kedua.
Masalah yang dihadapi
Dalam projek Mozilla Persona kami, kami perlu dapat mengendalikan sejumlah besar permintaan dengan ciri yang berbeza, jadi kami cuba menggunakan Node.JS.
Untuk tidak menjejaskan pengalaman pengguna, permintaan 'Interaktif' yang kami reka hanya memerlukan penggunaan pengkomputeran yang ringan, tetapi memberikan masa tindak balas yang lebih pantas supaya UI tidak berasa tersekat. Sebagai perbandingan, operasi 'Batch' mengambil masa kira-kira setengah saat untuk diproses dan mungkin terdapat kelewatan yang lebih lama disebabkan oleh sebab lain.
Untuk reka bentuk yang lebih baik, kami telah menemui banyak penyelesaian yang memenuhi keperluan semasa kami.
Memandangkan kebolehskalaan dan kos, kami menyenaraikan keperluan utama berikut:
Melalui perkara di atas kita boleh menapis dengan jelas dan dengan tujuan
Pilihan 1: Proses terus dalam urutan utama.
Apabila utas utama memproses data secara langsung, hasilnya sangat buruk:
Anda tidak boleh memanfaatkan sepenuhnya CPU berbilang teras Dalam permintaan/tindak balas interaktif, anda mesti menunggu permintaan semasa (atau respons) diproses, yang tidak elok.
Satu-satunya kelebihan penyelesaian ini ialah ia cukup mudah
function myRequestHandler(request, response) [ // Let's bring everything to a grinding halt for half a second. var results = doComputationWorkSync(request.somesuch); }
Dalam program Node.JS, jika anda ingin mengendalikan berbilang permintaan pada masa yang sama dan ingin memprosesnya secara serentak, maka anda akan menghadapi masalah.
Kaedah 2: Sama ada hendak menggunakan pemprosesan tak segerak.
Adakah terdapat peningkatan prestasi yang besar jika kaedah tak segerak digunakan di latar belakang?
Jawapannya tidak semestinya ia bergantung pada sama ada berjalan di latar belakang masuk akal
Sebagai contoh, dalam situasi berikut: Jika prestasi tidak lebih baik daripada pemprosesan segerak apabila menggunakan JavaScript atau kod tempatan pada urutan utama untuk melakukan pengiraan, anda tidak semestinya perlu menggunakan kaedah tak segerak di latar belakang untuk memproses
Sila baca kod berikut
function doComputationWork(input, callback) { // Because the internal implementation of this asynchronous // function is itself synchronously run on the main thread, // you still starve the entire process. var output = doComputationWorkSync(input); process.nextTick(function() { callback(null, output); }); } function myRequestHandler(request, response) [ // Even though this *looks* better, we're still bringing everything // to a grinding halt. doComputationWork(request.somesuch, function(err, results) { // ... do something with results ... });
}
关键点就在于NodeJS异步API的使用并不依赖于多进程的应用
方案三:用线程库来实现异步处理。
只要实现得当,使用本地代码实现的库,在 NodeJS 调用的时候是可以突破限制从而实现多线程功能的。
有很多这样的例子, Nick Campbell 编写的 bcrypt library 就是其中优秀的一个。
如果你在4核机器上拿这个库来作一个测试,你将看到神奇的一幕:4倍于平时的吞吐量,并且耗尽了几乎所有的资源!但是如果你在24核机器上测试,结果将不会有太大变化:有4个核心的使用率基本达到100%,但其他的核心基本上都处于空闲状态。
问题出在这个库使用了NodeJS内部的线程池,而这个线程池并不适合用来进行此类的计算。另外,这个线程池上限写死了,最多只能运行4个线程。
除了写死了上限,这个问题更深层的原因是:
内建线程机制的组件库在这种情况下并不能有效地利用多核的优势,这降低了程序的响应能力,并且随着负载的加大,程序表现越来越差。
方案四:使用 NodeJS 的 cluster 模块
NodeJS 0.6.x 以上的版本提供了一个cluster模块 ,允许创建“共享同一个socket”的一组进程,用来分担负载压力。
假如你采用了上面的方案,又同时使用 cluster 模块,情况会怎样呢?
这样得出的方案将同样具有同步处理或者内建线程池一样的缺点:响应缓慢,毫无优雅可言。
有时候,仅仅添加新运行实例并不能解决问题。
方案五:引入 compute-cluster 模块
在 Persona 中,我们的解决方案是,维护一组功能单一(但各不相同)的计算进程。
在这个过程中,我们编写了 compute-cluster 库。
这个库会自动按需启动和管理子进程,这样你就可以通过代码的方式来使用一个本地子进程的集群来处理数据。
使用例子:
const computecluster = require('compute-cluster'); // allocate a compute cluster var cc = new computecluster({ module: './worker.js' }); // run work in parallel cc.enqueue({ input: "foo" }, function (error, result) { console.log("foo done", result); }); cc.enqueue({ input: "bar" }, function (error, result) { console.log("bar done", result); });
fileworker.js 中响应了 message 事件,对传入的请求进行处理:
process.on('message', function(m) { var output; // do lots of work here, and we don't care that we're blocking the // main thread because this process is intended to do one thing at a time. var output = doComputationWorkSync(m.input); process.send(output); });
Tanpa menukar kod panggilan, modul kluster pengiraan boleh disepadukan dengan API tak segerak sedia ada, supaya pemprosesan selari berbilang teras sebenar boleh dicapai dengan jumlah kod terkecil.
Mari kita lihat prestasi penyelesaian ini dari empat aspek.
Keupayaan selari berbilang teras: Proses kanak-kanak menggunakan semua teras.
Responsif: Memandangkan proses pengurusan teras hanya bertanggungjawab untuk memulakan proses kanak-kanak dan menyampaikan mesej, ia melahu pada kebanyakan masa dan boleh mengendalikan permintaan yang lebih interaktif.
Walaupun mesin berada di bawah tekanan beban berat, kami masih boleh menggunakan penjadual sistem pengendalian untuk meningkatkan keutamaan proses pengurusan teras.
Kesederhanaan: API tak segerak digunakan untuk menyembunyikan butiran pelaksanaan khusus Kami boleh menyepadukan modul ini dengan mudah ke dalam projek semasa tanpa mengubah kod panggilan.
Sekarang mari kita lihat jika kita boleh mencari jalan supaya walaupun beban tiba-tiba melonjak, kecekapan sistem tidak akan turun secara tidak normal.
Sudah tentu, matlamat terbaik ialah walaupun tekanan meningkat, sistem masih boleh berjalan dengan cekap dan mengendalikan seberapa banyak permintaan yang mungkin.
Untuk membantu melaksanakan penyelesaian yang baik, kelompok pengiraan melakukan lebih daripada sekadar mengurus proses anak dan menghantar mesej juga;
Ia merekodkan bilangan proses kanak-kanak yang sedang berjalan dan purata masa yang diambil oleh setiap proses kanak-kanak untuk diselesaikan.
Dengan rekod ini, kami boleh meramalkan berapa lama masa yang diambil sebelum proses anak bermula.
Mengikut ini, ditambah dengan parameter yang ditetapkan oleh pengguna (max_request_time), kami boleh menutup terus permintaan tersebut yang mungkin tamat masa tanpa diproses.
Ciri ini memudahkan untuk mendasarkan kod anda pada pengalaman pengguna. Sebagai contoh, "Pengguna tidak boleh menunggu lebih daripada 10 saat untuk log masuk." Ini kira-kira bersamaan dengan menetapkan max_request_time kepada 7 saat (masa penghantaran rangkaian perlu diambil kira).
Selepas kami menguji perkhidmatan Persona secara tekanan, hasilnya sangat memuaskan.
Di bawah keadaan tekanan yang sangat tinggi, kami masih dapat menyediakan perkhidmatan kepada pengguna yang disahkan, dan juga menyekat beberapa pengguna yang tidak disahkan dan memaparkan mesej ralat yang berkaitan.