Node事件循環(huán)和多進程

nodejs事件循環(huán)與多進程

why

  • 事件循環(huán)對于深入理解nodejs異步至關(guān)重要
    • fs, net,http,events
  • 事件循環(huán)是企業(yè)面試中的最高頻考題之一
  • 能駕馭nodejs多進程是一名資深前端工程師的標志

課程介紹

  • 了解事件循環(huán)的概念
  • 學習瀏覽器中的事件循環(huán)機制
  • 學習nodejs中的事件循環(huán)機制
  • 了解多進程,多線程之間的區(qū)別
  • 學習nodejs中的多進程并使用cluster來開啟多進程

學習目標

  • 深入掌握瀏覽器與nodejs中的事件循環(huán)機制,并且能理解它們之間的區(qū)別
  • 使用cluster開啟多進程

第一章 事件循環(huán)介紹

瀏覽器中的事件循環(huán)

為了協(xié)調(diào)事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網(wǎng)絡(networking)等,用戶代理(user agent)必須使用事件循環(huán)(event loops)。

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.

  • 事件:PostMessage, MutationObserver等
  • 用戶交互: click, onScroll等
  • 渲染: 解析dom,css等
  • 腳本:js腳本執(zhí)行

nodejs中的事件循環(huán)

事件循環(huán)允許Node.js執(zhí)行非阻塞I / O操作 - 盡管JavaScript是單線程的 - 通過盡可能將操作卸載到系統(tǒng)內(nèi)核。
由于大多數(shù)現(xiàn)代內(nèi)核都是多線程的,因此它們可以處理在后臺執(zhí)行的多個操作。當其中一個操作完成時,內(nèi)核會告訴Node.js,以便可以將相應的回調(diào)添加到輪詢隊列中以最終執(zhí)行。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed. We'll explain this in further detail later in this topic.

  • 事件: EventEmitter
  • 非阻塞I / O:網(wǎng)絡請求,文件讀寫等
  • 腳本:js腳本執(zhí)行

事件循環(huán)的本質(zhì)

在瀏覽器或者nodejs環(huán)境中,運行時對js腳本的調(diào)度方式就叫做事件循環(huán)。

setTimeout(() => {
  console.log('setTimeout')
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
});

console.log('main');

// 1. main 2. promise 3. setTimeout

第二章 瀏覽器事件循環(huán)

Javascript為什么是單線程的?

瀏覽器js的作用是操作DOM,這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應該以哪個線程為準?

任務隊列

單線程就意味著所有任務需要排隊,如果因為任務cpu計算量大還好,但是I/O操作cpu是閑著的。所以js就設計成了一門異步的語言,不會做無畏的等待。任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

(1)所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。

(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結(jié)果,就在"任務隊列"之中放置一個事件。

(3)一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結(jié)束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。

(4)主線程不斷重復上面的第三步。

setTimeout(() => {
  console.log('setTimeout')
}, 0);

console.log('main1');
console.log('main2');

主線程從"任務隊列"中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。

宏任務與微任務

除了廣義的同步任務和異步任務,JavaScript 單線程中的任務可以細分為宏任務(macrotask)和微任務(microtask)。

  • macrotask: script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。

  • microtask:process.nextTick, Promise, Object.observe, MutationObserver。

  1. 宏任務進入主線程,執(zhí)行過程中會收集微任務加入微任務隊列。
  2. 宏任務執(zhí)行完成之后,立馬執(zhí)行微任務中的任務。微任務執(zhí)行過程中將再次收集宏任務,并加入宏任務隊列。
  3. 反復執(zhí)行1,2步驟
image-20190526110746356.png
setTimeout(() => {
  console.log('setTimeout')
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
});

console.log('main');

// 1. main 2. promise 3. setTimeout

高頻面試題

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
  Promise.resolve().then(() => {
    console.log('promise2');
  });
});

console.log('main');

每輪事件循環(huán)執(zhí)行一個宏任務和所有的微任務。

setTimeout(() => {
  Promise.resolve().then(() => {
    console.log('promise');
  });
}, 0);

Promise.resolve().then(() => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
});

console.log('main');

任務隊列一定會保持先進先出的順序執(zhí)行。

第三章 nodejs事件循環(huán)

當Node.js啟動時會初始化event loop, 每一個event loop都會包含按如下六個循環(huán)階段,nodejs事件循環(huán)和瀏覽器的事件循環(huán)完全不一樣。

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

