1

前言

OpenTelemetry作为一个分布式追踪的项目,他支持非常多的语言,如Java,Golang,Python等,鉴于笔者的主力语言为Java,并且后续需要介绍OpenTelemetry的Java Agent实现,所以后续文章中的相关知识点都以Java或者Java Sdk的方式为主。

初识OpenTelemetry

在微服务广泛发展和使用的当下,对于整个微服务体系的使用情况的观察以及服务依赖调用情况都不再像以往那么清晰明了。而这正是OpenTelemetry能够为我们提供的能力。

OpenTelemetry源自OpenSencuc和OpenTracing的合并,它的目标是集成Trace,Metrics,Logging能力来提供可观测性。过去的分布式追踪往往是各做各的,没有固定的标准,各个分布式追踪方案各显神通,使用不同的协议,不同的标准。但是OpenTelemetry不同,它提供了一系列的标准,并且他的可插拔式的架构为将来的协议和数据结构扩展提供了便利的方式。

调用链Trace

分布式调用链,俗称调用链,用来记录请求的路径整体路径。下图是一个典型的的请求以及其RPC调用的链路:

从图中我们可以很清晰的了解到刚才的请求是怎么一个流转过程,经过了什么组件和服务,接口。这就是调用链的作用之一,让我们的请求链路更加透明清晰。

Span

Span在调用链中是一个基础的单元,一个调用链是由很多的Span组成的。在一个Span中会包含如下信息:

  1. 名称
  2. 父Span的ID,root节点的父Span为空
  3. 开始与结束时间戳
  4. Span Context
  5. Attributes
  6. Span事件
  7. Span Links
  8. Span状态
  9. Span Kind

以下是一个典型的Span结构:

{
  "trace_id": "7bba9f33312b3dbb8b2c2c62bb7abe2d",
  "parent_id": "",
  "span_id": "086e83747d0e381e",
  "name": "/v1/sys/health",
  "start_time": "2021-10-22 16:04:01.209458162 +0000 UTC",
  "end_time": "2021-10-22 16:04:01.209514132 +0000 UTC",
  "status_code": "STATUS_CODE_OK",
  "status_message": "",
  "attributes": {
    "net.transport": "IP.TCP",
    "net.peer.ip": "172.17.0.1",
    "net.peer.port": "51820",
    "net.host.ip": "10.177.2.152",
    "net.host.port": "26040",
    "http.method": "GET",
    "http.target": "/v1/sys/health",
    "http.server_name": "mortar-gateway",
    "http.route": "/v1/sys/health",
    "http.user_agent": "Consul Health Check",
    "http.scheme": "http",
    "http.host": "10.177.2.152:26040",
    "http.flavor": "1.1"
  },
  "events": [
    {
      "name": "",
      "message": "OK",
      "timestamp": "2021-10-22 16:04:01.209512872 +0000 UTC"
    }
  ]
}

Span Context

Span Context可以理解为上下文,是Span中包含的不可变的对象。在Span Context中包含了:

  • traceId:调用链ID
  • spanId:Span的ID
  • traceFlag:二进制形式的调用链标志位,一般用于表示调用链是否采样(isSampled)
  • traceState:承载调用链信息的K-V结构列表

Attributes

Attributes是一个用来携带信息的K-V结构。

在java sdk中可以通过:

Span.current().setAttribute("My Attributes", "attr");

