分布式追踪

本文介绍了分布式追踪的重要性和原理,重点关注OpenTracing标准和Jaeger的实现。详细阐述Jaeger的架构、基础编程接口,并给出测试部署与实际应用的例子,最后对比了主流的日志追踪方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式追踪

Logging、Metrics、Logging

logging_metrics_tracing.png

如图所属的韦恩图描述了三个核心概念:

  • Logging:即日志描述,用于记录一系列的离散事件。
  • Metrics:即指标,具有原子性。每个指标是一个逻辑计量单位。用于记录一段时间内的相关指标的状态。
  • Tracing:即追踪。用于记录单次请求范围以内的信息。常用于微服务中分析服务调用路径和排查系统性能问题。

这三者之间并不是相互独立的,会有一定程度的重叠,完善的监控系统一般是包含多个维度的。

分布式追踪的原理:

分布式追踪的概念可以看看谷歌的论文《Dapper, a large-scale distributed systems tracing infrastructure》

Dapper的核心思想是在分布式请求的上下文中记录 span id 和 parent id,用于记录上下级关系。
在这里插入图片描述

分布式日志追踪的作用:

  • 追踪单体服务中的执行逻辑,如函数的监控。由于单体服务中可能存在异步执行,直接输出日志不便于分析执行逻辑,而分布式日志追踪系统可以提供可视化页面,方便分析、发现问题。
  • 追踪分布式环境中微服务的调用、执行情况。
OpenTracing

OpenTracing是CNCF托管的分布式追踪项目,官方定位是针对分布式系统的追踪的API标准库,旨在为不同的分布式追踪系统提供统一的对外API接入层。它位于应用程序/类库追踪或日志分析程序之间。
在这里插入图片描述

OpenTracing 数据模型

OpenTracing 中的 Trace(调用链)通过归属于此调用链的 Span 来隐性的定义。
特别说明,一条 Trace(调用链)可以被认为是一个由多个 Span 组成的有向无环图(DAG图),SpanSpan 的关系被命名为 References。Trace调用链可以用树形结构或者基于时间轴的时序图表示。下图为树形结构表示调用关系:

在这里插入图片描述

每个 Span 包含以下的状态:(译者注:由于这些状态会反映在 OpenTracing API 中,所以会保留部分英文说明)

  • An operation name,操作名称
  • A start timestamp,起始时间
  • A finish timestamp,结束时间
  • Span Tag,一组键值对构成的 Span 标签集合。键值对中,键必须为 string,值可以是字符串,布尔,或者数字类型。
  • Span Log,一组 span 的日志集合。
    每次 log 操作包含一个键值对,以及一个时间戳。

键值对中,键必须为 string,值可以是任意类型。
但是需要注意,不是所有的支持 OpenTracing 的 Tracer,都需要支持所有的值类型。

  • SpanContext,Span 上下文对象 (下面会详细说明)
  • References(Span间关系),相关的零个或者多个 Span(Span 间通过 SpanContext 建立这种关系)

每一个 SpanContext 包含以下状态:

  • 任何一个 OpenTracing 的实现,都需要将当前调用链的状态(例如:trace 和 span 的 id),依赖一个独特的 Span 去跨进程边界传输
  • Baggage Items,Trace 的随行数据,是一个键值对集合,它存在于 trace 中,也需要跨进程边界传输

更多关于 OpenTracing 数据模型的知识,请参考 OpenTracing语义标准

Jaeger

Jeager架构介绍

img

jaeger组件介绍:

  • jaeger-client:jaeger 的客户端,实现了opentracing协议;
  • jaeger-agent:jaeger client的一个代理程序,client将收集到的调用链数据发给agent,然后由agent发给collector;
  • jaeger-collector:负责接收jaeger client或者jaeger agent上报上来的调用链数据,然后做一些校验,比如时间范围是否合法等,最终会经过内部的处理存储到后端存储;
  • jaeger-query:专门负责调用链查询的一个服务,有自己独立的UI;
  • spark-job:基于spark的运算任务,可以计算服务的依赖关系,调用次数等;
基础编程接口
config配置
cfg := &config.Configuration{
    Sampler: &config.SamplerConfig{ //采样类型设置
        Type:  samplerType, 
        Param: samplerParam,
    },
    Reporter: &config.ReporterConfig{ 
        LogSpans: true,
        LocalAgentHostPort:"${your_agent_ip}:6831",
    },
}