image-20190528144107202.png

注意: 圖中的每個方框被稱作事件循環(huán)的一個”階段(phase)”, 這6個階段為一輪事件循環(huán)。

階段概覽

  • timers(定時器) : 此階段執(zhí)行那些由 setTimeout()setInterval() 調(diào)度的回調(diào)函數(shù).

  • I/O callbacks(I/O回調(diào)) : 此階段會執(zhí)行幾乎所有的回調(diào)函數(shù), 除了 close callbacks(關(guān)閉回調(diào)) 和 那些由 timerssetImmediate()調(diào)度的回調(diào).

    setImmediate 約等于 setTimeout(cb,0)

  • idle(空轉(zhuǎn)), prepare : 此階段只在內(nèi)部使用

  • poll(輪詢) : 檢索新的I/O事件; 在恰當?shù)臅r候Node會阻塞在這個階段

  • check(檢查) : setImmediate() 設置的回調(diào)會在此階段被調(diào)用

  • close callbacks(關(guān)閉事件的回調(diào)): 諸如 socket.on('close', ...) 此類的回調(diào)在此階段被調(diào)用

在事件循環(huán)的每次運行之間, Node.js會檢查它是否在等待異步I/O或定時器, 如果沒有的話就會自動關(guān)閉.

如果event loop進入了 poll階段,且代碼未設定timer,將會發(fā)生下面情況:

  • 如果poll queue不為空,event loop將同步的執(zhí)行queue里的callback,直至queue為空,或執(zhí)行的callback到達系統(tǒng)上限;
  • 如果poll queue為空,將會發(fā)生下面情況:
    • 如果代碼已經(jīng)被setImmediate()設定了callback, event loop將結(jié)束poll階段進入check階段,并執(zhí)行check階段的queue (check階段的queue是 setImmediate設定的)
    • 如果代碼沒有設定setImmediate(callback),event loop將阻塞在該階段等待callbacks加入poll queue,一旦到達就立即執(zhí)行

如果event loop進入了 poll階段,且代碼設定了timer:

  • 如果poll queue進入空狀態(tài)時(即poll 階段為空閑狀態(tài)),event loop將檢查timers,如果有1個或多個timers時間時間已經(jīng)到達,event loop將按循環(huán)順序進入 timers 階段,并執(zhí)行timer queue.

代碼執(zhí)行1

path.resolve() 方法會把一個路徑或路徑片段的序列解析為一個絕對路徑。

fs.readFile 異步地讀取文件的全部內(nèi)容。

__dirname 總是指向被執(zhí)行文件夾的絕對路徑

var fs = require('fs');
var path = require('path');

function someAsyncOperation (callback) {
  // 花費2毫秒
  fs.readFile(path.resolve(__dirname, '/read.txt'), callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;

setTimeout(function () {
  var delay = Date.now() - timeoutScheduled;
  console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
});

代碼執(zhí)行2

var fs = require('fs');

function someAsyncOperation (callback) {
  var time = Date.now();
  // 花費9毫秒
  fs.readFile('/path/to/xxxx.pdf', callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;
var delay = 0;

setTimeout(function () {
  delay = Date.now() - timeoutScheduled;
}, 5);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
  console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
});

代碼執(zhí)行3

在nodejs中, setTimeout(demo, 0) === setTimeout(demo, 1)

在瀏覽器里面 setTimeout(demo, 0) === setTimeout(demo, 4)

setTimeout(function timeout () {
  console.log('timeout');
},1);

setImmediate(function immediate () {
  console.log('immediate');
});
// setImmediate它有時候是1ms之前執(zhí)行,有時候又是1ms之后執(zhí)行?

因為event loop的啟動也是需要時間的,可能執(zhí)行到poll階段已經(jīng)超過了1ms,此時setTimeout會先執(zhí)行。反之setImmediate先執(zhí)行

var path = require('path');
var fs = require('fs');

fs.readFile(path.resolve(__dirname, '/read.txt'), () => {
    setImmediate(() => {
        console.log('setImmediate');
    })
    
    setTimeout(() => {
        console.log('setTimeout')
    }, 0)
});

process.nextTick

process.nextTick()不在event loop的任何階段執(zhí)行,而是在各個階段切換的中間執(zhí)行,即從一個階段切換到下個階段前執(zhí)行。

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});

設計原因

允許開發(fā)者通過遞歸調(diào)用 process.nextTick() 來阻塞I/O操作。

nextTick應用場景

  1. 在多個事件里交叉執(zhí)行CPU運算密集型的任務:
var http = require('http');

function compute() {
    
    process.nextTick(compute);//
}

http.createServer(function(req, res) {  // 服務http請求的時候,還能抽空進行一些計算任務
     res.writeHead(200, {'Content-Type': 'text/plain'});
     res.end('Hello World');
}).listen(5000, '127.0.0.1');

compute();

在這種模式下,我們不需要遞歸的調(diào)用compute(),我們只需要在事件循環(huán)中使用process.nextTick()定義compute()在下一個時間點執(zhí)行即可。在這個過程中,如果有新的http請求進來,事件循環(huán)機制會先處理新的請求,然后再調(diào)用compute()。反之,如果你把compute()放在一個遞歸調(diào)用里,那系統(tǒng)就會一直阻塞在compute()里,無法處理新的http請求了。

  1. 保持回調(diào)函數(shù)異步執(zhí)行的原則

當你給一個函數(shù)定義一個回調(diào)函數(shù)時,你要確保這個回調(diào)是被異步執(zhí)行的。下面我們看一個例子,例子中的回調(diào)違反了這一原則:

function asyncFake(data, callback) {        
    if(data === 'foo') callback(true);
    else callback(false);
}

asyncFake('bar', function(result) {
    // this callback is actually called synchronously!
});

為什么這樣不好呢?我們來看Node.js 文檔里一段代碼:

var client = net.connect(8124, function() { 
    console.log('client connected');
    client.write('world!\r\n');
});

在上面的代碼里,如果因為某種原因,net.connect()變成同步執(zhí)行的了,回調(diào)函數(shù)就會被立刻執(zhí)行,因此回調(diào)函數(shù)寫到客戶端的變量就永遠不會被初始化了。

這種情況下我們就可以使用process.nextTick()把上面asyncFake()改成異步執(zhí)行的:

function asyncReal(data, callback) {
    process.nextTick(function() {
        callback(data === 'foo');       
    });
}
  1. 用在事件觸發(fā)過程中

    EventEmitter有2個比較核心的方法, on和emit。node自帶發(fā)布/訂閱模式

var EventEmitter = require('events').EventEmitter;

function StreamLibrary(resourceName) { 
    this.emit('start');
}
StreamLibrary.prototype.__proto__ = EventEmitter.prototype;   // inherit from EventEmitter

var stream = new StreamLibrary('fooResource');

stream.on('start', function() {
    console.log('Reading has started');
});

function StreamLibrary(resourceName) {      
    var self = this;

    process.nextTick(function() {
        self.emit('start');
    });  // 保證訂閱永遠在發(fā)布之前

    // read from the file, and for every chunk read, do:        
    
}

第四章 nodejs多進程

本章概要

  • 為什么要使用多進程
  • 多進程和多線程介紹
  • nodejs開啟多線程和多進程的方法
  • cluster原理介紹

為什么需要多進程

  • nodejs單線程,在處理http請求的時候一個錯誤都會導致整個進程的退出,這是災難級的。

多進程和多線程介紹

進程是資源分配的最小單位,線程是CPU調(diào)度的最小單位

"進程——資源分配的最小單位,線程——程序執(zhí)行的最小單位"

線程是進程的一個執(zhí)行流,是CPU調(diào)度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。一個進程由幾個線程組成,線程與同屬一個進程的其他的線程共享進程所擁有的全部資源。

一個進程下面的線程是可以去通信的,共享資源

進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產(chǎn)生影響,而線程只是一個進程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程沒有單獨的地址空間,一個線程死掉就等于整個進程死掉。

  • 谷歌瀏覽器

    • 進程: 一個tab就是一個進程
    • 線程: 一個tab又由多個線程組成,渲染線程,js執(zhí)行線程,垃圾回收,service worker 等等
  • node服務

    ab是apache自帶的壓力測試工具。

    ab -n1000 -c20 '192.168.31.25:8000/'

    • 進程:監(jiān)聽某一個端口的http服務
    • 線程: http服務由多個線程組成,比如:
      • 主線程:獲取代碼、編譯執(zhí)行
      • 編譯線程:主線程執(zhí)行的時候,可以優(yōu)化代碼
      • Profiler線程:記錄哪些方法耗時,為優(yōu)化提供支持
      • 其他線程:用于垃圾回收清除工作,因為是多個線程,所以可以并行清除

