随着微服务架构的普及,系统变得越来越复杂。一个请求,可能会经过多个服务,调用多个数据库,访问多个缓存。出了问题的时候,很难定位是哪个服务出了问题,也很难知道请求在各个服务中的耗时情况。

传统的日志和监控,已经很难满足微服务架构下的排查需求了。这时候,分布式追踪(Distributed Tracing)就派上用场了。分布式追踪,可以追踪一个请求在各个服务中的完整调用链,包括调用顺序、耗时、状态等,让你一目了然地看到请求的完整路径,快速定位问题。

Jaeger,就是目前最流行的分布式追踪工具之一。它是Uber开源的,功能强大,界面友好,已经加入了CNCF(云原生计算基金会),和Kubernetes、Prometheus等云原生工具配合得很好。

今天,我就来推荐Jaeger这个工具,介绍分布式追踪的基本概念、Jaeger的架构和功能、如何部署和使用Jaeger、以及Jaeger在实际项目中的应用场景和最佳实践。希望能帮助正在做微服务架构的朋友,提升排查问题的效率。

一、为什么需要分布式追踪

在介绍Jaeger之前,先说说为什么需要分布式追踪。

在单体架构时代,一个请求都在一个应用里处理,出了问题,看应用日志就能定位。但是,在微服务架构时代,一个请求可能会经过多个服务,比如:

用户下单 → 订单服务 → 库存服务 → 支付服务 → 物流服务 → 通知服务

一个下单请求,可能会经过五六个服务,每个服务又可能调用数据库、缓存、消息队列等。出了问题的时候,你可能只看到下单失败了,但是不知道是哪个服务出了问题,也不知道请求卡在哪一步了。

这时候,你可能会:

  1. 看订单服务的日志,发现调用库存服务超时了
  2. 看库存服务的日志,发现数据库查询慢了
  3. 看数据库的慢查询日志,发现某个SQL没有走索引
  4. 优化SQL,问题解决

这个过程,需要登录多个服务器,查看多个日志,时间对齐,人工串联调用链,非常耗时,效率很低。而且,如果服务更多,调用链更复杂,人工排查几乎不可能。

分布式追踪,就是解决这个问题的。它可以自动追踪一个请求在各个服务中的完整调用链,包括:

  • 请求经过了哪些服务,调用顺序是什么
  • 每个服务的耗时是多少,哪个服务最慢
  • 每个服务的状态是什么,有没有报错
  • 请求调用了哪些数据库、缓存、外部接口
  • 每个调用的参数和返回值是什么(可选)

有了分布式追踪,出了问题,你只需要在追踪系统里搜索这个请求,就能看到完整的调用链,一眼就能看出是哪个服务出了问题,卡在哪一步了,耗时多少,错误信息是什么。排查问题的效率,提升了不止一个数量级。

除了排查问题,分布式追踪还有很多其他用途:

  • 性能分析:分析请求的耗时分布,找到性能瓶颈,优化慢服务
  • 依赖分析:分析服务之间的依赖关系,了解系统的架构
  • 容量规划:根据请求量和耗时,做容量规划,合理分配资源
  • SLA监控:监控服务的响应时间和错误率,确保服务满足SLA

所以,分布式追踪,是微服务架构下不可或缺的工具。

二、分布式追踪的基本概念

在介绍Jaeger之前,先了解一下分布式追踪的基本概念。

分布式追踪的概念,最早是由Google在2010年的论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》中提出的。后来,Twitter基于这篇论文,开发了Zipkin,开源了分布式追踪系统。再后来,Uber开发了Jaeger,也是开源的分布式追踪系统。

虽然不同的分布式追踪系统实现不同,但是基本概念是类似的:

Trace(追踪)

一个Trace,代表一个完整的请求链路。比如,用户的一次下单请求,就是一个Trace。一个Trace,由多个Span组成。

Span(跨度)

