Linux高性能服務器編程模式

本文時間:2018-11-21,作者:krircc, 簡介:天青色

歡迎向Rust中文社區投稿,投稿地址,好文將在以下地方直接展示

  1. Rust中文社區首頁
  2. Rust中文社區Rust文章欄目
  3. 知乎專欄Rust語言

高性能服務器至少要滿足如下幾個需求:

  • 效率高:既然是高性能,那處理客戶端請求的效率當然要很高了
  • 高可用:不能隨便就掛掉了
  • 編程簡單:基於此服務器進行業務開發需要足夠簡單
  • 可擴展:可方便的擴展功能
  • 可伸縮:可簡單的通過部署的方式進行容量的伸縮,也就是服務需要無狀態

而滿足如上需求的一個基礎就是高性能的IO!

講到高性能IO繞不開Reactor模式,它是大多數IO相關組件如Netty、Redis在使用的IO模式

幾乎所有的網絡連接都會經過讀請求內容——》解碼——》計算處理——》編碼回覆——》回覆的過程

Socket

Socket之間建立鏈接及通信的過程!實際上就是對TCP/IP連接與通信過程的抽象:

  • 服務端Socket會bind到指定的端口上,Listen客戶端的"插入"
  • 客戶端Socket會Connect到服務端
  • 當服務端Accept到客戶端連接後
  • 就可以進行發送與接收消息了
  • 通信完成後即可Close

阻塞IO(BIO)、非阻塞IO(NBIO)、同步IO、異步IO

  • 一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作
  • 阻塞IO和非阻塞IO的區別在於第一步:發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO;如果不阻塞,那麼就是非阻塞IO
  • 同步IO和異步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那麼就是同步IO,因此阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO;如果不阻塞,而是操作系統幫你做完IO操作再將結果返回給你,那麼就是異步IO

BIO優點

  • 模型簡單
  • 編碼簡單

BIO缺點

  • 性能瓶頸低

缺點:主要瓶頸在線程上。每個連接都會建立一個線程。雖然線程消耗比進程小,但是一臺機器實際上能建立的有效線程有限,且隨着線程數量的增加,CPU切換線程上下文的消耗也隨之增加,在高過某個閥值後,繼續增加線程,性能不增反降!而同樣因爲一個連接就新建一個線程,所以編碼模型很簡單!

就性能瓶頸這一點,就確定了BIO並不適合進行高性能服務器的開發!

NBIO:

  • Acceptor註冊Selector,監聽accept事件
  • 當客戶端連接後,觸發accept事件
  • 服務器構建對應的Channel,並在其上註冊Selector,監聽讀寫事件
  • 當發生讀寫事件後,進行相應的讀寫處理

優點

  • 性能瓶頸高

缺點

  • 模型複雜
  • 編碼複雜
  • 需處理半包問題

NBIO的優缺點和BIO就完全相反了!性能高,不用一個連接就建一個線程,可以一個線程處理所有的連接!相應的,編碼就複雜很多,從上面的代碼就可以明顯體會到了。還有一個問題,由於是非阻塞的,應用無法知道什麼時候消息讀完了,就存在了半包問題!需要自行進行處理!例如,以換行符作爲判斷依據,或者定長消息發生,或者自定義協議!

NBIO雖然性能高,但是編碼複雜,且需要處理半包問題!爲了方便的進行NIO開發,就有了Reactor模型!

Proactor和Reactor

Proactor和Reactor是兩種經典的多路複用I/O模型,主要用於在高併發、高吞吐量的環境中進行I/O處理。

I/O多路複用機制都依賴於一個事件分發器,事件分離器把接收到的客戶事件分發到不同的事件處理器中,如下

event

select,poll,epoll

在操作系統級別select,poll,epoll是3個常用的I/O多路複用機制,簡單瞭解一下將有助於我們理解Proactor和Reactor。

select

select的原理如下:

select

用戶程序發起讀操作後,將阻塞查詢讀數據是否可用,直到內核準備好數據後,用戶程序纔會真正的讀取數據。

poll與select的原理相似,用戶程序都要阻塞查詢事件是否就緒,但poll沒有最大文件描述符的限制。

epoll

epoll是select和poll的改進,原理圖如下:

epoll

epoll使用“事件”的方式通知用戶程序數據就緒,並且使用內存拷貝的方式使用戶程序直接讀取內核準備好的數據,不用再讀取數據

Proactor

Proactor是一個異步I/O的多路複用模型,原理圖如下:

proactor

  • 用戶發起IO操作到事件分離器
  • 事件分離器通知操作系統進行IO操作
  • 操作系統將數據存放到數據緩存區
  • 操作系統通知分發器IO完成
  • 分離器將事件分發至相應的事件處理器
  • 事件處理器直接讀取數據緩存區內的數據進行處理

