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

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

  1. Spring Boot 中使用自定义注解,AOP 切面打印出入参日志及Dubbo链路追踪透传traceId

    一.使用背景 开发排查系统问题用得最多的手段就是查看系统日志,在分布式环境中一般使用 ELK 来统一收集日志,但是在并发大时使用日志定位问题还是比较麻烦,由于大量的其他用户/其他线程的日志也一起输出穿 ...

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

    第一种:CSS实现 <style><!-- .fileInputContainer{        height:256px;        background:url(upfil ...

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

    一.要解决什么问题: 使用elk的过程中发现如下问题: 1.无法准确定位一个请求经过了哪些服务 2.多个请求线程的日志交替打印,不利于查看按时间顺序查看一个请求的日志. 二.期望效果 能够查看一个请求 ...

  4. APP消息推送:通知和透传

    目前市场上的消息推送方式有两种:通知和透传.什么是透传?透传即是透明传送,即传送网络无论传输业务如何,只负责将需要传送的业务传送到目的节点,同时保证传输的质量即可,而不对传输的业务进行处理.透传消息, ...

  5. 手把手教你开发BLE数据透传应用程序

    如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对B ...

  6. unity ugui消息透传

    公司要做一个这东西. A是滑动区域,ScrollRect组件. B是各种选项. C是拾取到鼠标(或触点)的选项. D是拖放区域. 大概要求是这样. 因为B的条目很多,放在A里可以滑动查看.如果要选择一 ...

  7. sim800c GPRS模块的透传模式

    一.透传模式 基于sim800c GPRS模块在建立TCP/IP连接情况下,可以设置进入透传模式,用来接收和发送数据,一旦进入即从串口收到的数据将被打包,然后发送.接收同理. 注意在透传模式下所有的A ...

  8. 11.源码分析---SOFARPC数据透传是实现的?

    先把栗子放上,让大家方便测试用: Service端 public static void main(String[] args) { ServerConfig serverConfig = new S ...

  9. SWA2G422&485JK2G基础篇: STM32+W5500实现MQTT通信控制,485/422透传通信

    说明 这节实现的功能: STM32+W5500实现MQTT通信控制 细节功能: 1.DHCP动态获取IP 2.DNS域名解析 3.网口<--MQTT-->485/422透传通信 测试准备工 ...

随机推荐

  1. Linux 默认线程栈大小 调优

    Linux 线程栈介绍 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等:和堆相比,栈通常很小. Linux 查询线程栈 1.查看默认的 ...

  2. mysql awr v1.0.3修正说明以及发布

    本版本计划修正或者包含如下内容: 1.innodb buffer_pool只是分配的vm大小,实际并不一定真正使用这么多,还可能会有内存泄露,故调整从innodb_buffer_pool_stats获 ...

  3. shell的交互式和非交互式登录

    工作中经常碰见环境变量加载问题,归根结底就是配置文件的加载问题. 一般会有四种模式:交互式登陆.非交互式登陆.交互式非登陆.非交互非登陆. 交互式和非交互式对环境变量的加载: +----------- ...

  4. 使用python玩跳一跳亲测使用步骤详解

    玩微信跳一跳,测测python跳一跳,顺便蹭一蹭热度: 参考博文 使用python玩跳一跳超详细使用教程 WIN10系统,安卓用户请直入此: python辅助作者github账号为:wangshub. ...

  5. 【笔记】Cocos2dx学习笔记

    自建场景类 自建场景类BaseScene继承与Scene类,在init函数中添加了默认的,键盘与鼠标事件的响应,添加了一个用于读取XML文件的字典,添加了一个结束场景的方法. 类的声明代码如下: #i ...

  6. struts2注释返回json数据

  7. tomcat启动后8005端口未被占用

    8005端口是tomcat本身的端口,如果这个端口在启动时未被tomcat占用的话,就无法使用它自带的shutdown.sh脚本关闭tomcat 接下来我以tomcat-9.0.12为例说明 下载to ...

  8. C/C++.判断文件是否存在(_access)

    1. int _access(char* path,int mode)头文件<io.h>功能:确定文件或文件夹的访问权限.如果指定的存取方式有效,则函数返回0,否则函数返回-1. 参数pa ...

  9. 字符串转换整数 (atoi)

    题目: 请你来实现一个 atoi 函数,使其能将字符串转换成整数. 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止. 当我们寻找到的第一个非空字符为正或者负号时,则将该 ...

  10. (转)决定系数R2

    有些讲得太烂了,我来通俗的梳理一下R2. Calculating R-squared 在线性回归的模型下,我们可以计算SE(line), SE(y均值). The statistic R2descri ...