一个Span,代表Trace中的一个工作单元,也就是一次调用。比如,订单服务调用库存服务,就是一个Span。每个Span,包含以下信息:

  • 操作名称:比如"调用库存服务"
  • 开始时间和结束时间:可以计算耗时
  • Span ID:Span的唯一标识
  • Trace ID:Trace的唯一标识,同一个Trace的所有Span共享一个Trace ID
  • Parent Span ID:父Span的ID,用来串联调用链
  • Tags:标签,键值对,用来记录一些元信息,比如HTTP方法、URL、状态码、错误信息等
  • Logs:日志,记录一些时间点的事件,比如异常堆栈

通过Trace ID和Parent Span ID,就可以把一个请求的所有Span串联起来,形成一个完整的调用链。

上下文传播(Context Propagation)

分布式追踪的关键,是上下文传播。也就是,把Trace ID和Span ID,从一个服务传播到下一个服务,这样才能把不同服务的Span串联起来。

上下文传播,通常是通过HTTP Header或者消息队列的属性来实现的。比如,在HTTP请求中,把Trace ID和Span ID放在Header里,下游服务从Header里取出Trace ID和Span ID,创建自己的Span,然后继续传播给下一个服务。

目前,分布式追踪的上下文传播,已经有了统一的标准,就是OpenTracing。OpenTracing是一个厂商中立的分布式追踪API标准,支持多种编程语言,Jaeger、Zipkin等都支持OpenTracing。有了OpenTracing,你可以在代码里用统一的API来埋点,不需要绑定具体的追踪系统,以后想换追踪系统也很方便。

三、Jaeger是什么

了解了分布式追踪的基本概念,现在来介绍Jaeger。

Jaeger是Uber在2015年开发的分布式追踪系统,2016年开源,2017年加入CNCF,2019年毕业,成为CNCF的毕业项目。Jaeger的名字,来源于《太空堡垒卡拉狄加》(Battlestar Galactica)中的一艘飞船,意思是"猎人"。

Jaeger的特点:

  1. 开源免费:Jaeger是完全开源的,基于Apache 2.0协议,可以免费使用,也可以二次开发。
  2. 云原生:Jaeger是为云原生环境设计的,和Kubernetes、Docker、Prometheus等云原生工具配合得很好,部署简单,扩展性好。
  3. 高性能:Jaeger的后端是用Go写的,性能很高,支持高并发、大数据量的追踪数据。
  4. 多语言支持:Jaeger支持多种编程语言的客户端,包括Go、Java、Python、Node.js、C#、Ruby等,几乎覆盖了所有主流语言。
  5. 支持OpenTracing:Jaeger支持OpenTracing标准,你可以用OpenTracing的API来埋点,和Jaeger解耦。
  6. 界面友好:Jaeger的UI界面很友好,功能强大,可以搜索Trace、查看调用链、分析服务依赖、监控系统性能等。
  7. 可扩展:Jaeger的架构是可扩展的,可以根据数据量的大小,灵活扩展后端的存储和查询能力。
  8. 社区活跃:Jaeger的社区很活跃,版本更新快,问题响应及时,文档也比较完善。

因为这些特点,Jaeger已经成为目前最流行的分布式追踪工具之一,很多公司都在用,包括Uber、Adobe、Cisco、Symantec等。

四、Jaeger的架构

Jaeger的架构,主要由以下几个组件组成:

1. Client Libraries(客户端库)

客户端库,是集成在你的应用代码里的,用来创建Span,记录追踪信息,然后把追踪数据发送给Jaeger的后端。

Jaeger提供了多种语言的客户端库,你可以根据自己的技术栈选择。客户端库支持OpenTracing标准,你也可以直接用OpenTracing的API来埋点。

客户端库,通常会把追踪数据先存在本地,然后批量异步发送给Agent,这样不会影响应用的性能。

2. Agent(代理)

Agent是一个网络守护进程,监听UDP端口,接收客户端库发送的追踪数据,然后批量转发给Collector。

Agent通常部署在每个主机上(或者每个K8s节点上),作为客户端库和Collector之间的中间层。Agent的作用是:

  • 减轻客户端库的负担,客户端库只需要把数据发给本地的Agent,不需要关心Collector的地址
  • 做一些数据处理,比如批量、压缩、采样等
  • 隔离Collector,Collector的地址变化不会影响客户端库

3. Collector(收集器)

