歡迎光臨
每天分享高質量文章

【剖析 | SOFARPC 框架】系列之 SOFARPC 執行緒模型剖析

SOFA

Scalable Open Financial Architecture

是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。


本文為《剖析 | SOFARPC 框架》第五篇

《剖析 | SOFARPC 框架》系列由 SOFA 團隊和原始碼愛好者們出品,

專案代號:,官方目錄目前已經全部認領完畢。


但我們同樣期待大家根據自己的實際應用,編寫 SOFARPC 相關內容,可以留言提供相關連結,我們也會選擇性的給大家寄送禮物哦 ^_^

  前言

上一篇,我們介紹了 SOFARPC 同步非同步的實現,本文我們將會介紹 SOFARPC 中的執行緒模型。


本文會從同步非同步,阻塞非阻塞開始講起,進而探討常見的執行緒模型設計,之後,我們會介紹下 SOFABolt 中對 Netty 的模型使用,最後詳解 SOFARPC 在一次呼叫過程中各個步驟執行的執行緒。

  幾種常見的 IO 模型

首先介紹一下 Linux 的幾種 IO 模型,以行程從 Socket 中讀取資料為例。實際上,行程最終是透過 recvfrom 系統呼叫來讀取資料。這個時候,系統核心在收到之後,根據 IO 模型的不同,處理是不同的。

註意,圖下的紅色部分表示阻塞時間。

1. 阻塞 I/O


阻塞 I/O(blocking I/O) 模型是最流行,最簡單易用的 I/O 模型,預設情況下,所有套接字和檔案描述符就是阻塞的。阻塞 I/O 將使請求行程阻塞,直到請求完成或出錯。

2. 非阻塞 I/O


非阻塞 I/O(nonblocking I/O)的含義:如果 I/O 操作會導致請求行程休眠,則不要把它掛起,也就是不會讓出 CPU,而是傳回一個錯誤告訴它(可能是 EWOULDBLOCK 或者 EAGAIN)。

3. I/O 復用


I/O 多路復用(I/O multiplexing)會用到 select 或者 poll 或者 epoll 函式,這幾個函式也會使行程阻塞,但是和阻塞 I/O 所不同的的,函式可以同時阻塞多個 I/O 操作。而且可以同時對多個讀操作,多個寫操作的 I/O 函式進行檢測,直到有資料可讀或可寫時,才真正呼叫 I/O 操作函式。

4. 訊號驅動式 I/O


訊號驅動 I/O(signal-driver I/O)使用訊號,讓核心在描述符就緒時傳送 SIGIO 訊號通知我們進行處理,這時候我們就可以開始真正的讀了。

5. 非同步 I/O


非同步 I/O(asynchronous I/O) 由 POSIX 規範定義,包含一系列以 aio 開頭的介面。一般地說,這些函式的工作機制是:告知核心啟動某個操作,並讓核心在整個操作(包括將資料從核心空間複製到使用者空間)完成後通知我們。


這種模型與訊號驅動模型的主要區別是:訊號驅動 I/O 是由核心通知我們何時可以啟動一個 I/O 操作,而非同步 I/O 模型是由核心通知我們 I/O 操作何時完成。

五種常見的 IO 模型彙總

綜上,我們給出一個大家比較熟知的比較圖。方便理解。

  JAVA BIO & NIO

在瞭解了核心層面上這幾個執行緒模型之後,我們要給大家介紹下 JAVA BIO 和 JAVA NIO。

1. JAVA BIO

首先我們給大家看一個直接使用 JAVA BIO 寫得一個服務端。


傳統的BIO裡面socket.read(),如果TCP RecvBuffer裡沒有資料,呼叫會一直阻塞,直到收到資料,傳回讀到的資料。

2. JAVA NIO

對於 NIO,如果 TCP 的 buffer 中有資料,就把資料從網絡卡讀到記憶體,並且傳回給使用者;反之則直接傳回 0,永遠不會阻塞。下麵是一段比較典型的 NIO 的處理程式碼。


我們可以將 JAVA NIO 和多路復用結合起來。這裡也是最簡單的 Reactor 樣式:註冊所有感興趣的事件處理器,單執行緒輪詢選擇就緒事件,執行事件處理器。


這裡簡單比較了一下以前的 BIO 和現在的 NIO,新的 NIO 給我們帶來瞭如下的好處。

  • 事件驅動模型

  • 單執行緒處理多工

  • 非阻塞 I/O,I/O 讀寫不再阻塞,而是傳回 0

  • 基於塊的傳輸,比基於流的傳輸更高效

  • 更高階的 IO 函式,零複製

  • 允許 IO 多路復用

  Reactor 執行緒模型

