SOFATracer(一) : OpenTraceing API 的实现

OpenTraceing 规范

SOFATracer 对 OpenTraceing 的实现

SOFATracer 就是根据 OpenTracing 规范 衍生出来的分布式 链路跟 踪的解决方案。

概念

OpenTracing 标准中有三个重要的相互关联的类型,分别是Tracer, SpanSpanContext

【下面的概念说明过程中,如不做说明,所使用的案例代码均以SOFATracer中的实现为例。】

Tracer

一个 trace 代表一个潜在的,分布式的,存在并行数据或并行执行轨迹(潜在的分布式、并行)的系统。一个trace可以认为是多个span的有向无环图(DAG)。

Tracer接口用来创建Span,以及处理如何处理Inject(serialize) 和 Extract (deserialize),用于跨进程边界传递。

SOFATracerSofaTracer这个类实现了 opentracingTracer 接口,并在此规范接口上做了一些扩展。看下Tracer 中声明的方法:

public interface Tracer {
    //启动一个新的span
    SpanBuilder buildSpan(String operationName);
    //将SpanContext上下文Inject(注入)到carrier
    <C> void inject(SpanContext spanContext, Format<C> format, C carrier);
    //将SpanContext上下文从carrier中Extract(提取)
    <C> SpanContext extract(Format<C> format, C carrier);   
    
    interface SpanBuilder {
    // 省略
    } 
}
复制代码

所以从接口定义来看,要实现一个Tracer,必须要实现其以下的几个能力:

启动一个新的span

SOFATracer 实现了 TracerbuildSpan 方法:

@Override
public SpanBuilder buildSpan(String operationName) {
    return new SofaTracerSpanBuilder(operationName);
}
复制代码

operationName :操作名称,字符串类型,表示由Span完成的工作 (例如,RPC方法名称、函数名称或一个较大的计算任务中的阶段的名称)。操作名称应该用泛化的字符串形式标识出一个Span实例。

何为泛化的字符串形式,比如现在有一个操作:获取用户 ;下面有几种标识方式:

  • 1、/get
  • 2、/get/user
  • 3、/get/user/123

方式1过于抽象,方式3过于具体。方式2是正确的操作名。

将SpanContext上下文Inject(注入)到carrier

@Override
public <C> void inject(SpanContext spanContext, Format<C> format, C carrier) {
    RegistryExtractorInjector<C> registryInjector = TracerFormatRegistry.getRegistry(format);
    if (registryInjector == null) {
        throw new IllegalArgumentException("Unsupported injector format: " + format);
    }
    registryInjector.inject((SofaTracerSpanContext) spanContext, carrier);
}
复制代码
  • SpanContext :实例
  • format(格式化)描述,一般会是一个字符串常量,但不做强制要求。通过此描述,通知Tracer实现,如何对SpanContext进行编码放入到carrier中。 carrier,根据format确定。Tracer实现根据format声明的格式,将SpanContext序列化到carrier对象中。

RegistryExtractorInjector 见后面

将SpanContext上下文从carrier中Extract(提取)

@Override
public <C> SpanContext extract(Format<C> format, C carrier) {
    RegistryExtractorInjector<C> registryExtractor = TracerFormatRegistry.getRegistry(format);
    if (registryExtractor == null) {
        throw new IllegalArgumentException("Unsupported extractor format: " + format);
    }
    return registryExtractor.extract(carrier);
}
复制代码
  • 格式描述符(format descriptor)(通常但不一定是字符串常量),告诉Tracer的实现如何在载体对象中对SpanContext进行编码
  • 载体(carrier),其类型由格式描述符指定。Tracer的实现将根据格式描述对此载体对象中的SpanContext进行编码

返回一个SpanContext实例,可以使用这个SpanContext实例,通过Tracer创建新的Span

Format

Tracer的注入和提取来看,format都是必须的。

Inject(注入)和Extract(提取)依赖于可扩展的format参数。format参数规定了另一个参数"carrier"的类型,同时约束了"carrier"SpanContext是如何编码的。所有的Tracer实现,都必须支持下面的format

  • Text Map: 基于字符串:字符串的map,对于keyvalue不约束字符集。
  • HTTP Headers: 适合作为HTTP头信息的,基于字符串:字符串的map。(RFC 7230.在工程实践中,如何处理HTTP头具有多样性,强烈建议tracer的使用者谨慎使用HTTP头的键值空间和转义符)
  • Binary: 一个简单的二进制大对象,记录SpanContext的信息。

