伺服器端程式設計之 IO 模型

水目沾發表於2019-04-03

引言

  從 T 跳槽到 A 之後,我的程式語言也從 C++ 轉為 了 Java。在 T 做的偏伺服器端開發,而在 A 更偏向於業務開發。上週在 A 公司組內做了一個《伺服器端高效能網路程式設計》的分享,我訝異於組內的十個人竟然沒有一個人做過直接基於 TCP/IP 協議的開發,更多的是 Web 後臺的業務開發。連 Java 最強大的網路庫 Netty,用過的人也只有一個。但也不難理解---A 公司的中介軟體平臺,將業務與底層進行了隔離,讓程式設計師可以專心於業務開發。

  孰優孰劣?不能一概而論,還記得跳槽 A 公司之前面試過 N 公司,面試官問我 HTTP 協議、RPC 框架等知識,我也是一知半解。要什麼 HTTP 協議、要什麼 RPC ,我們直接 TCP/IP。其實這也是 T 公司基礎設施不夠完善的一種表現,這些在我的另一篇文章《我在騰訊和阿里的見聞》中談過,感興趣的可以移步。

  作為 Java Web 程式設計師,你有想過 Web 伺服器(Nginx,Tomcat,Jetty等)是如何接受你的 HTTP 請求的嗎?你知道 其實 HTTP 也是基於 TCP/IP 的文字協議嗎?之所以取這樣的標題,希望 Java Web 程式設計師對伺服器端的某些工作原理有一些簡單的瞭解,也不至於在面試的時候一問三不知。

一些概念

  在編寫伺服器端網路程式時,我們最常見到阻塞、非阻塞、同步和非同步這四個詞。它們的解釋分別如下:

  • 阻塞: 阻塞呼叫是指呼叫返回之前,當前執行緒會被掛起,只有當呼叫得到結果後才返回。
  • 非阻塞:與阻塞相反,非阻塞呼叫是指在不能立即得到結果之前,該函式不會將當前執行緒阻塞,而是立即返回。
  • 同步:所謂同步,就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不返回。等前一件做完了才能做下一件事。
  • 非同步:非同步的概念和同步相對。當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果。實際處理這個呼叫的部件在完成後,通過狀態、通知和回撥來通知呼叫者。

  常常有人弄不清阻塞/非阻塞與同步/非同步之間的關係,容易將他們混為一談。阻塞/非阻塞更多的用來形容某次呼叫的屬性(比如 read(),write() 是否是阻塞/非阻塞 )所以應用範圍比較窄;而同步/非同步則更上層,通常指各個功能/執行緒之間的關係(比如 Thread1 和 Thread2 是同步執行還是非同步執行)。

五種 IO 模型

  伺服器端 IO 主要分為兩種:磁碟 IO 和網路 IO,在講伺服器端高效能網路程式設計時更多時候我們講的是網路 IO 模型。一次完整的伺服器端處理網路請求流程圖如下(簡化版,以 Web 伺服器為例):

伺服器端程式設計之 IO 模型

  這張圖比較簡單,但是很多人在沒看到這張圖之前肯定都以為每次網路讀(recvfrom())或者寫(sendto())都是在網路卡與使用者程式之間進行操作,其實不是。從上圖可以看出,資料無論從網路卡到使用者空間還是從使用者空間到網路卡都需要經過核心。從磁碟上讀寫資料也是如此。所以就有了 mmap 技術,感興趣的可以自行百度。應用程式(Web 伺服器也屬於應用程式,這裡需要再統一幾個概念:使用者程式、應用程式、Web 伺服器程式,它們相對於核心來說都是應用程式,所以後面文章中統一成應用程式)需要通過系統呼叫(例如recvfrom/sendto)向核心讀寫資料,核心再進一步操作網路卡。

  根據應用程式系統呼叫方式的阻塞、非阻塞,作業系統在處理應用程式請求時處理方式的同步、非同步處理的不同,參考《UNIX 網路程式設計卷 I》可以分為 5 種 IO 模型:

1、阻塞 IO 模型(blocking IO)

伺服器端程式設計之 IO 模型
  描述:應用程式進行 recvfrom 系統呼叫時將阻塞在此呼叫,直到該套接字上有資料並且複製到使用者空間緩衝區。該模式一般配合多執行緒使用,應用程式每接收一個連線,為此連線建立一個執行緒來處理該連線上的讀寫以及業務處理。

  優點:程式設計簡單,適合教學。《UNIX網路程式設計卷I》上很多例子都是基於這種模式。   缺點:如果套接字上沒有資料,程式將一直阻塞。這時其他套接字上有資料也不能進行及時處理。如果是多執行緒方式,除非連線關閉否則執行緒會一直存在,而執行緒的建立、維護和銷燬非常消耗資源,所以能建立的連線數量非常有限。