到底選擇多進程還是多線程?

多進程還是多線程一般是結(jié)合起來使用,千萬不要陷入一種非此即彼的誤區(qū)。

image-20190530114414446.png

1)需要頻繁創(chuàng)建銷毀的優(yōu)先用線程

這種原則最常見的應用就是Web服務器了,來一個連接建立一個線程,斷了就銷毀線程,要是用進程,創(chuàng)建和銷毀的代價是很難承受的

2)需要進行大量計算的優(yōu)先使用線程

所謂大量計算,當然就是要耗費很多CPU,切換頻繁了,這種情況下線程是最合適的。

這種原則最常見的是圖像處理、算法處理。

3)強相關(guān)的處理用線程,弱相關(guān)的處理用進程

什么叫強相關(guān)、弱相關(guān)?理論上很難定義,給個簡單的例子就明白了。

一般的Server需要完成如下任務:消息收發(fā)、消息處理?!跋⑹瞻l(fā)”和“消息處理”就是弱相關(guān)的任務,而“消息處理”里面可能又分為“消息解碼”、“業(yè)務處理”,這兩個任務相對來說相關(guān)性就要強多了。因此“消息收發(fā)”和“消息處理”可以分進程設計,“消息解碼”、“業(yè)務處理”可以分線程設計。

4)可能要擴展到多機分布的用進程,多核分布的用線程

5)都滿足需求的情況下,用你最熟悉、最拿手的方式

總結(jié): 線程快而進程可靠性高。

nodejs多線程

伴隨10.5.0的發(fā)布,Node.js 新增了對多線程的實驗性支持(worker_threads模塊)。2018年

nodejs主流還是只有多進程的方案,多線程可以等api穩(wěn)定之后再使用。

創(chuàng)建多進程

利用cluster開啟多進程

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length; // 獲取CPU的個數(shù)
 
if (cluster.isMaster) {
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
 
    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    http.createServer(function(req, res) {
        res.writeHead(200);
        res.end("hello world\n");
    }).listen(8000);
}

稍微優(yōu)化下:

var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
 
if (cluster.isMaster) {
    for (var i = 0; i &lt; numCPUs; i++) {
        cluster.fork();
    }
    // 其它代碼
    
} else {
    require("./app.js");
}

多進程和單進程性能對比

多進程的性能要明顯好于單進程

ab是apache自帶的壓力測試工具。推薦大家用mac

ab -n1000 -c20 '192.168.31.25:8000/'

  • n 請求數(shù)量
  • c 并發(fā)數(shù)

nodejs調(diào)試方法

https://code.visualstudio.com/Docs/editor/debugging

vscode的 .vscode文件下面配置 launch.json

{
    // 使用 IntelliSense 了解相關(guān)屬性。 
    // 懸停以查看現(xiàn)有屬性的描述。
    // 欲了解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [

        
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/chapter4/http_cluster.js"
        }
    ]
}

cluster相關(guān)API

Process 進程 、child_process 子進程 、Cluster 集群

process進程

process 對象是 Node 的一個全局對象,提供當前 Node 進程的信息,他可以在腳本的任意位置使用,不必通過 require 命令加載。

屬性

  1. process.argv 屬性,返回一個數(shù)組,包含了啟動 node 進程時的命令行參數(shù)
  2. process.env 返回包含用戶環(huán)境信息的對象,可以在 腳本中對這個對象進行增刪改查的操作
  3. process.pid 返回當前進程的進程號
  4. process.platform 返回當前的操作系統(tǒng)
  5. process.version 返回當前 node 版本

方法

  1. process.cwd() 返回 node.js 進程當前工作目錄
  2. process.chdir() 變更 node.js 進程的工作目錄
  3. process.nextTick(fn) 將任務放到當前事件循環(huán)的尾部,添加到 ‘next tick’ 隊列,一旦當前事件輪詢隊列的任務全部完成,在 next tick 隊列中的所有 callback 會被依次調(diào)用
  4. process.exit() 退出當前進程,很多時候是不需要的
  5. process.kill(pid[,signal]) 給指定進程發(fā)送信號,包括但不限于結(jié)束進程

