Dubbo透传traceId/logid的一种思路

前言:
  随着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信息的统计, 安全验证工作等等. 值得我们去深入研究.

原文地址:https://www.cnblogs.com/mumuxinfei/p/9226881.html

时间: 2024-08-30 08:32:40

Dubbo透传traceId/logid的一种思路的相关文章

HTML+js+css实现点击图片弹出上传文件窗口的两种思路

第一种:CSS实现 <style><!-- .fileInputContainer{        height:256px;        background:url(upfile.png);        position:relative;        width: 256px;    }    .fileInput{        height:256px;        overflow: hidden;        font-size: 300px;        po

dubbo traceId透传实现日志链路追踪(基于Filter和RpcContext实现)

一.要解决什么问题: 使用elk的过程中发现如下问题: 1.无法准确定位一个请求经过了哪些服务 2.多个请求线程的日志交替打印,不利于查看按时间顺序查看一个请求的日志. 二.期望效果 能够查看一个请求完整的链路日志,不受其它请求日志的干扰. 三.动手实现 消费端需要做什么: 1.新建一个拦截器,拦截所有请求,在调用接口前生成一个链路id(traceId)并放入log4j的MDC和dubbo的RpcContext的attachment,此处拦截器是基于jfinal实现,spring mvc可用其它

二层透传介绍

二层协议透明传输的基本原理 工作流程 1.在骨干网的用户接入侧替换原始二层协议报文的组播目的MAC地址为特定的组播MAC地址.(修改源组播MAC地址) 2.修改MAC地址后的报文在骨干网中根据配置的透明传输方式决定是否对报文进行处理.(查看骨干网透传规则) 3.当该二层协议报文到达出节点时,通过匹配设备上配置的特殊组播目的MAC和二层协议的映射关系,将报文的组播目的MAC还原成该二层协议标准的组播目的MAC地址,并根据配置的透明传输方式决定是否处理该报文.(转换找出映射关系,还原组播MAC地址,

vlan 透传 vlan转换 vlan嵌套

//-------------------------------------------------------------------------------------- 什么叫vlan透传呢? 就是不管你上来的数据什么,我都会让你通过.这里说明了数据的类型,不管什么数据. 为什么会有vlan透传这个概念. 原来vlan的目的就是为了进行隔离,不同vlan的数据不让通过.起到了数据隔离的作用. 但是有时候,我想变成一个直通的数据,所以我需要让我的数据全部通过,所以有vlan透传. vlan

视频流网络透传分析

最近在翻看以前写的文档,把这篇word文档复制下来,保留一下. 序号 版本 作者 描述 1 V0.1 YJ.Yan 初始版本 2 V0.2 YJ.Yan 添加方案细节描述以及可参考方案 1.      基本概念 a)       NAT 网络地址转换(NAT,Network AddressTranslation)属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术,它被广泛应用于各种类型Internet接入方式和各种类型的网络中.原因很简单,NAT不仅完美地解决了IP

推送、透传、MQ

推送: 网页推送,是指将经过整理的信息资源以网页的形式迅速转发至用户的界面,实现用户的多层次需求,使得用户能够自己设定所需要的信息频道,并直接在用户端接收定制信息的实现方式. 推送功能:在手机通信中,大多智能机都支持推送功能.比如,你手机上有一个即时消息软件,当它在运行时它是和服务器相连的:但是一旦退出后,你就失去了连接.这时推送服务就开始工作了. 程序后台运行时都将会采用这样的一种提醒方式,比如提醒你升级,实时更新消息等. 官方的解释:所谓信息推送,就是"web广播",是通过一定的技

unity ugui消息透传

公司要做一个这东西. A是滑动区域,ScrollRect组件. B是各种选项. C是拾取到鼠标(或触点)的选项. D是拖放区域. 大概要求是这样. 因为B的条目很多,放在A里可以滑动查看.如果要选择一个B,需要长按B,待时间足够之后生产一个新的C.拖动到D区域释放,则给D添加一个节点.其他区域则取消. 如果按住B的时间不够长,又动了鼠标(或触点),则当前滑动操作由A响应,产生A的滑动效果. 这里涉及到一个消息透传的问题. 解释一下自己在做的过程中采用的2个方案以及各自问题. 涉及的函数有以下几个

音频透传背后的技术实现

现在市面上流行的电视盒大部分都是Android,"音频透传"是一个经常见到的词,那到底什么是音频透传.音频透传背后的技术实现到底如何,引起了我的兴趣,因此花了点时间研究了一下.由于是针对全志H8的电视盒方案进行分析,因此分析的结果不具有普遍性,可能其它的方案在技术实现上有所不同. 在开始分析前先查找了一下关于"透传"这个概念的解释,根据度娘的说法是"透传即是透明传送,即传送网络无论传输业务如何,只负责将需要传送的业务传送到目的节点,同时保证传输的质量即可,

Nginx多级反向代理下的IP透传

透传IP 为何要做透传IP 在使用了CDN做加速站点静态资源加速后,当用户请求的静态资源没能命中,此时CDN会到源站请求内容,那么此时访问源站的IP为CDN节点的IP,不仅如此,可能经我们的WAF防火墙和前端的负载均衡(SLB)后更不容易获取到真实的用户IP信息,我们如果要统计用户的访问IP和地区就变得比较麻烦,因为可能不是真实的IP,必须使用一个什么机制将用户IP传递到最终后端的应用服务器才行. 实验环境 访问流程 主机 IP配置 备注 Chrome 10.0.0.1 Windows浏览器 L