Rumah > Artikel > hujung hadapan web > Analisis ringkas kaedah Node mengendalikan tugas intensif CPU
Apakah kaedah Node untuk mengendalikan tugas intensif CPU? Artikel berikut akan membawa anda melalui cara Node mengendalikan tugas intensif CPU. Saya harap ia akan membantu anda.
Kami telah mendengar lebih kurang perkataan berikut dalam kerja harian kami:
Nod ialah
非阻塞I/O
(tidak menyekat I/ O) dan事件驱动
(didorong peristiwa)JavaScript运行环境
(masa jalan), jadi ia sangat sesuai untuk membina aplikasi intensif I/O, seperti perkhidmatan Web.
Saya tidak tahu sama ada anda akan mempunyai keraguan yang sama seperti saya apabila anda mendengar perkataan yang serupa: 单线程的Node为什么适合用来开发I/O密集型应用?
Secara logiknya, bahasa yang menyokong multi-threading (seperti Java dan Golang) jangan lakukan ini Adakah pekerjaan itu lebih berfaedah?
Untuk memahami masalah di atas, kita perlu tahu apa yang dirujuk oleh utas tunggal Node. [Tutorial berkaitan yang disyorkan: tutorial video nodejs]
Malah, kami mengatakan bahawa Node adalah satu-benang . JavaScript代码
kami berjalan dalam urutan yang sama (kita boleh memanggilnya 主线程
), 而不是说Node只有一个线程在工作
. Malah, lapisan bawah Node akan menggunakan 多线程能力
libuv untuk melaksanakan sebahagian daripada kerja (pada asasnya operasi berkaitan I/O) dalam beberapa 主线程之外
urutan apabila tugasan ini selesai, hasilnya akan dikembalikan dalam bentuk 回调函数
Kepada persekitaran pelaksanaan JavaScript bagi utas utama. Anda boleh lihat gambarajah skematik:
Nota: Gambar di atas ialah versi ringkas Node事件循环
(Gelung Acara). gelung akan mempunyai lebih banyak peringkat, seperti pemasa dsb.
Daripada analisis di atas, kita tahu bahawa Node akan menyuraikan semua operasi I/O melalui keupayaan multi-threading of libuv Laksanakan dalam utas yang berbeza, dan operasi lain dilaksanakan dalam utas utama. Jadi mengapa pendekatan ini lebih sesuai untuk aplikasi intensif I/O daripada bahasa lain seperti Java atau Golang? Mari kita ambil pembangunan perkhidmatan web sebagai contoh bahasa pengaturcaraan belakang utama seperti Java dan Golang adalah 并发模型是基于线程
(Berasaskan Benang), yang bermaksud bahawa mereka akan mencipta 单独的线程
untuk setiap permintaan rangkaian untuk diproses. . Walau bagaimanapun, untuk aplikasi web, ia adalah terutamanya untuk 数据库的增删改查,或者请求其它外部服务等网络I/O操作
, dan operasi ini akhirnya diserahkan kepada 操作系统的系统调用来处理的(无需应用线程参与),并且十分缓慢(相对于CPU时钟周期来说)
, jadi urutan yang dibuat adalah 无事可做
pada kebanyakan masa, dan perkhidmatan kami perlu menanggung tambahan 线程切换
🎜 > Overhed. Tidak seperti bahasa ini, Node tidak mencipta urutan untuk setiap permintaan 所有请求的处理
semuanya berlaku dalam urutan utama, jadi tiada overhed 线程切换
dan ia juga akan memproses ini secara tak segerak dalam bentuk 线程池
I/O
operasi, dan kemudian beritahu utas utama hasil dalam bentuk acara untuk mengelak daripada menyekat pelaksanaan utas utama, jadi ia 理论上
lebih cekap. Perlu diingat di sini bahawa saya baru sahaja mengatakan bahawa Node理论上
lebih pantas, tetapi ia tidak semestinya benar. Ini kerana pada hakikatnya prestasi sesuatu perkhidmatan akan dipengaruhi oleh banyak aspek Kami hanya mempertimbangkan faktor 并发模型
di sini, dan faktor lain seperti penggunaan masa jalan juga akan mempengaruhi prestasi perkhidmatan tersebut ialah bahasa dinamik, dan jenis data perlu disimpulkan semasa masa jalan, manakala JavaScript
dan Golang
kedua-duanya adalah bahasa statik Jenis data mereka boleh ditentukan pada masa penyusunan, jadi mereka sebenarnya boleh melaksanakan dengan lebih pantas dan menggunakan kurang memori. Akan ada lebih sedikit. Java
tugas, utas utama akan disekat. Mari lihat contoh tugas intensif CPU: CPU密集型
// node/cpu_intensive.js const http = require('http') const url = require('url') const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { hardWork() resp.write('hard work') resp.end() } else if (urlParsed.pathname === '/easy_work') { resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
Dalam kod di atas kami melaksanakan perkhidmatan HTTP dengan dua antara muka: /hard_work
antara muka ialah CPU密集型接口
kerana ia memanggil fungsi hardWork
iniCPU密集型
dan /easy_work
antara muka ini adalah sangat mudah, hanya kembalikan rentetan kepada pelanggan secara langsung. Mengapa dikatakan fungsi hardWork
ialah CPU密集型
? Ini kerana ia menjalankan operasi aritmetik pada 运算器
dalam i
CPU tanpa melakukan sebarang operasi I/O. Selepas memulakan perkhidmatan Node kami, kami cuba memanggil antara muka /hard_word
:
Kami dapat melihat bahawa antara muka /hard_work
akan tersekat Ini kerana Ia memerlukan a banyak CPU
pengiraan, jadi ia mengambil masa yang lama untuk diselesaikan. Pada masa ini, mari kita lihat sama ada antara muka /easy_work
mempunyai sebarang kesan:
Kami mendapati bahawa selepas /hard_work
menduduki sumber CPU, yang tidak bersalah /easy_work
antara muka juga Terperangkap. Sebabnya ialah fungsi hardWork
menyekat utas utama Nod dan logik /easy_work
tidak akan dilaksanakan. Perlu dinyatakan di sini bahawa hanya persekitaran pelaksanaan satu utas berdasarkan gelung acara seperti Node akan mengalami masalah ini dalam bahasa Berasaskan Benang seperti Java dan Golang tidak akan mengalami masalah ini. Jadi bagaimana jika perkhidmatan kami benar-benar perlu menjalankan CPU密集型
tugas? Anda tidak boleh menukar bahasa, bukan? Di manakah All in JavaScript
yang dijanjikan? Jangan risau, Node telah menyediakan banyak penyelesaian untuk kita hadapi CPU密集型任务
Izinkan saya memperkenalkan tiga penyelesaian yang biasa digunakan kepada anda ialah: Cluster Module
, Child Process
dan Worker Thread
.
Node melancarkan modul Kluster sangat awal (versi v0.8). Fungsi modul ini adalah untuk lulus 一个父进程启动一群子进程来对网络请求进行负载均衡
. Disebabkan oleh had panjang artikel, kami tidak akan menerangkan secara terperinci tentang API modul Kluster. Pembaca yang berminat boleh membaca dokumentasi rasmi kemudian. Di sini kami melihat secara langsung cara menggunakan modul Kluster untuk mengoptimumkan CPU di atas -senario intensif:
// node/cluster.js const cluster = require('cluster') const http = require('http') const url = require('url') // 获取CPU核数 const numCPUs = require('os').cpus().length const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { console.log(`worker ${worker.process.pid} is online`) }) cluster.on('exit', (worker, code, signal) => { // 某个工作进程挂了之后,我们需要立马启动另外一个工作进程来替代 console.log(`worker ${worker.process.pid} exited with code ${code}, and signal ${signal}, start a new one...`) cluster.fork() }) } else { // 工作进程启动一个HTTP服务器 const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { hardWork() resp.write('hard work') resp.end() } else if (urlParsed.pathname === '/easy_work') { resp.write('easy work') resp.end() } else { resp.end() } }) // 所有的工作进程都监听在同一个端口 server.listen(8080, () => { console.log(`worker ${process.pid} server is up...`) }) }
Dalam kod di atas, kami menggunakan fungsi cluster.fork
untuk mencipta bilangan yang sama 工作进程
berdasarkan bilangan teras CPU peranti semasa dan proses pekerja ini semua sedang mendengar pada port 8080
. Melihat ini, anda mungkin bertanya sama ada akan ada masalah jika semua proses mendengar pada port yang sama Malah, tidak akan ada masalah di sini, kerana lapisan bawah modul Cluster
akan melakukan beberapa kerja supaya yang terakhir. satu mendengar pada port 8080
ialah 主进程
, dan proses utama ialah 所有流量的入口
, yang akan menerima sambungan HTTP dan menghalakannya ke proses pekerja yang berbeza. Tanpa berlengah lagi, mari jalankan perkhidmatan nod ini:
Daripada output di atas, kluster telah memulakan 10 pekerja (komputer saya mempunyai 10 teras) ) untuk mengendalikan permintaan web Di kali ini, kami akan meminta antara muka /hard_work
sekali lagi:
Kami mendapati permintaan ini masih tersekat, dan kemudian kami akan melihat modul Kluster masalah 其它请求也被阻塞
tidak diselesaikan:
Kita dapat melihat bahawa 9个请求
sebelumnya mengembalikan keputusan dengan lancar, tetapi apabila ia datang kepada 第10个请求
antara muka kami tersekat Jom, kenapa ni? Sebabnya ialah kami telah membuka sejumlah 10 proses pekerja Strategi pengimbangan beban lalai yang digunakan oleh proses utama semasa menghantar trafik ke proses anak ialah round-robin
(giliran), jadi permintaan ke-10 (sebenarnya ke-11, kerana ia. termasuk Permintaan kerja keras pertama) baru sahaja dikembalikan kepada pekerja pertama, dan pekerja ini belum selesai memproses tugasan hard_work
, jadi tugasan easy_work
tersekat. Algoritma pengimbangan beban kluster boleh diubah suai melalui cluster.schedulingPolicy
Pembaca yang berminat boleh melihat dokumentasi rasmi.
从上面的结果来看Cluster Module似乎解决了一部分
我们的问题,可是还是有一些请求受到了影响。那么Cluster Module在实际开发里面能不能被用来解决这个CPU密集型
任务的问题呢?我的意见是:看情况。如果你的CPU密集型接口调用不频繁
而且运算时间不会太长
,你完全可以使用这种Cluster Module来优化。可是如果你的接口调用频繁并且每个接口都很耗时间的话,可能你需要看一下采用Child Process
或者Worker Thread
的方案了。
最后我们总结一下Cluster Module有什么优点:
资源利用率高
:可以充分利用CPU的多核能力
来提升请求处理效率。API设计简单
:可以让你实现简单的负载均衡
和一定程度的高可用
。这里值得注意的是我说的是一定程度的高可用,这是因为Cluster Module的高可用是单机版的
,也就是当宿主机器挂了,你的服务也就挂了,因此更高的高可用肯定是使用分布式集群做的。进程之间高度独立
,避免某个进程发生系统错误导致整个服务不可用。优点说完了,我们再来说一下Cluster Module不好的地方:
资源消耗大
:每一个子进程都是独立的Node运行环境
,也可以理解为一个独立的Node程序,因此占用的资源也是巨大的
。进程通信开销大
:子进程之间的通信通过跨进程通信(IPC)
来进行,如果数据共享频繁是一笔比较大的开销。没能完全解决CPU密集任务
:处理CPU密集型任务时还是有点抓紧见肘
。在Cluster Module中我们可以通过启动更多的子进程来将一些CPU密集型的任务负载均衡到不同的进程里面,从而避免其余接口卡死。可是你也看到了,这个办法治标不治本
,如果用户频繁调用CPU密集型的接口,那么还是会有一大部分请求会被卡死的。优化这个场景的另外一个方法就是child_process
模块。
Child Process
可以让我们启动子进程
来完成一些CPU密集型任务。我们先来看一下主进程master_process.js
的代码:
// node/master_process.js const { fork } = require('child_process') const http = require('http') const url = require('url') const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { // 对于hard_work请求我们启动一个子进程来处理 const child = fork('./child_process') // 告诉子进程开始工作 child.send('START') // 接收子进程返回的数据,并且返回给客户端 child.on('message', () => { resp.write('hard work') resp.end() }) } else if (urlParsed.pathname === '/easy_work') { // 简单工作都在主进程进行 resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
在上面的代码中对于/hard_work
接口的请求,我们会通过fork
函数开启一个新的子进程
来处理,当子进程处理完毕我们拿到数据后就给客户端返回结果。这里值得注意的是当子进程完成任务后我没有释放子进程的资源,在实际项目里面我们也不应该频繁创建和销毁子进程因为这个消耗也是很大的,更好的做法是使用进程池
。下面是子进程
(child_process.js)的实现逻辑:
// node/child_process.js const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { if (message === 'START') { // 开始干活 hardWork() // 干完活就通知子进程 process.send(message) } })
子进程的代码也很简单,它在启动后会通过process.on
的方式监听来自父进程的消息,在接收到开始命令后进行CPU密集型
的计算,得出结果后返回给父进程。
运行上面master_process.js
的代码,我们可以发现即使调用了/hard_work
接口,我们还是可以任意调用/easy_work
接口并且马上得到响应的,此处没有截图,过程大家脑补一下就可以了。
除了fork
函数,child_process
还提供了诸如exec
和spawn
等函数来启动子进程,并且这些进程可以执行任何的shell
命令而不只是局限于Node脚本,有兴趣的读者后面可以通过官方文档了解一下,这里就不过多介绍了。
最后让我们来总结一下Child Process
的优点有哪些:
灵活
:不只局限于Node进程,我们可以在子进程里面执行任何的shell
命令。这个其实是一个很大的优点,假如我们的CPU密集型操作是用其它语言实现
的(例如c语言处理图像),而我们不想使用Node或者C++ Binding重新实现一遍的话我们就可以通过shell
命令调用其它语言的程序,并且通过标准输入输出
和它们进行通信从而得到结果。细粒度的资源控制
:不像Cluster Module,Child Process方案可以按照实际对CPU密集型计算的需求大小动态调整子进程的个数,做到资源的细粒度控制,因此它理论上
是可以解决Cluster Module解决不了的CPU密集型接口调用频繁
的问题。不过Child Process的缺点也很明显:
资源消耗巨大
:上面说它可以对资源进行细粒度控制
的优点时,也说了它只是理论上
可以解决CPU密集型接口频繁调用的问题
,这是因为实际场景下我们的资源也是有限的
,而每一个Child Process都是一个独立的操作系统进程,会消耗巨大的资源。因此对于频繁调用的接口我们需要采取能耗更低的方案也就是下面我会说的Worker Thread
。进程通信麻烦
:如果启动的子进程也是Node应用的话还好办点,因为有内置的API
来和父进程通信,如果子进程不是Node应用的话,我们只能通过标准输入输出
或者其它方式来进行进程间通信,这是一件很麻烦的事。无论是Cluster Module还是Child Process其实都是基于子进程的,它们都有一个巨大的缺点就是资源消耗大
。为了解决这个问题Node从v10.5.0版本(v12.11.0 stable)开始就支持了worker_threads
模块,worker_thread是Node对于CPU密集型操作
的轻量级的线程解决方案
。
Node的Worker Thread
和其它语言的thread是一样的,那就是并发
地运行你的代码。这里要注意是并发
而不是并行
。并行
只是意味着一段时间内多件事情同时发生
,而并发
是某个时间点多件事情同时发生
。一个典型的并行
例子就是React的Fiber架构
,因为它是通过时分复用
的方式来调度不同的任务来避免React渲染
阻塞浏览器的其它行为的,所以本质上它所有的操作还是在同一个操作系统线程
执行的。不过这里值得注意的是:虽然并发
强调多个任务同时执行,在单核CPU的情况下,并发会退化为并行
。这是因为CPU同一个时刻只能做一件事,当你有多个线程需要执行的话
就需要通过资源抢占
的方式来时分复用
执行某些任务。不过这都是操作系统需要关心的东西,和我们没什么关系了。
上面说了Node的Worker Thead和其他语言线程的thread类似的地方,接着我们来看一下它们不一样的地方。如果你使用过其它语言的多线程编程方式,你会发现Node的多线程和它们很不一样,因为Node多线程数据共享起来
实在是太麻烦了
!Node是不允许你通过共享内存变量
的方式来共享数据的,你只能用ArrayBuffer
或者SharedArrayBuffer
的方式来进行数据的传递和共享。虽然说这很不方便,不过这也让我们不需要过多考虑多线程环境下数据安全等一系列问题
,可以说有好处也有坏处吧。
接着我们来看一下如何使用Worker Thread
来处理上面的CPU密集型任务,先看一下主线程(master_thread.js)的代码:
// node/master_thread.js const { Worker } = require('worker_threads') const http = require('http') const url = require('url') const server = http.createServer((req, resp) => { const urlParsed = url.parse(req.url, true) if (urlParsed.pathname === '/hard_work') { // 对于每一个hard_work接口,我们都启动一个子线程来处理 const worker = new Worker('./child_process') // 告诉子线程开始任务 worker.postMessage('START') worker.on('message', () => { // 在收到子线程回复后返回结果给客户端 resp.write('hard work') resp.end() }) } else if (urlParsed.pathname === '/easy_work') { // 其它简单操作都在主线程执行 resp.write('easy work') resp.end() } else { resp.end() } }) server.listen(8080, () => { console.log('server is up...') })
在上面的代码中,我们的服务器每次接收到/hard_work
请求都会通过new Worker
的方式启动一个Worker
线程来处理,在worker处理完任务之后我们再将结果返回给客户端,这个过程是异步的。接着再看一下子线程
(worker_thead.js)的代码实现:
// node/worker_thread.js const { parentPort } = require('worker_threads') const hardWork = () => { // 100亿次毫无意义的计算 for (let i = 0; i { if (message === 'START') { hardWork() parentPort.postMessage() } })
在上面的代码中,worker thread在接收到主线程的命令后开始执行CPU密集型
操作,最后通过parentPort.postMessage
的方式告知父线程任务已经完成,从API上看父子线程通信还是挺方便的。
Akhir sekali, kami meringkaskan kelebihan dan kekurangan Benang Pekerja. Pertama sekali, saya rasa kelebihannya ialah:
资源消耗小
: Berbeza daripada pendekatan berasaskan proses Modul Kluster dan Proses Kanak-kanak, Benang Pekerja adalah berdasarkan benang yang lebih ringan, jadi overhed sumbernya Ya 相对较小的
. Walau bagaimanapun, 麻雀虽小五脏俱全
, setiap Worker Thread
mempunyai sistem v8引擎实例
dan 事件循环
bebasnya sendiri. Ini bermakna walaupun 主线程卡死
Worker Thread
kami boleh terus bekerja Berdasarkan ini, kami sebenarnya boleh melakukan banyak perkara yang menarik. 父子线程通信方便高效
: Tidak seperti dua kaedah sebelumnya, Worker Thread tidak perlu berkomunikasi melalui IPC, dan semua data dikongsi dan dipindahkan dalam proses. Walau bagaimanapun, Worker Thread tidak sempurna:
线程隔离性低
: Memandangkan sub-thread tidak dilaksanakan dalam 独立的环境
, ia akan tetap berlaku jika sub-benang tertentu hang Menjejaskan benang lain, dalam hal ini anda perlu mengambil beberapa langkah tambahan untuk melindungi benang yang tinggal daripada terjejas. 线程数据共享实现麻烦
: Berbanding dengan bahasa bahagian belakang yang lain, perkongsian data Node masih lebih menyusahkan, tetapi ini sebenarnya mengelakkan keperluan untuk mempertimbangkan banyak isu keselamatan data di bawah berbilang benang. Dalam artikel ini, saya memperkenalkan kepada anda mengapa Node sesuai untuk aplikasi intensif I/O tetapi sukar untuk mengendalikan tugas intensif CPU. sebab, dan memberikan anda tiga pilihan untuk mengendalikan tugas intensif CPU dalam pembangunan sebenar. Setiap pilihan sebenarnya ada kelebihan dan kekurangan, dan kita mesti memilih mengikut situasi sebenar, 永远不要为了要用某个技术而一定要采取某个方案
.
Untuk lebih banyak pengetahuan berkaitan nod, sila lawati: tutorial nodejs!
Atas ialah kandungan terperinci Analisis ringkas kaedah Node mengendalikan tugas intensif CPU. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!