事件

  1. beforeExit 事件,在 Node 清空了 EventLoop 之后,再沒有任何待處理任務時觸發(fā),可以在這里再部署一些任務,使得 Node 進程不退出,顯示的終止程序時(process.exit()),不會觸發(fā)

  2. exit 事件,當前進程退出時觸發(fā),回調(diào)函數(shù)中只允許同步操作,因為執(zhí)行完回調(diào)后,進程金輝退出

  3. uncaughtException 事件,當前進程拋出一個沒有捕獲的錯誤時觸發(fā),可以用它在進程結(jié)束前進行一些已分配資源的同步清理操作,嘗試用它來恢復應用的正常運行的操作是不安全的

    重點關(guān)注

  4. warning 事件,任何 Node.js 發(fā)出的進程警告,都會觸發(fā)此事件

child_process

nodejs中用于創(chuàng)建子進程的模塊,node中大名鼎鼎的cluster是基于它來封裝的。

  1. exec()

異步衍生出一個 shell,然后在 shell 中執(zhí)行命令,且緩沖任何產(chǎn)生的輸出,運行結(jié)束后調(diào)用回調(diào)函數(shù)

var exec = require('child_process').exec;

var ls = exec('ls -c', function (error, stdout, stderr) {
  if (error) {
    console.log(error.stack);
    console.log('Error code: ' + error.code);
  }
  console.log('Child Process STDOUT: ' + stdout);
});

由于標準輸出和標準錯誤都是流對象(stream),可以監(jiān)聽data事件,因此上面的代碼也可以寫成下面這樣。

var exec = require('child_process').exec;
var child = exec('ls');

child.stdout.on('data', function(data) {
  console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
  console.log('stdout: ' + data);
});
child.on('close', function(code) {
  console.log('closing code: ' + code);
});

上面的代碼還有一個好處。監(jiān)聽data事件以后,可以實時輸出結(jié)果,否則只有等到子進程結(jié)束,才會輸出結(jié)果。所以,如果子進程運行時間較長,或者是持續(xù)運行,第二種寫法更好。

  1. execSync()

exec()的同步版本

  1. execFile()

execFile方法直接執(zhí)行特定的程序shell,參數(shù)作為數(shù)組傳入,不會被bash解釋,因此具有較高的安全性。

const {execFile} = require('child_process');
execFile('ls',['-c'], (error, stdout, stderr) => {
    if(error) {
        console.error(`exec error: ${error}`);
        return;
    }
    console.log(`${stdout}`);
    console.log(`${stderr}`);
});
  1. spawn()

spawn方法創(chuàng)建一個子進程來執(zhí)行特定命令shell,用法與execFile方法類似,但是沒有回調(diào)函數(shù),只能通過監(jiān)聽事件,來獲取運行結(jié)果。它屬于異步執(zhí)行,適用于子進程長時間運行的情況。

const { spawn } = require('child_process');

var child = spawn('ls', ['-c'],{
    encoding: 'UTF-8'
});

child.stdout.on('data', function(data) {
    console.log('data', data.toString('utf8'))
});
child.on('close',function(code) {
    console.log('closing code: ' + code);
  });

spawn返回的結(jié)果是Buffer需要轉(zhuǎn)換為utf8

  1. fork()

fork方法直接創(chuàng)建一個子進程,執(zhí)行Node腳本,fork('./child.js') 相當于 spawn('node', ['./child.js']) 。與spawn方法不同的是,fork會在父進程與子進程之間,建立一個通信管道pipe,用于進程之間的通信,也是IPC通信的基礎。

main.js

var child_process = require('child_process');
var path = require('path');

var child = child_process.fork(path.resolve(__dirname, './child.js'));
child.on('message', function(m) {
  console.log('主線程收到消息', m);
});
child.send({ hello: 'world' });

child.js

process.on('message', function (m) {
    console.log('子進程收到消息', m);
});
process.send({ foo: 'bar' });

cluster

node進行多進程的模塊

屬性和方法

  1. isMaster 屬性,返回該進程是不是主進程
  2. isWorker 屬性,返回該進程是不是工作進程
  3. fork() 方法,只能通過主進程調(diào)用,衍生出一個新的 worker 進程,返回一個 worker 對象。和process.child的區(qū)別,不用創(chuàng)建一個新的child.js
  4. setupMaster([settings]) 方法,用于修改 fork() 默認行為,一旦調(diào)用,將會按照cluster.settings進行設置。
  5. settings 屬性,用于配置,參數(shù) exec: worker文件路徑;args: 傳遞給 worker 的參數(shù);execArgv: 傳遞給 Node.js 可執(zhí)行文件的參數(shù)列表

