【剖析 | SOFARPC 框架】之SOFARPC 同步非同步實現剖析
前言
這一篇,我們為大家帶來了開發過程中,最常接觸到的同步非同步呼叫解析。本文會介紹下同步非同步的使用場景,以及 SOFARPC 中的程式碼實現機制,為了方便大家理解和閱讀程式碼。不會過多的設計程式碼實現細節,更多的還是希望大家從中有所收穫,並能夠獨立閱讀核心程式碼。
原理剖析
SOFARPC 以基於 Netty 實現的網路通訊框架 SOFABolt 用作遠端通訊框架,使用者不用關心如何實現私有協議的細節,直接使用內建 RPC 通訊協議,啟動客戶端與服務端,同時註冊使用者請求處理器即可完成遠端呼叫:
SOFARPC 服務呼叫提供同步 Sync、非同步 Future、回撥 Callback 以及單向 Oneway 四種呼叫型別:
來自 SOFABolt 官網
這裡我們先提供一張整體的圖,後面每個方式原理介紹的時候,我會進行更加詳細的解釋。讀者可以重點閱讀以下部分的圖示,根據阻塞時間的長短,會有不同的標識。
Sync 同步呼叫
同步呼叫是指的客戶端發起呼叫後,當前執行緒會被阻塞,直到等待服務端返回結果或者出現了超時異常,再進行後續的操作,是絕大多數 RPC 的預設呼叫方式,無需進行任何設定即可。
這種呼叫方式,當前執行緒發起呼叫後阻塞請求執行緒,需要在指定的超時時間內等到響應結果才能完成本次呼叫。如果超時時間內沒有得到響應結果,那麼丟擲超時異常。Sync 同步呼叫模式最常用,注意要根據對端的處理能力合理設定超時時間。
如上圖所示,這裡主要是描述了客戶端的處理邏輯,其中客戶端執行緒和 RPC 內部部分處理並不在一個執行緒裡。所以這裡客戶端執行緒包含其中一部分操作,後文的圖中也是類似。其中紅色的樹狀框表示客戶端的執行緒阻塞。
可以看到,客戶端在程式碼片段2中,發起 RPC 呼叫,那麼除非本次 RPC 徹底完成,或者 RPC 在指定時間內丟擲超時異常,否則紅框一直阻塞,程式碼片段3沒有機會執行。
Future 非同步呼叫
客戶端發起呼叫後不會同步等待服務端的結果,而是獲取到 RPC框架給到的一個 Future 物件,呼叫過程不會阻塞執行緒,然後繼續執行後面的業務邏輯。服務端返回響應結果被 RPC 快取,當客戶端需要響應結果的時候需要主動獲取結果,獲取結果的過程阻塞執行緒。
如上圖所示,程式碼片段2發起 RPC 呼叫後,RPC 框架會立刻返回一個 Future 物件。給到程式碼片段2,程式碼片段2可以選擇等待結果,或者也可以繼續執行程式碼片段3,等程式碼片段3執行完成後,再獲取 Future 中的值。
Callback 回撥呼叫
客戶端提前設定回撥實現類,在發起呼叫後不會等待結果,但是注意此時是通過上下文或者其他方式向 RPC 框架註冊了一個 Callback 物件,結果處理是在新的執行緒裡執行。RPC在獲取到服務端的結果後會自動執行該回調實現。
如圖所示,客戶端程式碼段2發起 RPC 呼叫後,並不關心結果,此時也不會有結果。只是將自己的一個 Callback 物件傳遞給 RPC 框架,RPC 框架發起呼叫後,立即返回。之後自己等待呼叫結果,在有了呼叫結果,或者超過業務配置的超時時間後,將響應結果或者超時的異常,進行 callback 的回撥。一般的,一個 callback 的結果需要包含兩個部分
public interface InvokeCallback {
/**
* Response received.
*
* @param result
*/
public void onResponse(final Object result);
/**
* Exception caught.
*
* @param e
*/
public void onException(final Throwable e);
}
如果是正常返回,則 RPC 框架回呼叫戶傳入 callback 物件的 onResponse 方法,如果是框架層的異常,比如超時,那麼會呼叫 onException 方法。
Oneway 單向呼叫
客戶端傳送請求後不會等待服務端返回的結果,並且會忽略服務端的處理結果,
當前執行緒發起呼叫後,使用者並不關心呼叫結果,只要請求已經發出就完成本次呼叫。單向呼叫不關心響應結果,請求執行緒不會被阻塞,使用 Oneway 呼叫需要注意控制呼叫節奏防止壓垮接收方。注意 Oneway 呼叫不保證成功,而且發起方無法知道呼叫結果。因此通常用於可以重試,或者定時通知類的場景,呼叫過程是有可能因為網路問題、機器故障等原因導致請求失敗,業務場景需要能接受這樣的異常場景才能夠使用。
呼叫方式比較
呼叫方式 | 優點 | 不足 | 使用場景 |
---|---|---|---|
Sync | 簡單 | 同步阻塞 | 大部分場景 |
Oneway | 簡單,不阻塞 | 無結果 | 不需要結果,業務不需要保證呼叫成功的場景 |
Future | 非同步,可獲取結果 | 需要再次呼叫 get方法獲取結果 | 同線程內多次 RPC呼叫。且沒有先後關係 |
Callback | 非同步,不需要手動獲取結果 | 使用稍微複雜。且不能在當前程式碼段直接操作結果 | 當前不關心結果。但是最終依賴結果做一些其他事情的場景 |
原始碼剖析
下面我們以 SOFARPC 中的 BOLT 協議為基礎,介紹一些 RPC 框架下面的程式碼層面的設計。主要介紹程式碼結構和相互的呼叫關係。
對 BOLT 的包裝主要在
com.alipay.sofa.rpc.transport.bolt.BoltClientTransport
業務方並不直接使用 BOLT 定義的一些型別,而是使用 RPC 定義的一些型別。這些型別被適配到 BOLT 的型別上,使得 RPC 框架對使用者提供了統一的 API,和底層是否採用 BOLT 不強相關。
Sync 同步呼叫
SOFARPC 中的的同步呼叫是由 Bolt 通訊框架來實現的。核心程式碼實現在
com.alipay.remoting.BaseRemoting#invokeSync
com.alipay.remoting.rpc.protocol.RpcResponseProcessor#doProcess
使用時無需特殊配置。
Future 非同步呼叫
使用 Future 非同步呼叫 SOFABoot 配置服務引用需要設定
<sofa:global-attrs type="future"/>
元素的 type 屬性宣告呼叫方式為 future:
如上設定為 Future 呼叫的方式。客戶端獲取響應結果有兩種方式:
1.通過 SofaResponseFuture 直接獲取結果。第一個引數是獲取結果的超時時間,第二個引數表示是否清除執行緒上下文中的結果。
String result =(String)SofaResponseFuture.getResponse(timeout,true);
2.獲取原生 Futrue,該種方式獲取JDK原生的 Future,引數表示是否清除執行緒上下文中的結果。因為響應結果放在JDK原生的 Future,需要通過JDK Future的get()方法獲取響應結果。
Future future = SofaResponseFuture.getFuture(true);
當前執行緒發起呼叫得到 RpcResponseFuture 物件,當前執行緒繼續執行下一次呼叫。在任意時刻使用RpcResponseFuture 物件的 get() 方法來獲取結果,如果響應已經回來此時就馬上得到結果;如果響應沒有回來則阻塞住當前執行緒直到響應回來或者超時時間到。
Callback 回撥呼叫
目前支援 bolt 協議。客戶端回撥類需要實現
com.alipay.sofa.rpc.core.invoke.SofaResponseCallback
介面
使用 SOFABoot的話配置
<sofa:global-attrs type="callback" callback-ref="callback"/>
如上設定是服務級別的設定,也可以進行呼叫級別的設定:
RpcInvokeContext.getContext().setResponseCallback(sofaResponseCallbackImpl);
當前執行緒發起呼叫則本次呼叫馬上結束執行下一次呼叫。發起呼叫時需要註冊回撥,該回調需要分配非同步執行緒池以待響應回來後在回撥的非同步執行緒池來執行回撥邏輯。
Oneway 單向呼叫
使用 Oneway 單向呼叫 SOFABoot 配置服務引用需要設定
<sofa:global-attrs type="oneway"/>
元素的type屬性宣告呼叫方式 oneway
技術實現
超時計算
在同步中,有個很重要的事情就是超時計算。同步 Sync/非同步Future/回撥Callback三種通訊模型,通過採用HashedWheelTimer 進行超時控制,對這部分感興趣的,可以參考螞蟻通訊框架實踐,這裡 不再重複說明。
這裡畫出一張超時的時間圖,對 SOFARPC 中 Tracer 中的超時中涉及到的時間點做一個介紹。
通過這張圖中的介紹,加上 SOFATracer 的日誌列印,我們可以在實際的線上環境中,判斷出來,哪一部分耗時比較嚴重,來定位一些超時的問題。
對於 SOFARPC 框架的使用方來說,很多時候是非常關心超時時間的,因為超時時間如果設定時間過長,會阻塞業務執行緒,極端場景下,可能會拖垮整個系統。RPC框架允許使用者設定不同級別的超時時間來控制。
/**
* 決定超時時間
*
* @param request 請求
* @param consumerConfig 客戶端配置
* @param providerInfo 服務提供者資訊
* @return 呼叫超時
*/
private int resolveTimeout(SofaRequest request, ConsumerConfig consumerConfig, ProviderInfo providerInfo) {
// 先去呼叫級別配置
Integer timeout = request.getTimeout();
if (timeout == null) {
// 取客戶端配置(先方法級別再介面級別)
timeout = consumerConfig.getMethodTimeout(request.getMethodName());
if (timeout == null || timeout < 0) {
// 再取服務端配置
timeout = (Integer) providerInfo.getDynamicAttr(ATTR_TIMEOUT);
if (timeout == null) {
// 取框架預設值
timeout = getIntValue(CONSUMER_INVOKE_TIMEOUT);
}
}
}
return timeout;
}
目前,我們
1.先取呼叫級別,這個是通過呼叫執行緒上下文可以設定的。
2.然後取客戶端配置的消費者超時時間,先取方法級別配置,如果沒有,取介面級別。
3.如果還是沒有取到,這時候,我們取服務提供方的超時時間,這個會通過註冊中心傳遞下來。
4.最終,我們取預設的超時時間,目前這個超時時間是3s。
注意,在真實的場景下,超時控制實際上是一個比較有挑戰的事情,一旦出現 JVM層面的 STW,時間控制就會變得不夠準確。因此,如果系統層面存在某些效能問題,也會影響超時的計算,這時候,會看到,已經超過了超時時間,但是客戶端並沒有及時終止。
執行緒模型
在上面介紹同步非同步等多種呼叫方式中,最重要的需要理解同步/非同步、阻塞/非阻塞的幾種組合情況,並且能知道什麼事情在什麼執行緒裡操作,這會涉及到具體的執行緒模型,由於篇幅原因,本文不做介紹,我們會在下一篇中帶來 SOFARPC 的執行緒模型剖析文章。
總結
SOFARPC 同步/非同步/回撥/單向呼叫通過引用呼叫型別(預設為同步呼叫)四種呼叫方式。
在 Sync 上,支援方法級別,介面級別,方法級別的超時設定。呼叫會阻塞請求執行緒,待響應返回後才能進行下一個請求。這是最常用的一種通訊模型。
在 Callback 上,支援方法級別,介面級別,執行緒級別的回撥設定。是真正的非同步呼叫,永遠不會阻塞執行緒,結果處理是在非同步執行緒裡執行。
在 Future 上,對使用者提供了統一的 API 操作。支援原生 Future 和自定義 Future。使用者 可以直接在當前執行緒上下文獲取。在呼叫過程不會阻塞執行緒,但獲取結果的過程會阻塞執行緒。
在 Oneway 上,設定簡單。直接支援。為了防止應用出現型別轉換異常,根據返回值設定不同的預設值。不關心響應,請求執行緒不會被阻塞,但使用時需要注意控制呼叫節奏,防止壓垮接收方。
在超時控制上,結合 BOLT 和 Tracer,將一些關鍵的時間節點進行了整理。使得排查和判斷超時問題更加方便。到這裡,我們就對 RPC 框架中的同步非同步實現進行了一些詳細的分析,並深入介紹了 SOFARPC 中的實現細節,感謝大家。
長按關注,獲取分散式架構乾貨
歡迎大家共同打造 SOFAStack https://github.com/alipay