Reactor

Reactor是一個同步的I/O多路複用模型,它沒有Proactor模式那麼複雜,原理圖如下:

reactor

  • 用戶發起IO操作到事件分離器
  • 事件分離器調用相應的處理器處理事件
  • 事件處理完成,事件分離器獲得控制權,繼續相應處理

Proactor和Reactor的比較

  • Reactor模型簡單,Proactor複雜
  • Reactor是同步處理方式,Proactor是異步處理方式
  • Proactor的IO事件依賴操作系統,操作系統須支持異步IO
  • 同步與異步是相對於服務端與IO事件來說的,Proactor通過操作系統異步來完成IO操作,當IO完成後通知事件分離器,而Reactor需要自己完成IO操作

Reactor多線程模型

前面已經簡單介紹了Proactor和Reactor模型,在實際中Proactor由於需要操作系統的支持,實現的案例不多,有興趣的可以看一下Boost Asio的實現,我們主要說一下Reactor模型,Netty也是使用Reactor實現的。

但單線程的Reactor模型每一個用戶事件都在一個線程中執行:

  • 性能有極限,不能處理成百上千的事件
  • 當負荷達到一定程度時,性能將會下降
  • 單某一個事件處理器發送故障,不能繼續處理其他事件

多線程Reactor

使用線程池的技術來處理I/O操作,原理圖如下:

muti-thread

  • Acceptor專門用來監聽接收客戶端的請求
  • I/O讀寫操作由線程池進行負責
  • 每個線程可以同時處理幾個鏈路請求,但一個鏈路請求只能在一個線程中進行處理

主從多線程Reactor

在多線程Reactor中只有一個Acceptor,如果出現登錄、認證等耗性能的操作,這時就會有單點性能問題,因此產生了主從Reactor多線程模型,原理如下:

master-worker

  • Acceptor不再是一個單獨的NIO線程,而是一個獨立的NIO線程池
  • Acceptor處理完後,將事件註冊到IO線程池的某個線程上
  • IO線程繼續完成後續的IO操作
  • Acceptor僅僅完成登錄、握手和安全認證等操作,IO操作和業務處理依然在後面的從線程中完成

Reactor模式結構

在解決了什麼是Reactor模式後,我們來看看Reactor模式是由什麼模塊構成。圖是一種比較簡潔形象的表現方式,因而先上一張圖來表達各個模塊的名稱和他們之間的關係:

Reactor_Structures

  • Handle:即操作系統中的句柄,是對資源在操作系統層面上的一種抽象,它可以是打開的文件、一個連接(Socket)、Timer等。由於Reactor模式一般使用在網絡編程中,因而這裏一般指Socket Handle,即一個網絡連接(Connection,在Java NIO中的Channel)。這個Channel註冊到Synchronous Event Demultiplexer中,以監聽Handle中發生的事件,對ServerSocketChannnel可以是CONNECT事件,對SocketChannel可以是READ、WRITE、CLOSE事件等。

  • Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到來,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的執行返回的事件類型。這個模塊一般使用操作系統的select來實現。在Java NIO中用Selector來封裝,當Selector.select()返回時,可以調用Selector的selectedKeys()方法獲取Set<SelectionKey>,一個SelectionKey表達一個有事件發生的Channel以及該Channel上的事件類型。上圖的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是對的,那內部實現應該是select()方法在事件到來後會先設置Handle的狀態,然後返回。不瞭解內部實現機制,因而保留原圖。

  • Initiation Dispatcher:用於管理Event Handler,即EventHandler的容器,用以註冊、移除EventHandler等;另外,它還作爲Reactor模式的入口調用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。

  • Event Handler:定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。

  • Concrete Event Handler:事件EventHandler接口,實現特定事件處理邏輯。

優點

1)響應快,不必爲單個同步時間所阻塞,雖然Reactor本身依然是同步的;

2)編程相對簡單,可以最大程度的避免複雜的多線程及同步問題,並且避免了多線程/進程的切換開銷;

3)可擴展性,可以方便的通過增加Reactor實例個數來充分利用CPU資源;

4)可複用性,reactor框架本身與具體事件處理邏輯無關,具有很高的複用性;

缺點

1)相比傳統的簡單模型,Reactor增加了一定的複雜性,因而有一定的門檻,並且不易於調試。

2)Reactor模式需要底層的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系統的select系統調用支持,如果要自己實現Synchronous Event Demultiplexer可能不會有那麼高效。