前面說了,我們有了 JAVA NIO ,可以用多路復用。有些同學可能會問,不能直接使用嗎?答案是可以直接使用。但是技術層面上的問題雖然解決了,在工程層面,實現一個高效沒有問題的架構依然很難,而且這種多路復用,對程式設計思維有比較大的挑戰,所以,工程層面還不夠。


因此,有了 Reactor 程式設計模型,一般情況下,I/O 復用機制需要事件分發器,以上這個分發事件的模型太簡單了。實際使用起來會有一些效能問題。目前比較流行的是 Reactor 和 Proactor,本文不介紹 Proactor 模型,有興趣的同學可以自己學習。


標準/典型的 Reactor 中定義了三個角色:



而一個標準的操作流程則是

  • 步驟1:等待事件到來(Reactor 負責)。

  • 步驟2:將讀就緒事件分發給使用者定義的處理器(Reactor 負責)。

  • 步驟3:讀資料(使用者處理器負責)。

  • 步驟4:處理資料(使用者處理器負責)。

在這個標準之下,Reactor有幾種演進樣式可以選擇。註意 Reactor 重點描述的是 IO部分的操作,包括兩部分,連線建立和 IO讀寫。

1. 單執行緒模型

Reactor 單執行緒模型指的是所有的IO操作都在同一個NIO執行緒上面完成,NIO執行緒的職責如下:

  1. 作為 NIO 服務端,接收客戶端的 TCP 連線;

  2. 作為 NIO 客戶端,向服務端發起 TCP 連線;

  3. 讀取通訊對端的請求或者應答訊息;

  4. 向通訊對端傳送訊息請求或者應答訊息。



這是最基本的單 Reactor 單執行緒模型。其中 Reactor 執行緒,負責多路分離套接字,有新連線到來觸發 connect 事件之後,交由 Acceptor 進行處理,有 IO 讀寫事件之後交給 hanlder 處理。

Acceptor 主要任務就是構建 handler ,在獲取到和 client 相關的 SocketChannel 之後 ,系結到相應的 handler上,對應的 SocketChannel 有讀寫事件之後,基於 reactor 分發,hanlder 就可以處理了(所有的 IO 事件都系結到selector 上,由 Reactor 分發)。

該模型 適用於處理器鏈中業務處理元件能快速完成的場景。不過,這種單執行緒模型不能充分利用多核資源,所以實際使用的不多。

2. 多執行緒模型

Reactor 多執行緒模型與單執行緒模型最大的區別就是將 IO 操作和非 IO 操作做了分離。效率提高。



Reactor多執行緒模型的特點:

  1. 有專門一個NIO執行緒-Acceptor 執行緒用於監聽服務端,主要接收客戶端的 TCP 連線請求;

  2. 網路 IO 操作-讀、寫等由一個單獨的 NIO 執行緒池負責,執行緒池可以採用標準的 JDK 執行緒池實現,它包含一個任務佇列和 N 個可用的執行緒,由這些 NIO 執行緒負責訊息的解碼、處理和編碼;

3. 主從多執行緒模型

這個也是目前大部分 RPC 框架,或者服務端處理的主要選擇。

Reactor 主從多執行緒模型的特點:

服務端用於接收客戶端連線的不再是個1個單獨的 NIO 執行緒,而是一個獨立的 NIO 執行緒池。



主要的工作流程

  1. MainReactor 將連線事件分發給 Acceptor

  2. Acceptor 接收到客戶端 TCP 連線請求處理完成後(可能包含接入認證,黑名單等),將新建立的 SocketChannel 註冊到 IO 執行緒池(sub reactor執行緒池)的某個IO執行緒上,Acceptor 執行緒池僅僅只用於客戶端的登陸、握手和安全認證。

  3. SubReactor 負責 SocketChannel 的讀寫和編解碼工作。其 IO 執行緒負責後續的 IO 操作。

  SOFARPC 執行緒模型

整體執行緒模型

對於 SOFARPC 來說,和底層的 SOFABolt 一起,在使用 Netty 的 Reactor 主從模型的基礎上,支援業務執行緒池的選擇。


執行緒模型

目前 SOFARPC 服務端的執行緒模型在綜合考慮,和一些歷史壓測的資料支撐的情況下,我們選了主從執行緒模型,並對序列化和業務程式碼執行使用一個 BizThreadPool(允許對執行緒池的核心執行緒數,佇列等進行調整),或者自定義的執行緒池。將序列化,反序列化等這些耗時的操作全部放在了Biz執行緒池中,這樣可以有效地提高系統的整體吞吐量。


