• Dubbo透传traceId/logid的一种思路


    转载:https://www.cnblogs.com/mumuxinfei/p/9226881.html

    前言:
      随着dubbo的开源, 以及成为apache顶级项目. dubbo越来越受到国内java developer欢迎, 甚至成为服务化自治的首选方案. 随着微服务的流行, 如何跟踪整个调用链, 成了一个课题. 大家能够达成一致的思路, 在调用中添加traceId/logid信息, 至于如何实现, 各家都有自己的思路.
      本文将对比几种方案, 重点讲解利用dubbo的自定义filter的机制, 来实现traceId/logid的透传.

    方案一:
      这个方案也是最直接的方法, 正如所谓所见即所得, 就是在dubbo的接口参数添加traceId/logid参数.
      比如如下的sample代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @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交互的上下文信息.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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接口类:

    1
    2
    3
    4
    5
    public interface IEchoService {
     
        String echo(String name);
     
    }

      编写服务端代码(producer):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @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):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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配置.
      执行的接入如下:

    1
    2
    3
    4
    5
    6
    7
    服务端输出:
    name = lilei, traceId = 100001
    name = hanmeimei, traceId = null
     
    客户端输出:
    {traceId=100001}
    {}

      从服务端的输出信息中, 我们可以惊喜的发现, traceId确实传递过去了, 但是只有第一次有, 第二次没有. 而从客户端对RpcContext的内容输出, 也印证了这个现象, 同时产生这个现象的本质原因是是RpcContext对象的attachment在一次rpc交互后被清空了.
      给RpcContext的clearAttachments方法, 设置断点后复现. 我们可以找到如下调用堆栈.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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, 让我们来分析其代码.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @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, 比如像这样.

    1
    2
    3
    4
    5
    6
    7
    // *) 第一调用
    RpcContext.getContext().setAttachment("traceId""100001");
    service.echo("lilei");
     
    // *) 第二次调用
    RpcContext.getContext().setAttachment("traceId""100001");
    service.echo("hanmeimei");

      只是感觉吃像相对难看了一点, 有没有更加优雅的方案呢? 我们踏着五彩霞云的盖世大英雄马上就要来了.

    自定义filter方案:
      我们先引入一个工具类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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文件)内容如下:

    1
    traceIdFilter=com.test.dubbo.TraceIdFilter

      然后我们给dubbo的producer和consumer都配置对应的filter项.
      服务端:

    1
    2
    <dubbo:service interface="com.test.dubbo.IEchoService" ref="echoService" version="1.0.0"
            filter="traceIdFilter"/>

      客户端:

    1
    2
    <dubbo:reference interface="com.test.dubbo.IEchoService" id="echoService" version="1.0.0"
                     filter="traceIdFilter"/>

      服务端的测试代码小改为如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @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;
        }
     
    }

      客户端的测试代码片段为:

    1
    2
    3
    4
    5
    6
    // *) 第一调用
    RpcContext.getContext().setAttachment("traceId""100001");
    service.echo("lilei");
     
    // *) 第二次调用
    service.echo("hanmeimei");

      同样的代码, 测试结果如下

    1
    2
    3
    4
    5
    6
    7
    服务端输出:
    name = lilei, traceId = 100001
    name = hanmeimei, traceId = 100001
     
    客户端输出:
    {traceId=100001}
    {}

      符合预期, 感觉这个方案就非常优雅了. RpcContext的attachment依旧被清空(ConsumerContextFilter在自定义的Filter后执行), 但是每次rpc交互前, traceId/logid会被重新注入, 保证跟踪线索透传成功.

    总结:
      关于这个方案, 在服务A, 服务B, 服务C之间连续传递测试, 依旧成功. 总的来说, 该方案还是可行的, dubbo的自定义filter机制也算是dubbo功能扩展的一个补充. 我们可以做很多工作, 比如耗时记录, metric信息的统计, 安全验证工作等等. 值得我们去深入研究.

  • 相关阅读:
    困扰几周了,请教啊,android与websevice数据交互很诡异的问题
    最新版本_adt-bundle-windows-x86_64-20140702 无法建立avd
    android向web提交数据,中文乱码
    activity怎么控制fragment中的textview组件
    关于云储存或者百度云的基础问题, 用java/android 实现上传文件到云储存(比如百度云)
    短信列表如何让同一个号码的短信只显示一条,刚刚加载短信列表会加载所有的数据列。求指教
    Android图片上传到服务器的问题
    安卓模拟器这么慢,大家都怎么调试的?
    浏览器前缀-----[译]Autoprefixer:一个以最好的方式处理浏览器前缀的后处理程序
    windows 下安装nodejs及其配置环境
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/12016162.html
Copyright © 2020-2023  润新知