分析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_SetFlag_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的上下文信息

  1. B3Propagation<K>.B3Injector/B3Extractor
  2. 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

他们具有什么样的特征以及在实现上的区别,更加深入的考察能够帮助我们更好的做架构的选型。如果想要客制化链路追踪系统,这方面的理解也是必不可少的。以后还会给大家解读更多链路追踪系统的设计和实现。

(0)

相关推荐