zipkin:自定义链路传播(四)

  • 链路传播定义的是中间件的Server/Producer端将链路信息注入到载体的行为;以及Client/Consumer如何从载体中抽取链路信息的行为;
  • 载体可以是消息体或者请求体。

背景

上一篇提到通过zipkin:实现zipkin-spring-boot-starter(三)降低接入链路跟踪门槛以后,开发同学接入的积极性提高,同时也反馈目前支持链路跟踪的中间件满足不了需求,比如说无法跟踪通过redis实现的轻量级mq以及阿里云的mns等。

但是站在框架维护者的角度考虑:对这类消息中间件的链路跟踪非常棘手,为什么? 因为这类消息中间件没有扩展机制,开发者发送到消息队列中的业务消息就是就是消息的全部。若要对消息进行链路跟踪,那么势必会对消息体进行侵入:将链路信息放到业务消息中。若侵入用户消息体,那么就要对业务消息体的格式有强要求,这是不行的、会引发出很多问题。

那RocketMQ为什么没有这个问题呢?因为RocketMQ将消息体分成了两个部分:meta信息和业务数据;不管消息体中的业务数据如何变化都可以将链路上下文放到meta信息中,从而实现链路跟踪。

所以现在面临的问题:

  1. 框架维护者的角度:不能以侵入业务消息体为代价进行链路跟踪
  2. 业务开发者的角度:需要对此类消息中间件进行链路跟踪

现在是一个两难的局面,只能大家各往后退一步:

  1. 框架维护者:提供链路信息的提取和注入工具
  2. 业务开发者:自行在生产端将当前链路信息注入到消息体中;在消费端提取消息体中的链路信息提取出来

嗯,也是一个不错的方案!

zipkin链路信息

聊链路传播之前,我们来看看链路传播到底传播了哪些信息,这是从dubbo链路提取出来的链路信息:

  • X-B3-SpanId=c4d1b2bf028149ec
  • X-B3-ParentSpanId=bfb6542ce3a8f6ca,
  • X-B3-Sampled=1
  • X-B3-TraceId=bfb6542ce3a8f6ca

简单解释一下:

  • X-B3-SpanId:当前span的id
  • X-B3-ParentSpanId:当前span的父spanid
  • X-B3-Sampled:采样标识符,即是否上报的zipkin-server端
  • X-B3-TraceId:当前trace的id

链路传播

在官方的代码中,链路传播分为以下几个步骤:

1. 定义具体的传播行为

  • brave.propagation.Propagation.Setter:将链路信息set到指定的载体中
  • brave.propagation.Propagation.Getter:将链路信息从指定的载体中get出来

2. 提取或者注入链路信息

  • brave.propagation.Propagation#injector:与Setter结合使用,将内存中的链路信息注入到载体中
  • brave.propagation.Propagation#extractor:与Getter结合使用,将载体中的链路信息提取到内存中

ok,进入实操阶段,先看看dubbo链路传播是怎样实现的

官方dubbo链路传播

1. 定义具体的传播行为:将请求体中的attachement作为链路信息的载体,把链路信息直接put到attachment中

    static final Setter<DubboClientRequest, String> SETTER =
        new Setter<DubboClientRequest, String>() {
            @Override public void put(DubboClientRequest request, String key, String value) {
                request.putAttachment(key, value);
            }
        };

2. 提取或者注入链路信息:提取内存中的链路信息并放到载体中

	//1. 设置具体的链路传播行为
	TraceContext.Injector<DubboClientRequest> injector = tracing.propagation().injector(SETTER);

	//2. 从dubbo请求上下文中获取载体信息,即attachment
    Map<String, String> attachments = RpcContext.getContext().getAttachments();
    DubboClientRequest request = new DubboClientRequest(invocation, attachments);
    
    //3. 将内存中的链路信息设置到载体中
    injector.inject(span.context(), request);

为了节省篇幅,这里只分析了如何将链路信息注入到载体中,如果感兴趣可以阅读zipkin官方dubbo链路传播源码~

自定义链路传播

1. 定义具体的传播行为:考虑到通用性、减少对第三方的依赖,这里使用java.util.Properties作为载体

public class CustomPropagation {
    public static final Propagation.Setter<Properties, String> SETTER = (carrier, key, value) -> {
        carrier.setProperty(key, value);
    };

    public static final Propagation.Getter<Properties, String> GETTER = (carrier, key) -> {
        return carrier.getProperty(key);
    };
}

2. 提取或者注入链路信息

    /**
     * 将当前链路上下文信息注入到开发者业务方法中,适用于redis、mns等链路传递
     *
     * @param tracing  链路核心类
     * @param spanName spanName
     * @param func     开发者的业务方法
     * @param <R>      业务方法的返回值
     * @return 业务方法返回
     */
    public static <R> R injectTraceInfo(Tracing tracing, @NotNull String spanName, Function<Properties, R> func) {
        Tracer tracer = tracing.tracer();
        Span span = tracer.nextSpan().name(spanName).start();
        //设置brave链路信息传播行为
        TraceContext.Injector<Properties> injector = tracing.propagation().injector(CustomPropagation.SETTER);
        Properties properties = new Properties();
        //将链路信息注入到Properties中
        injector.inject(span.context(), properties);
        span.kind(Kind.PRODUCER);
        try (SpanInScope ws = tracer.withSpanInScope(span)) {
        	//执行业务方法
            return func.apply(properties);
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.finish();
        }
    }

    /**
     * 从properties中提取链路信息并注入到当前链路上下文中,使用redis、mns等链路传递
     *
     * @param tracing    链路核心类
     * @param spanName   spanName
     * @param properties 链路信息
     * @param func       开发者的业务方法
     * @param <R>        业务方法的返回值
     * @return 业务方法返回
     */
    public static <R> R extractTraceInfo(Tracing tracing, @NotNull String spanName,
                                         Properties properties, Function<Span, R> func) {
        //设置链路传播行为
        TraceContext.Extractor<Properties> extracted = tracing.propagation().extractor(CustomPropagation.GETTER);
        //提取载体中的链路信息
        TraceContextOrSamplingFlags traceInfo = extracted.extract(properties);
        Tracer tracer = tracing.tracer();
        Span span;
        if (traceInfo != null && traceInfo.context() != null) {
        	//将链路信息注入到载体中
            span = tracer.newChild(traceInfo.context()).name(spanName).start();
        } else {
            span = tracer.newTrace().name(spanName).start();
        }
        span.kind(Kind.CONSUMER);
        try (SpanInScope ws = tracer.withSpanInScope(span)) {
            return func.apply(span);
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.finish();
        }
    }

开发者只要在Lambda表达式中传入业务方法即可将链路衔接起来,是不是很方便~

代码加上注释不难理解,这里就不再对代码单独解释了。下面看看RocketMQ如何使用自定义链路传播打通producer和consumer的链路

RocketMQ自定义链路传播实战


    @Override
    public SendResult send(Message message) {
        return traceMessage(message, message1 -> producer.send(message1));
    }

    private <T> T traceMessage(Message message, Function<Message, T> func) {
        return TraceUtil.injectTraceInfo(tracing, "ONS/SEND", properties -> {
            Span span = tracing.tracer().currentSpan();
            //TraceUtil.injectTraceInfo已经将内存中的链路信息提取到properties了
            //所以这里将properties设置到message的元信息中即可
            message.setUserProperties(properties);
            T res = func.apply(message);
            if (res != null && res.getClass() == SendResult.class) {
                String messageId = ((SendResult)res).getMessageId();
                span.tag("mq.msgId", messageId);
            }
            span.tag("mq.topic", message.getTopic());
            return res;
        });
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章