本文的目標是改進dubbo破镰,在各個dubbo服務之間透傳traceId者冤,實現(xiàn)服務跟蹤
一薪寓、關于RPC
在大型系統(tǒng)中,一個對外http服務的背后往往隱匿了多個內(nèi)部服務之間的相互調(diào)用。因為性能雷绢、開發(fā)成本層面的考量,http協(xié)議并不適合內(nèi)部服務之間的調(diào)用,為此產(chǎn)生了thrift赘理、dubbo 等優(yōu)秀RPC框架。
thrift 的由facebook 開發(fā)扇单,跨語言支持豐富是其最大的亮點商模,thrift定義了一種接口定于語言,通過自動化工具蜘澜,生成client 端和server端代碼施流。
dubbo 是由一個由阿里開發(fā)的開源分布式服務框架,相對于于thrift鄙信,dubbo實現(xiàn)了完善的服務治理功能瞪醋,包括:服務發(fā)現(xiàn)、路由規(guī)則装诡、配置規(guī)則银受、服務降級、負載均衡等鸦采。
二蚓土、關于服務跟蹤
在微服務的趨勢下,一次調(diào)用產(chǎn)生的日志分布在不同的機器上赖淤,雖然可以使用ELK的技術蜀漆,將所有服務的日志灌入es中,但是如何將這寫日志“穿起來”是一個關鍵問題咱旱。
一般的做法是在系統(tǒng)的邊界生成一個traceId确丢,向調(diào)用鏈上的后繼服務傳遞traceId绷耍,后繼服務使用traceId 打印相應日志,并再向后繼服務傳遞traceId鲜侥。簡稱“traceId透傳”褂始。
在使用http協(xié)議作為服務協(xié)議的系統(tǒng)里,可以統(tǒng)一使用一個封裝好的http client做traceId透傳描函。但是dubbo實現(xiàn)traceId透傳就稍微復雜些了崎苗。
三、dubbo traceId 透傳測試
首先拋開實現(xiàn)舀寓,看一下實現(xiàn)透傳成功的測試case
評價指標:
(1) 調(diào)用鏈上的所有dubbo服務都有一個同樣的traceId
??(2) 對業(yè)務代碼無侵入胆数,來的業(yè)務代碼無需修改升級
??(3) 對性能無太大的影響
測試用例:
/**
* 測試Service
* Created by WuMingzhi on 2017/3/19.
*/
public interface GiftService {
int getPrice(int giftId);
}
/**
* provider端
* Created by WuMingzhi on 2017/3/19.
*/
public class GiftServiceImpl implements GiftService{
private static Random random = new Random();
@Override
public int getPrice(int i) {
System.out.println("gift provider traceId:" + TraceIdUtil.getTraceId());
int price = random.nextInt(i + 100);
System.out.println("set gift price: " + price);
return price;
}
}
/**
* consumer 端
* Created by WuMingzhi on 2017/3/19.
*/
public class TestGiftTraceId {
@Resource
private GiftService giftService;
@Test
public void TestGiftTraceId() throws InterruptedException {
while (true){
// consumer 端設置一個 traceId
TraceIdUtil.setTraceId(UUID.randomUUID().toString());
System.out.println("gift consumer traceId:" + TraceIdUtil.getTraceId());
int price = giftService.getPrice(100);
System.out.println("get gift price: " + price);
Thread.sleep(1000);
}
}
}
測試結果:
consumer端
gift consumer traceId:53bf37ca-6ce9-401a-b33f-87d6f3c96cfa
get gift price: 183
gift consumer traceId:a79a2a0a-29fa-48d4-b4f6-14bdd3504603
get gift price: 144
gift consumer traceId:cd058847-8683-452b-ac4c-43bd94557a06
get gift price: 178
gift consumer traceId:fd5a72a1-f060-4aef-8864-baf1d7ee1fac
get gift price: 187
provider端
gift provider traceId:53bf37ca-6ce9-401a-b33f-87d6f3c96cfa
set gift price: 183
gift provider traceId:a79a2a0a-29fa-48d4-b4f6-14bdd3504603
set gift price: 144
gift provider traceId:cd058847-8683-452b-ac4c-43bd94557a06
set gift price: 178
gift provider traceId:fd5a72a1-f060-4aef-8864-baf1d7ee1fac
set gift price: 187
結論 consumer 和provider 具有相同的traceId, 透傳成功
四互墓、dubbo 源碼分析
下圖為dubbo的線程派發(fā)模型:
Proxy 是dubbo 使用javassist為consumer 端service生成的動態(tài)代理instance必尼。
?? Implement 是provider端的service實現(xiàn)instance。
traceId透傳即要求Proxy 和 Implement具有相同的traceId篡撵。dubbo具有良好的分層特征判莉,transport的對象是RPCInvocation。所以Proxy將traceId放入RPCInvocation育谬,交由Client進行序列化和TCP傳輸券盅,Server反序列化得到RPCInvocation,取出traceId膛檀,交由Implement即可渗饮。
下圖為consumer端 JavassistProxyFactory 的代碼分析
下圖為consumer端 InvokerInvocationHandler 的代碼分析
下圖為provider端 DubboProtocol 的代碼分析
五、dubbo 透傳traceId的實現(xiàn)
修改了dubbo 的兩個類宿刮,添加了一個類互站,只列出關鍵代碼。
package com.alibaba.dubbo.rpc.proxy;
/**
* traceId工具類這個類是新添加的
* Created by WuMingzhi on 17/3/18.
*/
public class TraceIdUtil {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<String>();
public static String getTraceId() {
return TRACE_ID.get();
}
public static void setTraceId(String traceId) {
TRACE_ID.set(traceId);
}
}
package com.alibaba.dubbo.rpc.proxy;
/**
* InvokerHandler 這個類 是修改的
* @author william.liangf
*/
public class InvokerInvocationHandler implements InvocationHandler {
private final Invoker<?> invoker;
public InvokerInvocationHandler(Invoker<?> handler){
this.invoker = handler;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
// 這里將cosumer 端的traceId放入RpcInvocation
RpcInvocation rpcInvocation = new RpcInvocation(method, args);
rpcInvocation.setAttachment("traceId", TraceIdUtil.getTraceId());
return invoker.invoke(rpcInvocation).recreate();
}
}
package com.alibaba.dubbo.rpc.protocol.dubbo;
/**
* dubbo protocol support.
*
* @author qian.lei
* @author william.liangf
* @author chao.liuc
*/
public class DubboProtocol extends AbstractProtocol {
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
//如果是callback 需要處理高版本調(diào)用低版本的問題
if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
String methodsStr = invoker.getUrl().getParameters().get("methods");
boolean hasMethod = false;
if (methodsStr == null || methodsStr.indexOf(",") == -1){
hasMethod = inv.getMethodName().equals(methodsStr);
} else {
String[] methods = methodsStr.split(",");
for (String method : methods){
if (inv.getMethodName().equals(method)){
hasMethod = true;
break;
}
}
}
if (!hasMethod){
logger.warn(new IllegalStateException("The methodName "+inv.getMethodName()+" not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) +" ,invocation is :"+inv );
return null;
}
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
// 這里將收到的consumer端的traceId放入provider端的thread local
TraceIdUtil.setTraceId(inv.getAttachment("traceId"));
return invoker.invoke(inv);
}
throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
}
}
}
六僵缺、更多思考
本文只介紹了如何在dubbo系統(tǒng)之間透傳traceId胡桃,但沒有介紹使用traceId進行日志跟蹤的case。建議讀者實現(xiàn)兩個層次的日志跟蹤:
??(1)線程日志跟蹤:在調(diào)用的入口處設置線程的traceId(來著上游線程或者自動生成)磕潮;在業(yè)務代碼里使用統(tǒng)一的LogFactory 獲取自動打印traceId的 logger 打印函數(shù)的調(diào)用信息翠胰。通過traceId 即可grep 出一次調(diào)用的所有日志。
??(2)dubbo日志跟蹤:類似上文自脯,dubbo日志跟蹤連接了不同服務之間的線程日志之景,使得在dubbo下實現(xiàn)服務跟蹤成為可能。