前言:
隨着dubbo的開源, 以及成為apache頂級項目. dubbo越來越受到國內java developer歡迎, 甚至成為服務化自治的首選方案. 隨着微服務的流行, 如何跟蹤整個調用鏈, 成了一個課題. 大家能夠達成一致的思路, 在調用中添加traceId/logid信息, 至於如何實現, 各家都有自己的思路.
本文將對比幾種方案, 重點講解利用dubbo的自定義filter的機制, 來實現traceId/logid的透傳.
方案一:
這個方案也是最直接的方法, 正如所謂所見即所得, 就是在dubbo的接口參數添加traceId/logid參數.
比如如下的sample代碼:
@Getter
@Setter
class EchoReq {
// *) 消息
private String message;
// *) 跟蹤ID
private String traceId;
}
// *) dubbo的接口定義
interface EchoService {
String echo1(EchoReq req);
String echo2(String message, String traceId);
}
相信大家一看就明白了其中的思路, 這種思路確實簡單粗暴. 對於對於有潔癖的程序員而言, 在業務接口中, 生硬地添加traceId/logid, 顯然破壞"無侵入性"原則.
方案二:
該方案需要修改dubbo源碼, 通過把traceId/logid注入到RPCInvocation對象(dubbo底層transport實體)中, 從而實現traceId/logid的透傳.

本文不再詳細展開, 有興趣的可以參看博文: dubbo 服務跟蹤.
RpcContext方案:
在具體講解自定義filter來實現透傳traceId/logid的方案前, 我們先來研究下RpcContext對象. 其RpcContext本質上是個ThreadLocal對象, 其維護了一次rpc交互的上下文信息.
public class RpcContext {
// *) 定義了ThreadLocal對象
private static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal() {
protected RpcContext initialValue() {
return new RpcContext();
}
};
// *) 附帶屬性, 這些屬性可以隨RpcInvocation對象一起傳遞
private final Map<String, String> attachments = new HashMap();
public static RpcContext getContext() {
return (RpcContext)LOCAL.get();
}
protected RpcContext() {
}
public String getAttachment(String key) {
return (String)this.attachments.get(key);
}
public RpcContext setAttachment(String key, String value) {
if(value == null) {
this.attachments.remove(key);
} else {
this.attachments.put(key, value);
}
return this;
}
public void clearAttachments() {
this.attachments.clear();
}
}
注: RpcContext里的attachments信息會填入到RpcInvocation對象中, 一起傳遞過去.
因此有人就建議可以簡單的把traceId/logid注入到RpcContext中, 這樣就可以簡單的實現traceId/logid的透傳了, 事實是否如此, 先讓我們來一起實踐一下.
定義dubbo接口類:
public interface IEchoService {
String echo(String name);
}
編寫服務端代碼(producer):
@Service("echoService")
public class EchoServiceImpl implements IEchoService {
@Override
public String echo(String name) {
String traceId = RpcContext.getContext().getAttachment("traceId");
System.out.println("name = " + name + ", traceId = " + traceId);
return name;
}
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring-dubbo-test-producer.xml");
System.out.println("server start");
while (true) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
}
}
}
}
編寫客戶端代碼(consumer):
public class EchoServiceConsumer {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring-dubbo-test-consumer.xml");
IEchoService service = (IEchoService) applicationContext
.getBean("echoService");
// *) 設置traceId
RpcContext.getContext().setAttachment("traceId", "100001");
System.out.println(RpcContext.getContext().getAttachments());
// *) 第一調用
service.echo("lilei");
// *) 第二次調用
System.out.println(RpcContext.getContext().getAttachments());
service.echo("hanmeimei");
}
}
注: 這邊的代碼, 暫時忽略掉了dubbo producer/consumer的xml配置.
執行的接入如下:
服務端輸出:
name = lilei, traceId = 100001
name = hanmeimei, traceId = null
客戶端輸出:
{traceId=100001}
{}
從服務端的輸出信息中, 我們可以驚喜的發現, traceId確實傳遞過去了, 但是只有第一次有, 第二次沒有. 而從客戶端對RpcContext的內容輸出, 也印證了這個現象, 同時產生這個現象的本質原因是是RpcContext對象的attachment在一次rpc交互后被清空了.
給RpcContext的clearAttachments方法, 設置斷點后復現. 我們可以找到如下調用堆棧.
java.lang.Thread.State: RUNNABLE at com.alibaba.dubbo.rpc.RpcContext.clearAttachments(RpcContext.java:438) at com.alibaba.dubbo.rpc.filter.ConsumerContextFilter.invoke(ConsumerContextFilter.java:50) at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) at com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:77) at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:227) at com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72) at com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52) at com.alibaba.dubbo.common.bytecode.proxy0.echo(proxy0.java:-1) at com.test.dubbo.EchoServiceConsumer.main(EchoServiceConsumer.java:20)
其最直接的調用為dubbo自帶的ConsumerContextFilter, 讓我們來分析其代碼.
@Activate(
group = {"consumer"},
order = -10000
)
public class ConsumerContextFilter implements Filter {
public ConsumerContextFilter() {
}
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);
}
Result var3;
try {
var3 = invoker.invoke(invocation);
} finally {
RpcContext.getContext().clearAttachments();
}
return var3;
}
}
確實在finally代碼片段中, 我們發現RpcContext在每次rpc調用后, 都會清空attachment對象.
既然我們找到了本質原因, 那么解決方法, 可以在每次調用的時候, 重新設置下traceId, 比如像這樣.
// *) 第一調用
RpcContext.getContext().setAttachment("traceId", "100001");
service.echo("lilei");
// *) 第二次調用
RpcContext.getContext().setAttachment("traceId", "100001");
service.echo("hanmeimei");
只是感覺吃像相對難看了一點, 有沒有更加優雅的方案呢? 我們踏着五彩霞雲的蓋世大英雄馬上就要來了.
自定義filter方案:
我們先引入一個工具類:
public class TraceIdUtils {
private static final ThreadLocal<String> traceIdCache
= new ThreadLocal<String>();
public static String getTraceId() {
return traceIdCache.get();
}
public static void setTraceId(String traceId) {
traceIdCache.set(traceId);
}
public static void clear() {
traceIdCache.remove();
}
}
然后我們定義一個filter類:
package com.test.dubbo;
public class TraceIdFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String traceId = RpcContext.getContext().getAttachment("traceId");
if ( !StringUtils.isEmpty(traceId) ) {
// *) 從RpcContext里獲取traceId並保存
TraceIdUtils.setTraceId(traceId);
} else {
// *) 交互前重新設置traceId, 避免信息丟失
RpcContext.getContext().setAttachment("traceId", TraceIdUtils.getTraceId());
}
// *) 實際的rpc調用
return invoker.invoke(invocation);
}
}
在resource目錄下, 添加META-INF/dubbo目錄, 繼而添加com.alibaba.dubbo.rpc.Filter文件

