node开启子线程_多进程 amp; Node.js web 实现
更好閱讀體驗:
多進程 & Node.js 實現 · 語雀?www.yuque.com進程與線程
進程和線程的誕生要從多任務談起,多任務是指操作系統可以在同一時間內運行多個應用程序,CPU 按順序執行代碼,在同一時間內只能處理一個任務,而在單核時代主流操作系統都有了多任務能力,主要靠快速在多個任務之間切換,讓人感覺多個任務同時執行
進程是指操作系統正在運行的應用程序,而一個進程內部可能有多個并發的子任務,這就是線程
Web 服務器模型
Web 服務器需要同時處理多個用戶的請求,返回給用戶響應內容,有幾種不同的服務器模型實現多任務
多進程單線程
這種服務模型通過進程復制實現同時響應多個請求,每個請求使用一個單獨的進程處理,但操作系統復制進程需要復制進程內部狀態,這樣相同的狀態在內存中存在多份,對內存有一定的開銷,可以同時處理的請求數和內存大小正相關
單進程多線程
為了避免復制多進程帶來的內存浪費問題,多線程被引入 Web 服務器模型,一個線程響應一個用戶請求,線程可以共享進程的內存,不會造成內存浪費,同時線程相對于進程的內存開銷要小得多。但每個線程有自己的獨立堆棧,需要占據一定的內存空間,因此只是緩解了多進程帶來的資源浪費問題
另外操作系統在切換線程的同時需要切換線程的 context,當線程數量過多時 CPU 會被耗在 context 切換中。同時一個線程的崩潰可能會導致整個進程 crash,為服務器帶來了相當程度的穩定性風險
多進程多線程
顧名思義多進程多線程模型就是啟用多個進程,在每個進程內啟用多個線程來解決高并發問題,集成了多進程和多線程模型的好處,但當用戶量足夠大的時候也同時擁有了另外兩種模型的缺陷
當并發數達到千萬級內存好用問題就會暴露出來,這就是著名的 C10k 問題,C10k 問題的本質在于:為了處理高并發創建的進程線程太多,數據拷貝頻繁、進程/線程上下文切換消耗大, 導致操作系統崩潰
事件驅動
為了解決 Web 高并發問題 Nginx 使用了事件驅動的模型,在一個 CPU 上使用單進程、單線程來響應用戶請求,把最耗時的阻塞任務 I/O 任務異步化,處理完成后通過事件通知主進程給用戶響應,在等待 I/O 任務的時候處理下一個請求
這樣的模型性能取決于 CPU 的運算能力,但不受多進程、多線程模式中資源上限的影響,非常適合 Web I/O 密集的特征,成了現在 Web 服務器的主流模型
master-worker 模式
Node.js 本身就使用的事件驅動模型,為了解決單進程單線程對多核使用不足問題,可以按照 CPU 數目多進程啟動,理想情況下一個每個進程利用一個 CPU
Node.js 提供了 child_process 模塊支持多進程,通過 child_process.fork(modulePath) 方法可以調用指定模塊,衍生新的 Node.js 進程 worker.js
const http = require('http'); const randomPort = parseInt(Math.random() * 10000); http.createServer((req, res) => {res.end('Hello world') }).listen(randomPort);master.js
const { fork } = require('child_process'); const os = require('os');for (let i = 0, len = os.cpus().length; i < len; i++) {fork('./worker.js'); }使用 node master.js 啟動,會復制 CPU 數量的進程數執行 worker.js,使用 ps aux | grep worker.js 可以看到對應的進程
undefined 5271 4931720 21584 0:00.13 /usr/local/bin/node ./worker.js undefined 5270 4931720 21624 0:00.13 /usr/local/bin/node ./worker.js undefined 5269 4931720 21640 0:00.13 /usr/local/bin/node ./worker.js undefined 5268 4931720 21636 0:00.12 /usr/local/bin/node ./worker.js undefined 5267 4931720 21616 0:00.13 /usr/local/bin/node ./worker.js undefined 5266 4931720 21696 0:00.12 /usr/local/bin/node ./worker.js undefined 5265 4931720 21648 0:00.13 /usr/local/bin/node ./worker.js undefined 5264 4931720 21640 0:00.12 /usr/local/bin/node ./worker.js這就是 Master-Worker 模式,主進程負責調度和管理工作進程,工作進程負責具體業務邏輯處理
進程通信
主進程管理工作進程,經常需要和工作進程通信,通過 child_process 復制的進程和主進程通信可以使用 WebWorker API worker.js
const http = require('http'); const randomPort = parseInt(Math.random() * 10000);http.createServer((req, res) => {res.end('Hello world') }).listen(randomPort);process.on('message', msg => {console.log(`worker get message: ${msg}`); });process.send(`${randomPort} ready`);master.js
const { fork } = require('child_process'); const os = require('os');for (let i = 0, len = os.cpus().length; i < len; i++) {const worker = fork('./worker.js');worker.on('message', msg => {console.log(`master get message: ${msg}`);});worker.send('ok'); }句柄傳遞
在上面的例子中每個工作進程都使用了一個隨機端口,如果設置成一樣的會出現端口號被占用的錯誤
Error: listen EADDRINUSE :::9527at Server.setupListenHandle [as _listen2] (net.js:1360:14)at listenInCluster (net.js:1401:12)at Server.listen (net.js:1485:7)這個問題可以通過 master 監聽 80 端口,分發請求給工作進程,工作進程使用不同的端口號解決,所以上面例子使用了隨機端口號
但進程每接收一個連接會使用一個文件描述符,上面的模型因為使用了代理服務,每次連接需要消耗兩個文件描述符,而操作系統的文件描述符是有限的,代理方案浪費了一倍的文件描述符影響了系統吞吐量
為了解決這個問題 master 可以把句柄(標識資源的引用,內部包含了指向對象的文件描述符)發送給工作進程
send(message, handler);也就是說 master 進程接收到請求后把 socket 直接發送給 worker,不用為了和 worker 連接重新創建一個 socket master.js
const { fork } = require('child_process'); const net = require('net'); const os = require('os');const workers = []; for (let i = 0, len = os.cpus().length; i < len; i++) {const worker = fork('./worker.js');workers.push(worker); }const server = net.createServer(); server.listen(9527, () => {workers.forEach(worker => {worker.send('SERVER', server);});server.close(); });在主進程中創建一個 tcp server,監聽 9527 端口后把 tcp server 發送給所有 worker,然后關閉 tcp server,所有監聽交給 worker 處理 worker.js
const http = require('http');// 創建 http 服務器,不監聽任何端口號 const httpServer = http.createServer((req, res) => {res.end(`Hello world by ${process.pid}n`); });process.on('message', (msg, tcpServer) => {// 如果是 master 傳遞來的 tcp serverif (msg === 'SERVER') {// 新連接建立的時候觸發tcpServer.on('connection', socket => {// 把 tcp server 的連接轉給 http server 處理httpServer.emit('connection', socket);});} });這樣寫之后為什么多個進程可以監聽同樣的端口號,不報 EADDRINUSE 錯誤了呢?
Node.js 對每個端口監聽的時候設置了 SO_REUSEADDR 選項,允許不同的進程對相同的端口號監聽
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));獨立啟動的進程服務器 socket 的文件描述符(listenfd)不同,所以監聽相同的端口號會失敗,而上面代碼 socket 都使用 master 發送的 socket,所以可以監聽成功
多個應用監聽相同的端口號時文件描述符同一時間只能被一個進程占用,也就是說網絡請求向服務器發送的時候只有一個進程可以搶占到對請求提供服務
穩定性
利用 master 和 worker 的通信機制可以讓 master 對 worker 進行管理
worker 自動重啟
master.js
const { fork } = require('child_process'); const net = require('net'); const os = require('os');const workers = {};function createWorker(server) {const worker = fork('./worker.js');worker.send('SERVER', server);workers[worker.pid] = worker;console.log(`worker ${worker.pid} created`);worker.on('exit', () => {// worker 進程退出,自動重新創建console.log(`worker ${worker.pid} exited`);delete workers[worker.pid];createWorker(server);}); }const server = net.createServer(); server.listen(9527);for (let i = 0, len = os.cpus().length; i < len; i++) {createWorker(server); }master 關閉自動關閉 worker
master.js
process.on('exit', () => {for (const pid in workers) {workers[pid].kill();} });cluster 模塊
上面講的內容可以通過 Node.js 的內置模塊 cluster 實現
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length;if (cluster.isMaster) {console.log(`主進程 ${process.pid} 正在運行`);// 衍生工作進程for (let i = 0; i < numCPUs; i++) {cluster.fork();}cluster.on('exit', (worker, code, signal) => {console.log(`工作進程 ${worker.process.pid} 已退出`);}); } else {// 工作進程可以共享任何 TCP 連接,在本例子中,共享的是 HTTP 服務器。http.createServer((req, res) => {res.writeHead(200);res.end('你好世界n');}).listen(8000);console.log(`工作進程 ${process.pid} 已啟動`); }cluster 事件
cluster 模塊也暴露了一些事件給開發者更多的定制性
總結
以上是生活随笔為你收集整理的node开启子线程_多进程 amp; Node.js web 实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arcgis 分区 属性值_如何使用Ar
- 下一篇: 阮一峰es6电子书_ES6理解进阶【大前