来自定义你想要设置的Attributes。当然在OpenTelemetry中默认内置的那些Instrumentation都会有定义一些指定标准化的的Attributes,详情可以参照[Semantic Attributes](
https://opentelemetry.io/docs...)

Span事件

Span事件(Events)是一种事件机制,可以将事件触发与具体的Span进行绑定,然后在调用链页面展示出来。如下事例:

Span.current().addEvent("My Event");

Span Links

Span Links是一种能够将调用链关联起来的技术,通过配置关联的Span,可以在页面中展现关联的调用链信息。不过请注意Span Links必须要在Span创建时才能添加,不像Events和Attributes一样能在Span创建之后添加。例子如下:

Tracer tracer = GlobalOpenTelemetry.getTracer("1111");

Span span = tracer.spanBuilder("start")
        .addLink(SpanContext.create("ee868088dfd10adbaa459c9aa353b112", "53b11b6c55010604",
                TraceFlags.getDefault(), TraceState.getDefault())).startSpan();
span.end();

Span状态

Span状态(Status)是定义好的Span的状态,有如下几种:

  • Unset
  • Ok
  • Error

Span Kind

Span Kind是指Span类型,有如下几种:

  • Server
  • Client
  • Producer
  • Consumer
  • Internal

顾名思义Server/Client指的是服务端/客户端,Producer/Consumer指的是生产者/消费者,显然这个一般适用于消息队列,Internal是内部组件产生的Span

Trace构建的原理

简单来说的话Trace是由众多的Span组成的,而Span则是由众多的Instrumentation库组成的,这些库由开源作者构建,用于支持不同的组件,如http请求,kafka,redis等等。依托于这些Instrumentation,调用链可以生成对应的Span。

生成Span自然不是难题,问题在于是如何将这些Span串联起来的。在Trace中有一个唯一的标识TraceID,而且在Span中也有一个SpanId和ParentSpanId,借助这些信息,在Span将所有数据推送到服务端后,服务端就能根据这些信息进行重组,然后在界面上进行展示。

但是又存在一个问题,TraceId以及ParentSpanId是如何在Span间进行传递的呢?

这里就涉及到了Trace的底层原理了。在这里以Java Sdk来举例。在Sdk中会定义一个Context类用于建立一个内存中的线程隔离的存储机制来存储上游传递的数据。一般来说上游往下游传递数据每个插件都是不同的形式。例如如果是http请求,那就借助Header,如果是Kafka,也是借助于Kafka自带的prop来进行数据传递。之后在下游获取到数据后利用Context将其存放入内存中,这个过程被称为extract,在数据要再往下传递时,需要将内存中数据取出,在解析成Header或是其他的形式,这个被称为inject。调用链信息正是以此来传递的。

Trace就是依靠traceparent来进行传递的,traceparent不仅包含了traceId,还包含了一些isSample等等的基础信息。

Metrics

Metrics是一种度量标准,用于展现应用的CPU,内存等等指标级的度量信息。

OpenTelemetry定义了三种metrics仪器:

  1. counter: 累加值,这类指标不会减少,只会不断的累加上去
  2. measure: 一段时间的数据聚合值,表示的是一段时间内的数据累加值
  3. observer: 抓取当前时间的一系列特定值

实际上OpenTelemetry提供了许多基础的指标计算方式,例如:LongCounter,LongUpDownCounter,DoubleHistogram,DoubleGauge等等。

Meter meter = GlobalOpenTelemetry.meterBuilder("my-meter-instrumentation")
                .setInstrumentationVersion("1.0.0")
                .build();

LongCounter counter = meter
        .counterBuilder("my_metrics")
        .setDescription("My Metrics")
        .setUnit("1")
        .build();

counter.add(100);

上述代码是一个简单的创建指标的的例子,这里创建了一个固定值为100的名为my_metrics的指标,由于是counter,所以最终指标名为my_metrics_total

Logs

日志也是OpenTelemetry的一大功能之一,不过截止到本文发布前,Logs功能还未GA,因此存在变数,后续我们在聊到Agent相关内容时会再简单聊聊这部分内容,在这里就先一笔带过。

Baggage

Baggage用于在Span间传递数据。

设想一个场景,你希望在链路的当前的Span中将某些数据传递下去,使用attributes显示然是不行的,因此需要一些手段将其传递下去,Baggage就是为此而设计的。

其实Baggage的原理基本和调用链的traceId的传递基本相似,不同之处是它定义了一个名为baggage的key,而这个key中包含的值是以K-V形式组织的,因此你可以传递自己想要的值下去。

在早期Baggage底层维护了一个Map来存储这些数据,后来在某个版本后改成了用数组的形式,每两个数组位置分别存储一对K-V,并且做了一些特殊的处理来实现删除等操作,有兴趣的可以去看看源码。

总结

在本文中我们简单的介绍了OpenTelemetry的一些使用和实现的原理,在后续的文章中会更多的介绍整个OpenTelemetry的体系,请期待后续!

参考文档:

[1] https://opentelemetry.io/docs


骑牛上青山
1.2k 声望22 粉丝