注:文章中使用的dubbo源碼版本為2.5.4
零、文章目錄
- Dubbo的三種RPC調用方式
- 關鍵類介紹
- DefaultFuture實現
- 調用入口流程
- 服務引用方請求響應模型總結
一、Dubbo的三種RPC調用方式
1.1 異步&無返回值
a)服務引用配置如下:
<dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
<dubbo:method name="sayHello" async="true" return="false"/>
</dubbo:reference>
-
async="true"
; -
return="false"
;
b)服務調用方式如下:
String hello = demoService.sayHello("world" + i);
- 因為使用了異步無返回值的模式,所以hello的值一直為null;
1.2 異步&有返回值
a)服務引用配置如下:
<dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
<dubbo:method name="sayHello" async="true"/>
</dubbo:reference>
-
async="true"
;
b)服務調用方式如下:
demoService.sayHello("world" + i);
Future<String> temp = RpcContext.getContext().getFuture();
String hello = temp.get();
- 因為使用了異步模式,
demoService.sayHello()
被調用后立即返回(此時RPC調用結果還未生成,RPC的執行過程不阻塞業務請求線程); - 服務引用方業務請求線程可以在合適的時候執行
RpcContext.getContext().getFuture()
獲取RPC調用結果;
1.3 異步變同步
a)服務引用配置如下:
<dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
<dubbo:method name="sayHello" async="false"/>
</dubbo:reference>
-
async="false"
,默認值;
b)服務調用方式如下:
String hello = demoService.sayHello("world" + i);
- 因為使用了同步模式,
demoService.sayHello()
被調用后直到收到RPC響應才返回,sayHello()
方法返回時得到hello
的值,期間業務請求線程被阻塞。
二、關鍵類介紹
2.1 RPC請求消息封裝(Request)
dubbo的交換層定義了RPC請求的封裝類Request
,它包含了一個RPC請求所具備的關鍵信息。
public class Request {
//請求ID生成器,AtomicLong.inc實現
private static final AtomicLong INVOKE_ID = new AtomicLong(0);
//RPC調用的請求ID,在單個Client端內全局唯一
private final long mId;
//RPC請求響應消息協議版本
private String mVersion;
//是否為雙向請求響應
private boolean mTwoWay = true;
//實際RPC調用的請求數據,對應了Invocation類,調用參數都封裝在這里了
private Object mData;
//Request初始化時,生成請求ID
public Request() {
mId = newId();
}
//請求ID生成方法
private static long newId() {
// getAndIncrement()增長到MAX_VALUE時,再增長會變為MIN_VALUE,負數也可以做為ID
return INVOKE_ID.getAndIncrement();
}
}
2.2 RPC響應消息封裝(Response)
dubbo的交換層定義了RPC響應的封裝類Response
,它包含了一個RPC響應所具備的關鍵信息。
public class Response {
/**
* ok.
*/
public static final byte OK = 20;
/**
* clien side timeout.
*/
public static final byte CLIENT_TIMEOUT = 30;
/**
* server side timeout.
*/
public static final byte SERVER_TIMEOUT = 31;
// ...省略一些狀態碼...
//RPC調用的請求ID,默認為0,從Request中獲取
private long mId = 0;
//RPC請求響應消息協議版本
private String mVersion;
//響應狀態碼,默認OK,出現異常時重新設置
private byte mStatus = OK;
//響應錯誤信息
private String mErrorMsg;
//實際RPC調用的響應數據,對應實際實現類的方法執行結果
private Object mResult;
}
2.3 RPC調用Future接口(ResponseFuture)
dubbo的交換層定義了RPC調用的響應Future接口ResponseFuture
,它封裝了請求響應模式,例如提供了將異步網絡通信轉換成同步RPC調用的關鍵方法Object get(int timeoutInMillis)
。
public interface ResponseFuture {
/**
* 獲取RPC遠程執行結果,異步IO轉同步RPC的關鍵方法
*/
Object get() throws RemotingException;
/**
* 獲取RPC遠程執行結果,異步IO轉同步RPC的關鍵方法
*/
Object get(int timeoutInMillis) throws RemotingException;
/**
* set callback. 響應回調模式
*/
void setCallback(ResponseCallback callback);
/**
* RPC調用是否完成
*/
boolean isDone();
}
三、DefaultFuture實現
DefaultFuture
是ResponseFuture
接口的實現類,具體實現了接口定義的方法。
3.1 請求響應信息的承載
-
DefaultFuture
提供構造方法,在HeaderExchangeChannel.request()
中被調用,用于構建RPC調用Future并返回給調用方使用; -
DefaultFuture
中包含以下關鍵屬性,用于承載請求響應信息;
public class DefaultFuture implements ResponseFuture {
//<請求ID,消息通道> 的映射關系
private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>();
//<請求ID,未完成狀態的RPC請求> 的映射關系
private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>();
//RPC調用的請求ID,構造器中從Request獲取
private final long id;
//消息通道,構造器傳入
private final Channel channel;
//RPC請求消息,構造器傳入
private final Request request;
//RPC響應消息
private volatile Response response;
//RPC響應回調器
private volatile ResponseCallback callback;
//構造器
public DefaultFuture(Channel channel, Request request, int timeout) {
this.channel = channel;
this.request = request;
this.id = request.getId();
this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
// put into waiting map.
FUTURES.put(id, this);
CHANNELS.put(id, channel);
}
}
3.2 RPC執行超時檢測
-
DefaultFuture
定義了RPC執行超時時間timeout
和RPC執行開始時間start
,他們都在DefaultFuture
構建時被初始化; -
DefaultFuture
中啟動了一個后臺守護線程,用于周期性執行 “RPC超時檢測任務RemotingInvocationTimeoutScan
”。該任務不斷檢測 “未完成狀態的RPC請求FUTURE” 中哪些已經超時(通過比較System.currentTimeMillis() - start
與timeout
) - 對已超時的RPC請求,構建相應的
超時響應Response
并觸發received()
方法。
public class DefaultFuture implements ResponseFuture {
//RPC超時輪詢線程,不斷輪詢超時狀態的FUTURES,主動移除并返回超時結果
static {
Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
th.setDaemon(true);
th.start();
}
//RPC執行超時時間,構造器傳入,默認值1s
private final int timeout;
//DefaultFuture構建時間
private final long start = System.currentTimeMillis();
}
3.3 異步網絡通信轉同步RPC調用
-
DefaultFuture
實現了ResponseFuture
接口的重要方法get(int timeout)
,用于同步等待RPC執行結果的返回(成功/異常/超時); -
DefaultFuture
提供了靜態方法received(Channel channel, Response response)
,用于對客戶端收到的RPC執行響應Response
進行處理。處理邏輯就是講響應結果放入response
并通知在done
上組織等待的業務線程,同時通知本次RPC請求綁定的回調器Callback
;
public class DefaultFuture implements ResponseFuture {
//響應消息處理互斥鎖,get()、doReceived()、setCallback()方法中使用
private final Lock lock = new ReentrantLock();
//請求響應模式Condition,通過get()中的await和doReceived()中的signal完成IO異步轉RPC同步
private final Condition done = lock.newCondition();
//RPC響應消息接收方法
public static void received(Channel channel, Response response) {
try {
DefaultFuture future = FUTURES.remove(response.getId());
if (future != null) {
future.doReceived(response);
} else {
logger.warn("The timeout response finally returned at "
+ (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
+ ", response " + response
+ (channel == null ? "" : ", channel: " + channel.getLocalAddress()
+ " -> " + channel.getRemoteAddress()));
}
} finally {
CHANNELS.remove(response.getId());
}
}
//將響應結果放入response,通知在done上等待的業務線程,并執行invokeCallback方法觸發所有綁定的Callbask執行
private void doReceived(Response res) {
lock.lock();
try {
response = res;
if (done != null) {
done.signal();
}
} finally {
lock.unlock();
}
if (callback != null) {
invokeCallback(callback);
}
}
//RPC執行結果同步獲取方法,RPC的同步請求模式就依賴此方法完成,依賴done.await同步等待RPC執行結果
public Object get(int timeout) throws RemotingException {
if (timeout <= 0) {
timeout = Constants.DEFAULT_TIMEOUT;
}
if (!isDone()) {
long start = System.currentTimeMillis();
lock.lock();
try {
while (!isDone()) {
done.await(timeout, TimeUnit.MILLISECONDS);
if (isDone() || System.currentTimeMillis() - start > timeout) {
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
if (!isDone()) {
throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
}
}
return returnFromResponse();
}
}
3.4 異步網絡通信轉同步RPC調用的兩個關鍵點
a)發起RPC調用請求的業務線程,是如何同步阻塞等待直到RPC響應返回的?
- 業務請求線程調用
HeaderExchangeClient.request()
方法發送RPC請求消息到網絡,然后直接調用DefaultFuture.get()
方法阻塞等待RPC執行結果; -
get()
阻塞等待的本質:循環檢測Response結果是否被設置成功,如果不成功使用Condition.await()
阻塞直到結果返回; -
NettyClient
接收到RPC響應消息時,會調用DefaultFuture.received()
方法,該方法中觸發了Condition.signal()
通知業務請求線程解除阻塞等待狀態;
b)對于全雙工的網絡通信,在多線程并發請求響應的情況下,如果找到RPC響應Response
對應的RPC請求Request
?
- 對于不同的服務消費者客戶端,請求響應自然與其網絡通道
Channel
綁定,不會存在消費者A接收到消費者B的RPC響應的情況; - 對于同一服務消費者客戶端,在RPC請求
Request
構建時生成并攜帶全局唯一自增ID,RPC響應Response
會攜帶該ID返回。消費者客戶端只需維護 “唯一ID與RPC請求的關系Map<Long, DefaultFuture> FUTURES
”即可定位RPC響應對應的RPC調用上下文;
四、調用入口流程
在 dubbo剖析:五 請求發送與接收 中講到,服務引用方調用dubbo的代理對象發起RPC請求時,最終會執行到DubboInvoker.doInvoke()
方法:
protected Result doInvoke(final Invocation invocation) throws Throwable {
//入參構建及獲取ExchangeClient
RpcInvocation inv = (RpcInvocation) invocation;
ExchangeClient currentClient;
if (clients.length == 1) {
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
//case1. 異步,無返回值
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return new RpcResult();
}
//case2. 異步,有返回值
else if (isAsync) {
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
return new RpcResult();
}
//case3. 異步轉同步(默認的通信方式)
else {
RpcContext.getContext().setFuture(null);
return (Result) currentClient.request(inv, timeout).get();
}
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
- case1.異步&無返回值:直接調用
ExchangeClient.send()
方法,不需要請求響應模式的封裝; - case2.異步&有返回值:調用
ExchangeClient.request()
方法后,將ResponseFuture
放入RPC請求上下文中,直接返回; - case3.異步轉同步:調用
ExchangeClient.request()
方法后,直接繼續調用ResponseFuture.get()
同步等待獲取RPC執行結果;
五、服務引用方請求響應模型總結
服務引用方請求響應模型總結
- 藍色代表“發送RPC請求”過程,由業務請求線程執行,通過
NettyChannel
將請求數據放入Netty
的IO任務隊列后,構建ResponseFuture
并返回。此時RPC請求發送及響應接收并未真正完成; - 紫色是基于
Netty
的網絡消息收發過程,通過當前網絡通道綁定的NioEventLoop線程
輪詢完成; - 橙色代表“接收RPC響應”過程,該過程在 Dubbo業務線程池 中執行,處理RPC響應消息并交由
ResponseFuture
觸發接收響應的邏輯; - 綠色代表“獲取RPC調用結果”過程,由業務請求線程執行,阻塞直到從
ResponseFuture
中獲取到RPC響應結果;