• Go微服务框架gokratos学习05:分布式链路追踪 OpenTelemetry 使用


    一、分布式链路追踪发展简介

    1.1 分布式链路追踪介绍

    关于分布式链路追踪的介绍,可以查看我前面的文章 微服务架构学习与思考(09):分布式链路追踪系统-dapper论文学习(https://www.cnblogs.com/jiujuan/p/16097314.html) 。

    这里的 OpenTelemetry 有一段发展历程。

    APM(Application Performance Monitoring) 和 Distributed Tracing(分布式跟踪),后者是前者的子集。

    微服务架构流行起来后,为了监控和定位微服务中请求链路过长导致的定位和监控问题,分布链路监控也蓬勃发展起来。出现了

    很多有名的产品,比如:Jaeger,Pinpoint,Zipkin,Skywalking 等等。这里有个问题,就是每家都有自己的一套数据采集标准和SDK。

    为了统一这些标准,国外的人们就创建了 OpenTracingOpenCensus 2 个标准。最先出现的是 OpenTracing。为了统一标准,后来两者合并为 OpenTelemetry

    1.2 OpenTracing

    OpenTracing 制定了一套与平台无关、厂商无关的协议标准,使得开发人员能够方便的添加或更换底层APM的实现。

    它是 CNCF 的项目。OpenTracing 协议的产品有 Jaeger、Zipkin 等等。

    OpenTracing 数据模型

    • Trace(s):

    Trace(s) 在 OpenTracing 中是被 spans 隐式定义的。一个 trace 可以被认为是由一个或多个 span 组成的有向无环图。

    比如,下图示例就表示一个 trace 由 8 个 span 组成,也就是一次链路追踪由 8 个 span 组成:

    单个 trace(链路) 中 span 之间的关系
    
    
            [Span A]  ←←←(the root span)
                |
         +------+------+
         |             |
     [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
         |             |
     [Span D]      +---+-------+
                   |           |
               [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                           ↑
                                           ↑
                                           ↑
                             (Span G `FollowsFrom` Span F)
    

    用时间轴来可视化这次链路追踪图,更容易理解:

    Temporal relationships between Spans in a single Trace
    
    
    ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
    
     [Span A···················································]
       [Span B··············································]
          [Span D··········································]
        [Span C········································]
             [Span E·······]        [Span F··] [Span G··] [Span H··]
    

    (来自:https://opentracing.io/specification/)

    • Span:

    Span 是一次链路追踪里的基本组成元素,一个 Span 表示一个独立工作单元,比如一次 http 请求,一次函数调用等。每个 span 里元素:

    • An operation name,服务/操作名称
    • A start timestamp,开始时间
    • A finish timestamp,结束时间
    • Span Tags,key:value 数据形式,用户自定义的标签,主要用途是链路记录信息的查询过滤。
    • Span Logs,key:value 数据形式,主要用途是记录某些事件和事件发生的时间。
    • SpanContext 看下面解释
    • References,对 0 或 更多个相关 span 的引用(通过 SpanContext 来引用)
    • SpanContext:

    SpanContext 携带跨进程(跨服务)通信的数据。它的组成:

    • 在系统中表示 span 的信息。比如 span_id, trace_id。
    • Baggage Items,为整条追踪链路保存跨进程(跨服务)的数据,数据形式是 key:value
    • References

    多个 span 中的对应关系。OpenTracing 目前定义了 2 种关系:ChildOfFollowsFrom

    • ChildOf,一个子 span 可能是父 span 的 ChildOf
        [-Parent Span---------]
             [-Child Span----]
    
        [-Parent Span--------------]
             [-Child Span A----]
              [-Child Span B----]
            [-Child Span C----]
             [-Child Span D---------------]
             [-Child Span E----]
    
    • FollowsFrom,一些父 span 不依赖任何的子 span
        [-Parent Span-]  [-Child Span-]
    
    
        [-Parent Span--]
         [-Child Span-]
    
    
        [-Parent Span-]
                    [-Child Span-]
    

    (来自:https://opentracing.io/specification/)

    1.3 OpenCensus

    为什么又出现个 OpenCensus 这个项目?因为它有个好爹:google。要知道分布式跟踪的基础论文就是谷歌提出。

    其实,刚开始它并不是要抢 OpenTracing 的饭碗,它只是为了把 Go 语言的 Metrics 采集、链路跟踪与 Go 语言自带的

    profile 工具打通,统一用户的使用方式。但是随着项目发展,它也想把链路相关的统一一下。它不仅要做 Metrics 基础指标监控,

    还要做 OpenTracing 的老本行:分布式跟踪。

    1.4 OpenTracing 与 OpenCensus 对比

    2 者功能对比

    image-20220605225353808

    image-20220605224745472

    1.5 OpenTelemetry

    这样出现 2 个标准也不是个事啊,如是就出现了 OpenTelemetry,它把 2 者合并在一起了。

    OpenTelemetry 的核心工作目前主要集中在 3 个部分:

    1. 规范的制定和协议的统一,规范包含数据传输、API 的规范,协议的统一包含:HTTP W3C 的标准支持及GRPC等框架的协议标准
    2. 多语言 SDK 的实现和集成,用户可以使用 SDK 进行代码自动注入和手动埋点,同时对其他三方库(Log4j、LogBack等)进行集成支持;
    3. 数据收集系统的实现,当前是基于 OpenCensus Service 的收集系统,包括 Agent 和 Collector。

    (1.4 1.5来自: https://github.com/open-telemetry/docs-cn)

    OpenTelemetry 的最终形态就是实现 Metrics、Tracing、Logging 的融合。

    OpenTelemetry 整体架构图:

    image-20220606140340397

    (来自:https://opentelemetry.io/docs/)

    Tracing API 中几个重要概念:

    • TracerProvider:是 API 的入口点,提供了对 tracer 的访问。在代码里主要是创建一个 Tracer,一般是第三方分布式链路管理软件提供具体实现。默认是一个空的 TracerProvider(""),虽然也创建 Tracer,但是内部不会执行数据流传输逻辑。
    • Tracer:负责创建 span,一个 tracer 表示一次完整的追踪链路。tracer 由一个或多个 span 组成。跟上面的 OpenTracing 数据模型很像,所以说是两者合并。
    • Span:一次链路追踪操作里的基本操作元素。比如一次函数调用,一次 http 请求。

    里面还有很多详细介绍:https://opentelemetry.io/docs/reference/specification/trace/api/

    还有一个数据采样,https://www.cnblogs.com/jiujuan/p/16097314.html - 前面学习 dapper 论文的这篇文章有介绍。

    小结:

    一条链路追踪信息:

    有一条链路 trace,它是由一个或多个 span 组成, span 里会记录各种链路中的信息,跨进程的信息,各种 span 之间的关系。

    使用哪种链路管理软件,则由 traceprovider 来设置。可以是 Jaeger,Pinpoint,Zipkin,Skywalking 等等。

    span 中的信息收集到链路管理软件,然后可以用图来展示记录的链路信息和链路之间的关系。

    二、jaeger 简介

    Jaeger 是受到 Dapper 和 OpenZipkin 启发,是 Uber 开发的一款分布式链路追踪系统。

    它用于监控微服务和排查微服务中出现的故障。

    jaeger 架构图

    image-20220606204357332

    (来自:https://www.jaegertracing.io/docs/1.35/architecture/)

    jaeger 安装:

    参考我前面文章 :https://www.cnblogs.com/jiujuan/p/13235748.html docker all-in-one 安装

    三、kratos 中链路追踪使用代码

    前面介绍了那么多,应该对 opentelemetry 大致有了一个了解。下面就在 kratos 中使用 opentelemetry。

    这里使用 jaeger 作为链路追踪的管理软件。

    go 1.17

    go-kratos 2.2.1

    jaeger 1.35

    下面代码来自 go-kratos 官方例子。

    server 端

    在 main.go 中,有 grpc server 和 http server。

    第一步,设置 TraceProvider()

    // get trace provider
    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
    	}
    
    	// New trace provider
    	tp := tracesdk.NewTracerProvider(
    		tracesdk.WithSampler(tracesdk.AlwaysSample()),
    		// always be sure to batch in production
    		tracesdk.WithBatcher(exp),
    		// Record information about this application in an Resource.
    		tracesdk.WithResource(
    			resource.NewWithAttributes(
    				semconv.SchemaURL,
    				semconv.ServiceNameKey.String(Name), // service name,实例名称
    				attribute.String("env", Env),        // environment
    				attribute.String("ID", Version),     // version
    			)),
    	)
    	return tp, nil
    }
    

    第二步,grpc server

    url := "http://jaeger:14268/api/traces"
    if os.Getenv("jaeger_url") != "" {
        url = os.Getenv("jeager_url")
    }
    
    tp, err := tracerProvider(url) // tracer provider
    if err != nil {
        log.Error(err)
    }
    
    s := &server{}
    
    // grpc server
    grpcSrv := grpc.NewServer(
        grpc.Address(":9000"),
        grpc.Middleware(
            middleware.Chain(
                recovery.Recovery(),
                tracing.Server(tracing.WithTracerProvider(tp)), //设置trace,传入 trace provider
                logging.Server(logger),
            ),
        ),
    )
    
    

    第三步,http server

    func main() {
    	logger := log.NewStdLogger(os.Stdout)
    	log := log.NewHelper(logger)
    
    	tp, err := tracerProvider("http://jaeger:14268/api/traces")
    	if err != nil {
    		log.Error(err)
    	}
    
    	httpSrv := http.NewServer(
    		http.Address(":8080"),
    		http.Middleware(
    			middleware.Chain(
    				recovery.Recovery(),
    				// Configuring tracing middleware
    				tracing.Server(
    					tracing.WithTracerProvider(tp), // 提供 trace provider
    				),
    				logging.Server(logger),
    			),
    		),
    	)
    	s := &server{}
    	pb.RegisterUserHTTPServer(httpSrv, s)
    
    	app := kratos.New(
    		kratos.Name(Name),
    		kratos.Server(
    			httpSrv,
    		),
    	)
    
    	if err := app.Run(); err != nil {
    		log.Error(err)
    	}
    }
    

    client 端

    grpc client 和 http client

    grpc client:

    // create grpc conn
    // only for demo, use single instance in production env
    conn, err := grpc.DialInsecure(ctx,
       grpc.WithEndpoint("127.0.0.1:9000"),
       grpc.WithMiddleware(middleware.Chain(
    	   tracing.Client( //trace client
    		   tracing.WithTracerProvider(s.tracer),
    	   ),
    	   recovery.Recovery(),
       )),
       grpc.WithTimeout(time.Second*2),
      )
    if err != nil {
        return nil, err
    }
    

    http client:

    http.NewClient(ctx, http.WithMiddleware(
        tracing.Client(
            tracing.WithTracerProvider(s.tracer),
        ),
    ))
    

    四、在student项目里使用链路追踪

    在前面的 go-kratos gorm 练习项目中加入链路追踪。
    https://github.com/jiujuan/go-kratos-demos/tree/master/student。

    4.1 编写代码

    第一步,在 internal/server 下新建 pkg/tracer 文件夹,tracer.go 程序

    把 tracer.go 作为一个独立文件

    package tracer
    
    import (
    	"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.10.0"
    )
    
    type Conf struct {
    	Name string
    	Env  string
    	Ver  string
    	Url  string
    }
    
    func NewConf(name, env, ver, url string) *Conf {
    	return &Conf{
    		Name: name,
    		Env:  env,
    		Ver:  ver,
    		Url:  url,
    	}
    }
    
    func (c *Conf) TracerProvider() (*tracesdk.TracerProvider, error) {
    	exp, err := jaeger.New(
    		jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(c.Url)),
    	)
    	if err != nil {
    		return nil, err
    	}
    
    	tp := tracesdk.NewTracerProvider(
    		tracesdk.WithSampler(tracesdk.AlwaysSample()),
    		tracesdk.WithBatcher(exp),
    		tracesdk.WithResource(
    			resource.NewWithAttributes(
    				semconv.SchemaURL,
    				semconv.ServiceNameKey.String(c.Name),
    				attribute.String("env", c.Env),
    				attribute.String("ver", c.Ver),
    			)),
    	)
    	return tp, nil
    }
    
    

    第二步,在 internal/server/grpc.go:NewGRPCServer() 函数加入链路追踪代码:

    var opts = []grpc.ServerOption{
    	grpc.Middleware(
    		recovery.Recovery(),
    		tracing.Server(), // 链路追踪
    	),
    }
    

    第三步,在 internal/server/grpc.go:NewHTTPServer() 函数加入链路追踪代码:

    var opts = []http.ServerOption{
    	http.Middleware(
    		recovery.Recovery(),
    		tracing.Server(), // 链路追踪
    	),
    }
    

    第四步,在 cmd/student/main.go 加入如下代码:

    // 配置,启动链路追踪
    url := "http://192.168.0.103:14268/api/traces"
    Name = "kratos.service.student"
    id = "kratos.id.student.1"
    Version = "test-V0.0.1"
    traceconf := tracer.NewConf(Name, id, Version, url)
    tp, _ := traceconf.TracerProvider()
    otel.SetTracerProvider(tp) // 为全局链路追踪
    

    上面这段程序可以用 wire 配置程序。

    完整代码地址:完整代码地址:https://github.com/jiujuan/go-kratos-demos/tree/master/student

    4.2 测试

    请先自行安装 jaeger。

    可以用 docker all-in-one 快速安装,详细命令请参考:https://www.cnblogs.com/jiujuan/p/13235748.html

    第一步,启动kratos服务

    $ cd cmd/student
    $ kratos run
    INFO msg=config loaded: config.yaml format: yaml
    INFO msg=[gRPC] server listening on: [::]:9000
    INFO msg=[HTTP] server listening on: 127.0.0.1:8080
    

    第二步,使用 curlie - https://github.com/rs/curlie 测试:

    $ curlie  http://127.0.0.1:8080/student/3
    HTTP/1.1 200 OK
    Content-Type: application/json
    Content-Length: 34
    
    {
        "name": "jimmy",
        "status": 0,
        "id": 3
    }
    

    第三步:打开 jaeger web ui 查看结果

    http://192.168.0.103:16686/search

    image-20220612191752291

    image-20220612191904418

    完整代码地址:完整代码地址:https://github.com/jiujuan/go-kratos-demos/tree/master/student

    五、参考

  • 相关阅读:
    java 单例模式
    java 设计模式
    android 设计模式
    我的坦克兵爷爷也曾扬威世界
    我的坦克兵爷爷也曾扬威世界
    LD_LIBRARY_PATH设置问题
    LD_LIBRARY_PATH设置问题
    销售员和程序员
    销售员和程序员
    如何成为Python高手
  • 原文地址:https://www.cnblogs.com/jiujuan/p/16349519.html
Copyright © 2020-2023  润新知