3) Reactor模式在IO讀寫數據時還是在同一個線程中實現的,即使使用多個Reactor機制的情況下,那些共享一個Reactor的Channel如果出現一個長時間的數據讀寫,會影響這個Reactor中其他Channel的相應時間,比如在大文件傳輸時,IO操作就會影響其他Client的相應時間,因而對這種操作,使用傳統的Thread-Per-Connection或許是一個更好的選擇,或則此時使用Proactor模式。

Reactor中的組件

  • Reactor:Reactor是IO事件的派發者。
  • Acceptor:Acceptor接受client連接,建立對應client的Handler,並向Reactor註冊此Handler。
  • Handler:和一個client通訊的實體,按這樣的過程實現業務的處理。一般在基本的Handler基礎上還會有更進一步的層次劃分, 用來抽象諸如decode,process和encoder這些過程。比如對Web Server而言,decode通常是HTTP請求的解析, process的過程會進一步涉及到Listener和Servlet的調用。業務邏輯的處理在Reactor模式裏被分散的IO事件所打破, 所以Handler需要有適當的機制在所需的信息還不全(讀到一半)的時候保存上下文,並在下一次IO事件到來的時候(另一半可讀了)能繼續中斷的處理。爲了簡化設計,Handler通常被設計成狀態機,按GoF的state pattern來實現。

Rust異步網絡編程

Rust的高性能異步網絡編程模式目前是基於miofutures這兩個庫構建的生態。

Tokio則連接這2個庫構建了一個異步非阻塞事件驅動編程平臺。

什麼是 mio,futures,tokio

1- Mio

Mio是Rust的輕量級快速低級IO庫,專注於非阻塞API,事件通知以及用於構建高性能IO應用程序的其他有用實用程序.

特徵

  • 快速 - 相當於OS設施級別的最小開銷(epoll,kqueue等..)
  • 非阻塞TCP,UDP。
  • 由epoll,kqueue和IOCP支持的I/O事件通知隊列。
  • 運行時零分配
  • 平臺特定擴展。

平臺支持

  • Linux
  • OS X
  • Windows
  • FreeBSD
  • NetBSD
  • Solaris
  • Android
  • iOS
  • Fuchsia (experimental)

2- futures

Rust中的零成本異步編程庫,Futures可在沒有標準庫的情況下工作,例如在裸機環境中。

提供了許多用於編寫異步代碼的核心抽象:

  • Future是由異步計算產生的單一最終值。一些編程語言(例如JavaScript)將此概念稱爲“promise”。
  • Streams表示異步生成的一系列值。
  • Sinks支持異步寫入數據。
  • Executors負責運行異步任務。

還包含異步I/O和跨任務通信的抽象。

所有這些是任務系統的基礎,它是輕量級線程(協程)的一種形式。使用FutureStreamsSinks構建大型異步計算,然後將其生成作爲獨立完成的任務運行,但不阻塞運行它們的線程。

3- Tokio

Tokio : Rust編程語言的異步運行時,提供異步事件驅動平臺,構建快速,可靠和輕量級網絡應用。利用Rust的所有權和併發模型確保線程安全

  • 基於多線程,工作竊取的任務調度程序。
  • 一個反應器操基於作系統的事件隊列(epoll的,kqueue的,IOCP等)的支持。
  • 異步TCP和UDP套接字。

這些組件提供構建異步應用程序所需的運行時組件。

快速

Tokio構建於Rust之上,提供極快的性能,使其成爲高性能服務器應用程序的理想選擇。

1:零成本抽象

與完全手工編寫的等效系統相比,Tokio的運行時模型不會增加任何開銷。

使用Tokio構建的併發應用程序是開箱即用的。Tokio提供了針對異步網絡工作負載調整的多線程,工作竊取任務調度程序。

2:非阻塞I/O

Tokio由操作系統提供的非阻塞,事件I/O堆棧提供支持。

可靠

雖然Tokio無法阻止所有錯誤,但它的目的是最小化它們。Tokio在運送關鍵任務應用程序時帶來了安心。

1- 所有權和類型系統

Tokio利用Rust的類型系統來提供難以濫用的API。

2- Backpressure

Backpressure開箱即用,無需使用任何複雜的API。

3- 取消

Rust的所有權模型允許Tokio自動檢測何時不再需要計算。Tokio將自動取消它而無需用戶調用cancel函數。

輕量級

Tokio可以很好地擴展,而不會增加應用程序的開銷,使其能夠在資源受限的環境中茁壯成長。

1- 沒有垃圾收集器

因爲Tokio使用Rust,所以不包括垃圾收集器或其他語言運行時。

2- 模塊化

Tokio是一個小組件的集合。用戶可以選擇最適合手頭應用的部件,而無需支付未使用功能的成本。

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