Dubbo透傳traceId/logid的一種思路

 

前言:
  隨着dubbo的開源, 以及成爲apache頂級項目. dubbo愈來愈受到國內java developer歡迎, 甚至成爲服務化自治的首選方案. 隨着微服務的流行, 如何跟蹤整個調用鏈, 成了一個課題. 你們可以達成一致的思路, 在調用中添加traceId/logid信息, 至於如何實現, 各家都有本身的思路.
  本文將對比幾種方案, 重點講解利用dubbo的自定義filter的機制, 來實現traceId/logid的透傳.java

 

方案一:
  這個方案也是最直接的方法, 正如所謂所見即所得, 就是在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, 顯然破壞"無侵入性"原則.spring

 

方案二:
  該方案須要修改dubbo源碼, 經過把traceId/logid注入到RPCInvocation對象(dubbo底層transport實體)中, 從而實現traceId/logid的透傳.
  apache

  本文再也不詳細展開, 有興趣的能夠參看博文: dubbo 服務跟蹤安全

 

RpcContext方案:
  在具體講解自定義filter來實現透傳traceId/logid的方案前, 咱們先來研究下RpcContext對象. 其RpcContext本質上是個ThreadLocal對象, 其維護了一次rpc交互的上下文信息. app

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的透傳了, 事實是否如此, 先讓咱們來一塊兒實踐一下.ide

  定義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信息的統計, 安全驗證工做等等. 值得咱們去深刻研究.

相關文章
相關標籤/搜索