链路传播
定义的是中间件的Server/Producer端将链路信息注入到载体的行为;以及Client/Consumer如何从载体中抽取链路信息的行为;载体
可以是消息体或者请求体。上一篇提到通过zipkin:实现zipkin-spring-boot-starter(三)包降低接入链路跟踪门槛以后,开发同学接入的积极性提高,同时也反馈目前支持链路跟踪的中间件满足不了需求
,比如说无法跟踪通过redis实现的轻量级mq以及阿里云的mns等。
但是站在框架维护者的角度考虑:对这类消息中间件的链路跟踪非常棘手,为什么?
因为这类消息中间件没有扩展机制,开发者发送到消息队列中的业务消息就是就是消息的全部。若要对消息进行链路跟踪,那么势必会对消息体进行侵入:将链路信息放到业务消息中。
若侵入用户消息体,那么就要对业务消息体的格式有强要求,这是不行的、会引发出很多问题。
那RocketMQ为什么没有这个问题呢?因为RocketMQ将消息体分成了两个部分:meta信息和业务数据;不管消息体中的业务数据如何变化都可以将链路上下文放到meta信息中,从而实现链路跟踪。
所以现在面临的问题:
现在是一个两难的局面,只能大家各往后退一步:
嗯,也是一个不错的方案!
聊链路传播之前,我们来看看链路传播到底传播了哪些信息,这是从dubbo链路提取出来的链路信息:
简单解释一下:
在官方的代码中,链路传播分为以下几个步骤:
1. 定义具体的传播行为
2. 提取或者注入链路信息
ok,进入实操阶段,先看看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 业务方法的返回值
* @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 业务方法的返回值
* @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的链路
@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;
});
}