回憶一下你剛開始學習socket編程的場景:先建立類型爲STREAM的socket,再調用listen、accept、read方法,把程序運行起來後,就完成了最簡單的,以單進程的方式運行TCP服務器。固然,這樣的服務器性能是不好的,只能用於響應數量極少的狀況。python
實際的應用場景須要服務器響應更多的請求,所以須要對原始的服務器進行擴展。一般來講,這種擴展並不困難,最容易想到的辦法是增長工做進程的數量。這種經過增長工做進程數量來提高服務器負載能力的模型容易擴展,不少流行的HTTP服務器,好比Apache,NGINX或者Lighttpd都採用了這種模型。編程
增長工做進程數量方法可以有效利用多核處理器來提高網絡服務器的負載能力,可是會帶來一個新問題。bash
就性能而言,一般有三種設計TCP服務器的方法。服務器
第一種方法以下圖所示。網絡
這是最簡單的模型,該模型只會使用到單個CPU。單個工做進程執行accept()方法以接受新鏈接並處理請求。這是使用Lighttpd推薦的設置。負載均衡
第二種方法以下圖所示。socket
父進程新建套接字後,調用listen建立鏈接。父進程建立多個子進程,子進程調用accept,處理請求。這個模型可使多個請求分散到多個CPU中。這是NGINX的標準模型性能
第三種方法以下圖所示。學習
經過socket的SO_REUSEPORT,能夠爲每一個工做進程建立專用的監聽套接字。這樣能夠避免多個進程競爭使用套接字,也能夠更好地實現負載均衡。測試
有兩種方式能夠經過accept方法實如今多個進程中處理新的鏈接。好比下面的這段Python代碼,文件命名爲block-accept.py
# coding=utf-8
import os
import socket
sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sd.bind(('127.0.0.1', 1024))
sd.listen(10)
for i in range(3):
if os.fork() == 0:
while True:
cd, _ = sd.accept()
cd.close()
print('worker %d' % i)
os.wait()
複製代碼
這段代碼同時在多個進程中調用阻塞的accept()方法,來跨進程共享一個接受隊列,咱們將這個方法稱爲「阻塞accpet」方法。
還有一種方法,咱們稱之爲「epoll-accept」方法。能在Linux系統上運行的代碼以下(文件命名爲epoll-and-accept.py):
# coding=utf-8
import os
import select
import socket
sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sd.bind(('127.0.0.1', 1025))
sd.listen(10)
sd.setblocking(False)
for i in range(3):
if os.fork() == 0:
ed = select.epoll()
ed.register(sd, select.EPOLLIN)
while True:
try:
ed.poll()
except IOError:
continue
try:
cd, _ = sd.accept()
except socket.error:
continue
cd.close()
print('worker %d' % i)
os.wait()
複製代碼
上面的代碼中,每一個工做進程都有本身的epoll。只有當epoll報告新鏈接到來時,纔會調用accept()方法。
上面兩段代碼看起來很是類似,不過工做的效果卻有細微的差異。能夠用nc命令來對服務器作一些測試
$ python blocking-accept.py &
$ for i in `seq 6`; do nc localhost 1024; done
worker 0
worker 1
worker 2
worker 0
worker 1
worker 2
$ python epoll-and-accept.py &
$ for i in `seq 6`; do nc localhost 1025; done
worker 2
worker 1
worker 1
worker 2
worker 2
worker 2
複製代碼
採用blocking-accept模型的服務器將鏈接均分給了全部的工做進程,每一個進程都正好分配到了兩個鏈接;採用epoll-accpet模型的服務器則更傾向於將新鏈接分配給最後一個工做進程。
也就是說,針對不一樣的模型,Linux會執行不一樣的負載均衡策略。
在blocking-accept模型中,Linux執行round-robin負載均衡。每一個阻塞在accept()的工做進程會被加入到某個隊列中,而後按先進先出的順序處理鏈接。
在epoll-accept模型中,Linux彷佛會優先選擇最後加入的工做進程,順序是後進先出。最後被加入隊列的進程會先得到新鏈接。所以最忙的工做進程會獲取作多的工做量,由於新進的鏈接老是優先分配給它。
Linux提供了一個套接字選項用於解決這個問題--SO_REUSEPORT。使用該選項後,新建的鏈接被拆分爲多個單獨的接受隊列。通常狀況下,這意味着每一個工做進程只有一個專用隊列。
因爲Linux經過簡單的哈希邏輯分散負載,而且多個工做進程不共享隊列,所以每一個工做進程能得到大體相同數量的鏈接,以期達到更好的負載均衡效果。
不過在某些場景中,這種工做模式可能會致使問題。假設有A,B,C三個工做進程,分別對應a,b,c三個接受隊列。當一個工做進程被某個工做卡住的時候,對應的隊列也會被卡住,三分之一的工做都會受到影響。若是使用的第二種模式的話,就不會出現這樣的問題。
當前並無一種完美作法能將請求分佈到多個工做進程中。使用第二種模式很好擴展而且有利於保持最大延遲,可是沒法讓多個工做進程中均衡地承擔工做量。第三種模式能很好地平衡工做負載,可是在高負載狀況下,會增長服務的最大延遲。