特別的,這裡對於 essay-header部分,我們將反序列化放在了 Worker執行緒中,這樣,可以在對效能影響極低的情況下,可以提供一些額外的好處,比如允許業務配置介面對應的執行緒池。

預設執行步驟

一次比較完整的 RPC 呼叫的時候,以下為預設的執行執行緒:


  • 客戶端

    長連線:Netty-Worker 執行緒

    序列化請求/反序列化響應:發起請求的執行緒,如果是 callback,是新的一個執行緒

    心跳:Netty-Worker 執行緒

  • 服務端

    埠:Netty-Boss 執行緒

    長連線:Netty-Worker 執行緒

    心跳:Netty-Worker 執行緒

    反序列化請求Header:Netty-Worker 執行緒

    反序列化請求Body/序列化響應:SOFARPC 業務執行緒池

自定義業務執行緒池

SOFARPC 支援自定義業務執行緒池,可以為指定服務設定一個獨立的業務執行緒池,和 SOFARPC 自身的業務執行緒池是隔離的。多個服務可以共用一個獨立的執行緒池。

實現原理

自定義執行緒池管理器封裝服務介面和自定義執行緒池對映關係,使用者建立配置自定義執行緒池,提供指定服務註冊自定義執行緒池。

BOLT 支援部分反序列化,所以框架會在 IO 執行緒池提前反序列化請求的 Header 頭部資料,註意,這部分一個普通的 Map,操作很快,一般不會成為瓶頸,Body 資料還是在業務執行緒內反序列化。


核心程式碼在自定義執行緒池管理器裡:

com.alipay.remoting.rpc.protocol.RpcRequestProcessor#process 選擇執行緒池 UserThreadPoolManager 註冊執行緒池。

感興趣的同學可以去看下。

使用方式

請求處理過程,預設是一個執行緒池,當這個執行緒池出現問題則會造成整體的吞吐量降低。而有些業務場景,希望對核心的請求處理過程單獨分配一個執行緒池。SOFARPC 提供執行緒池選擇器設定到使用者請求處理器裡面,呼叫過程即可根據選擇器的邏輯來選擇對應的執行緒池避免不同請求互相影響。

透過sofa:global-attrs元素的 thread-pool-ref 屬性為該服務設定自定義執行緒池。

<bean id="customExcutor" class="com.alipay.sofa.rpc.server.UserThreadPool" init-method="init">    <property name="corePoolSize" value="10" />    <property name="maximumPoolSize" value="10" />    <property name="queueSize" value="0" />
bean>

service ref="helloService" interface=" com.alipay.sofa.rpc.service.HelloService">    binding.bolt>        global-attrs thread-pool-ref="customExcutor"/>    

binding.bolt>
service>

  總結

透過這篇文章,我們介紹了幾種常見的 IO 模型,介紹了 JAVA 中的 IO和 NIO,同時也介紹了 IO 模型在工程上實踐不錯的 Reactor 模型。


最後,介紹了 SOFARPC 的執行緒模型,希望大家對整個執行緒模型有一定的理解,如果對 SOFARPC 執行緒模型和自定義執行緒池有疑問的,也歡迎留言與我們討論。

文中提到的連結:

  One more thing

歡迎加入 ,參與 SOFABolt 原始碼解析


SOFABolt 原始碼解析目錄:

我們會逐步詳細介紹每部分的程式碼設計和實現,預計會按照如下的目錄進行,以下也包含目前的原始碼分析文章的認領情況:

  • 【待認領】SOFABolt 的設計與私有協議解析

  • 【待認領】SOFABolt 的連結管理功能分析

  • 【待認領】SOFABolt 的超時控制機制及心跳機制

  • 【待認領】SOFABolt 編解碼機制(Codec)

  • 【待認領】SOFABolt 序列化機制(Serializer)

  • 【待認領】SOFABolt 協議框架解析


領取方式:

直接回覆本公眾號想認領的文章名稱,我們將會主動聯絡你,確認資質後,即可加入,It’s your show time!


相關連結

SOFA 檔案: http://www.sofastack.tech/

SOFA: https://github.com/alipay

SOFARPC: https://github.com/alipay/sofa-rpc

SOFABolt: https://github.com/alipay/sofa-bolt


長按關註,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay

贊(0)

分享創造快樂