其中SamplerConfig用于设置采样类型,Type分为:

  • const,全量采集。param采样率设置0,1 分别对应打开和关闭
  • probabilistic ,概率采集。param默认万份之一,0~1之间取值,
  • rateLimiting ,限速采集。param每秒采样的个数
  • remote 动态采集策略。param值于probabilistic的参数一样。在收到实际值之前的初始采样率。改值可以通过环境变量的JAEGER_SAMPLER_PARAM设定
生成jaeger tracer
func (c Configuration) NewTracer(options ...Option) (opentracing.Tracer, io.Closer, error)
设置为全局的单例tracer
func SetGlobalTracer(tracer Tracer)
生成开始一个Span
StartSpan(operationName string, opts ...StartSpanOption) Span
获得span的上下文
func ContextWithSpan(ctx context.Context, span Span) context.Context
生成子Span
func StartSpanFromContext(ctx context.Context, operationName string, opts ...StartSpanOption) (Span, context.Context)
记录关于Span相关信息
//method 1、Adds a tag to the span.
SetTag(key string, value interface{}) Span

//method 2、有类型检查、但相较LogKV比较麻烦
LogFields(fields ...log.Field)

//method 3、缺乏类型检查
LogKV(alternatingKeyValues ...interface{})
跨网络的Span传递

跨网络的Span传递需要使用Inject和Extract。

Client端
  • 添加import

    import (
        "github.com/opentracing/opentracing-go/ext"
    )
    
  • 添加Inject

    ext.SpanKindRPCClient.Set(reqSpan)
    ext.HTTPUrl.Set(reqSpan, reqURL)
    ext.HTTPMethod.Set(reqSpan, "GET")
    span.Tracer().Inject(
        span.Context(),
        opentracing.HTTPHeaders,
        opentracing.HTTPHeadersCarrier(req.Header),
    )
    
Server端
  • 添加import

    import (
        opentracing "github.com/opentracing/opentracing-go"
        "github.com/opentracing/opentracing-go/ext"
        otlog "github.com/opentracing/opentracing-go/log"
        "github.com/yurishkuro/opentracing-tutorial/go/lib/tracing"
    )
    
  • 从request抽取出span context

    spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
    
  • 通过饮用从Client端传来的span context生成新的child span

    span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx))
    defer span.Finish()
    
所需库
  • 安装支持go的jaeger client library

    go get -u github.com/uber/jaeger-client-go/
    cd $GOPATH/src/github.com/uber/jaeger-client-go/
    git submodule update --init --recursive
    make install
    
  • 安装opentracing

    go get -u github.com/opentracing/opentracing-go
    

测试部署

本部分主要用于测试,使用了官方提供的all-in-one。其中包含了Jaeger-uicollectorquery,并将数据存储与内存之中。正式环境中的部署可参考官网的介绍。all-in-one的docker部署命令:

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.17

成功启动后,可以通过http://localhost::16686访问 Jaeger-UI。

在这里插入图片描述

官方还提供了样例App HotROD,其部署指令为:

$ docker run --rm -it \
  --link jaeger \
  -p8080-8083:8080-8083 \
  -e JAEGER_AGENT_HOST="jaeger" \
  jaegertracing/example-hotrod:1.17 \
  all

成功启动后可以在http://localhost:8080查看HotROD的界面,点击界面中的按钮,即可在Jaeger产生Tracer记录。

例子

单体应用
package main

import (
	"context"
	"fmt"
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/log"
	"github.com/uber/jaeger-client-go"
	jaegercfg "github.com/uber/jaeger-client-go/config"
	"io"
	"time"
)

func initJaeger(serviceName string) (opentracing.Tracer, io.Closer) {
	cfg := &jaegercfg.Configuration{
		Sampler: &jaegercfg.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &jaegercfg.ReporterConfig{
			LogSpans: true,
			LocalAgentHostPort:"172.22.177.56:6831",
		},
		ServiceName: serviceName,
	}
	tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: Reporting spancannot init Jaeger: %v\n", err))
	}
	return tracer, closer
}