編輯(com.alibaba.dubbo.rpc.Filter文件)內容如下:
traceIdFilter=com.test.dubbo.TraceIdFilter
然后我們給dubbo的producer和consumer都配置對應的filter項.
服務端:
<dubbo:service interface="com.test.dubbo.IEchoService" ref="echoService" version="1.0.0"
filter="traceIdFilter"/>
客戶端:
<dubbo:reference interface="com.test.dubbo.IEchoService" id="echoService" version="1.0.0"
filter="traceIdFilter"/>
服務端的測試代碼小改為如下:
@Service("echoService")
public class EchoServiceImpl implements IEchoService {
@Override
public String echo(String name) {
String traceId = TraceIdUtils.getTraceId();
System.out.println("name = " + name + ", traceId = " + traceId);
return name;
}
}
客戶端的測試代碼片段為:
// *) 第一調用
RpcContext.getContext().setAttachment("traceId", "100001");
service.echo("lilei");
// *) 第二次調用
service.echo("hanmeimei");
同樣的代碼, 測試結果如下
服務端輸出:
name = lilei, traceId = 100001
name = hanmeimei, traceId = 100001
客戶端輸出:
{traceId=100001}
{}
符合預期, 感覺這個方案就非常優雅了. RpcContext的attachment依舊被清空(ConsumerContextFilter在自定義的Filter后執行), 但是每次rpc交互前, traceId/logid會被重新注入, 保證跟蹤線索透傳成功.
總結:
關於這個方案, 在服務A, 服務B, 服務C之間連續傳遞測試, 依舊成功. 總的來說, 該方案還是可行的, dubbo的自定義filter機制也算是dubbo功能擴展的一個補充. 我們可以做很多工作, 比如耗時記錄, metric信息的統計, 安全驗證工作等等. 值得我們去深入研究.
