從網絡IO看高性能框架

前言: 我是一名golang後端開發工程師,不是Java,也不是拍黃片,對,就是那個號稱原生支持高併發的“夠浪!”。那爲什麼go能支持高併發?原生支持高併發又是何解?跟着我,一起探討一下所謂的高併發是怎麼回事…

閱讀本文你將收穫:

  • 知道框架高性能的根本原因
  • 瞭解進程,線程切換開銷在哪裏
  • 熟悉阻塞與非阻塞IO,同步與異步調用的區別

大綱:

  1. 討論一個高性能框架甚至語言的時候,我們在討論什麼?
  2. 三大網絡模型
    • 阻塞IO+多進程
    • 阻塞IO+多線程
    • 非阻塞IO+IO多路複用
  3. 五種網絡IO簡介
  4. 網絡IO的本質
  5. 如何區分阻塞IO和非阻塞IO
  6. 如何區分同步和異步
  7. 個人整理的網絡IO思維導圖

1.討論一個高性能框架甚至語言的時候,我們在討論什麼

我相信大家肯定聽過什麼阻塞/非阻塞IO,同步/異步調用,我也嘗試過死記概念,結果大家應該都有體會,過一陣子就忘記了。知其然而不知其所以然~然並卵。

大家在選擇一門語言或者一個框架的時候肯定優先看他的性能,也就是併發量,例如常用的測試手段,就是用該語言或者框架寫個http server服務器,對於http請求返回一個“hello,world!”,利用wrk進行壓測,看看每分鐘請求量最高能到多少,在4核8G的Ubuntu服務器上跑該http服務,利用wrk壓測,gin框架每分鐘能處理的請求量接近300W!這是相當優秀的!

前一陣子在go meet up深圳討論語言性能的時候,有位老哥說同等業務與機器,PHP每秒請求量大概在300多,處理三萬併發量的服務程序,go需要一臺服務器,而PHP需要一百臺。我當時非常震驚,爲什麼語言之間的差別這麼大,是什麼原因造成這個巨大的差別呢?我問Boss Lee(meet up講師,一位技術大佬),他跟我說因爲PHP是一個請求開一個進程處理,注意是進程而不是線程!

那爲什麼用進程處理請求會造成性能差別這麼大,甚至到了一百臺服務器的差別呢?(一百臺服務器一年得上百萬吧~)

經過我查閱資料,得出了是網絡IO模型造成了性能根本上的差別這一結論!

這裏直接說結論:PHP是阻塞IO+多進程模型,大名鼎鼎的Netty(JAVA)框架是主從reactor+worker threads 模式。

爲什麼?因爲CPU切換進程或線程所帶來的性能損耗是巨大的,主從reactor模式解決了IO分發的高效率問題!

這裏先記住結論,後文看解析

2.三大網絡模型

2.1阻塞IO+多進程

服務器初始監聽在lisnted_fd套接字上,此時一個客戶端發起連接請求,連接成功後產生連接套接字,此時父進程fork出一個子進程,子進程拿到連接套接字,並以此與客戶端通信。在這種網絡模型下,父進程關心的是監聽套接字,子進程關心的是連接套接字。

連接分配第一個客戶端.png
(圖·連接分配第一個客戶端.png)
連接分配第二個客戶端.png
(圖·連接分配第二個客戶端.png)

這種網絡模型編程簡單,但是效率不高。

2.2阻塞IO+多線程

進程切換上下文代價是相當高的,有一種類似進程,但是切換開銷比進程小的東西,那就是線程。

爲什麼說線程切換比進程切換開銷要小呢?
因爲線程由操作系統內核管理,在同一個進程中,所有的線程共享該進程的整個虛擬地址空間,包括代碼、數據、堆、共享庫等。
我們的代碼被CPU執行需要一些數據支撐的,這就是所謂的上下文,包括但不限於程序計數器需要告訴CPU代碼執行到哪裏了,寄存器中存放了一些計算中間值,內從中存放了當前一些變量等。從一個計算場景切換到另一個計算場景,這些值都需要重新載入,這就是上下文切換。

2.2非阻塞IO+IO多路複用

使用poll和epoll可以設計出基於套接字滿足高性能,高併發的事件驅動程序。
事件驅動模型,叫做**reactor模型,或者Even loop模型。**是不是很熟悉?這個模型的核心有兩點:

  1. 存在一個無限循環的事件分發線程,叫reactor線程,或者Even loop線程。這個分發線程背後的技術就是poll與epoll這類的IO多路複用技術。
  2. 所有的IO操作都可抽象爲事件,每個事件必須有回調函數來處理。acceptor上有連接建立,已連接套接字的發送緩衝區可以寫,通信管道pipe上有數據可以讀,這些事件通過事件分發,都能被檢測並調用回調函數處理。
  • 單reactor模型 + worker threads
    該模型是將acceptor上連接建立事件,和已連接套接字的IO事件的分發由一個reactor線程去執行,由工作線程去處理耗時操作,例如數據庫讀取,文件解析,計算等等。
    單reactor模型 + worker threads.png
    (圖·單reactor模型 + worker threads.png)

  • 主從reactor模型 + worker threads
    當所有acceptor的連接建立事件和已連接套接字的IO事件交由一個reactor線程處理,在併發量較高的情況下,這個reactor線程會忙不過來,表現在客戶端連接建立成功率偏低。

那麼主從模式的核心思想就在於,主reactor上只監聽acceptor上成功建立的連接事件,並將其分發給從reactor線程,從reactor線程只需要負責已連接套接字上的IO事件。

主從reactor模型 + worker threads.png
(圖·主從reactor模型 + worker threads.png)

**總結:**我們通過主reactor線程來分發成功建立的套接字,通過從reactor線程來分發已連接套接字上的IO事件,通過工作線程來處理耗時操作!更進一步—通過用戶態自己建立的協程機制來調度業務處理程序,用戶態自己管理協程間切換,避免了CPU切換線程,又能爲程序帶來更高的處理效率!

3. 五種網絡IO簡介

  • 阻塞IO
  • 非阻塞IO
  • IO多路複用
  • 異步IO
  • 信號驅動IO

阻塞IO:
當應用程序調用阻塞IO完成某個操作時,應用程序會被掛起,感覺上應用程序像是被“阻塞”了一樣。實際上,內核所做的事情是將CPU時間切換給了其他有需要的進程,網絡應用程序在這種情況下就會得不到CPU時間做該做的事情。

非阻塞IO:
當應用程序調用非阻塞IO完成某個操作時,內核立即返回,不會把CPU時間讓出給其他進程,應用程序在返回後可以得到足夠的CPU時間做其他的事情。

IO多路複用:
我們可以把標準輸入、套接字都看作IO的一路,多路複用的意思,就是在任何一路IO有“事件”發生的情況下,通知應用程序去處理相應的IO事件,這樣我們的程序就“好像”在同一時刻處理多個IO事件。

異步IO:
當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的部件在完成後,通過狀態、通知和回調來通知調用者。

信號驅動IO:
應用進程使用 sigaction 系統調用,內核立即返回,應用進程可以繼續執行。當數據報準備好讀取時,內核就爲該進程產生一個SIGIO信號,我們隨後可以在信號處理函數中讀取數據報,也可以立即通知主循環,讓他讀取數據。

4.網絡IO的本質

網絡IO的本質就是socket流的讀取,通常一次IO讀操作會涉及到兩個對象和兩個階段。

兩個對象:

  1. 用戶進程(線程)
  2. 內核對象

兩個階段:

  1. 等待數據流準備
  2. 從內核像進程複製數據

對於socket流而言:

  1. 第一步通常涉及等待網絡上的數據分組到達,然後被複制到內核的某個緩衝區。
  2. 第二步把數據從內核緩衝區複製到進程緩衝區。

5. 如何區分阻塞IO和非阻塞IO

阻塞IO發起的read請求,線程會被掛起,一直等到內核數據準備好,並把數據從內核區域拷貝到應用程序的緩衝區中,拷貝完成後,read請求調用才返回。
阻塞IO.png
(圖·阻塞IO.png)

非阻塞IO的read請求在數據爲準備的情況下立即返回,應用程序可以不斷輪詢內核,直到數據準備好,內核將數據拷貝到應用程序緩衝區並完成這次read調用。
非阻塞IO.png
(圖·非阻塞IO.png)

6. 如何區分同步和異步

同步調用異步調用是對於獲取數據的過程而言的,前面的幾種最後獲取數據的read操作調用,都是同步的,即在read調用時,內核將數據從內核空間拷貝到應用程序空間,這個過程是在read函數中同步進行的。
同步調用.png
(圖·同步調用.png)

當我們發起異步讀(aio_read)之後,就立即返回,內核自動將數據從內核空間拷貝到應用程序空間,這個拷貝過程是異步的,內核自動完成的,和前面的同步操作不一樣,應用程序並不需要主動發起拷貝動作。
異步調用.png
(圖·異步調用.png)

7. 個人整理的網絡IO思維導圖

閱讀網絡IO相關資料並整理成思維導圖,花了我將近一個月的空餘時間,每天下班加班幹完活兒就是看極客時間盛延敏老師的《網絡編程實戰》,他寫的言簡意賅,評論區大家的討論也非常之精彩,如果大家想深入瞭解網絡編程這塊,還是推薦大家直接購買盛延敏老師的專欄。

網絡編程思維導圖.jpg
(圖·網絡編程思維導圖.png)

有需要的可以關注我的微信公衆號《從菜鳥到大佬》,聯繫我,我分享給你xmind的思維導圖原版,大家一起學習,一起進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章