事件

  1. fork 事件,當新的工作進程被 fork 時觸發(fā),可以用來記錄工作進程活動
  2. listening 事件,當一個工作進程調(diào)用 listen() 后觸發(fā),事件處理器兩個參數(shù) worker:工作進程對象
  3. message事件, 比較特殊需要去在單獨的worker上監(jiān)聽。
  4. online 事件,復制好一個工作進程后,工作進程主動發(fā)送一條 online 消息給主進程,主進程收到消息后觸發(fā),回調(diào)參數(shù) worker 對象
  5. disconnect 事件,主進程和工作進程之間 IPC 通道斷開后觸發(fā)
  6. exit 事件,有工作進程退出時觸發(fā),回調(diào)參數(shù) worker 對象、code 退出碼、signal 進程被 kill 時的信號
  7. setup 事件,cluster.setupMaster() 執(zhí)行后觸發(fā)

文檔地址:

https://nodejs.org/api/child_process.html 多看文檔!

cluster多進程模型

每個worker進程通過使用child_process.fork()函數(shù),基于IPC(Inter-Process Communication,進程間通信),實現(xiàn)與master進程間通信。

那我們直接用child_process.fork()自己實現(xiàn)不就行了,干嘛需要cluster呢?

這樣的方式僅僅實現(xiàn)了多進程。多進程運行還涉及父子進程通信,子進程管理,以及負載均衡等問題,這些特性cluster幫你實現(xiàn)了。
image-20190530231222075.png

最初的多進程模型

最初的 Node.js 多進程模型就是這樣實現(xiàn)的,master 進程創(chuàng)建 socket,綁定到某個地址以及端口后,自身不調(diào)用 listen 來監(jiān)聽連接以及 accept 連接,而是將該 socket 的 fd 傳遞到 fork 出來的 worker 進程,worker 接收到 fd 后再調(diào)用 listen,accept 新的連接。但實際一個新到來的連接最終只能被某一個 worker 進程 accpet 再做處理,至于是哪個 worker 能夠 accept 到,開發(fā)者完全無法預知以及干預。這勢必就導致了當一個新連接到來時,多個 worker 進程會產(chǎn)生競爭,最終由勝出的 worker 獲取連接。

image-20190530231552279.png

相信到這里大家也應該知道這種多進程模型比較明顯的問題了

  • 多個進程之間會競爭 accpet 一個連接,產(chǎn)生驚群現(xiàn)象,效率比較低。
  • 由于無法控制一個新的連接由哪個進程來處理,必然導致各 worker 進程之間的負載非常不均衡。

這其實就是著名的”驚群”現(xiàn)象。

簡單說來,多線程/多進程等待同一個 socket 事件,當這個事件發(fā)生時,這些線程/進程被同時喚醒,就是驚群??梢韵胍?,效率很低下,許多進程被內(nèi)核重新調(diào)度喚醒,同時去響應這一個事件,當然只有一個進程能處理事件成功,其他的進程在處理該事件失敗后重新休眠(也有其他選擇)。這種性能浪費現(xiàn)象就是驚群。

驚群通常發(fā)生在 server 上,當父進程綁定一個端口監(jiān)聽 socket,然后 fork 出多個子進程,子進程們開始循環(huán)處理(比如 accept)這個 socket。每當用戶發(fā)起一個 TCP 連接時,多個子進程同時被喚醒,然后其中一個子進程 accept 新連接成功,余者皆失敗,重新休眠。

http.Server繼承了net.Server, http客戶端與http服務端的通信均依賴于socket(net.Socket)。

const net = require('net');
const fork = require('child_process').fork;

var handle = net._createServerHandle('0.0.0.0', 3000);

for(var i=0;i<4;i++) {
    console.log('11111111111111111111111111')
   fork('./worker').send({}, handle);
}
const net = require('net');
process.on('message', function(m, handle) {
  start(handle);
});

var buf = 'hello nodejs';
var res = ['HTTP/1.1 200 OK','content-length:'+buf.length].join('\r\n')+'\r\n\r\n'+buf;

var data = {};

function start(server) {
    server.listen();
    server.onconnection = function(err,handle) {
        var pid = process.pid;
        if (!data[pid]) {
            data[pid] = 0;
        }
        data[pid] ++;
        console.log('got a connection on worker, pid = %d', process.pid, data[pid]);
        var socket = new net.Socket({
            handle: handle
        });
        socket.readable = socket.writable = true;
        socket.end(res);
    }
}
image-20190602164750971.png