Collector接收Agent发送的追踪数据,做一些验证、转换、索引等处理,然后把数据存储到后端的存储系统中。

Collector是无状态的,可以水平扩展,根据数据量的大小,部署多个Collector实例,做负载均衡。

4. Storage(存储)

Storage是存储追踪数据的后端。Jaeger支持多种存储后端:

  • Cassandra:Jaeger最早支持的存储,分布式、高可用、可扩展,适合大数据量
  • Elasticsearch:支持全文搜索,查询灵活,适合需要复杂查询的场景
  • Kafka:作为中间缓冲,把追踪数据先写到Kafka,然后再从Kafka消费写到其他存储,适合高吞吐的场景
  • Memory:内存存储,只用于测试和演示,数据重启就丢失
  • Badger:嵌入式的本地存储,适合单机部署和测试

你可以根据自己的需求和数据量,选择合适的存储后端。

5. Query Service(查询服务)

Query Service提供查询追踪数据的API,从存储中读取追踪数据,返回给UI界面。

Query Service也是无状态的,可以水平扩展。

6. UI(用户界面)

UI是Jaeger的Web界面,提供可视化的操作,包括:

  • 搜索Trace:根据服务、操作、时间、标签等条件搜索Trace
  • 查看调用链:以时间轴的方式,展示一个Trace的完整调用链,每个Span的耗时、状态、标签、日志等
  • 服务依赖图:展示服务之间的依赖关系图,一目了然地看到系统的架构
  • 性能分析:分析服务的响应时间、错误率、吞吐量等性能指标
  • 系统监控:监控Jaeger系统本身的健康状态

UI界面很友好,操作简单,功能强大,是Jaeger的一大亮点。

Jaeger的整体架构,是一个典型的微服务架构,各个组件职责清晰,可独立扩展,灵活部署。你可以根据自己的规模,选择合适的部署方式:

  • All-in-one:所有组件都在一个进程里,用内存存储,适合本地开发和测试
  • 生产部署:各个组件独立部署,用Cassandra或者Elasticsearch做存储,适合生产环境

五、如何部署Jaeger

Jaeger的部署很简单,特别是在Kubernetes环境下,用官方的Helm Chart或者Operator,几分钟就能部署好。

下面简单介绍几种部署方式:

1. 本地开发:All-in-one Docker镜像

如果只是在本地开发和测试,可以用Jaeger官方的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 9411:9411 \
  jaegertracing/all-in-one:1.6

启动之后,访问 http://localhost:16686 就能打开Jaeger的UI界面了。

2. Kubernetes部署:Helm Chart

如果是在Kubernetes集群里部署,推荐用官方的Helm Chart:

# 添加Jaeger的Helm仓库
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts

# 安装Jaeger
helm install jaeger jaegertracing/jaeger

Helm Chart支持很多配置项,比如存储后端、副本数、资源限制、Ingress等,可以根据自己的需求配置。

3. Kubernetes部署:Jaeger Operator

除了Helm Chart,Jaeger还提供了Kubernetes Operator,可以更方便地管理Jaeger实例:

# 安装Jaeger Operator
kubectl create namespace observability
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/io.jaegertracing_v1_jaeger_crd.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml

# 创建Jaeger实例
kubectl apply -f - <<EOF
apiVersion: io.jaegertracing/v1
kind: Jaeger
metadata:
  name: simplest
EOF

Operator会自动创建Jaeger的各个组件,管理Jaeger的生命周期,非常方便。

4. 生产环境部署建议

在生产环境部署Jaeger,有一些建议:

  • 存储选择:数据量大的话,推荐用Cassandra或者Elasticsearch做存储,不要用内存存储
  • 高可用:Collector、Query、存储都要部署多个实例,做高可用,避免单点故障
  • 资源规划:根据数据量,合理规划各个组件的资源,特别是存储的资源
  • 安全:UI和API要做认证和授权,不要暴露在公网上
  • 监控:Jaeger本身也要做监控,确保Jaeger系统的健康
  • 数据保留:设置合理的数据保留时间,定期清理过期数据,避免存储无限增长

六、如何在应用中使用Jaeger

部署好Jaeger之后,就需要在你的应用代码里集成Jaeger的客户端库,埋点,发送追踪数据。

