链路追踪背景
对于早期系统或者服务来说,开发人员一般通过打日志的方式来进行埋点(常用的数据采集方式),然后再根据日志系统和性能监控定位及分析问题。对于单体的应用通过日志系统完全可以定位到问题,从而排查异常。但是对于微服务来说,各个服务之前存在调用关系,服务之间是相互依赖的,一个服务异常可能其由于其他服务引起的,那么就需要查看和该服务相关联的其他服务的所有日志信息排查时那个服务引造成的。
随着服务数量的增多和内部调用链的复杂化,开发者仅凭借日志和性能监控,难以做到全局的监控,运维难度极大。为了解决这个问题,业界推出了分布式链路追踪组件。Google 内部开发了 Dapper 用于收集更多的复杂分布式系统的行为信息;同时,也有很多其他公司开发了自己的链路追踪组件Twitter 开源了分布式链路追踪组件 Zipkin等。
Dapper,大规模分布式系统的跟踪系统
服务监控应该至少包含如下的内容:
微服务 - 应用性能监测 · 链路追踪 · 概念规范 · 产品接入 · 方法级追踪 · 创建指标跨度
微服务 - Nginx网关 · 进程机制 · 限流熔断 · 性能优化 · 动态负载 · 高可用
分布式链路追踪
分布式链路追踪(Distributed Tracing)是一种用于监视和诊断分布式系统性能问题的技术。在大规模的分布式系统中,由于服务之间的复杂交互,单个请求可能会在多个服务之间传输,并涉及多个计算节点和数据存储。在这样的环境中,出现性能问题时,追踪问题的根本原因可能会非常困难。
分布式链路追踪通过在整个请求处理路径上添加唯一标识符,并记录请求在不同服务和组件之间的传输情况和时间戳,从而使开发人员能够跟踪请求的完整生命周期。当出现性能问题时,开发人员可以利用这些信息来确定瓶颈所在,优化系统性能。
常见的分布式链路追踪工具包括Zipkin、Jaeger和OpenTelemetry等。这些工具提供了可视化界面和分析工具,帮助开发人员更轻松地理解分布式系统的运行状况。
在dapper中介绍了Tracing 链路追踪是一种用于分析和监视应用程序的方法,尤其是那些使用微服务体系结构构建的分布式的应用程序。一个完整请求链路的追踪(TraceID)用于查出本次请求调用的所有服务/接口/组件等,调用的每个服务/接口/组件等都被称为跨度(Span),用来记录调用顺序,上游跨度(ParenetID)用来记录调用的层级关系。调用时间周期Timestamp,是把请求发出、接收、处理的时间都记录下来。跨度还可以记录一些其它属性信息,比如发起调用服务名称、被调服务名称、返回结果、IP、请求状态、日志、故障等。最后再把拥有相同(TraceID)的跨度(Span)合成一个更大范围的试图,就形成了一个完整的单次请求调用链。
这个概念都是Google Dapper论文中提出的:
Google-Dapper 是 Google 内部长期经过打磨后形成的产品,于2010年公布,对外是一篇论文,讲述的是分布式链路追踪的理论和 Dapper 的设计思想。大致由 [植入应用、收集跟踪数据、图形化UI] 三部分组成。后续市场的发展,有很多链路追踪系统也是基于 Dapper 论文的思想和理论为基础的。
通过统一服务监控追踪标准,OpenTracing
项目横空出世并得到开发者的认可,为分布式追踪,提供统一的概念、规范和接口。它是一个轻量级的标准化层,并不是功能实现代码,它只是为跟踪数据,用代码定义了一套数据模型,和一套API,是供统一遵循的规范,用于在应用程序中创建和管理这些数据模型。现在大多数链路跟踪产品系统都在尽量兼容遵循 OpenTracing 设计原则。
OpenTracing官方标准-中文版
opentracing
OpenTracing中文文档
OpenTracing 仅包含 Model 和 API 的定义,不会将产生的数据发送到第三方;需要进一步集成第三方的SDK,发送到第三方并呈现。
OpenTracing
OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
trace
:race代表了一个事务或者流程在(分布式)系统中的执行过程span
: span表示一个调用,这个调用可以使一个方法,一个数据库,一个rpc服务等。
虽然分布式系统很复杂,但是使用OpenTracing让监控一个分布式调用过程简单化,并快速配置一个监控系统。
OpenTracing 有一套自己的一个 Library 库定义了一套通用的数据上报接口,要求各个分布式追踪系统都来实现这套接口。这样一来,应用程序只需要对接 OpenTracing,而无需关心后端采用的到底是什么分布式追踪系统,因此开发者可以无缝切换分布式追踪系统,也使得在通用代码库增加对分布式追踪的支持成为可能。
Twitter Zipkin是一款分布式链路追踪组件,由 Twitter 开源,同样也兼容 OpenTracing API:它基于 Google Dapper 的论文设计。
https://github.com/openzipkin
云原生链路监控组件 Uber Jaeger受 Dapper 和 OpenZipkin 的启发,由 Uber 开源的分布式追踪系统,兼容 Open Tracing API。它用于微服务的监控和排查,支持分布式上下文传播、分布式事务的监控、报错分析、服务的调用网络分析以及性能/延迟优化。Jaeger 的服务端使用 Go 语言实现。
Jaeger架构:
OpenTelemetry:
基于 OpenTracing,新项目 OpenTelemetry 是 OpenTracing + OpenCensus 的产物,包含一组用于分布式跟踪和监视的工具的集合,支持集成更多的框架和语言。它仅用来生成/收集和导出更多种遥测数据(指标/链路/日志),可将数据发送到任何可观测性后端进行分析。旨在提供一种检测跟踪代码的标准方法,收集有关通过系统的请求流的数据,以帮助分析软件的性能和行为。同样,数据存储和可视化呈现留给其它工具去完成(如Jaeger、Prometheus)。
OpenTelemetry
OpenTelemetry GO API
OpenTelemetry GO API中文文档
概念
可观测性
可观测性使我们能够从外部了解一个系统,通过提出关于该系统的问题而无需了解其内部工作方式,因此应用程序代码必须发出诸如 信号 的东西,例如 跟踪、指标 和 日志,通过这些信号来观测系统。
日志是由服务或其他组件发出的带时间戳的消息。跨度表示一项工作或操作的单位。跟踪,记录了请求(由应用程序或最终用户发起)在多服务架构中传播时所经过的路径。为了使系统可观察,必须进行仪表化:也就是说,系统组件的代码必须发出跟踪、指标和日志。
如何生成跟踪,指标和日志呢,可以通过使用 OpenTelemetry API
进行手动仪表化,在特点的地方的生成。
配置OpenTelemetry API
为了创建跟踪或跨度,你需要先创建一个tracer
和/或meter provider
。通常情况下,我们建议SDK应该为这些对象提供一个单一的默认provider。然后你将从该provider获取一个tracer或meter实例,跟踪和快读都是从这个实力创建。
如果你正在构建一个服务进程,你还需要使用适当的选项配置OpenTelemetr SDK
,以将你的遥测数据导出到某个分析后端。
一旦你配置了API和SDK,你就可以自由地通过从provider中获取的tracer和meter对象创建跟踪和度量事件,创建遥测数据。
一旦你创建了遥测数据,你就需要将其发送到某个地方。OpenTelemetry支持从你的进程直接发送数据到分析后端的两种主要方法:直接从进程发送
或通过OpenTelemetry Collector
进行代理发送。
专业术语:
OpenTelemetry API
:在OpenTelemetry项目中,用于定义如何根据数据源生成遥测数据。
Exporter
:提供将遥测数据传递给消费者的功能。由仪表化库和收集器使用。
Metric
:(度量)将数据点记录为时间序列,包括原始测量值或预定义的聚合
OTel
:OpenTelemetry的缩写。
OTelCol
:OpenTelemetry Collector的缩写。
SDK
:软件开发工具包的缩写。指实现OpenTelemetryAPI的遥测SDK的库。
Span
:跨度,表示Trace内的单个操作。
Trace
:跟踪,Span的DAG,其中Span之间的边被定义为父/子关系。
Tracer
:跟踪器,负责创建Span。
使用两个开源项目:OpenTelemetry
和 Jaeger
。
OpenTelemetry 可以用于从应用程序收集数据。它是一组工具、API 和 SDK 集合,我们可以使用它们来检测、生成、收集和导出遥测数据(指标、日志和追踪),以帮助分析应用的性能和行为。
OpenTelemetry 提供了一个与供应商无关的可观测性标准,因为它旨在标准化跟踪的生成。通过 OpenTelemetry,我们可以将检测埋点与后端分离。这意味着我们不依赖于任何工具(或供应商)。
OpenTelemetry 为我们提供了创建跟踪数据的工具,为了获取这些数据,我们首先需要检测应用程序来收集数据。为此,我们需要使用 OpenTelemetry SDK。
应用程序的遥测数据可以发送到 OpenTelemetry Collectors 收集器。
jaeger是用来可是监控的数据。
链路追踪实现
- 步骤
在OpenTelemetry中,将数据发送到指定的UI工具通常涉及以下几个步骤:
-
Instrumentation(仪表化):首先,你需要在你的应用程序中进行仪表化。这包括在代码中插入适当的代码以捕获关键事件和跟踪。这些事件可以是HTTP请求、数据库查询、函数调用等。OpenTelemetry提供了一系列语言库和框架的支持,使得在你的应用中添加仪表化变得相对简单。(基于OpenTelemetry API 和OpenTelemetry SDK实现)
-
数据导出器(Exporters):OpenTelemetry提供了各种Exporter,用于将从应用程序中收集的跟踪数据、指标和日志发送到不同的目的地。你需要选择适合你用例的Exporter。例如,如果你想将数据发送到UI工具,你可能需要使用与该UI工具集成的Exporter,或者编写自定义的Exporter。
-
配置Exporter:一旦选择了Exporter,你需要配置它,以确保数据被正确发送到UI工具。配置通常包括指定目标地址、端口、认证信息等。
-
启动应用程序:最后,你需要启动你的应用程序,让它开始收集跟踪数据、指标和日志,并将其发送到UI工具。
-
在UI工具中配置和查看数据:最终,在UI工具中配置数据源,并查看你的应用程序的跟踪数据、指标和日志。这可能涉及到在UI工具中创建仪表板或设置警报,以便监视应用程序的性能和健康状况。
- 安装jaeger ui工具
除了jaeger也可以选择其他工具。
jaeger官网
在jaeger中数据集采集的方式有两种jaeger-client
和jaeger-agent
前者是整合其他语言的SDK,后者是go语言专属的api,它们的实例将opentracing
和opentelemetry
采集的数据发送到到jaeger-collector
,再由jaeger-collector缓存存储到数据库中,最后jaeger-ui
工具展示数据,jaeger-query
数据查询数据。
jaeger提供了All in One
一件安装jaeger所有的插件,参考官网
安装完之后输入地址http://localhost:16686如下所示:
Jeager官方文档翻译之——jeager架构(个人笔记)感谢作者Iron。
- 数据采集
jaeger-client
和jaeger-agent
发送数据是系统自动的,只需要开发者配置好otel-exporter
即可,将其设置为otel-jaeger SDK的jaeger-exporter
,这样系统会自动创建jaeger-client或者jaeger-agent向,远程jaeger-collector发送采集的信息。
现在到了关键的一部,采集程序信息,用于仪表化、生成、收集和导出诸如跟踪、度量、日志等遥测数据。
什么是OpenTelemetry?
Opentelemetry Traces数据模型介绍
otel-exporter
采集的信息需要发送到指定的collector
才有意义,所以需要配置otel-exporter。各个框架都配置了exporter,例如jaeger-exporter等。
import "go.opentelemetry.io/otel/exporters/jaeger"
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
Tracer Provider
Tracer Provider是 Tracers 的工厂,在大多数应用程序中,Tracer Provider 会初始化一次,并且其生命周期与应用程序的生命周期相匹配。Tracer Provider 初始化还包括 Resource 和 Exporter 初始化。这通常是使用 OpenTelemetry 进行跟踪的第一步。在某些语言的 SDK 中,已经为您初始化了全局 Tracer Provider。
//设置Exporter,将Jaeger的exporter设置为otel的Exporter
// 返回以Jaeger为exporter的traceProvider
tp, tpErr := tracerprovicer.JaegerTraceProvider()
if tpErr != nil {
log.Fatal(tpErr)
}
// 设置otel的provider
otel.SetTracerProvider(tp)
// 设置传播提取器
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
Tracer Provider 初始化还包括 Resource 和 Exporter 初始化
Tracer Provider的初始化还有一部分在Tracer Exporter中。
Tracer Exporter
跟踪导出器将跟踪发送给消费者。该使用者可以是用于调试和开发时的标准输出、OpenTelemetry Collector 或您选择的任何开源或供应商后端。
package tracerprovicer
import (
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func JaegerTraceProvider() (*sdktrace.TracerProvider, error) {
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("todo-service"),
semconv.DeploymentEnvironmentKey.String("production"),
)),
)
return tp, nil
}
Tracer
Tracer 创建的跨度包含有关给定操作并记录操作信息。
// 创建trace实例
tr := otel.Tracer("redis-conn")
Span
Span 是 Traces 的构建块,是追踪的基本单元。
spanCtx, span := tr.Start(c.Request.Context(), "get-todo")
span.End()
Context(上下文连接)
上下文传播是实现分布式跟踪的核心概念。通过上下文传播,Span 可以相互关联并组装成跟踪,无论 Span 是在哪里生成的。
总结
tracer跨度代表一个工作或操作单元,Span 是 Traces 的构建块,在 OpenTelemetry 中包含以下信息:
{
"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"
}
]}
完整代码实例如下:
package tracerprovicer
import (
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func JaegerTraceProvider() (*sdktrace.TracerProvider, error) {
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("todo-service"),
semconv.DeploymentEnvironmentKey.String("production"),
)),
)
return tp, nil
}
package main
import (
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"log"
"net/http"
"oteldemo/tracing"
)
var (
cli *redis.Client
r *gin.Engine
)
func main() {
//设置Exporter,将Jaeger的exporter设置为otel的Exporter
// 返回以Jaeger为exporter的traceProvider
tp, tpErr := tracerprovicer.JaegerTraceProvider()
if tpErr != nil {
log.Fatal(tpErr)
}
// 设置otel的provider
otel.SetTracerProvider(tp)
// 设置传播提取器
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
// 创建gin实例
ginWebServer()
//gin OpenTelemetry工具化
r.Use(otelgin.Middleware("todo-service"))
r.GET("/todo", func(c *gin.Context) {
// 创建trace实例
tr := otel.Tracer("redis-conn")
// 创建span实例
spanCtx, span := tr.Start(c.Request.Context(), "get-todo")
span.End()
tp.Shutdown(spanCtx)
c.JSON(http.StatusOK, "ok")
})
r.Run(":8001")
}
func ginWebServer() {
r = gin.Default()
}
func redisConn() {
// connectMongo
cli = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
})
return
}
jaeger collector默认地址:http://localhost:14268/api/traces
项目地址:open-telemetry-demo
在初始化
Tracer Provider
时同时初始化otel-exporter
和otel-resource
,semconv.ServiceNameKey.String("todo-service")
部分是填写服务名称,semconv.DeploymentEnvironmentKey.String("production")
部分是填写开发环境。对于每一个
provider
创建的tr := otel.Tracer("redis-conn")
填写tracer名称,spanCtx, span := tr.Start(c.Request.Context(), "get-todo")
填写的是span名称。