链路追踪目前在微服务中已经有了成熟的方案,其中包括代码侵入式的和非侵入式的,各有利弊,看具体的业务所需选择
本文主要讲解的是侵入式的链路追踪,选择 OpenTelemetry 加 jaeger 实现链路追踪
其中具体的demo 代码实现选择python和go, 但是OpenTelemetry 官方文档提供了丰富的各种语言的SDK以及很多有趣的定制化需求
https://opentelemetry.io/docs/instrumentation/
该协议的数据模型包括Trace(链路) 和 Span, 一条链路由多个Span 组成
可以将Span 理解成一次方法调用, 一个程序块的调用, 或者一次RPC/数据库访问.只要是一个具有完整时间周期的程序访问,都可以被认为是一个span
下面的示例Trace就是由8个Span组成:
1. 单个Trace中,span间的因果关系
2.
3.
4. [Span A] ←←←(the root span)
5. |
6. +------+------+
7. | |
8. [Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
9. | |
10. [Span D] +---+-------+
11. | |
12. [Span E] [Span F] >>> [Span G] >>> [Span H]
13. ↑
14. ↑
15. ↑
16. (Span G 在 Span F 后被调用, FollowsFrom)
有些时候,使用下面这种,基于时间轴的时序图可以更好的展现Trace(调用链):
1. 单个Trace中,span间的时间关系
2.
3.
4. ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
5.
6. [Span A···················································]
7. [Span B··············································]
8. [Span D··········································]
9. [Span C········································]
10. [Span E·······] [Span F··] [Span G··] [Span H··]
每个Span包含以下的状态:
每一个SpanContext包含以下状态:
一个Span可以与一个或者多个SpanContexts存在因果关系。OpenTracing目前定义了两种关系:ChildOf
(父子) 和 FollowsFrom
(跟随)。这两种关系明确的给出了两个父子关系的Span的因果模型。 将来,OpenTracing可能提供非因果关系的span间关系。(例如:span被批量处理,span被阻塞在同一个队列中,等等)。
ChildOf
引用: 一个span可能是一个父级span的孩子,即"ChildOf"关系。在"ChildOf"引用关系下,父级span某种程度上取决于子span。下面这些情况会构成"ChildOf"关系:
下面都是合理的表述一个"ChildOf"关系的父子节点关系的时序图
1. [-Parent Span---------]
2. [-Child Span----]
3.
4. [-Parent Span--------------]
5. [-Child Span A----]
6. [-Child Span B----]
7. [-Child Span C----]
8. [-Child Span D---------------]
9. [-Child Span E----]
FollowsFrom
引用: 一些父级节点不以任何方式依赖他们子节点的执行结果,这种情况下,我们说这些子span和父span之间是"FollowsFrom"的因果关系。"FollowsFrom"关系可以被分为很多不同的子类型,未来版本的OpenTracing中将正式的区分这些类型
下面都是合理的表述一个"FollowFrom"关系的父子节点关系的时序图。
1. [-Parent Span-] [-Child Span-]
2.
3.
4. [-Parent Span--]
5. [-Child Span-]
6.
7.
8. [-Parent Span-]
9. [-Child Span-]
Tracer
, Span
和 SpanContext
Tracer
接口用来创建Span
,以及处理如何处理Inject
(serialize) 和 Extract
(deserialize),用于跨进程边界传递。它具有如下官方能力:
创建一个新Span
必填参数
"get_user"
作为操作名,比 "get_user/314159"
更好。例如,假设一个获取账户信息的span会有如下可能的名称:
操作名 | 指导意见 |
---|---|
get | 太抽象 |
get_account/792 | 太明确 |
get_account | 正确的操作名,关于account_id=792 的信息应该使用Tag操作 |
可选参数
SpanContext
,如果可能,同时快速指定关系类型,ChildOf
还是 FollowsFrom
。返回值,返回一个已经启动Span
实例
将SpanContext
上下文Inject(注入)到carrier
必填参数:
SpanContext
**实例Tracer
实现,如何对SpanContext
进行编码放入到carrier中。Tracer
实现根据format声明的格式,将SpanContext
序列化到carrier对象中。将SpanContext
上下文从carrier中Extract(提取)
必填参数:
Tracer
实现,如何从carrier中解码SpanContext
。Tracer
实现根据format声明的格式,从carrier中解码SpanContext
。返回值,返回一个SpanContext
实例,可以使用这个SpanContext
实例,通过Tracer
创建新的Span
。
注意,对于Inject(注入)和Extract(提取),format是必须的。
Inject(注入)和Extract(提取)依赖于可扩展的format参数。format参数规定了另一个参数"carrier"的类型,同时约束了"carrier"中SpanContext
是如何编码的。所有的Tracer实现,都必须支持下面的format。
SpanContext
的信息。Span
结束后(span.finish()
),除了通过Span
获取SpanContext
外,下列其他所有方法都不允许被调用。通过Span
获取SpanContext
不需要任何参数
返回值:Span
构建时传入的SpanContext
。这个返回值在Span
结束后(span.finish()
),依然可以使用。
复写操作名(operation name)
必填参数
Span
时,传入的操作名。结束Span
可选参数
为Span
设置tag
必填参数
注意,OpenTracing标准包含**“standard tags,标准Tag”**,此文档中定义了Tag的标准含义。
Log结构化数据
必填参数
可选参数
注意,OpenTracing标准包含**“standard log keys,标准log的键”**,此文档中定义了这些键的标准含义。
设置一个baggage(随行数据)元素
Baggage元素是一个键值对集合,将这些值设置给给定的Span
,Span
的SpanContext
,以及所有和此Span
有直接或者间接关系的本地Span
。 也就是说,baggage元素随trace一起保持在带内传递。(带内传递,在这里指,随应用程序调用过程一起传递)
Baggage元素为OpenTracing的实现全栈集成,提供了强大的功能 (例如:任意的应用程序数据,可以在移动端创建它,显然的,它会一直传递了系统最底层的存储系统。由于它如此强大的功能,他也会产生巨大的开销,请小心使用此特性。
再次强调,请谨慎使用此特性。每一个键值都会被拷贝到每一个本地和远程的下级相关的span中,因此,总体上,他会有明显的网络和CPU开销。
必填参数
获取一个baggage元素
必填参数
返回值,相应的baggage value,或者可以标识元素值不存在的返回值(如Null)。
相对于OpenTracing中其他的功能,SpanContext
更多的是一个“概念”。也就是说,OpenTracing实现中,需要重点考虑,并提供一套自己的API。 OpenTracing的使用者仅仅需要,在创建span、向传输协议Inject(注入)和从传输协议中Extract(提取)时,使用SpanContext
和references,
OpenTracing要求,SpanContext
是不可变的,目的是防止由于Span
的结束和相互关系,造成的复杂生命周期问题。
遍历所有的baggage元素
遍历模型依赖于语言,实现方式可能不一致。在语义上,要求调用者可以通过给定的SpanContext
实例,高效的遍历所有的baggage元素
所有的OpenTracing API实现,必须提供某种方式的NoopTracer
实现。NoopTracer
可以被用作控制或者测试时,进行无害的inject注入(等等)。例如,在 OpenTracing-Java实现中,NoopTracer
在他自己的模块中。
有些语言的OpenTracing实现,为了在串行处理中,传递活跃的Span
或SpanContext
,提供了一些工具类。例如,opentracing-go
中,通过context.Context
机制,可以设置和获取活跃的Span
。
jaeger 是用go 语言开源的一款链路追踪项目,提供了多种语言的SDK
docker-compose yaml 文件:
version: '2'
services:
jaeger:
image: jaegertracing/all-in-one:1.37
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
- COLLECTOR_OTLP_ENABLED=true
ports:
- "5775:5775/udp"
- "6831:6831/udp"
- "16686:16686"
- "6832:6832/udp"
- "5778:5778"
- "4317:4317"
- "4318:4318"
- "14268:14268"
- "14250:14250"
- "9411:9411"
networks:
- jaeger-example
networks:
jaeger-example:
官方demo github 地址:
https://github.com/jaegertracing/jaeger/tree/main/examples/hotrod
架构说明:
https://www.jaegertracing.io/docs/1.21/architecture/
Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。
Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。
分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示
简介
OpenTelemetry 的官方定位就是将分布式系统可观测,其遵守了openTracing 和 OpenCensus 协议,将各个服务的追踪日志聚合
和 jaeger 一样 openTelemetry 的设计也非常灵活, 提供了多种语言的SDK, 使用简单。
并且 jaeger 可以非常好的与 openTelemetry 交互,可以使用 openTelemetry 来搜集各个服务的代码埋点日志,然后将其发送至 jaeger-agent,这样就可以与 jaeger 的 强大生态共存。
官方使用SDK文档
https://opentelemetry.io/docs/instrumentation/
这里只是摘抄了官方最简单的使用方式体验下其强大之处,更多复杂的使用建议参考上边的 opentelemetry 官方文档
Python 服务代码接入
from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
resource = Resource(attributes={
SERVICE_NAME: "your-service-name"
})
# 为 jaeger 部署的服务地址
jaeger_exporter = JaegerExporter(
agent_host_name="192.168.146.189",
agent_port=6831,
)
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(jaeger_exporter)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("print") as span:
print("foo")
span.set_attribute("printed_string", "foo")
Go 服务代码接入
package main
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)
const (
service = "trace-demo"
environment = "production"
id = 1
)
// tracerProvider returns an OpenTelemetry TracerProvider configured to use
// the Jaeger exporter that will send spans to the provided url. The returned
// TracerProvider will also use a Resource configured with all the information
// about the application.
func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
// Create the Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// Always be sure to batch in production.
tracesdk.WithBatcher(exp),
// Record information about this application in a Resource.
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(service),
attribute.String("environment", environment),
attribute.Int64("ID", id),
)),
)
return tp, nil
}
func main() {
tp, err := tracerProvider("http://192.168.146.189:14268/api/traces")
if err != nil {
log.Fatal(err)
}
// Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it.
otel.SetTracerProvider(tp)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Cleanly shutdown and flush telemetry when the application exits.
defer func(ctx context.Context) {
// Do not make the application hang when it is shutdown.
ctx, cancel = context.WithTimeout(ctx, time.Second*5)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}(ctx)
tr := tp.Tracer("component-main")
ctx, span := tr.Start(ctx, "foo")
defer span.End()
bar(ctx)
}
func bar(ctx context.Context) {
// Use the global TracerProvider.
tr := otel.Tracer("component-bar")
_, span := tr.Start(ctx, "bar")
span.SetAttributes(attribute.Key("testset").String("value"))
defer span.End()
// Do bar...
}
官方的gin 链路追踪中间件:otelgin
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
自定义gin 中间件,自定义中间件的好处就是灵活,可以定制化对应的业务逻辑:
package middlewares
import (
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/gin-gonic/gin"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
"micro/order-web/global" // 全局配置文件
)
func Trace() gin.HandlerFunc {
return func(ctx *gin.Context) {
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
LocalAgentHostPort: fmt.Sprintf("%s:%d", global.ServerConfig.JaegerInfo.Host, global.ServerConfig.JaegerInfo.Port),
},
ServiceName: global.ServerConfig.JaegerInfo.Name,
}
tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))
if err != nil {
panic(err)
}
opentracing.SetGlobalTracer(tracer)
defer closer.Close()
startSpan := tracer.StartSpan(ctx.Request.URL.Path)
defer startSpan.Finish()
ctx.Set("tracer", tracer)
ctx.Set("parentSpan", startSpan)
ctx.Next()
}
}
完整代码
package main
import (
"context"
"log"
"net/http"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
oteltrace "go.opentelemetry.io/otel/trace"
)
const (
service = "gin-trace-demo"
environment = "production"
id = 1
)
var tracer = otel.Tracer(service)
type UserInfo struct {
Id string `json:"id"`
Name string `json:"name"`
}
func GetUsers(c *gin.Context) {
id := c.Param("id")
// 实例化开始span: 从ctx 中获取span
// 可以在实例化span 的时候就设置一些属性
ctx, span := tracer.Start(c.Request.Context(), "getUser", oteltrace.WithAttributes(attribute.String("id", id)))
defer span.End()
// 模拟执行获取用户信息的业务流程
name := getUser(ctx, id)
userInfo := UserInfo{
Id: id,
Name: name,
}
c.JSON(http.StatusOK, userInfo)
}
func getUser(ctx context.Context, id string) string {
// 实例化子span
_, childSpan := tracer.Start(ctx, "getUserSubProcess")
defer childSpan.End()
// 也可以在实例化后设置span的一些属性
var name string
if id == "123" {
name = "zhouzy1"
}
childSpan.SetAttributes(attribute.String("name", name))
childSpan.SetStatus(codes.Ok, "get userInfo success")
return name
}
func main() {
tp, err := initTracer()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}()
r := gin.New()
r.Use(otelgin.Middleware("my-server"))
r.GET("/users/:id", GetUsers)
_ = r.Run(":8080")
}
func initTracer() (*sdktrace.TracerProvider, error) {
// 使用jaeger 作为exporter
exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://192.168.146.189:14268/api/traces")))
if err != nil {
return nil, err
}
// 初始化 tracer provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(service),
attribute.String("environment", environment),
attribute.Int64("ID", id),
)),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return tp, nil
}