1. 程式人生 > >Node.js V0.12新特性之Cluster輪轉法負載均衡

Node.js V0.12新特性之Cluster輪轉法負載均衡

回顧Node內建的cluster模組

Node.js固有的單執行緒模型經常被認為是它的一個軟肋。不管你的機器上有多少CPU核心,Node.js能用上的也僅僅是其中之一(某些操作會被有條件地解除安裝到執行緒池中。大多數程式只是在CPU的總時間上分了一杯羹,所以更好地利用可用的處理能力並不能起到多大作用)。

所以Node.js從v0.8開始,新增加了一個內建的‘cluster’模組。你可以用cluster模組設定一個主程序作為管理者,由一或多個工人程序完成實際工作。

讓建立“發完就忘”的多程序伺服器變得更容易是其目標之一。在完美的世界中,你應該可以一行程式碼都不用改,就能讓一個已有的單程序程式繁衍出任意多的工人程序。

當然,事情不可能那麼容易,但對於沒有共享狀態,或共享狀態很少,或把共享狀態存放在資料庫或Web服務之類的外部資源中的程式而言,cluster模組的確讓這件事變得簡單直接了。把那樣的程式變成叢集化的通常只需要幾行程式碼:

var cluster = require('cluster');
    var os = require('os');

    if (cluster.isMaster)
      // 繁衍工人程序,數量跟系統中的CPU數量一樣
      for (var i = 0, n = os.cpus().length; i < n; i += 1)
        cluster.fork();
    else
      // 啟動程式
      app();

這個程式不需要知道它執行在叢集化環境中。比如說你有一個這樣的app()


cluster模組最神奇之處在於所有工人執行緒都可以繫結到相同的請求處理埠和地址上。另外,它可以確保接進來的連線會被均勻地分配給監聽著的工人執行緒...最起碼理論上是這樣的。var http = require('http');

    function app() {
      var server = http.createServer(function(req, res) {
        res.end('OK');
      });
      server.listen(8080, 'www.example.com');
    }

Node.js v0.8和v0.10中分配連線的演算法很簡單。當工人程序呼叫http.Server#listen()net.Server#listen()時,Node.js會給主程序傳送一條訊息,讓它建立一個伺服器socket,繫結好,並分享給這個工人程序。如果已經有繫結好的socket了,主程序就會跳過“建立和繫結”那一步,只需分享已有的socket就可以了。

也就是說所有的工人程序監聽的都是同一個socket。有新連線進來時,作業系統會喚醒一個工人程序。被喚醒的工人程序就會接受連線,開始提供服務。

一切都還不錯。作業系統會針對執行中的程序收集大量的指標,所以應該最有資格決定喚醒哪個程序。

用現實檢驗理論

現在,我們進入了理論與雜亂的現實相遇的環節,因為它慢慢地水落石出了,作業系統並不能總是跟程式設計師想得一樣做到‘最好’。特別是在某些情況下,我們觀測到–特別是在Linux和Solaris中–大多數連線最終都落在了兩或三個程序裡。

從作業系統的角度來看這是可以理解的:上下文切換(掛起一個程序,然後重新啟用另一個)是相當昂貴的操作。如果你有n個程序全都等在同一個socket上,那麼喚醒最近被阻塞的程序是明智之舉,因為那樣可以最大限度地避免上下文切換。(當然,排程器是一種複雜而又多變的野獸;上面只是對真實情況泛泛的解釋。基本前提是那些得到優待的程序會仍然受到優待)。

並不是所有的程式都會受到這個怪癖的影響,實際上大多數都不會,但那些確實會受到影響的會出現非常不均衡的負載。

一旦確定了根本原因,緩解措施就可以用上了。但還沒有特別令人滿意的。比如暫時放棄監聽socket以便讓其它工人程序有機會接受新連線,這有點兒用,但還不夠。‘選定幾個’中的連線數從90%下降到了60-70%,有改善,但還是不夠好。更別提它對那些要處理非常短命的連線的程式的劇烈影響了。

更重要的是,我們清楚地意識到,就像隨機數的生成一樣,接入連線的分配太重要了,不能靠運氣。經過多次討論,我們達成了共識-我們最後,也是最好的希望是可以簡單地拋棄目前的做法,切換到完全不同的東西上。這就是Node.js v0.11.2中的cluster模組換成了round-robin方式的原因,新連線由主程序接受,然後由它選擇一個工人程序把連線交出去。

現在這個選擇工人程序的演算法還不是特別精巧。就像它的名字一樣,它用的是輪轉法– 只是拿起下一個可用的工人程序– 但經過核心開發人員和使用者的測試,證明它很好用:連線在工人程序之間分配得很均衡。我們正在考慮將選定的演算法變成可以由開發人員配置或插入的東西。

如果你還想用老辦法分配連線,可以在程式中設定cluster.schedulingPolicy:

var cluster = require('cluster');
  // 在呼叫其他cluster函式前設定這個
  cluster.schedulingPolicy = cluster.SCHED_NONE;
  cluster.fork();

或者通過環境變數NODE_CLUSTER_SCHED_POLICY調整排程策略:

$ export NODE_CLUSTER_SCHED_POLICY="none" # "rr" is round-robin
$ node app.js

如果不想影響你的shell環境,就放在一行命令裡:

$ env NODE_CLUSTER_SCHED_POLICY="none" node app.js

Windows上的注意事項

MS Windows是預設使用老辦法的唯一平臺。為了達到效能最優,Node.js在Windows上使用了IOCP。儘管這在大多數情況下都不錯,但這樣將HANDLE物件(連線)傳送給其它程序代價十分高昂。 儘管有可能在libuv中解決這個問題,但我們還不清楚是否真的有必要這麼做:Windows的埠幾乎不會受到負載均衡問題的影響,而Linux和Solaris的埠確實會受影響。