Dubbo框架如何和调用链整合

前言:dubbo的自定义filter的机制, 来实现traceId/logid的透传.

 

方案一:

@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, 显然破坏"无侵入性"原则.

 

方案二:

640?wx_fmt=png

 

RpcContext方案:维护了一次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文件

  640?wx_fmt=png

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会被重新注入, 保证跟踪线索透传成功.

 

总结:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值