2、非阻塞 IO 模型(nonblocking IO)

伺服器端程式設計之 IO 模型
  描述:應用程式每次呼叫 recvfrom 即使沒有資料準備好也不會阻塞,會繼續往下執行,避免了程式阻塞在某個連線上的弊端。

  優點:程式碼編寫相對簡單,程式不會阻塞,可以在同一執行緒中處理所有連線。

  缺點:需要頻繁的輪詢,比較耗CPU,在併發量很大的時候將花費大量時間在沒有任何資料的連線上輪詢。所以該模型只在專門提供某種功能的系統中才會出現。

3、IO 複用模型(IO multiplexing)

伺服器端程式設計之 IO 模型
  描述:應用程式阻塞於 select/poll/epoll 等系統函式等待某個連線變成可讀(有資料過來),再呼叫 recvfrom 從連線上讀取資料。雖然此模式也會阻塞在 select/poll/epoll 上,但與阻塞IO 模型不同它阻塞在等待多個連線上有讀(寫)事件的發生,明顯提高了效率且增加了單執行緒/單程式中並行處理多連線的可能。

  優點:統一管理連線,不一定採用多執行緒的方式,同時也不需要輪詢。只需要阻塞於 select 即可,可以同時管理多個連線。

  缺點:當 select/poll/epoll 管理的連線數過少時,這種模型將退化成阻塞 IO 模型。並且還多了一次系統呼叫:一次 select/poll/epoll 一次 recvfrom。

4、訊號驅動 IO 模型(signal-driven IO)

伺服器端程式設計之 IO 模型
  描述:應用程式建立 SIGIO 訊號處理程式,此程式可處理連線上資料的讀寫和業務處理。並向作業系統安裝此訊號,程式可以往下執行。當核心資料準備好會嚮應用程式傳送訊號,觸發訊號處理程式的執行。再在訊號處理程式中進行 recvfrom 和業務處理。

  優點:非阻塞

  缺點:在前一個通知訊號沒被處理的情況下,後一個訊號來了也不能被處理。所以在訊號量大的時候會導致後面的訊號不能被及時感知。

5、非同步 IO 模型(asynchronous IO)

伺服器端程式設計之 IO 模型
  描述:應用程式通過 aio_read 告知核心啟動某個操作,並且在整個操作完成之後再通知應用程式,包括把資料從核心空間拷貝到使用者空間。訊號驅動 IO 是核心通知我們何時可以啟動一個 IO 操作,而非同步 IO 模型是由核心通知我們 IO 操作何時完成。

注:前 4 種模型都是帶有阻塞部分的,有的阻塞在等待資料準備好,有的阻塞在從核心空間拷貝資料到使用者空間。而這種模型應用程式從呼叫 aio_read 到資料被拷貝到使用者空間,不用任何阻塞,所以該種模式叫非同步 IO 模型。這五種模型的取名和並列方式我是保留意見的,感覺容易迷惑讀者。 優點:沒有任何阻塞,充分利用系統核心將 IO 操作與計算邏輯並行。

  缺點:程式設計複雜、作業系統支援不好。目前只有 windows 下的 iocp 實現了真正的 AIO。linux 下在 2.6 版本中才引入,目前並不完善,所以 Linux 下一般採用多路複用模型。

各 IO 模型對比

  前四種模型的主要區別於第一階段,因為他們的第二階段都是一樣的:在資料從核心拷貝到應用程式的緩衝區期間,程式阻塞於 recvfrom 呼叫。相反,非同步 IO 模型在這兩個階段都需要處理,從而不同於其他四種模型。

伺服器端程式設計之 IO 模型
  以上圖片所有原型都來自於《UNIX網路程式設計卷 I》,裡面有很多跟網路程式設計有關的知識點和例子,是程式設計師必備書籍,即使你是業務程式設計師也應該購買一本,知其然,知其所以然!

總結

  JDK 的網路程式設計相關的類、介面雖然不像 C++ 是直接依賴於作業系統的,但它的 IO 模型是離不開以上五種模型的。畢竟這是模型,與語言、作業系統無關。 IO 模型只是高效能網路程式設計中的基礎部分,光有好的 IO 模型還不行,我們還需要好的架構(執行緒模型)。執行緒模型是高效能網路程式設計的核心部分,在後面的文章中應該還會分析。

相關文章