1. 全链路监控的兴起与发展
当代的互联网的服务,通常都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,全链路监控组件就在这样的问题背景下产生了。
1.1 组件
2010年,google发表了一篇公开论文Google Dapper,这篇著名论文阐述了如何在超大规模系统上建设低损耗、应用级透明、大范围部署的链路追踪服务,在该文章的启发下,业界诞生了很多用于分布式链路追踪的开源组件,大部分组件都参考了google dapper的设计理念。发展至今,业界比较流行的开源组件有如下几种:
名称 | 开源公司 | git地址 | 开发语言 |
---|---|---|---|
pinpoint | naver | https://github.com/naver/pinpoint | java |
zipkin | twiter | https://github.com/openzipkin/zipkin | java |
jaeger | uber | https://github.com/jaegertracing/jaeger | go |
skywalking | 国内作者转Apache | https://github.com/apache/incubator-skywalking | java |
1.2 标准
在 Dapper 的启发下,业界诞生了很多用于分布式链路追踪的开源组件,为了保持对链路中每一个环节的记录与匹配,不仅需要在应用内部对跟踪信息进行传递,还需要让跟踪信息跨越不同的应用以及不同的分布式组件。这需要制定一套统一的标准,让微服务体系中的所有应用遵循这套标准来实现跟踪信息的描述和传递。
业内先后出现了两套比较著名的标准OpenCensus、OpenTracing,这两套标准都抽象出一套与编程语言以及业务逻辑无关的接口,用于对链路追踪的统一管理,为全链路监控的发展做出了巨大贡献。但是这两个项目的最大问题就是他们是两个项目,这两个项目并没有在一起工作,也没有为相互兼容而努力。世界上有两个相似但不完全相同的项目,这给开发者带来了困惑和不确定性。于是在2019年,在由谷歌、微软、Uber等公司组成的委员会的牵头下,OpenCensus和OpenTracing合并为一套标准,它就是 Open Telemetry。OpenTelemetry直接向后兼容OpenTracing和OpenCensus,成为了现在业内普遍使用的标准。
2. 全链路监控的设计理念
2.1 设计目标
分布式链路监控的组件设计时,通常需要从以下方面做考虑:
- 性能消耗:服务调用埋点本身会带来性能损耗,通常组件的性能消耗越小越好,并且可以实现采样机制,通过配置采样率来控制性能消耗。
- 应用级的透明:作为组件,应该尽可能少的甚者无侵入其他业务系统,对于使用方做到透明易用。
- 数据分析:提供功能齐全的分析后台,能够从多维度对数据进行分析
- 可扩展性:必须支持分布式部署,以支持巨量的trace日志分析
2.2 设计模型
在Google Dapper的设计中,一次完整的调用被称为trace,而其中一条链路的调用被称为span,一个trace是由许多span组成的一个树形结构,而一个span又包含了很多事件相关信息,被称为Annotation(注解)。
2.2.1 trace
race表示一次完整的跟踪,通常从请求到服务器开始,到服务器返回response结束,其通常包含一个唯一标识TraceId,你可以通过此TraceId来在后台查看此次调用的链路情况,下面是一个调用例子:
2.2.2 span
span是链路追踪的基本组成单元,其也包含了唯一标识SpanId,一个span可以表示追踪任何事件,不仅限于rpc调用、http调用、DB调用,也可以表示一次函数调用甚至部分函数调用,具体情况视使用情况而定。
由于trace是由span组成的树形结构,所以一个span不仅有自己的SpanId,还包含了parent_id以标识其父span,如果一个span没有父ID被称为root span。一个trace的所有span都共用一个TraceId。另外span还包含其他描述信息,比如时间戳以及由key-value形式存储的特定事件tag(Annotation)信息。
通常一个Span至少包含下面的信息:
type Span struct {
TraceID int64
SpanID int64
ParentID int64
StartTime time.Time
EndTime time.Time
Annotation []Annotation
}
把同一TraceID的Span收集起来,按时间排序timeline,把ParentID串起来就是调用栈,最后后台上会展示类似下面图片所示的结构:
2.2.3 调用过程追踪
每一次追踪的生成过程如下所示:
- 请求到来生成一个全局TraceID,通过TraceID可以串联起整个调用链,一个TraceID代表一次请求。
- 每一次链路的调用会生成一个span,span中记录了ParentId和SpanId,一个没有ParentId的span成为root span,可以看成调用链入口。
- 整个调用过程中每个请求都要透传TraceID和SpanID,每个服务将该次请求附带的TraceId作为自己的TraceId,并将该次请求附带的SpanID作为ParentId,并且生成自己的SpanID,并记录其他信息。
- 要查看某次完整的调用,只要根据TraceID查出所有调用记录,然后通过parent id和span id组织起整个调用父子关系。
2.3 接入模式
接入模式通常分为SDK和探针模式两种:
- SDK: 通过引入链路追踪 SDK 自动生成链路数据,并自动上报。对框架或业务代码会有侵入,有时监控逻辑的注入会比较复杂。
- 探针模式: 探针方式不需要在代码编译前引入 SDK ,而是在应用运行的过程中,通过一个 Agent 动态的拦截底层框架的行为,从而自动注入监控逻辑。像 Java 这样的编程语言可以通过字节码注入实现链路信息采集。这是一种最开发者最友好的方式,不需要任何代码层面的改动,但并不是每一种编程语言都能提供探针机制,因此 SDK 方式也被很多全链路监控组件采用
各组件的指标对比
3. OpenTelemetry Go SDK
3.1 Tracing Api
Tracing Api主要由下面几部分组成:
- TracerProvider: 提供tracer
- Tracer: 创建span
- Span: 监控某个操作
- SpanContext与Propagator: 用于传递Span信息
- SpanExporter: 用于导出Span数据
3.1.1 TracerProvider
TracerProvider用于获取一个Tracer,其结构如下:
type TracerProvider struct {
mu sync.Mutex
// tracer列表
namedTracer map[instrumentation.Library]*tracer
// span处理器
spanProcessors atomic.Value
// 采样器
sampler Sampler
// id生成器
idGenerator IDGenerator
// span的限制配置
spanLimits SpanLimits
resource *resource.Resource
}
// 获取一个tracer
func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {...}
// 关闭TracerProvider,同时会关闭其生成的所有tracer
func (p *TracerProvider) Shutdown(ctx context.Context) error {...}
同时在包go.opentelemetry.io/otel提供了一个全局TracerProvider,并定义了相应接口:
// 获取全局TracerProvider
func GetTracerProvider() trace.TracerProvider {...}
// 设置全局TracerProvider
func SetTracerProvider(tp trace.TracerProvider) {...}
// 直接使用全局TracerProvider来获取Tracer
func Tracer(name string, opts ...trace.TracerOption) trace.Tracer {...}
3.1.2 Tracer
Tracer用于创建Span:
type Tracer interface {
// 用于创建一个Span
Start(ctx context.Context, spanName string, opts ...SpanStartOption) (context.Context, Span)
}
通常Tracer在创建时不会指定配置,而是直接使用TracerProvider的配置。
3.1.3 Span
Span用于记录一个操作的相应信息:
type Span interface {
//结束Span
End(options ...SpanEndOption)
AddEvent(name string, options ...EventOption)
IsRecording() bool
RecordError(err error, options ...EventOption)
// 将Span的信息以SpanContext的形式返回
SpanContext() SpanContext
// 设置Span的状态
SetStatus(code codes.Code, description string)
// 设置Span的名字
SetName(name string)
// 设置Span的信息
SetAttributes(kv ...attribute.KeyValue)
// 获取相关的TracerProvider
TracerProvider() TracerProvider
}
一个常见的Span内部结构如下:
type recordingSpan struct {
mu sync.Mutex
// 父Span的信息
parent trace.SpanContext
// Span的种类
spanKind trace.SpanKind
// 名字
name string
// 开始时间
startTime time.Time
// 结束时间
endTime time.Time
// 状态
status Status
childSpanCount int
// 自身的Span信息
spanContext trace.SpanContext
// 标注信息
attributes []attribute.KeyValue
droppedAttributes int
events evictedQueue
links evictedQueue
executionTracerTaskEnd func()
tracer *tracer
}
3.1.4 SpanContext与Propagator
SpanContext用于记录Span的信息,其实现了context.Context的接口;而Propagator用于传播SpanContext
SpanContext结构如下,其最主要的信息是traceID和spanID:
type SpanContext struct {
traceID TraceID
spanID SpanID
traceFlags TraceFlags
traceState TraceState
remote bool
}
而Propagator通常有如下接口:
type TextMapPropagator interface {
// 将context中的信息注入map中
Inject(ctx context.Context, carrier TextMapCarrier)
// 将Map中的信息读出并放入context中
Extract(ctx context.Context, carrier TextMapCarrier) context.Context
// 获取所有fields
Fields() []string
}
3.1.5 SpanExporter
SpanExporter用于导出Span数据,其接口如下:
type SpanExporter interface {
// 导出Spans
ExportSpans(ctx context.Context, spans []ReadOnlySpan) error
// 关闭,会阻塞等待至所有Spans被导出
Shutdown(ctx context.Context) error
}
Open Telemetry支持的数据导出方式如下:
3.2 使用方式
在使用Tracer之前,需要先配置TracerProvider,然后从TracerProvider中取出一个Tracer,然后用Tracer生成一个Span用于记录相应的操作。
创建文件trace_example_test.go,将下面代码复制进去,启动测试用例。
package instrument
import (
"context"
"os"
"testing"
"time"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
sdkTrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func TestTrace(t *testing.T) {
// 创建exporter,数据直接输出至标准输出
exporter, err := stdouttrace.New(
stdouttrace.WithWriter(os.Stdout),
stdouttrace.WithPrettyPrint(),
stdouttrace.WithoutTimestamps(),
)
if err != nil {
panic(err)
}
// 配置traceProvider
tracerProvider := sdkTrace.NewTracerProvider(
// 配置数据导出
sdkTrace.WithBatcher(exporter),
// 配置基础信息
sdkTrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("test-service"),
)),
)
// 获取tracer
tracer := tracerProvider.Tracer("test")
// 开始span
ctx, span := tracer.Start(context.Background(), "sleep")
// sleep ls
time.Sleep(time.Second * 1)
// 结束span
span.End()
// 关闭
tracerProvider.Shutdown(ctx)
}
标准输出应能看到输出:
{
"Name": "sleep",
"SpanContext": {
"TraceID": "8b74ae6d227e052ec64afc6961e3da7b",
"SpanID": "a47174ce4a217248",
"TraceFlags": "01",
"TraceState": "",
"Remote": false
},
"Parent": {
"TraceID": "00000000000000000000000000000000",
"SpanID": "0000000000000000",
"TraceFlags": "00",
"TraceState": "",
"Remote": false
},
"SpanKind": 1,
"StartTime": "0001-01-01T00:00:00Z",
"EndTime": "0001-01-01T00:00:00Z",
"Attributes": null,
"Events": null,
"Links": null,
"Status": {
"Code": "Unset",
"Description": ""
},
"DroppedAttributes": 0,
"DroppedEvents": 0,
"DroppedLinks": 0,
"ChildSpanCount": 0,
"Resource": [
{
"Key": "service.name",
"Value": {
"Type": "STRING",
"Value": "test-service"
}
}
],
"InstrumentationLibrary": {
"Name": "test",
"Version": "",
"SchemaURL": ""
}
}