nginx proxy

Nginx 是俄羅斯人編寫的十分輕量級的 HTTP 服務器,Nginx,它的發(fā)音為“engine X”,是一個高性能的HTTP和反向代理服務器。異步非阻塞I/O,而且能夠高并發(fā)。

正向代理: 客戶端為代理,服務器不知道客戶端是誰。

反向代理: 服務器為代理,客戶端不知道服務器是誰。

nginx配置demo:

http { 
  upstream cluster { 
          server 127.0.0.1:3000;   // 掛掉
      server 127.0.0.1:3001;   // 掛掉
      server 127.0.0.1:3002; 
      server 127.0.0.1:3003; 
  } 
  server { 
       listen 80; 
       server_name www.domain.com; 
       location / { 
            proxy_pass http://cluster;
       } 
  }
}

nginx的實際應用場景:比較適合穩(wěn)定的服務

  • 靜態(tài)資源服務器: js, css, html
  • 企業(yè)級集群

守護進程: 退出命令行窗口之后,服務一直處于運行狀態(tài)

cluster多進程調(diào)度模型

cluster是由master監(jiān)聽請求,再通過round-robin算法分發(fā)給各個worker,避免了驚群現(xiàn)象的發(fā)生。

round-robin 輪詢調(diào)度算法的原理是每一次把來自用戶的請求輪流分配給內(nèi)部中的服務器

image-20190531115049093.png

cluster調(diào)度模型簡易demo

master.js

const net = require('net');
const fork = require('child_process').fork;

var workers = [];
for (var i = 0; i < 4; i++) {
   workers.push(fork('./worker'));
}

var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err,handle) {
    var worker = workers.pop();
    worker.send({},handle);
    workers.unshift(worker);
}
const net = require('net');
process.on('message', function (m, handle) {
  start(handle);
});

var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK','content-length:'+buf.length].join('\r\n')+'\r\n\r\n'+buf;

function start(handle) {
    console.log('got a connection on worker, pid = %d', process.pid);
    var socket = new net.Socket({
        handle: handle
    });
    socket.readable = socket.writable = true;
    socket.end(res);
}
cluster中的優(yōu)雅退出
  1. 關(guān)閉異常 Worker 進程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。
  2. Master 立刻 fork 一個新的 Worker 進程,保證在線的『工人』總數(shù)不變。
  3. 異常 Worker 等待一段時間,處理完已經(jīng)接受的請求后退出。
if (cluster.isMaster) {
    cluster.fork()
} else {
    // 出錯之后
    process.disconnect();  // exit()
}   
進程守護

master 進程除了負責接收新的連接,分發(fā)給各 worker 進程處理之外,還得像天使一樣默默地守護著這些 worker 進程,保障整個應用的穩(wěn)定性。一旦某個 worker 進程異常退出就 fork 一個新的子進程頂替上去。

這一切 cluster 模塊都已經(jīng)好處理了,當某個 worker 進程發(fā)生異常退出或者與 master 進程失去聯(lián)系(disconnected)時,master 進程都會收到相應的事件通知。

cluster.on('exit', function () {
    clsuter.fork();
});

cluster.on('disconnect', function () {
    clsuter.fork();
});
IPC通信

IPC通信就是進程間的通信。

雖然每個 Worker 進程是相對獨立的,但是它們之間始終還是需要通訊的,叫進程間通訊(IPC)。下面是 Node.js 官方提供的一段示例代碼

'use strict';
const cluster = require('cluster');

if (cluster.isMaster) {
  const worker = cluster.fork();
  worker.send('hi there');
  worker.on('message', msg => {
    console.log(`msg: ${msg} from worker#${worker.id}`);
  });
} else if (cluster.isWorker) {
  process.on('message', (msg) => {
    process.send(msg);
  });
}

細心的你可能已經(jīng)發(fā)現(xiàn) cluster 的 IPC 通道只存在于 Master 和 Worker 之間,Worker 與 Worker 進程互相間是沒有的。那么 Worker 之間想通訊該怎么辦呢?通過 Master 來轉(zhuǎn)發(fā)。

核心: worker直接的通信,靠master轉(zhuǎn)發(fā),利用workder的pid。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容