在上面的注入和提取代码中,有如下代码片段:

//注入
RegistryExtractorInjector<C> registryInjector  = 
    TracerFormatRegistry.getRegistry(format);
//提取
RegistryExtractorInjector<C> registryExtractor = 
    TracerFormatRegistry.getRegistry(format);
复制代码

来通过TracerFormatRegistry这个类来来看下 SOFATracer 中的 Format 的具体实现。

X-B3

在看Format之前,先了解下X-B3

Access-Control-Expose-Headers: 
X-B3-TraceId,X-B3-ParentSpanId,X-B3-SpanId
复制代码

HTTP请求时其span参数通过http headers来传递追踪信息;header中对应的key分别是:

  • X-B3-TraceId: 64 encoded bits(id被encode为hex Strings)
  • X-B3-SpanId : 64 encoded bits
  • X-B3-ParentSpanId: 64 encoded bits
  • X-B3-Sampled:(是否采样) Boolean (either “1” or “0”)(下面的调用是否进行采样)
  • X-B3-Flags:a Long

SOFATracer 中的 Format

具体代码在 tracer-core -> com.alipay.common.tracer.core.registy 包下:

  • TextMapFormatter
  • TextMapB3Formatter
  • HttpHeadersFormatter
  • HttpHeadersB3Formatter
  • BinaryFormater

BinaryFormater:这个的注入和提取实现没有编解码一说;本身就是基于二进制流的操作。

TextMapB3Formatter/TextMapFormatterHttpHeadersB3Formatter/HttpHeadersFormatter 区别就在于编解码不同。HttpHeadersB3Formatter使用的是 URLDecoder.decode && URLDecoder.encode ; TextMapB3Formatter 返回的是值本身(如果为空或者null则返回空字符串)。

TextMapFormatterTextMapB3Formatter区别在于注入或者提取是使用的key不用。TextMapB3Formatter中使用的是 x-b3-{} 的字符串作为key

Span

一个span代表系统中具有开始时间和执行时长的逻辑运行单元。span之间通过嵌套或者顺序排列建立逻辑因果关系。当Span结束后(span.finish()),除了通过Span获取SpanContext外,下列其他所有方法都不允许被调用。

同样先来看下opentracing规范api 定义的 span 的定义及方法:

public interface Span extends Closeable {
    SpanContext context();
    void finish();
    void finish(long finishMicros);
    void close();
    Span setTag(String key, String value);
    Span setTag(String key, boolean value);
    Span setTag(String key, Number value);
    Span log(Map<String, ?> fields);
    Span log(long timestampMicroseconds, Map<String, ?> fields);
    Span log(String event);
    Span log(long timestampMicroseconds, String event);
    Span setBaggageItem(String key, String value);
    String getBaggageItem(String key);
    Span setOperationName(String operationName);
    Span log(String eventName, /* @Nullable */ Object payload);
    Span log(long timestampMicroseconds, String eventName, /* @Nullable */ Object payload);
} 
复制代码

通过Span获取SpanContext

//SOFATracerSpan
@Override
public SpanContext context() {
    return this.sofaTracerSpanContext;
}
复制代码

返回值,Span构建时传入的SpanContext。这个返回值在Span结束后(span.finish()),依然可以使用。

复写操作名

@Override
public Span setOperationName(String operationName) {
    this.operationName = operationName;
    return this;
}
复制代码

operationName:新的操作名,覆盖构建Span时,传入的操作名。

结束Span

@Override
public void finish() {
    this.finish(System.currentTimeMillis());
}

@Override
public void finish(long endTime) {
    this.setEndTime(endTime);
    //关键记录:report span
    this.sofaTracer.reportSpan(this);
    SpanExtensionFactory.logStoppedSpan(this);
}
复制代码

有一个可选参数,如果指定完成时间则使用当前指定的时间;如果省略此参数,使用当前时间作为完成时间。finish方法中会将当前span进行report操作。

为Span设置tag

Tag是一个key:value格式的数据。key必须是String类型,value可以是字符串、布尔或者数字

  • 字符串类型的value 设置tag
