1. 程式人生 > >伺服器的併發與實現

伺服器的併發與實現

眾所周知,現在的伺服器可以處理多個socket連線,背後併發的實現主要有兩種途徑。

  1. 多執行緒同步阻塞
  2. I/O多路複用

socket的建立

聊到socket,就不得不提到socket的建立的流程。祭出經典的老圖: jpg

伺服器依次使用socket,bind,listen之後就會監聽對應的地址,此時accept會一直阻塞直到有連線建立,如果客戶端和伺服器建立了連線,那麼accept就會返回一個連線控制代碼,可以對連線進行讀資料或者寫資料。

同步阻塞

那麼問題來了,伺服器如果不做特殊處理的話,一次只能處理一個連線,新的連線來是需要等待上一個連線結束才能連線成功,這就是最開始的伺服器同步阻塞方法。

同步阻塞:程序發起IO系統呼叫後,程序被阻塞,轉到核心空間處理,整個IO處理完畢後返回程序。操作成功則程序獲取到資料。

多執行緒併發

可能以前的擁有的電腦人不多,這種方式一次只能連線一個倒也沒有問題,之後訪問的人開始多起來,設計者覺得這樣下去不行,就設計了多執行緒同步阻塞的方法。每次accept獲得一個控制代碼,就建立一個執行緒去處理連線,這下子就能同時處理多個連線呢。這就是經典的多執行緒同步阻塞的方法。

典型的多執行緒(程序)併發模型就是cgi

特點

伺服器和客戶端之間的併發,有以下特點:

  1. 外部連線很多,但很多連線是不活躍的連線,典型的如聊天的im系統。
  2. 少量的CPU消耗。
  3. 大部分的時間耗費在I/O阻塞和其他網路服務。
  4. 外部網路不穩定,客戶端收發資料慢很多。
  5. 對業務的請求處理很快,大部分時候毫秒級就可以完成。

問題

根據以上特點,我們可以得知伺服器有以下問題: 如果採用多執行緒同步阻塞,1個tcp連線需要建立1個執行緒,10k個連線需要建立10k個連線,然而大部分連線是不活躍,即便是需要處理業務邏輯,也可以快速返回結果,大部分時間也是處於I/O阻塞或網路等待。這就使得多個執行緒的建立很耗費資源,且執行緒的切換也是極其耗費CPU,這就很可能導致了CPU處理業務的消耗的資源不多,但是卻花了很多資源在程序切換上面。

I/O多路複用

多執行緒併發的問題是大部分socket都是閒置的狀態或者是處於IO阻塞的狀態,那能不能把阻塞的socket先扔到一邊去處理其他事情,來避免等待所帶來的資源耗費,也就是非阻塞IO的概念。

非阻塞IO

當用戶程序發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回一個error。從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

因此:使用非阻塞IO是需要不斷輪詢IO資料是否好了。

IO多路複用原理就是不斷輪詢多個socket,當其中的某個socket準備好了資料就返回,否則整個程序繼續阻塞,就可以讓一個程序在不太耗費資源的情況下處理多個連線,但是這個輪詢的操作是交給核心態去完成,也就避免了核心態和使用者態的切換的問題。

而目前的實現方法有select, poll, epoll,其中epoll的效能最好,用的也是最廣泛。

優點

  1. 避免了建立多個執行緒所耗費的資源以及時間。
  2. 對socket的輪詢是核心態的完成,不需要像多執行緒那樣切換需要耗費資源。

而epoll的實現可以做到效能幾乎不受連線數(單單是連線而沒有其他的操作)的影響。

當然多路複用IO也有自己的問題,也就是本身不支援多核的使用,需要另外解決多核的利用。

其中使用enroll的成熟程式有nginx,redis,nodej等。

伺服器的發展

根據知乎大佬的介紹,伺服器經過發展可以分為兩階段:

第一代伺服器模型

把傳輸層的tcp併發的連線放到IO多路複用去處理,應用層繼續使用多執行緒併發模型去做。這樣就可以大幅度減少執行緒的建立切換的資源耗費。 如:nginx + php-fpm(其實是php-fpm是多程序)

第二代伺服器模型

第二代伺服器模型是把應用層也使用IO多路複用去處理,減少應用層的等待外部介面呼叫阻塞等待,一般是大廠大流量併發需要用到。 如:

  1. nodejs的非同步回撥
  2. Go的goroutine