微服务化之后线上问题定位的体验跟以前完全不一样了。以前查故障登录服务器看日志就行现在一个请求从网关进来要过认证、订单、支付好几个服务中间还可能打了消息队列、查了缓存、调了外部接口。日志明明都在但一条请求的完整路径就是很难靠人肉翻日志拼出来。到了这个阶段分布式链路追踪就不是锦上添花而是排查慢请求、定位跨服务故障的基础设施了。Jaeger是云原生基金会孵化的老牌方案也是社区认可度很高的一套链路追踪系统。这几年我帮团队在多个Kubernetes集群里部署、维护Jaeger从最早图省事直接跑all-in-one镜像到后来老老实实搞生产级架构中间踩了不少坑。这篇文章就把能直接照抄的部署方式和trace管理经验整理出来。1. 部署前先想清楚三件事存储选型、组件拓扑、接入方式1.1 存储选型走完Demo之后别停在all-in-one我在不少团队见过同一个流程先在本地用Jaeger的all-in-one镜像把链路跑通UI里看到一堆漂亮的span然后想都没想就把它塞进Kubernetes。这种做法在演示环境完全没问题all-in-one把agent、collector、query打包成一个进程默认用内存存储几十秒就能起一个完整环境。但放到生产环境问题马上冒出来Pod重建或滚动升级历史trace数据跟着进程一起消失告警想复盘根本没得查单点瓶颈collector和query互相抢占资源流量稍微上来就内存溢出没法水平扩展链路数据量一增长整个服务直接不可用没有访问控制query的端口裸奔在集群里谁都能查到内部服务调用关系所以存储选型是第一步要定的。Jaeger的存储后端大致有几类我按实际使用体验排一下存储后端适合场景优点坑内存本地调试、Demo零依赖、起得最快重启即丢容量极小Badger小规模单实例嵌入式无需额外组件只能单节点不能横向扩Cassandra超大规模可扩展性好运维成本高团队不一定有人会管Elasticsearch中大规模生产标配查询能力强、生态成熟、和日志体系复用资源消耗大需要维护索引我自己最终选了Elasticsearch。原因不复杂团队里已有的日志系统就是ES运维体系能复用一套不用额外学Cassandra的操作另外Jaeger的UI查询、服务依赖图都依赖存储的聚合能力ES在这块的成熟度最高。如果你所在团队已经有人在维护ES那我建议直接选它这是生产环境里最稳妥、资料最多的路线。1.2 组件拓扑生产环境少了agent行不行Jaeger在Kubernetes里的部署形态通常涉及几个组件collector负责接收span并写入存储query负责读存储并提供查询UI和APIagent是可选的DaemonSet组件和业务Pod同节点运行用来接收本机UDP上报的span再转发给collector。这里有个常见的理解误区以为agent是必需的。实际上如果你使用OpenTelemetry SDK并且直接配置OTLP/gRPC上报完全可以不部署agent让业务Pod直连collector的Service。我最初在生产里保留了一个DaemonSet形态的agent后来发现大部分服务已经改为OTLP直连那个agent承担的流量很少最终在资源紧张时把它撤掉了。但这不代表你完全不需要agent。老应用、非Java技术栈还有一些只实现了Jaeger原生Thrift协议上报的场景用agent依旧方便。它的好处是业务Pod不需要知道collector的Service地址只需上报到localhost的6831端口由agent统一转发。这在多集群、多命名空间的环境里能减少配置分散。还有一个容易忽略的点collector的Service类型。通常用ClusterIP就够但如果你的监控链路要跨集群汇总或者团队习惯从办公网直连查询就需要额外规划网络策略。我踩过的一个坑是多个业务命名空间和jaeger所在的命名空间之间没有配置网络策略明明Service是通的却因为命名空间级别的隔离策略导致上报超时。这种问题排查起来非常隐蔽后面我会专门讲排查链路。1.3 接入方式预判SDK、网关还是旁路注入部署Jaeger只是搭好了管道真正让链路数据有价值的是让业务产生trace。接入方式直接决定了后续的部署形态代码埋点改业务代码在入口处创建trace在每次调用下游时创建span。灵活度最高但侵入性也最强老项目改造起来很磨人。SDK自动埋点比如Java的OpenTelemetry Agent启动时用-javaagent参数挂载不用改代码Tomcat、JDBC、HTTP Client这些常见库会自动生成span。这是目前我最推荐的方式。网关或服务网格接入如果你用的是Istio这类服务网格可以在数据平面注入sidecar让sidecar自动生成span。这样连SDK都不用装但对网格版本和流量管理方式有要求。这三种方式共同决定一件事上报到Jaeger的链路数据从哪里来。我的经验是新服务用OpenTelemetry Agent自动埋点加OTLP上报老服务如果代码改动成本太高暂时保留Jaeger原生agent上报如果线上已经上了服务网格那sidecar自动生成的span一样可以送到Jaeger不过要记得为网格单独配置采样和标签不然数据里全是网格流量真正业务链路的占比反而不好看。2. 用Helm Chart完成Jaeger生产级部署2.1 环境准备与仓库配置我假设你已经有一个能用的Kubernetes集群并且装好了Helm和kubectl。我这里用的是Helm 3语法上不用再单独配置tiller。先添加Jaeger官方维护的Helm仓库helm repo add jaegertracing https://jaegertracing.github.io/helm-charts helm repo update用官方chart而不是自己写一堆YAML主要原因是Jaeger的组件和配置项很多手写清单很容易漏掉健康检查、优雅停机、资源限制这些生产必需的东西。官方chart把这些都模板化了后续升级组件版本也方便。当然你完全可以基于官方chart的values做二次封装而不是在别人给的示例上乱改。安装前先确认一下chart里默认暴露的端口和参数helm search repo jaeger helm show values jaegertracing/jaeger这一步主要是让你心里有数知道默认会起什么组件、开了哪些端口。前面说了生产环境我不会用all-in-one模式所以后面要重点覆盖storage和组件开关的配置。2.2 接入已有Elasticsearch的values配置我的集群里已经有一套ES所以不会让chart再装一个新的ES那会浪费资源而且增加维护负担。我直接指定外部ES地址并通过Secret把账号密码传进去。先创建Secretkubectl -n observability create secret generic jaeger-es-credentials \ --from-literalusernameelastic \ --from-literalpasswordxxxxxxxx \ --dry-runclient -o yaml | kubectl apply -f -然后准备一份自己的values-jaeger.yaml关键部分说明一下provisionDataStore: cassandra: false elasticsearch: false storage: type: elasticsearch elasticsearch: host: elasticsearch-master.observability.svc.cluster.local port: 9200 user: elastic existingSecret: jaeger-es-credentials scheme: http collector: service: type: ClusterIP resources: requests: memory: 512Mi cpu: 250m limits: memory: 2Gi cpu: 2 query: service: type: ClusterIP ingress: enabled: true hosts: - host: jaeger.example.internal paths: - path: / resources: requests: memory: 256Mi cpu: 100m limits: memory: 1Gi cpu: 1 agent: enabled: false sampling: strategies: default_strategy: type: probabilistic param: 0.5注意这里我把agent显式关了因为新服务都走OTLP直连。如果你的环境还有老服务需要UDP上报那就把agent打开用DaemonSet方式跑agent: enabled: true strategy: DaemonSet安装命令很简单helm upgrade --install jaeger jaegertracing/jaeger \ -n observability \ -f values-jaeger.yaml如果你要临时看某个渲染后的资源长什么样可以加--debug --dry-run参数Helm会把最终生成的YAML打印出来。这个方法在调试values参数时特别有用比一遍遍apply再查Pod状态快得多。2.3 验证Pod状态与链路数据接收Pod都Running之后我最先做的是看日志而不是急着访问UI。collector的日志里如果出现ES连接错误后面所有trace都会写不进去kubectl -n observability logs deployment/jaeger-collector --tail50确认日志正常后我通过端口转发快速看一眼UIkubectl -n observability port-forward service/jaeger-query 16686:16686浏览器打开localhost:16686能看到Search页面和System Architecture页面但此时还没有数据。接下来需要跑一个带trace上报的测试服务。最简单的做法是直接用Jaeger客户端库发一条测试span或者用任意一个已启用追踪的业务应用打几个请求。我用Java服务举例假设它已经配置OTLP上报地址为jaeger-collector.observability.svc.cluster.local:4317。手动调用几次后在UI的Search页面按服务名搜索如果能看到对应的trace记录说明collector到ES、UI查ES这两条链路都是通的。等看到第一条trace数据部署这个环节就算跑通了。3. 生产级trace管理采样策略、索引生命周期与应用接入3.1 采样策略不是所有trace都值得存trace管理里最需要想清楚的事情是采样。刚开始接触分布式追踪的人很容易陷入误区全量采集一个span都不丢。实际上在高并发生产环境全量采集会产生非常夸张的存储开销。以一个每秒1000请求的服务为例一个请求平均产生20个span一天算下来是17.2亿个span哪怕压缩后再写ES集群也会被冲垮。所以生产环境必须做采样。Jaeger支持几种采样类型固定采样constparam1表示所有trace都采样param0表示全部丢弃。适合测试环境。概率采样probabilisticparam表示比例0.5就是一半trace会被采集。适合流量均匀的场景。限频采样ratelimiting每秒最多采样多少个trace。适合已知流量上限的场景。远程采样remote由collector动态下发采样策略采集端到端可以实时切换不需要重启服务。我生产环境的默认配置是概率采样比例通常设在0.1到0.5之间。核心链路、出问题影响面大的服务可以在策略里单独覆盖把它调到1.0全采相对边缘的批量任务甚至可以不采样或者用限频。这样既能保证核心问题查得到又不会把存储撑爆。如果你用远程采样就不用为每个服务单独改启动参数。collector会读取一份采样策略配置然后下发给你部署的所有业务客户端。前面values里写的sampling.strategies就是远程采样的初始模板。注意远程采样开了之后业务SDK这边的启动参数也要设置为remote否则它不会主动去collector拉取策略。3.2 Elasticsearch索引生命周期管理Jaeger写入ES的索引主要有三类jaeger-span-YYYY-MM-DD存储每个span明细jaeger-service-YYYY-MM-DD存储服务调用关系jaeger-dependencies-YYYY-MM-DD存储服务依赖分析结果如果不做任何索引生命周期管理这些索引会无限增长。我见过不少集群跑了大半年ES节点磁盘告警最后只能手动删索引。所以正式环境一定要配好索引生命周期策略ILM。先用Kibana或curl创建ILM策略比如保留30天滚动更新后超过30天的索引自动删除{ policy: { phases: { hot: { min_age: 0ms, actions: { rollover: { max_size: 30gb, max_age: 1d } } }, delete: { min_age: 30d, actions: { delete: {} } } } } }然后在Jaeger的配置里启用autoCreateIndex和ILM相关设置。以ES7为例Jaeger的chart里可以配置storage: elasticsearch: useAlias: true ilm: enabled: true policyName: jaeger-ilm-policy这是典型的一次配置长期受益的操作。很多团队没配ILM结果一到月初磁盘告警。配置完成后ES会自动rollover索引历史数据到期后自动清理不会再出现大半夜爬起来手动删索引的场面。3.3 应用接入Java服务通过OpenTelemetry上报trace部署和管理是一回事让业务真正接入是另一回事。我推荐Java服务直接用OpenTelemetry Java Agent。它最大的优势是不改代码就能自动采集HTTP请求、数据库调用、消息队列等常见中间件的span再通过OTLP协议上报到Jaeger的collector。以Spring Boot应用为例在启动参数里加java -javaagent:/path/to/opentelemetry-javaagent.jar \ -Dotel.traces.exporterotlp \ -Dotel.exporter.otlp.endpointhttp://jaeger-collector.observability.svc.cluster.local:4317 \ -Dotel.service.nameorder-service \ -jar app.jar注意-Dotel.service.name这个配置非常关键。Jaeger UI是按服务名分组的如果这个值没设置或者设置得不好所有服务的span都会堆到一个奇怪的默认服务名下排查问题时根本分不清是哪个服务。这是我在实际现场见过最多的问题之一。对于Go、Python服务OpenTelemetry也提供了对应SDK手动在入口处创建root span、每次下游调用加子span。细不细致取决于你对链路数据的期望但最开始接入时用Agent自动埋点就已经能覆盖大部分排查需求了。4. 排障实录trace丢失、查询慢、资源消耗的典型问题4.1 trace数据丢失的排查链路UI里就是没有数据部署完以后最常遇到的问题是业务已经起来了看起来也在调上报接口但Jaeger UI里仍然搜不到任何trace。这类问题我排查过很多次整理一条完整的排查链路。第一步先确认collector收到的数据量。看collector的指标如果span数量一直是0说明数据根本没到collector问题出在应用侧或网络链路。我用Prometheus监控Jaeger采集器自身指标其中jaeger_collector_spans_received_total是核心指标。第二步确认应用侧的上报配置。最常见的坑是地址写错。你可以在业务Pod里执行下面的命令直接测试能否连上collector端口kubectl -n your-ns exec -it deployment/xxx -- sh -c wget -qO- http://jaeger-collector.observability.svc.cluster.local:14268/api/services || echo connection failed如果这里都连不通就需要排查网络策略、Service和Endpoint。不少集群开启了NetworkPolicy默认拒绝跨命名空间流量Jaeger在observability命名空间你的业务在另一个命名空间两边没放行自然收不到。第三步如果collector能收到span、指标在增长UI里却没有数据那问题多半在查询链路。确认query连的ES和collector写的是同一个ES、同一个索引前缀。如果用了alias方式还要确认query配置的索引alias正确。第四步检查采样比例。如果你的采样策略是0那业务SDK不会上报任何数据UI自然空白。这看起来像废话但确实有人把param误写成0排查了大半天才反应过来。4.2 Elasticsearch查询变慢trace量大了之后UI卡顿trace量稍微上来以后UI的Search页面会明显变慢。典型表现是选一个时间范围搜索按钮点了半天才出结果甚至直接超时。根因方向通常有三个。第一个是查询时间范围太大。Jaeger UI默认会加载最近一段时间内所有服务名如果你选了过去7天甚至30天来搜索ES得扫描大量索引慢是必然的。排查问题时先把时间范围收窄到最近一小时再逐渐扩大。第二个是分片数设计不合理。Jaeger的ES索引每天按日期分如果索引偏大查询时扫描的分片数量太多。这里建议给Jaeger的span索引设置合适的主分片数。小规模集群设置3到5个分片足够不要默认5个就万事大吉还要观察实际数据量。第三个是query没有配置合适的资源。Jaeger query本身是轻量的但在数据量大时UI后端需要做聚合、服务名去重、trace树组装这些操作对CPU和内存也有要求。如果发现query频繁OOM或者GC停顿严重可以先提升resources配置再看ES层面的慢查询日志。4.3 资源消耗的现实用ES就要接受它的吞吐成本这是我在多个团队反复强调的一点既然选了ES做Jaeger存储就要接受它的资源开销。ES不是省油的灯每次span写入都要经过分词、倒排索引、刷新到磁盘等环节CPU和IO消耗都不低。生产环境我通常给ES预留至少8G内存并且使用SSD否则高峰期写入会延迟trace数据会积压。另一个暗坑是collector的批量写入参数。Jaeger的ES存储支持批量提交合理调大storage.elasticsearch.batch-size和间隔可以减少写ES的次数并提升吞吐。但不要调到无穷大ES单次bulk请求过大会导致内存压力。我们线上大概调到8000条批量、间隔5秒左右整体写入很平稳。还有一点Jaeger的collector默认是无状态的可以随意水平扩展。但如果ES写入能力不够扩展collector副本数并不会解决写入瓶颈只会让更多数据堵在ES这一层。先看ES的写入速率和线程池队列长度再决定是不是要扩大存储集群。5. 用trace数据排查一次真实的慢调用5.1 从现象到链路p99上升时第一步该查什么这里分享一个我最近实际处理的线上问题完整还原用Jaeger排查慢调用的思路。某天监控面板显示支付服务接口的p99从800ms涨到2.5s告警触发。按照以前的思路我会先登录各个服务看日志但这次直接打开Jaeger UI。在Search页面选择支付服务时间范围设为最近15分钟按duration排序挑一条耗时3秒多的trace点开。链路的完整结构就展示出来了网关用了50ms支付服务内部分了两个环节第一个是本地缓存查询只花了20ms第二个是调用外部清算接口光这一个span就占了2.2秒。后面还有一次数据库耗时400ms。之前猜想的数据库慢查询并不是主要原因真正的瓶颈在外部清算接口。顺着这个线索我在trace的标签里看到外部调用的URL和响应状态找清算服务负责人确认确实是他们的接口临时拥堵。整个过程只花了几分钟如果是人肉翻日志恐怕得折腾一个多小时。5.2 用trace标签定位服务间依赖的隐性问题这类问题排查多了你会发现链路追踪的价值不只是能看到慢在哪一步还能发现一些隐藏的依赖问题。有一次我们排查内存告警发现某服务的span上偶尔出现一个奇怪的标签显示它在调用一个已经下线半年的内部服务。代码里本来应该走新的接口但某个老逻辑分支还在旧地址上重试。Jaeger UI左侧的Trace详情页能完整展示每个span的标签和日志这些元数据在排障时帮助很大。因此我建议接入时尽量让埋点带上必要的业务标签例如用户ID、订单号、错误码等不用太多但要有。排查问题时可以通过标签快速过滤出符合条件的trace不用一条条翻。5.3 指标、日志、链路三件套如何配合生产可观测性最终不是靠Jaeger单打独斗。Prometheus负责指标告警告诉你系统哪里慢、哪里挂了Loki或者EFK负责日志告诉你具体报错内容Jaeger负责链路告诉你一次请求在服务间怎么走的。三者配合才是完整的排查体验。我的建议是告警由指标触发触发后先查Jaeger链路定位到具体服务再进入对应日志系统看细节。这个过程中如果链路和日志都能携带同一个请求ID效率会成倍提升。Jaeger span的baggage可以传递自定义键值对把请求ID放进去日志里也打印同样的ID跳转查找会非常顺畅。写在部署完成后的一些个人习惯如果你的集群规模不大、服务数量在二三十个以内按我前面给的方案已经够用。但有一件事要提醒Jaeger生产部署不是装完就结束了采样策略、索引生命周期、资源配比都需要按真实流量反复调。我个人现在比较推荐的做法是所有新项目统一走OpenTelemetry协议上报到Jaeger一方面协议统一另一方面后续即使要迁移到其他链路存储系统只要替换collector或者切换导出端点就可以不需要改业务代码里的埋点逻辑。旧服务再按优先级逐步接入没必要一次性强制完成否则很容易因为改动面太大被团队抵制。另外一个习惯是给Jaeger的query配置一个独立的域名并且在接入层做基本的访问控制。链路数据里往往带有服务名、接口名、调用参数这些敏感信息暴露在公网上是很危险的事。内网访问再加上一个简单的HTTP Basic Auth虽然简陋但能挡住很多无意的越权访问。如果你恰好正在把Jaeger往生产里搬希望这篇能帮你少踩几个坑。链路通了之后记得先花一个下午把采样策略和索引生命周期这两件事定了否则后面流量上来再回头补痛苦程度完全不是一个级别。