分析Zipkin/Brave中的B3
对 http://www.iocoder.cn/categories/Zipkin/ 的一些补充,分析基于Brave#release-5.11.2分支,可能未来会有所变化
开头先打个广告,我们团队目前正在找人,坐标上海,感兴趣可以投递呀,嘻嘻。高级Java开发工程师(框架开发)
协议的细节可以在如下地址找到
https://github.com/openzipkin/b3-propagation
Trace采样的四种状态
B3协议将Trace的状态(Sampling State)分为四种:
- Defer: 目前未知,需要后续确定
- Deny: 拒绝采样
- Accept: 已接受采样
- Debug: 强制接受采样,并设置Span.Tag{debug=True}
为了实现以上几种状态,在Brave里面使用了几个不同的bit位来标记不同的状态
Flags
所有Flag的定义可以在InternalPropagation这个暴露API的类中找到,https://github.com/openzipkin/brave/blob/release-5.11.2/brave/src/main/java/brave/internal/InternalPropagation.java#L34-L39
- FLAG_SAMPLED = 1 « 1 用于标记是否进行采样
- FLAG_SAMPLED_SET = 1 « 2 用于标记是否已经进行采样决定
- FLAG_DEBUG = 1 « 3 用于标记是否属于DEBUG类型
我们用一个Tuple如(i,j,k)
来表示这三个值的不同组合,从左到右分别是Flag_Sampled_Set
,Flag_Sampled
以及Flag_Debug
,则相应的状态表为
- (1, 1, 1) => Debug
- (1, 1, 0) => Accept
- (1, 0, 0) => Deny
- (0, 0, 0) => Defer aka Empty
其余的状态理论上都是不合法的状态,比如
- (0, 1, 0) => 第一个bit表示未标记采样,但第二个bit表示接受采样,这是互相矛盾的
- (1, 0, 1) => 第三个bit表示Debug状态,但第二个bit和第一个bit联合表示拒绝采样,也是矛盾的
package brave.internal;public abstract class InternalPropagation { <SNIP> public static int sampled(boolean sampled, int flags) { // 如果sampled为True表示接受采样,会同时设置FLAG_SAMPLED以及FLAG_SAMPLED_SET为1,用位或运算 if (sampled) { flags |= FLAG_SAMPLED | FLAG_SAMPLED_SET; } else { // 如果拒绝采样,会同时设置FLAG_SAMPLED=0以及FLAG_SAMPLED_SET=1 flags |= FLAG_SAMPLED_SET; flags &= ~FLAG_SAMPLED; } return flags; } <SNIP>}
以及所有合法的组合定义在SamplingFlags
类中,这个类是TraceContext
的基类,用于抽象表示TraceContext
的采样状态。以下几个SamplingFlags
对应于协议中标明的四个不同的状态,除此以外其他的组合理论上是不合法的。
package brave.propagation;<SNIP>import static brave.internal.InternalPropagation.FLAG_DEBUG;import static brave.internal.InternalPropagation.FLAG_SAMPLED;import static brave.internal.InternalPropagation.FLAG_SAMPLED_LOCAL;import static brave.internal.InternalPropagation.FLAG_SAMPLED_SET;//@Immutablepublic class SamplingFlags { public static final SamplingFlags EMPTY = new SamplingFlags(0); public static final SamplingFlags NOT_SAMPLED = new SamplingFlags(FLAG_SAMPLED_SET); public static final SamplingFlags SAMPLED = new SamplingFlags(NOT_SAMPLED.flags | FLAG_SAMPLED); public static final SamplingFlags DEBUG = new SamplingFlags(SAMPLED.flags | FLAG_DEBUG); <SNIP>}
Extract B3 Propagation Header
接下来分析B3Codec
是如何从HTTP Header中提取这些信息的,Brave中有两个类负责从HTTP的头部获取Trace的上下文信息
B3Propagation<K>.B3Injector/B3Extractor
B3SingleFormat
从注释看,B3SingleFormat
对应的是Single Header情况,如
b3: {x-b3-traceid}-{x-b3-spanid}-{if x-b3-flags 'd' else x-b3-sampled}-{x-b3-parentspanid}
而B3Propagation<K>
则负责解析Multiple Headers形式,由四个不同的Header组成的信息:
X-B3-TraceIdX-B3-ParentSpanIdX-B3-SpanIdX-B3-Sampled
解析的过程定义在extract
方法中,
static final class B3Extractor<C, K> implements TraceContext.Extractor<C> { @Override public TraceContextOrSamplingFlags extract(C carrier) { if (carrier == null) throw new NullPointerException("carrier == null"); // 首先尝试Single Header String b3 = getter.get(carrier, propagation.b3Key); TraceContextOrSamplingFlags extracted = b3 != null ? parseB3SingleFormat(b3) : null; if (extracted != null) return extracted; // 检查`X-B3-Sampled`字段,为了兼容性同时判断数字0/1和布尔值true/false String sampled = getter.get(carrier, propagation.sampledKey); // 这里使用Boolean类型是为了标记三种采样状态 // - null: Defer // - True: Accept // - False: Deny Boolean sampledV; if (sampled == null) { sampledV = null; // defer decision } else if (sampled.length() == 1) { // handle fast valid paths char sampledC = sampled.charAt(0); if (sampledC == '1') { sampledV = true; } else if (sampledC == '0') { sampledV = false; } else { Platform.get().log(SAMPLED_MALFORMED, sampled, null); return TraceContextOrSamplingFlags.EMPTY; // trace context is malformed so return empty } } else if (sampled.equalsIgnoreCase("true")) { // old clients sampledV = true; } else if (sampled.equalsIgnoreCase("false")) { // old clients sampledV = false; } else { Platform.get().log(SAMPLED_MALFORMED, sampled, null); return TraceContextOrSamplingFlags.EMPTY; // Restart trace instead of propagating false } // 检查`X-B3-Flags: 1`,是否是Debug状态 boolean debug = "1".equals(getter.get(carrier, propagation.debugKey)); String traceIdString = getter.get(carrier, propagation.traceIdKey); // 允许出现TraceID不存在的情况,此时创建一个仅包含Sampled和Debug状态的SamplingFlags if (traceIdString == null) return TraceContextOrSamplingFlags.create(sampledV, debug); // Try to parse the trace IDs into the context TraceContext.Builder result = TraceContext.newBuilder(); if (result.parseTraceId(traceIdString, propagation.traceIdKey) && result.parseSpanId(getter, carrier, propagation.spanIdKey) && result.parseParentId(getter, carrier, propagation.parentSpanIdKey)) { if (sampledV != null) result.sampled(sampledV.booleanValue()); if (debug) result.debug(true); // 试图创建一个完整的TraceContext return TraceContextOrSamplingFlags.create(result.build()); } return TraceContextOrSamplingFlags.EMPTY; // trace context is malformed so return empty } }<SNIP>}
然而这边通过B3Extractor
得到的TraceContext
并不是一个真正可用的实例,而是类似C语言中的一种union
类型,它封装了三个可能的结果,
- TraceContext: 对应完整的上下文信息, type=1
- TraceIdContext: 仅包含TraceID, type=2
- SamplingFlags: 仅包含flags, type=3
我们根据TraceIdContext
的注释可以知道,TraceIdContext
对应的是SpanID
是由外部系统控制的情况,比如Amazon X-Ray
Create next span with TraceContextOrSamplingFlags
接着我们可以根据这个union
类型TraceContextOrSamplingFlags
来创建Span
,代码在Tracer
里面,
package brave;public class Tracer {<SNIP> public Span nextSpan(TraceContextOrSamplingFlags extracted) { if (extracted == null) throw new NullPointerException("extracted == null"); // 先判断是不是完整的TraceContext TraceContext context = extracted.context(); if (context != null) return newChild(context); // 再判断是不是TraceIdContext TraceIdContext traceIdContext = extracted.traceIdContext(); if (traceIdContext != null) { return _toSpan(decorateContext( InternalPropagation.instance.flags(extracted.traceIdContext()), traceIdContext.traceIdHigh(), traceIdContext.traceId(), 0L, 0L, 0L, extracted.extra() )); } // 如果以上都不是,那么一定是SamplingFlags SamplingFlags samplingFlags = extracted.samplingFlags(); List<Object> extra = extracted.extra(); // 这里需要判断当前环境是否存在已知的TraceContext,将其认作隐式的父Span TraceContext implicitParent = currentTraceContext.get(); int flags; long traceIdHigh = 0L, traceId = 0L, localRootId = 0L, spanId = 0L; if (implicitParent != null) { // 如果存在TraceContext上下文,直接使用implicitParent作为ParentSpan flags = InternalPropagation.instance.flags(implicitParent); traceIdHigh = implicitParent.traceIdHigh(); traceId = implicitParent.traceId(); localRootId = implicitParent.localRootId(); spanId = implicitParent.spanId(); // 这里需要做的仅仅是把extra包合并在一起 extra = concatImmutableLists(extra, implicitParent.extra()); } else { // 否则的话,遵照SamplingFlags中指定的flags flags = InternalPropagation.instance.flags(samplingFlags); } // 创建一个新的Span // 如果这里的TraceID和SpanID都为空,会在decorateContext方法中对其进行补全 return _toSpan(decorateContext(flags, traceIdHigh, traceId, localRootId, spanId, 0L, extra)); }<SNIP>}
至此的话,我们就了解了Brave客户端是如何进行B3协议的解析,以及如何正确地映射相应的状态。这里再举个例子,比如,B3协议中提到的如下请求:
Server Tracer ┌───────────────────────┐ Health check request │ │┌───────────────────┐ │ TraceContext ││ GET /health │ Extract │ ┌───────────────────┐ ││ X-B3-Sampled: 0 ├─────────┼>│ NoOp │ │└───────────────────┘ │ └───────────────────┘ │ └───────────────────────┘
如果使用curl
这样的工具,只传递一个X-B3-Sampled: 0
,那么Extractor
就会创建一个SamplingFlags
,并且设置成(1, 0, 0)的状态,表明这个请求已经作出了采样决定,并且拒绝采样(Deny)。
分析B3协议及其实现的目的在于理解链路追踪系统的实现,在横向对比市面上不同的Tracing产品的时候能够有的放矢,目前比较火的几款开源的系统,比如
- Jaeger
- Skywalking
- Zipkin
他们具有什么样的特征以及在实现上的区别,更加深入的考察能够帮助我们更好的做架构的选型。如果想要客制化链路追踪系统,这方面的理解也是必不可少的。以后还会给大家解读更多链路追踪系统的设计和实现。