func worker(ctx context.Context){
	span, _ := opentracing.StartSpanFromContext(ctx, "span_child")
	defer span.Finish()

	span.SetTag("argv", 0)

	span.LogFields(log.String("funcName", "worker"))
}

func main() {
	tracer, closer := initJaeger("jaeger-demo")
	defer closer.Close()

	opentracing.SetGlobalTracer(tracer)

	span := tracer.StartSpan("span_root")
	defer span.Finish()

	ctx := opentracing.ContextWithSpan(context.Background(), span)

	go worker(ctx)

	span.LogFields(log.String("funcName", "main"))

	//等待协程执行完毕
	time.Sleep(time.Duration(5) * time.Second)
}
跨网络

client

import (
    "net/http"

    opentracing "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
)

// Tracer initialization, etc.
...

tracer := opentracing.GlobalTracer()

clientSpan := tracer.StartSpan("client")
defer clientSpan.Finish()

url := "http://localhost:8082/publish"
req, _ := http.NewRequest("GET", url, nil)

// Set some tags on the clientSpan to annotate that it's the client span. The additional HTTP tags are useful for debugging purposes.
ext.SpanKindRPCClient.Set(clientSpan)
ext.HTTPUrl.Set(clientSpan, url)
ext.HTTPMethod.Set(clientSpan, "GET")

// Inject the client span context into the headers
tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
resp, _ := http.DefaultClient.Do(req)

server:

import (
    "log"
    "net/http"

    opentracing "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
)

func main() {

    // Tracer initialization, etc.

    ...

    http.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) {
        // Extract the context from the headers
        spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
        serverSpan := tracer.StartSpan("server", ext.RPCServerOption(spanCtx))
        defer serverSpan.Finish()
    })

    log.Fatal(http.ListenAndServe(":8082", nil))
}

traceconfig.go

package traceconfig

import (
    "fmt"
    "io"

    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
    "github.com/uber/jaeger-client-go/config"
)

func TraceInit(serviceName string, samplerType string, samplerParam float64) (opentracing.Tracer, io.Closer) {
    cfg := &config.Configuration{
        ServiceName: serviceName,
        Sampler: &config.SamplerConfig{
            Type:  samplerType,
            Param: samplerParam,
        },
        Reporter: &config.ReporterConfig{
            LocalAgentHostPort: "127.0.0.1:6831",
            LogSpans:           true,
        },
    }

    tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("Init failed: %v\n", err))
    }
	opentracing.SetGlobalTracer(tracer)
	
    return tracer, closer
}
grpc

对于每个 gRPC 请求,也可以增加一个 UnaryServerInterceptor,为每个请求都记录一个 Span,这里用到了 gRPC 的 metadata 来传递 Trace ID 等信息。同样,这里生成 Trace ID 后,也将其作为 Request ID 使用。

参考链接:

Jaeger官方文档

Jaeger Notes

开放分布式追踪(OpenTracing)入门与 Jaeger 实现

目前主流开源的日志追踪方案的对比:

img

在现有系统引入时需要考虑以下因素:

  1. 低性能损耗
  2. 应用级的透明,尽量减少业务的侵入,目标是尽量少改或者不用修改代码
  3. 扩展性

基于以上调研,可以总结如下:

  • 如果是偏向于Java栈的应用,对跨语言和定制化需求低,可以优先考虑侵入性低的Apache SkyWalking,该项目是国人主导,有较多的公司在使用;
  • 考虑多语言支持、定制化和高扩展,优先选用 JaegerJaegerZipkin 比较类似,且兼容Zipkin原始协议,相比之下Jaeger 有一定的后发优势),JaegerZipkin相对于其它方案,更专注与Tracing本身,监控功能比较弱;
  • 偏向于纯Web应用,无需定制化且已经有搭建好的ELK日志系统可以考虑低成本的接入Elastic APM
  • CAT 基于日志全量采集指标数据,对于大规模的采集有一定优势,且集成了完善的监控报警机制,国内使用的公司多,但其不支持 OpenTracing
  • Pinpoint最主要的特点是侵入性低,拥有完整的APM和调用链跟踪功能,但是当前仅支持JavaPHP,也不支持 OpenTracing标准。

参考网页:

分布式追踪系统概述及主流开源系统对比

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值