在前面的文章中,我們分析了 dubbo 從 provider
進行服務暴露,然后把服務信息注冊到注冊中心上面解耦 consumer
與 provider
的調用。consumer
通過 javassist
創建代理對象引用遠程服務。當通過代理對象調用遠程服務的時候,講到進行真正調用的時候 dubbo 抽象出集群容錯(Cluster、Directory、Router、LoadBalance)從服務多個暴露方選取出一個合適的 Invoke 來進行調用。 dubbo 默認是通過 FailoverClusterInvoker
從多個 Invoke 中選擇出一個 Invoke 實例 InvokerWrapper
來進行遠程調用。本次分析主要包括以下 4 個部分:
- consumer 發送擴展
- consumer 發送原理
- consumer 接收原理
- dubbo 異步變同步
1、consumer 發送擴展
我們先來看一下 dubbo 中 consumer
端的請求發送原理,也就是從 InvokerWrapper#invoke 開始,在 consumer 服務引用分析的時候,我們知道根據 Invoke 調用的時候, dubbo 會創建 ProtocolListenerWrapper
與 ProtocolFilterWrapper 來用集成框架使用者的擴展包含:InvokerListener
與 Filter
。ProtocolListenerWrapper
在對象創建的時候就會調用InvokerListener#referred
擴展,所以在遠程服務調用的時候最主要的還是 Filter
擴展,下面我們就看一下在遠程調用的時候默認包括哪些 Filter
擴展:
- ConsumerContextFilter
- FutureFilter
- MonitorFilter
1.1 ConsumerContextFilter
ConsumerContextFilter
保存客戶端信息到 RpcContext
。
@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setLocalAddress(NetUtils.getLocalHost(), 0)
.setRemoteAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(invoker);
}
try {
return invoker.invoke(invocation);
} finally {
RpcContext.getContext().clearAttachments();
}
}
}
RpcContext
使用 ThreadLocal
來記錄一個臨時狀態。當接收到 RPC
請求,或發起 RPC
請求時,RpcContext
的狀態都會變化。
比如:A 調 B,B 再調 C,則 B 機器上,在 B 調 C 之前,
RpcContext
記錄的是 A 調 B 的信息,在 B 調 C 之后,RpcContext
記錄的是 B 調 C 的信息。
可以通過 RpcContext
上的 setAttachment
和 getAttachment
在服務消費方和提供方之間進行參數的隱式傳遞。
1.2 FutureFilter
FutureFilter
會來處理 dubbo 服務接口調用方配置 async="true"
來使用同步調用來是異步調用。
public class FutureFilter implements Filter {
protected static final Logger logger = LoggerFactory.getLogger(FutureFilter.class);
public Result invoke(final Invoker<?> invoker, final Invocation invocation) throws RpcException {
final boolean isAsync = RpcUtils.isAsync(invoker.getUrl(), invocation);
fireInvokeCallback(invoker, invocation);
//需要在調用前配置好是否有返回值,已供invoker判斷是否需要返回future.
Result result = invoker.invoke(invocation);
if (isAsync) {
asyncCallback(invoker, invocation);
} else {
syncCallback(invoker, invocation, result);
}
return result;
}
}
同步調用 dubbo 就會同步的返回 provider
方法調用返回的響應.如果是異步調用在進行調用的時候就會把請求信息發送到 provider
然后返回一個空的 RpcResult
。consumer
端如果要獲取響應需要通過以下方法獲取:
// 拿到調用的Future引用,當結果返回后,會被通知和設置到此Future
Future<Bar> barFuture = RpcContext.getContext().getFuture();
// 同理等待bar返回
Bar bar = barFuture.get();
1.3 MonitorFilter
MonitorFilter
其實是在分析之前 dubbo monitor
的時候就進行了詳細的分析。它主要是通過以下配置來激活 provider
與 consumer
端的指標監控。
<dubbo:monitor protocol="registry" />
我們還是簡單的來看一下它的源碼:
public class MonitorFilter implements Filter {
private MonitorFactory monitorFactory;
public void setMonitorFactory(MonitorFactory monitorFactory) {
this.monitorFactory = monitorFactory;
}
// 調用過程攔截
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (invoker.getUrl().hasParameter(Constants.MONITOR_KEY)) {
RpcContext context = RpcContext.getContext(); // 提供方必須在invoke()之前獲取context信息
String remoteHost = context.getRemoteHost();
long start = System.currentTimeMillis(); // 記錄起始時間戮
getConcurrent(invoker, invocation).incrementAndGet(); // 并發計數
try {
Result result = invoker.invoke(invocation); // 讓調用鏈往下執行
collect(invoker, invocation, result, remoteHost, start, false);
return result;
} catch (RpcException e) {
collect(invoker, invocation, null, remoteHost, start, true);
throw e;
} finally {
getConcurrent(invoker, invocation).decrementAndGet(); // 并發計數
}
} else {
return invoker.invoke(invocation);
}
}
}
當啟動 dubbo monitor
的時候會暴露一個遠程服務 MonitorService
接口服務服務,具體的處理類是 SimpleMonitorService
。而在 MonitorFilter#collect
方法里面 MonitorFactory
會創建一個 Monitor
接口實例(繼承于 MonitorService
)。其實就是 DubboMonitorFactroy#createMonitor
遠程引用 dubbo monitor
暴露的 MonitorService
服務。
public class DubboMonitorFactroy extends AbstractMonitorFactory {
private Protocol protocol;
private ProxyFactory proxyFactory;
public void setProtocol(Protocol protocol) {
this.protocol = protocol;
}
public void setProxyFactory(ProxyFactory proxyFactory) {
this.proxyFactory = proxyFactory;
}
@Override
protected Monitor createMonitor(URL url) {
url = url.setProtocol(url.getParameter(Constants.PROTOCOL_KEY, "dubbo"));
if (url.getPath() == null || url.getPath().length() == 0) {
url = url.setPath(MonitorService.class.getName());
}
String filter = url.getParameter(Constants.REFERENCE_FILTER_KEY);
if (filter == null || filter.length() == 0) {
filter = "";
} else {
filter = filter + ",";
}
url = url.addParameters(Constants.CLUSTER_KEY, "failsafe", Constants.CHECK_KEY, String.valueOf(false),
Constants.REFERENCE_FILTER_KEY, filter + "-monitor");
Invoker<MonitorService> monitorInvoker = protocol.refer(MonitorService.class, url);
MonitorService monitorService = proxyFactory.getProxy(monitorInvoker);
return new DubboMonitor(monitorInvoker, monitorService);
}
}
獲取到遠程服務 SimpleMonitorService
,最后在 MonitorFilter#collect
調用 MonitorService#collect
進行監控數據采集提供給 dubbo monitor
。調用過程如下所示:
2、consumer 發送原理
最終 consumer
會到 DubboInvoke
進行服務調用。它會在 AbstractInvoker#invoke
添加一些擴展參數到 RpcInvocation
這個遠程調用對象里面。添加的擴展參數包含:
- interface : 遠程調用的接口名稱
- group : 接口分組名稱
- token : 調用的 token 信息
- timeout : 調用服務的超時時間
- async : 是否異步調用
- id : 異步操作默認添加 invocation id,用于保證操作冪等
以及 RpcContext
傳遞過來的擴展參數(RpcContext#attachments
)。然后在 DubboInvoker#doInvoke
中會添加 path
(接口全類名) 以及 version
(版本信息)。再根據 dubbo 的調用模式進行遠程調用,包含以下三種調用模式:
- oneway 模式:
<dubbo:method>
標簽的return
屬性配置為false,則是oneway模式,利用ExchangeClient 對象向服務端發送請求消息之后,立即返回空 RpcResult 對象 - 異步模式:
<dubbo:method>
標簽的async
屬性配置為 ture,則是異步模式,直接返回空RpcResult
對象,由FutureFilter
和DefaultFuture
完成異步處理工作 - 同步模式:默認即是同步,則發送請求之后線程進入等待狀態,直到收到服務端的響應消息或者超時。
下面我們看一下 dubbo 同步調用時序圖:
ChannelFuture future = channel.write(message);
最終是調用 org.jboss.netty.channel.Channel
通過 socket 發送消息到從集群中選擇出的一個暴露服務信息的服務器發送網絡數據。
3、consumer 接收原理
我們都知道 dubbo 其實是通過 netty
來進行 socket 通信的。而在使用 netty
進行網絡編程的時候,其實核心就是就是實現 ChannelHandler
。而在 dubbo 中對應的實現類就是 NettyHandler
(高版本支持支持 netty 4
使用的是 NettyClientHandler
,NettyHandler 使用的是 netty 3.x
)。如果在 consumer
端(provider
也支持)需要使用 netty 4
進行業務處理,需要進行進行以下配置:
<dubbo:consumer client="netty4" />
所以 consumer
接收 provider
響應的入口就在 NettyClientHandler#channelRead
:
首先 ChannelHandler
用于接收 provider
端響應回來的請求,然后經過 5 個 dubbo 自定義的 ChannelHandler
。
- MultiMessageHandler:支持
MultiMessage
消息處理,也就是多條消息處理。 - HeartbeatHandler:
netty
心條檢測。如果心跳請求,發送心跳然后直接return
,如果是心跳響應直接return
。 - AllChannelHandler:使用線程池通過
ChannelEventRunnable
工作類來處理網絡事件。 - DecodeHandler:解碼
message
,解析成 dubbo 中的Response
對象 - HeaderExchangeHandler:處理解析后的
provider
端返回的Response
響應信息,把響應結果賦值到DefaultFuture
響應獲取阻塞對象中。
4、dubbo 異步變同步
我們都知道 dubbo 是基于 netty NIO
的非阻塞 并行調用通信。所以 dubbo 在 consumer
請求 provider
后響應都是異步的。但是在 dubbo 里面默認是同步返回的,那么 dubbo 是如何把異步響應變成同步請求的呢?帶著這個問題,首先我們來看一下 dubbo 里面的幾種請求方式。
4.1 異步且無返回值
這種請求最簡單,consumer
把請求信息發送給 provider
就行了。只是需要在 consumer
端把請求方式配置成異步請求就好了。如下:
<dubbo:method name="sayHello" return="false"></dubbo:method>
4.2 異步且有返回值
這種情況下 consumer
首先把請求信息發送給 provider
。這個時候在 consumer
端不僅把請求方式配置成異步,并且需要 RpcContext
這個 ThreadLocal
對象獲取到 Future
對象,然后通過 Future#get()
阻塞式獲取到 provider
的響應。那么這個 Future
是如果添加到 RpcContext
中呢?
在第二小節講服務發送的時候, 在 DubboInvoke
里面有三種調用方式,之前只具體請求了同步請求的發送方式而且沒有異步請求的發送。異步請求發送代碼如下:
DubboInvoker#doInvoke 中的 else if (isAsync) 分支
ResponseFuture future = currentClient.request(inv, timeout);
FutureAdapter<T> futureAdapter = new FutureAdapter<>(future);
RpcContext.getContext().setFuture(futureAdapter);
Result result;
if (RpcUtils.isAsyncFuture(getUrl(), inv)) {
result = new AsyncRpcResult<>(futureAdapter);
} else {
result = new RpcResult();
}
return result;
上面的代碼邏輯是直接發送請求到 provider
返回一個 ResponseFuture
實例,然后把這個 Future
對象保存到 RpcContext#LOCAL
這個 ThreadLocal
當前線程對象當中,并且返回一個空的 RpcResult
對象。如果要獲取到 provider
響應的信息,需要進行以下操作:
// 拿到調用的Future引用,當結果返回后,會被通知和設置到此Future
Future<String> temp= RpcContext.getContext().getFuture();
// 同理等待bar返回
hello=temp.get();
4.3 異步變同步(默認)
下面我們就來討論一下 dubbo 是如何把異步請求轉化成同步請求的。其實原理和異步請求的通過 Future#get
等待 provider
響應返回一樣,只不過異步有返回值是顯示調用而默認是 dubbo 內部把這步完成了。下面我們就來分析一下 dubbo 是如何把 netty 的異步響應變成同步返回的。(當前線程怎么讓它 "暫停",等結果回來后,再執行?)
我們都知道在 consumer
發送請求的時候會調用 HeaderExchangeChannel#request
方法:
HeaderExchangeChannel#request
public ResponseFuture request(Object request, int timeout) throws RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
}
// create request.
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setData(request);
DefaultFuture future = new DefaultFuture(channel, req, timeout);
try {
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
它首先會通過 dubbo 自定義的 Channel
、Request
與 timeout(int)
構造一個 DefaultFuture
對象。然后再通過 NettyChannel
發送請求到 provider
,最后返回這個 DefaultFuture
。下面我們來看一下通過構造方法是如何創建 DefaultFuture
的。我只把主要涉及到的屬性展示出來:
public class DefaultFuture implements ResponseFuture {
private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>();
private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>();
private final long id;
private final Channel channel;
private final Request request;
private final int timeout;
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);
}
}
這個 id
是在創建 Request
的時候使用 AtomicLong#getAndIncrement
生成的。從 1 開始并且如果它一直增加直到生成負數也能保證這臺機器這個值是唯一的,且不沖突的。符合唯一主鍵原則。 dubbo 默認同步變異步其實和異步調用一樣,也是在 DubboInvoker#doInvoke
實現的。
DubboInvoker#doInvoke
RpcContext.getContext().setFuture(null);
return (Result) currentClient.request(inv, timeout).get();
關鍵就在 ResponseFuture#get
方法上面,下面我們來看一下這個方法的源碼:
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();
}
其實就是 while 循環,利用 java 的 lock
機制判斷如果在超時時間范圍內 DefaultFuture#response
如果賦值成不為空就返回響應,否則拋出 TimeoutException
異常。下面我們就來看一下 DefaultFuture#response
是如何被賦值的。
還記得 consumer
接收 provider
響應的最后一步嗎?就是 DefaultFuture#received
,在 provider
端會帶回 consumer
請求的 id
。我們來看一下它的具體處理邏輯:
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());
}
}
它會從最開始通過構造函數傳進去的 DefaultFuture#FUTURES
根據請求的 id
拿到 DefaultFuture
,然后根據這個 DefaultFuture
調用 DefaultFuture#doReceived
方法。通過 Java 里面的 lock
機制把 provider
的值賦值給 DefaultFuture#response
。此時 consumer
也正在調用 DefaultFuture#get
方法進行阻塞,當這個 DefaultFuture#response
被賦值后,它的值就不為空。阻塞操作完成,且根據請求號的 id
把 consumer
端的 Request
以及 Provider
端返回的 Response
關聯了起來。
這個就是 Dubbo 異步轉同步的原理,是不是很巧妙,很簡單。 :)
參考資料: