做爲一名IT工程師,網絡通訊編程相信都會接觸到,好比Web開發的HTTP庫,Java中的Netty,或者C/C++中的Libevent,Libev等第三方通訊庫,甚至是直接使用Socket API,可是不少程序員都僅限於使用,對於使用的方式是否合理並無特別深的理解,好比有一股腦的使用線程池解決問題的(雖然大部分狀況採用多線程方案不會有什麼問題,可是編程複雜度比起單線程提高了不少,線程開的太多也會致使切換過於頻繁,性能未必有太大提高),也有始終用一條線程處理全部業務的,而後上線以後常常出現各類服務響應慢等問題。
在介紹TCP的網絡通訊編程時,不得不提到同步,異步,阻塞,非阻塞這幾個概念,C++系和Java系溝通網絡IO相關時,常常把這幾種混在一塊兒描述,好比同步阻塞,同步非阻塞,異步非阻塞等等,實際上,Linux AIO相關的API不多有使用在網絡編程上,用同步異步描述網絡IO並不許確,對於咱們經常使用的Socket API,好比:Connect、Send、 Recv、Close等,只有阻塞非阻塞之分,沒有同步異步之分,而各類應用提供的API接口能夠分同步異步,好比Redis,MySQL官方提供的庫大都是同步接口,Aerospike則既提供了同步接口,也提供了異步接口。python
下面咱們列一下在阻塞與非阻塞下,這些API都是如何表現的
看到了上面阻塞API執行的表現,那麼咱們假設一些異常狀況程序員
Connect: 在網絡狀況差的狀況下進行Connect操做 Send:Server端由於各類緣由不Recv數據,致使Client端發送緩衝區滿,Send沒法寫數據入緩衝區
按照上面的情景很容易想到,函數都會Blocking住,此時這條線程就被OS掛起,讓出CPU資源,而且該線程沒法處理其它業務,若是此時正處於請求高峯期,結果可想而知。
那麼如何解決這個問題呢,可能不少人首先想到的是多線程,可是多線程也會帶來一個問題,這個線程池建立多少合適,太少不夠用,太多資源佔用多,並且線程切換頻繁帶來的損耗也不小。
正確答案是使用Socket的非阻塞模式,如今的通訊框架基本都採用這種模式,好比一些成熟的第三方庫,Libevent, Libev等。
當使用非阻塞模式,再結合多路複用Epoll,一個解決C10k問題的高併發網絡框架基本就造成了。
接下來就介紹幾種基於多路複用的非阻塞服務端模型數據庫
常見的服務端模型
1.單進程單線程編程
好比事件驅動通訊框架Libevent,Libev,應用有知名的Redis,Memcache,比較適合沒有太多耗時任務的狀況。
簡單高效的代名詞,對於網絡IO來講,就是哪一個Socket Fd有讀/寫事件觸發了,就執行它的邏輯,缺點是當一個業務邏輯涉及到不少RPC調用時,業務代碼會分散在各處,可讀性比較差,後面會與常見的非事件驅動Web框架作個對比。後端
適合有比較耗時的業務的狀況,好比流媒體,文件傳輸服務器,數據庫代理等,其中又能夠劃分出以下兩種比較典型的狀況。網絡
圖一多線程
圖二併發
對於圖一,是比較常見的狀況,好比DB Proxy使用線程池的方式創建多個鏈接以提供並行處理的能力。
對於圖二,Accept IO Loop 只負責Accpet Client端的Fd, 而後將Fd傳遞給 Recv IO Loop,這樣有一個好處,每一個Client的請求都只會在一個Recv IO Loop中處理,從而保證了單個Client請求的有序性,而不像圖一中須要其它手段保證有序。框架
3.多進程
這種模式和單進程多線程的有點相似,並且若是Work是單線程的話,就能夠不用考慮多線程帶來的鎖問題。
Master進程能夠負責Accpet Client的鏈接,同時Recv數據並經過IPC的方式將數據包交予Work進程處理。
另外一種,Master只負責Accept Client端的連接,而後將Fd傳遞給Work,讓Work進行數據的收發與邏輯處理。
使用Send,Recv編寫簡單,適合於同步接口的封裝,好比Aerospike,HTTP,的同步接口均可以使用這種方式,問題是不適合作成異步接口,在Recv的時候該線程不能處理其它業務
2.阻塞/非阻塞的多路複用模式
既可封裝成同步接口,也能夠封裝成異步CallBack接口,擴展性更強,好比Aerospike的異步CallBack接口,優點是能夠進行多個請求發送,有數據可Recv時才處理
Client庫的並行調用
編寫一個應用時,咱們常常會遇到同時發送多個Req至服務端的場景,好比MySQL,HTTP,Redis或者自定義的協議,常用的一個方式是鏈接池,不一樣的Req分別用一個鏈接進行處理,這樣作的緣由是協議的特性決定的,由於使用一個鏈接,對於Rsp咱們沒法回溯哪一條Req,因此只能使用鏈接池方式,而咱們本身設計協議時,通常都會在協議頭都會增長一個惟一序列號,這樣Rsp返回就能夠經過該序列號找到對應的Req,瞭解了這一點在作Client端的併發調用時就能夠更清楚的選擇如下哪種模式了。
1.線程池:
比較常見的就是使用MySQL,Redis等開源產品的同步庫,線程池使用比較方便,可是問題也比較明顯,依賴線程的數量,設置太少,併發處理能力太弱,設置太多,線程切換頻繁。
2.鏈接池:
經過建立多個鏈接,並結合多路複用的方式進行操做。好比自行解析MySQL,Redis,HTTP等協議,直接操做Socket Fd,Aerospike的異步鏈接池就是使用這種方式,好處是能夠避免頻繁的線程切換,問題就是若是官方的庫沒有提供這種功能的話,就只能本身去解析協議,沒有線程池使用起來方便快捷。
3.鏈接複用:
通常咱們自定義的二進制協議,協議設計時都會帶有一個惟一的序列號,Rsp經過這個序列號來找到對應哪一個Req,這樣就能夠複用一條連接進行屢次發送,而無需使用上面提到的線程池和鏈接池方式了。
事件驅動型框架
在上面的服務器模型介紹中提到過事件驅動,簡單介紹了事件驅動的原理,就是利用多路複用Select/Epoll監聽一堆Socket Fd,當哪一個Socket Fd有讀/寫事件後,就處理它的事件。
如上圖,是一個很常見的請求流程,對於不少Web框架的用法來講,ServerA對於Client的請求代碼都是以下寫法:
def DoReqClient(req): res1 = Call_RPC_B() res2 = Call_RPC_C() Send_Rsp_To_Client()
ServerA對ServerB與ServerC的RPC都是同步的,ServerC的RPC須要等到ServerB完成後在執行,假如一個請求ServerB處理很慢,則處理這個任務的線程/進程就必須等待,若是ServerA是PHP-FPM就至關於一個進程堵住,徹底不能處理其餘任務,假如開啓了500個進程,則表示PHP-FPM最多隻能同時有500個這樣的請求,以後該機就徹底沒法處理新的請求,直到任務完成釋放一個進程。
可是咱們能夠看到對於這臺機器來講,他的單機性能徹底沒有機會發揮,所有堵塞在了網絡IO上,要解決併發量的問題,要麼用機器堆(錢多),要麼優化業務,看看如何提升後端業務的處理能力,還有一種就是採用事件驅動型框架,有網絡IO事件了才處理,這樣就不會有任何的網絡IO阻塞,惟一的缺點是業務代碼邏輯分散,好比上圖的ServerA若是換成事件驅動的寫法就會以下面的樣子。
def DoReqClient(req): ... Send_Req_To_ServerB() def DoRspServerB(rspB): ... Send_Req_To_ServerC() def DoRspServerC(rspC): ... Send_Rsp_To_Client()
然而壞消息是大部分Web框架並不支持這種事件驅動的模式,更多的都是使用第一種同步寫法,簡單快捷,便於快速開發出產品,由於初期的時候性能並非第一要務,快速產出纔是關鍵,畢竟經過堆機器有時候也能夠提升網站的併發能力,而當你開始考慮以更少的機器支撐更大的併發時,基於事件驅動模型的框架是一個不錯的選擇。
想要優化事件驅動邏輯分散的寫法能夠用到如今比較流行的協程,用同步的代碼實現異步的流程,如今不少語言都開始支持協程的語法,本文就不在具體展開了,有興趣的同窗能夠去了解一下,好比PHP同窗能夠看看咱們公司的Zan框架,Python同窗能夠了解一下tornado框架,或者直接學習一下Golang。
結語
經過以上的總結,咱們能夠知道服務器和客戶端的網絡通訊模型並無一個固定的模式,而是須要結合具體的協議,使用場景來判斷,何時單進程單線程就能知足需求,何時必須使用多線程,鏈接池。只有採用了合適的模型,才能爲一個高併發高性能應用打好堅實的基礎。