@Override
public Span setTag(String key, String value) {
    if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
        return this;
    }
    this.tagsWithStr.put(key, value);
    //注意:server 还是 client 在 OpenTracing 标准中是用 tags 标识的,所以在这里进行判断
    if (isServer()) {
        Reporter serverReporter = this.sofaTracer.getServerReporter();
        if (serverReporter != null) {
            this.setLogType(serverReporter.getReporterType());
        }
    } else if (isClient()) {
        Reporter clientReporter = this.sofaTracer.getClientReporter();
        if (clientReporter != null) {
            this.setLogType(clientReporter.getReporterType());
        }
    }
    return this;
}
复制代码
  • 布尔类型的value 设置tag
public Span setTag(String key, boolean value) {
    this.tagsWithBool.put(key, value);
    return this;
}
复制代码
  • 数字类型的value 设置tag
public Span setTag(String key, Number number) {
    if (number == null) {
        return this;
    }
    this.tagsWithNumber.put(key, number);
    return this;
}
复制代码

Log结构化数据

@Override
public Span log(long currentTime, Map<String, ?> map) {
    AssertUtils.isTrue(currentTime >= startTime, "current time must greater than start time");
    this.logs.add(new LogData(currentTime, map));
    return this;
}

@Override
public Span log(Map<String, ?> map) {
    return this.log(System.currentTimeMillis(), map);
}
复制代码
  • Map<String, ?> map : 键必须是字符串类型,值可以是任意类型
  • currentTime : 时间戳。如果指定时间戳,那么它必须在span的开始和结束时间之内。

设置一个baggage(随行数据)元素

Baggage元素是一个键值对集合,将这些值设置给给定的SpanSpanSpanContext,以及所有和此Span有直接或者间接关系的本地Span。 也就是说,baggage元素随trace一起保持在带内传递。(译者注:带内传递,在这里指,随应用程序调用过程一起传递)

Baggage元素为OpenTracing的实现全栈集成,提供了强大的功能 (例如:任意的应用程序数据,可以在移动端创建它,显然的,它会一直传递了系统最底层的存储系统。由于它如此强大的功能,他也会产生巨大的开销,请小心使用此特性。

再次强调,请谨慎使用此特性。每一个键值都会被拷贝到每一个本地和远程的下级相关的span中,因此,总体上,他会有明显的网络和CPU开销。

@Override
public Span setBaggageItem(String key, String value) {
    this.sofaTracerSpanContext.setBizBaggageItem(key, value);
    return this;
}
复制代码

SofaTracerSpan 中的属性

  • sofaTracer  : 当前 tracer
  • spanReferences : 当前span的关系,ChildOf(引用) or FollowsFrom(跟随)
  • tagsWithStr : String 类型的tag 集合
  • tagsWithBool : 布尔类型的tag集合
  • tagsWithNumber : 数值类型的tag集合
  • logs : log结构化数据列表,通过span.log(map)操作的map,均存储在logs中。
  • operationName:当前span的操作名
  • sofaTracerSpanContext:当前 spanContext
  • startTime : 当前span 开始时间
  • endTime : 当前span 结束时间,在finish方法中传入。
  • logType : report时才有意义:摘要日志类型,日志能够正确打印的关键信息;当前 span 的日志类型,如:客户端为 rpc-client-digest.log,服务端为 rpc-server-digest.log
  • parentSofaTracerSpan:父亲 span,当作为客户端结束并弹出线程上下文时,需要将父亲 span 再放入

SpanContext

opentracingSpanContext 接口中只有一个baggageItems方法,通过这个方法来遍历所有的baggage元素。

public interface SpanContext {
    Iterable<Map.Entry<String, String>> baggageItems();
}
复制代码

相对于OpenTracing中其他的功能,SpanContext更多的是一个“概念”。也就是说,OpenTracing实现中,需要重点考虑,并提供一套自己的API

OpenTracing的使用者仅仅需要,在创建span、向传输协议Inject(注入)和从传输协议中Extract(提取)时,使用SpanContextreferences

OpenTracing要求,SpanContext是不可变的,目的是防止由于Span的结束和相互关系,造成的复杂生命周期问题。

小结

本文简单罗列了下Opentraceing api中的一些基本概念,有个基本的认知。贴了一些SOFATracer中对于Opentraceing api的实现。后一篇来学习下SOFATracer中是如何使用disruptor这个并发框架的。

猜你喜欢

转载自juejin.im/post/5b655bcc51882519ad61a6d4
今日推荐