下面以Go语言为例,简单介绍一下如何在应用中使用Jaeger:

1. 安装客户端库

go get github.com/uber/jaeger-client-go
go get github.com/uber/jaeger-lib/metrics

2. 初始化Tracer

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

func InitJaeger(serviceName string) (opentracing.Tracer, io.Closer, error) {
    cfg := config.Configuration{
        ServiceName: serviceName,
        Sampler: &config.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans: true,
        },
    }
    tracer, closer, err := cfg.NewTracer()
    if err != nil {
        return nil, nil, err
    }
    opentracing.SetGlobalTracer(tracer)
    return tracer, closer, nil
}

3. 创建Span,记录追踪信息

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    // 从请求中提取Span上下文
    spanCtx, _ := opentracing.GlobalTracer().Extract(
        opentracing.HTTPHeaders,
        opentracing.HTTPHeadersCarrier(r.Header),
    )
    
    // 创建Span
    span := opentracing.StartSpan(
        "handle_request",
        opentracing.ChildOf(spanCtx),
    )
    defer span.Finish()
    
    // 设置标签
    span.SetTag("http.method", r.Method)
    span.SetTag("http.url", r.URL.Path)
    
    // 记录日志
    span.LogKV("event", "start processing")
    
    // 调用下游服务,把Span上下文传播过去
    callDownstreamService(span)
    
    // 处理完成
    span.LogKV("event", "end processing")
}

4. 传播上下文到下游服务

func callDownstreamService(span opentracing.Span) {
    req, _ := http.NewRequest("GET", "http://downstream-service/api", nil)
    
    // 把Span上下文注入到请求Header中
    opentracing.GlobalTracer().Inject(
        span.Context(),
        opentracing.HTTPHeaders,
        opentracing.HTTPHeadersCarrier(req.Header),
    )
    
    resp, err := http.DefaultClient.Do(req)
    // ...
}

通过这样的方式,就可以把一个请求在各个服务中的调用链串联起来,发送给Jaeger,然后在Jaeger的UI里查看。

当然,这只是最基础的用法。实际项目中,你可以用一些框架的集成,比如:

  • Go:grpc-middleware、go-kit、go-micro等,都有Jaeger的集成
  • Java:Spring Cloud Sleuth、Jaeger Java Client等
  • Python:Flask-OpenTracing、Django-OpenTracing等
  • Node.js:express-opentracing、koa-opentracing等

用这些框架的集成,可以减少很多手动埋点的工作,更方便地接入Jaeger。

七、Jaeger的应用场景

Jaeger的应用场景很多,下面介绍几个常见的:

场景一:快速定位问题

这是Jaeger最常用的场景。用户反馈某个请求慢或者失败了,你只需要在Jaeger里搜索这个请求,就能看到完整的调用链,一眼就能看出是哪个服务慢了,哪个服务报错了,卡在哪一步了。

比如,用户反馈下单慢,你在Jaeger里搜索下单的Trace,发现订单服务调用库存服务花了2秒,库存服务调用数据库花了1.8秒,数据库的一个SQL查询很慢。你就知道问题出在数据库的慢查询上,优化SQL就能解决问题。

有了Jaeger,排查问题的效率,从可能的几个小时,缩短到几分钟。

场景二:性能分析和优化

Jaeger可以分析请求的耗时分布,找到性能瓶颈。你可以看哪些服务的平均耗时最长,哪些操作的P99耗时很高,哪些请求的调用链特别长。

比如,你发现某个接口的P99耗时很高,在Jaeger里查看这个接口的Trace,发现调用了一个下游服务,这个下游服务的耗时波动很大,有时候快有时候慢。进一步分析,发现这个下游服务的数据库连接池配置不合理,高峰期连接不够用,导致等待。优化连接池配置之后,耗时就稳定了。

通过Jaeger的性能分析,可以持续优化系统的性能,提升用户体验。

场景三:服务依赖分析

Jaeger可以根据追踪数据,自动生成服务依赖图,展示服务之间的调用关系。你可以一目了然地看到系统的架构,哪些服务依赖哪些服务,哪些服务被哪些服务调用,流量是怎么流动的。

