SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分布式中間件,包含了建構金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。
本文為《剖析 | SOFARPC 架構》第四篇。
《剖析 | SOFARPC 架構》系列由 SOFA 團隊和源碼愛好者們出品,
項目代号:,文章尾部有參與方式,僅剩 2 篇。
前言
這一篇,我們為大家帶來了開發過程中,最常接觸到的同步異步調用解析。本文會介紹下同步異步的使用場景,以及 SOFARPC 中的代碼實作機制,為了友善大家了解和閱讀代碼。不會過多的設計代碼實作細節,更多的還是希望大家從中有所收獲,并能夠獨立閱讀核心代碼。
原理剖析
SOFARPC 以基于 Netty 實作的網絡通信架構 SOFABolt 用作遠端通信架構,使用者不用關心如何實作私有協定的細節,直接使用内置 RPC 通信協定,啟動用戶端與服務端,同時注冊使用者請求處理器即可完成遠端調用:
SOFARPC 服務調用提供同步 Sync、異步 Future、回調 Callback 以及單向 Oneway 四種調用類型:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yYyIDZykjZ4UmNzgDNllTO1UTY0IzY4YjZlFGOjBjM48CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
先提供一張整體的圖,後面每個方式原理介紹的時候,會進行更加詳細的解釋。讀者可以重點閱讀以下部分的圖示,根據阻塞時間的長短,會有不同的辨別。
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 調用不保證成功,而且發起方無法知道調用結果。
是以通常用于可以重試,或者定時通知類的場景,調用過程是有可能因為網絡問題、機器故障等原因導緻請求失敗,業務場景需要能接受這樣的異常場景才能夠使用。
以上介紹的調用方式比較
源碼剖析
下面我們以 SOFARPC 中的 BOLT 協定為基礎,介紹一些 RPC 架構下面的代碼層面的設計。主要介紹代碼結構和互相的調用關系:
對 BOLT 的包裝主要在
com.alipay.sofa.rpc.transport.bolt.BoltClientTransport
業務方并不直接使用 BOLT 定義的一些類型,而是使用 RPC 定義的一些類型。這些類型被适配到 BOLT 的類型上,使得 RPC 架構對使用者提供了統一的 API,和底層是否采用 BOLT 不強相關。
SOFARPC 中的的同步調用是由 Bolt 通信架構來實作的。核心代碼實作在
com.alipay.remoting.BaseRemoting#invokeSync
com.alipay.remoting.rpc.protocol.RpcResponseProcessor#doProcess
使用時無需特殊配置。
使用 Future 異步調用 SOFABoot 配置服務引用需要設定
<sofa:global-attrs type="future"/>
元素的 type 屬性聲明調用方式為 future:
如上設定為 Future 調用的方式。用戶端擷取響應結果有兩種方式:
- 通過 SofaResponseFuture 直接擷取結果。第一個參數是擷取結果的逾時時間,第二個參數表示是否清除線程上下文中的結果。
String result =(String)SofaResponseFuture.getResponse(timeout,true);
- 擷取原生 Futrue,該種方式擷取JDK原生的 Future,參數表示是否清除線程上下文中的結果。因為響應結果放在JDK原生的 Future,需要通過JDK Future的get()方法擷取響應結果。
Future future = SofaResponseFuture.getFuture(true);
目前線程發起調用得到 RpcResponseFuture 對象,目前線程繼續執行下一次調用。在任意時刻使用RpcResponseFuture 對象的 get() 方法來擷取結果,如果響應已經回來此時就馬上得到結果;如果響應沒有回來則阻塞住目前線程直到響應回來或者逾時時間到。
目前支援 bolt 協定。用戶端回調類需要實作
com.alipay.sofa.rpc.core.invoke.SofaResponseCallback
接口
使用 SOFABoot的話配置
<sofa:global-attrs type="callback" callback-ref="callback"/>
如上設定是服務級别的設定,也可以進行調用級别的設定:
RpcInvokeContext.getContext().setResponseCallback(sofaResponseCallbackImpl);
目前線程發起調用則本次調用馬上結束執行下一次調用。發起調用時需要注冊回調,該回調需要配置設定異步線程池以待響應回來後在回調的異步線程池來執行回調邏輯。
使用 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;
}
目前,我們
- 先取調用級别,這個是通過調用線程上下文可以設定的。
- 然後取用戶端配置的消費者逾時時間,先取方法級别配置,如果沒有,取接口級别。
- 如果還是沒有取到,這時候,我們取服務提供方的逾時時間,這個會通過注冊中心傳遞下來。
- 最終,我們取預設的逾時時間,目前這個逾時時間是3s。
注意,在真實的場景下,逾時控制實際上是一個比較有挑戰的事情,一旦出現 JVM層面的 STW,時間控制就會變得不夠準确。是以,如果系統層面存在某些性能問題,也會影響逾時的計算,這時候,會看到,已經超過了逾時時間,但是用戶端并沒有及時終止。
線程模型
在上面介紹同步異步等多種調用方式中,最重要的需要了解同步/異步、阻塞/非阻塞的幾種組合情況,并且能知道什麼事情在什麼線程裡操作,這會涉及到具體的線程模型,由于篇幅原因,本文不做介紹,我們會在下一篇中帶來 SOFARPC 的線程模型剖析文章。
總結
SOFARPC 同步/異步/回調/單向調用通過引用調用類型(預設為同步調用)四種調用方式。
在 Sync 上,支援方法級别,接口級别,方法級别的逾時設定。調用會阻塞請求線程,待響應傳回後才能進行下一個請求。這是最常用的一種通信模型。
在 Callback 上,支援方法級别,接口級别,線程級别的回調設定。是真正的異步調用,永遠不會阻塞線程,結果處理是在異步線程裡執行。
在 Future 上,對使用者提供了統一的 API 操作。支援原生 Future 和自定義 Future。使用者 可以直接在目前線程上下文擷取。在調用過程不會阻塞線程,但擷取結果的過程會阻塞線程。
在 Oneway 上,設定簡單。直接支援。為了防止應用出現類型轉換異常,根據傳回值設定不同的預設值。不關心響應,請求線程不會被阻塞,但使用時需要注意控制調用節奏,防止壓垮接收方。
在逾時控制上,結合 BOLT 和 Tracer,将一些關鍵的時間節點進行了整理。使得排查和判斷逾時問題更加友善。到這裡,我們就對 RPC 架構中的同步異步實作進行了一些詳細的分析,并深入介紹了 SOFARPC 中的實作細節,希望對大家有所啟發。