鏈路傳播
定義的是中間件的Server/Producer端將鏈路信息注入到載體的行爲;以及Client/Consumer如何從載體中抽取鏈路信息的行爲;載體
可以是消息體或者請求體。
背景
上一篇提到通過zipkin:實現zipkin-spring-boot-starter(三)降低接入鏈路跟蹤門檻以後,開發同學接入的積極性提高,同時也反饋目前支持鏈路跟蹤的中間件滿足不了需求
,比如說無法跟蹤通過redis實現的輕量級mq以及阿里雲的mns等。
但是站在框架維護者的角度考慮:對這類消息中間件的鏈路跟蹤非常棘手,爲什麼? 因爲這類消息中間件沒有擴展機制,開發者發送到消息隊列中的業務消息就是就是消息的全部。若要對消息進行鏈路跟蹤,那麼勢必會對消息體進行侵入:將鏈路信息放到業務消息中。
若侵入用戶消息體,那麼就要對業務消息體的格式有強要求,這是不行的、會引發出很多問題。
那RocketMQ爲什麼沒有這個問題呢?因爲RocketMQ將消息體分成了兩個部分:meta信息和業務數據;不管消息體中的業務數據如何變化都可以將鏈路上下文放到meta信息中,從而實現鏈路跟蹤。
所以現在面臨的問題:
- 框架維護者的角度:不能以侵入業務消息體爲代價進行鏈路跟蹤
- 業務開發者的角度:需要對此類消息中間件進行鏈路跟蹤
現在是一個兩難的局面,只能大家各往後退一步:
- 框架維護者:提供鏈路信息的提取和注入工具
- 業務開發者:自行在生產端將當前鏈路信息注入到消息體中;在消費端提取消息體中的鏈路信息提取出來
嗯,也是一個不錯的方案!
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;
});
}