Rumah  >  Artikel  >  hujung hadapan web  >  Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

青灯夜游
青灯夜游ke hadapan
2022-09-13 19:25:522075semak imbas

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.

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

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]

Nod bukan satu-benang

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:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

Nota: Gambar di atas ialah versi ringkas Node事件循环(Gelung Acara). gelung akan mempunyai lebih banyak peringkat, seperti pemasa dsb.

Node sesuai untuk aplikasi intensif I/O

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

Node tidak sesuai untuk tugas intensif CPU

Kami menyebut di atas bahawa kecuali untuk operasi berkaitan I/O, operasi lain Node akan dilaksanakan dalam urutan utama , jadi apabila Nod Apabila memproses beberapa

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_workantara muka ialah CPU密集型接口 kerana ia memanggil fungsi hardWorkiniCPU密集型dan /easy_workantara 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:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

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:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

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.

Modul Kluster

Pengenalan konsep

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:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

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:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

Kami mendapati permintaan ini masih tersekat, dan kemudian kami akan melihat modul Kluster masalah 其它请求也被阻塞 tidak diselesaikan:

Analisis ringkas kaedah Node mengendalikan tugas intensif CPU

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的优缺点

最后我们总结一下Cluster Module有什么优点:

  • 资源利用率高:可以充分利用CPU的多核能力来提升请求处理效率。
  • API设计简单:可以让你实现简单的负载均衡一定程度的高可用。这里值得注意的是我说的是一定程度的高可用,这是因为Cluster Module的高可用是单机版的,也就是当宿主机器挂了,你的服务也就挂了,因此更高的高可用肯定是使用分布式集群做的。
  • 进程之间高度独立,避免某个进程发生系统错误导致整个服务不可用。

优点说完了,我们再来说一下Cluster Module不好的地方:

  • 资源消耗大:每一个子进程都是独立的Node运行环境,也可以理解为一个独立的Node程序,因此占用的资源也是巨大的
  • 进程通信开销大:子进程之间的通信通过跨进程通信(IPC)来进行,如果数据共享频繁是一笔比较大的开销。
  • 没能完全解决CPU密集任务:处理CPU密集型任务时还是有点抓紧见肘

Child Process

在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还提供了诸如execspawn等函数来启动子进程,并且这些进程可以执行任何的shell命令而不只是局限于Node脚本,有兴趣的读者后面可以通过官方文档了解一下,这里就不过多介绍了。

Child Process的优缺点

最后让我们来总结一下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应用的话,我们只能通过标准输入输出或者其它方式来进行进程间通信,这是一件很麻烦的事。

Worker Thread

无论是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上看父子线程通信还是挺方便的。

Kebaikan dan keburukan Benang Pekerja

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.

Ringkasan

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!

Kenyataan:
Artikel ini dikembalikan pada:juejin.cn. Jika ada pelanggaran, sila hubungi admin@php.cn Padam