这对于理解系统架构、做架构优化、评估变更影响,都很有帮助。比如,你要修改某个服务,想知道哪些服务会受影响,看一下依赖图就知道了。

场景四:SLA监控和告警

Jaeger可以监控服务的响应时间和错误率,设置SLA(服务等级协议),当服务的响应时间或者错误率超过阈值的时候,自动告警。

比如,你要求订单服务的P99响应时间不超过500ms,错误率不超过0.1%。Jaeger监控到订单服务的P99响应时间超过了500ms,就自动告警,通知运维人员处理。

通过Jaeger的SLA监控,可以确保服务满足性能和可用性要求,提升用户体验。

场景五:容量规划

Jaeger可以统计每个服务的请求量、耗时、资源使用等数据,为容量规划提供依据。你可以根据这些数据,合理分配资源,避免资源不足或者浪费。

比如,你发现某个服务的请求量增长很快,按照这个趋势,三个月后服务器资源就不够用了。你就可以提前扩容,避免高峰期服务不可用。

八、最佳实践

最后,分享一些使用Jaeger的最佳实践:

1. 合理设置采样率

如果你的系统流量很大,全量采集追踪数据,存储和网络开销都会很大。这时候,需要合理设置采样率,只采集一部分请求的追踪数据。

Jaeger支持多种采样策略:

  • 常量采样:固定采样率,比如10%
  • 概率采样:按照概率采样
  • 限速采样:每秒最多采样多少个
  • 自适应采样:根据系统负载自动调整采样率

建议根据你的数据量和存储能力,设置合理的采样率,既能满足排查问题的需求,又不会造成太大的开销。

2. 合理设置标签和日志

Span的标签和日志,是排查问题的重要信息。但是,也不要设置太多,否则会增加存储开销,也影响查询性能。

建议:

  • 标签:设置一些关键的、用于搜索和过滤的标签,比如服务名、操作名、HTTP方法、URL、状态码、用户ID、订单ID等
  • 日志:记录一些关键的事件和错误信息,比如异常堆栈、关键参数等,不要记录太多无关的信息

3. 注意敏感信息

追踪数据里,可能会包含一些敏感信息,比如用户密码、身份证号、银行卡号等。这些信息,不应该被记录到追踪系统里,否则会有安全风险。

建议在埋点的时候,对敏感信息做脱敏处理,或者不记录敏感信息。

4. 做好错误追踪

当请求出错的时候,要在Span里记录错误信息,比如错误码、错误消息、异常堆栈等,并且设置错误标签(error=true)。这样,在Jaeger里就能快速筛选出出错的请求,查看错误信息,定位问题。

5. 结合日志和监控

分布式追踪,不是孤立的,要和日志、监控结合起来,形成完整的可观测性体系。

  • 日志:记录详细的事件和错误信息
  • 监控:监控系统的指标和状态,及时告警
  • 追踪:追踪请求的完整调用链,定位问题

三者结合,才能全面地了解系统的状态,快速排查和解决问题。

6. 定期清理过期数据

追踪数据量很大,如果不清理,存储会无限增长。建议设置合理的数据保留时间,比如保留7天或者30天,定期清理过期数据。

同时,可以对历史数据做归档,把需要长期保留的数据归档到便宜的存储里,需要的时候再查询。

结语

分布式追踪,是微服务架构下不可或缺的工具。Jaeger,作为目前最流行的分布式追踪工具之一,功能强大,界面友好,云原生,开源免费,是微服务架构下提升排查效率的利器。

如果你正在做微服务架构,还没有用分布式追踪,强烈建议你试试Jaeger。它会让你排查问题的效率,提升不止一个数量级。

当然,Jaeger只是一个工具,工具再好,也需要合理地使用。希望本文的介绍,能帮助你快速上手Jaeger,把它用好,让技术为业务服务。

最后,用一句话总结:"可观测性,是微服务架构的基石。分布式追踪,是可观测性的重要组成部分。Jaeger,是分布式追踪的优秀选择。"

愿每一个微服务实践者,都能拥有完善的可观测性体系,让系统更稳定,让排查更高效。