ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务全链路追踪与OpenTelemetry实战指南

微服务全链路追踪与OpenTelemetry实战指南 最近后台经常有人问我说服务拆分了排查一个问题要在五六套系统之间来回跳日志对不上时间轴捋不清线上一个慢请求到底卡在哪个环节全靠猜问我有没有什么好的办法。其实这就是典型的分布式链路追踪要解决的问题。我之前在项目里踩过不少坑也对比过几套方案最终落地用的是 OpenTelemetry。今天就把这套东西从原理到实战完完整整拆开聊一聊希望能帮到正在做微服务改造或者被线上排查折磨得焦头烂额的朋友。1. 为什么需要全链路监控从一次凌晨两点的线上事故说起先说一个我印象非常深刻的例子。当时我们的系统已经拆成了差不多二十个微服务前端一个请求进来要经过网关、认证服务、订单服务、库存服务、支付服务中间还穿插着MQ异步消息和Redis缓存。看起来架构很清晰每个服务也都做了基本的日志记录但真正出问题的时候你会发现这些日志就像是散落在一大片森林里的线索你根本不知道该从哪里开始找。那次事故是用户反馈下单经常超时而且不是全部超时是有时候快有时候慢非常随机。我们几个后端工程师花了将近三个小时分别去不同的服务上看日志用时间戳手动拼凑调用关系最后才勉强定位到是库存服务某个节点上的一次Redis连接池获取等待时间过长导致的。整个过程极其痛苦而且大家也意识到随着服务规模继续扩大这种人工拉网式排查的方式肯定行不通。这就是链路追踪存在的根本意义。它要解决的核心问题就三个第一一个请求在整个分布式系统里到底经过了哪些服务、哪些组件第二每一步耗时是多少瓶颈在哪里第三如果某个环节报错了错误信息是什么根因在哪儿。有了链路追踪你就相当于给每个请求发了一张从进入到离开的完整行程单而不是像以前那样手里只有一堆模糊的、不成体系的片段。而且链路追踪解决的不只是排查效率问题。它对你的容量评估、性能优化、架构决策都有帮助。比如你可以通过链路分析清楚地看到哪些服务是调用的热点流量是集中在某个下游服务上哪些接口存在明显的长尾延迟这些数据反过来会指导你做缓存设计、异步化改造甚至是决定某个服务要不要继续拆分。从这个角度看链路追踪不是一个好用的小工具而是一套方法论是微服务架构演进过程中绕不开的基础设施。2. 选型分析为什么最终选择了OpenTelemetry当时团队里讨论过几个方案包括自研埋点上报、Zipkin配合Sleuth以及直接上SkyWalking最后我们选择了OpenTelemetry。这个决定不是拍脑袋做的我把当时对比的几个维度和大家分享一下方便你在自己做选型的时候也有个参考。先说一下对Zipkin和Sleuth这套组合的看法。Spring Cloud Sleuth配合Zipkin在很多老项目里都有应用它的优势在于和Spring Cloud生态结合得比较好接入简单社区资料也多。但问题是Sleuth现在已经进入了维护模式Spring官方把可观测性这部分的能力统一收编到了Micrometer Tracing里面而Micrometer Tracing底层又默认支持OpenTelemetry。所以如果新项目还执着于Sleuth等于刚起步就选了一个正在被淘汰的路线。再说SkyWalking。SkyWalking做得确实非常成熟它提供了完整的APM能力包括指标、链路、日志、告警功能很全UI也做得不错。但SkyWalking的思路更多是基于Java Agent的方式做字节码增强部署和运维有自己的一套体系。如果你想用OpenTelemetry的生态组件或者想把链路数据同时用于监控告警和日志关联再或者你有自建数据平台的需求SkyWalking的演进灵活度可能就没那么高。OpenTelemetry最大的优势在于它是CNCF的项目是目前可观测性领域事实上的标准。它做的事情很纯粹就是负责数据的产生、采集、上下文传播和导出不绑定任何后端的存储和展示方案。今天你觉得Jaeger好用数据导到Jaeger明天你想换成Tempo或者自建一套ClickHouse存储OpenTelemetry的架构都可以灵活适配它不会把你锁死在某个厂商或者某个特定平台上。这一点对中大型团队来说非常关键因为可观测性数据的存储和分析一定会经历一个逐步演进的过程你需要的是插拔能力而不是一锤子买卖。除了技术层面的考虑OpenTelemetry的社区活跃度也是我们非常看重的。它背后有Google、Microsoft、AWS等一大批头部厂商在贡献代码目前已经是云原生可观测性的主流选择。这意味着什么意味着你招聘到的工程师大概率都接触过、了解过它意味着遇到问题你更容易在社区找到答案也意味着未来出现的新框架、新中间件第一优先适配的协议肯定就是OpenTelemetry的协议。当时我们内部做过一个简单的对比表格我放在下面供你参考对比项OpenTelemetryZipkin SleuthSkyWalking生态标准CNCF标准厂商中立Spring生态绑定较深独立APM体系数据采集方式API Agent SDK多种形式API埋点为主Java Agent为主后端存储灵活性高支持Jaeger/Tempo/自建需要配合Zipkin Server自带存储和分析平台社区活跃度非常高Sleuth已进入维护模式较高社区以国内为主学习曲线中等低中等长期演进能力强弱中等当然强调一下这并不意味着SkyWalking不好。如果你的团队需要开箱即用、不想自己折腾存储方案、想要一个比较完整的可视化大屏直接上SkyWalking也是一个非常务实的选择。我们选择OpenTelemetry更看重的是它在复杂架构下的灵活性和标准化程度。3. 核心概念与实践Trace、Span和上下文传播在正式动手接入之前有几个核心概念是必须要清楚的否则你在配置和使用的时候会很困惑。OpenTelemetry里最基础的概念就是Trace和Span。Span跨度是链路追踪的最小工作单元它代表了一次具体的操作比如一次HTTP调用、一次数据库查询、一次方法执行。每个Span都记录了操作名称、开始时间和结束时间、状态成功或失败、以及一组可附加的属性和事件。属性是键值对形式的数据比如订单号、用户ID、HTTP状态码事件则是带时间戳的日志点用于记录某个时间点上发生的具体事情。Trace链路是由多个Span组成的有向无环图它代表了一个请求从进入系统到最终返回的全部过程。你可以把Trace理解成一次完整的旅行而Span就是旅行中的每一个站点每个站点之间有明确的父子关系。比如一个请求先经过网关网关作为父Span然后调用订单服务生成一个子Span订单服务再去查询数据库生成一个孙子Span这样一层一层嵌套最终串成一条完整的链路。在实际传输过程中Trace和Span的传递靠的是上下文传播。每个Span和父Span之间通过一个traceparent协议头来关联这个协议头里包含了当前Trace的全局唯一ID、当前Span的唯一ID以及采样标记。当一个服务接收到了带有traceparent头的请求它会解析出这个Trace ID和父Span ID然后创建新的子Span时带上这些信息这样整个链路就能跨服务地关联起来。我举一个具体的例子。假设用户在前端页面点击了提交订单这个请求发到了网关服务此时网关会生成一个Span A并分配一个Trace ID比如4bf92f3577b34da6a3ce929d0e0e4736。网关调用订单服务时会在HTTP请求头中带上traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01这里的第一个部分4bf92f3577b34da6a3ce929d0e0e4736就是Trace ID第二个部分00f067aa0ba902b7是当前Span也就是Span A的ID。订单服务收到请求后解析出这些信息创建一个新的Span BB的父Span就是A同时B也复用同一个Trace ID。这样从网关到订单一路传下去整条链路就打通了。理解了这个模型你就明白了为什么链路追踪能跨服务串起调用核心就在于HTTP头、MQ消息属性这类载体上的上下文注入和提取。4. 从零开始集成Spring Boot项目接入OpenTelemetry理论说完了下面进入实操环节。我以最常见的Java后端技术栈——Spring Boot项目为例演示如何一步步接入OpenTelemetry。4.1 添加依赖我们使用OpenTelemetry的Java SDK同时结合Micrometer Tracing来对接Spring Boot 3.x的健康检查和指标体系。在pom.xml中添加以下核心依赖dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-bom/artifactId version1.36.0/version typepom/type scopeimport/scope /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-api/artifactId /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-sdk/artifactId /dependency dependency groupIdio.opentelemetry/groupId artifactIdopentelemetry-exporter-otlp/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-tracing-bridge-otel/artifactId /dependency这里使用BOMBill of Materials来统一管理版本避免各个OpenTelemetry组件版本不一致引发的兼容性问题。opentelemetry-exporter-otlp负责把链路数据通过OTLP协议导出到后端的Collector或Jaeger等存储系统。4.2 环境变量与配置在application.yml中做基础配置management: tracing: sampling: probability: 1.0 otlp: tracing: endpoint: http://127.0.0.1:4317sampling.probability控制采样率。生产环境一般不建议配置成1.0因为数据量可能会很大具体采样策略我在后面的避坑环节详细说。endpoint是你的OTLP Collector或Jaeger的接收地址默认使用gRPC协议端口是4317如果你的服务端只开了HTTP端口也可以用HTTP协议地址改成http://127.0.0.1:4318。4.3 自定义Span和埋点依赖配置好之后Spring Boot的自动配置会帮我们处理大量的埋点工作。比如通过RestTemplate、WebClient发起HTTP调用通过JdbcTemplate或MyBatis执行SQL这些操作都会自动生成对应的Span。但有一些业务方法是无法自动感知的需要手动埋点。手动埋点的方式很简单在需要跟踪的方法上加上WithSpan注解Service public class OrderService { private static final Tracer tracer GlobalOpenTelemetry.getTracer(order-service); WithSpan(create-order) public Order createOrder(OrderRequest request) { // 手动为Span添加自定义属性 Span span Span.current(); span.setAttribute(order.id, request.getOrderId()); span.setAttribute(user.id, request.getUserId()); // 业务逻辑 return doCreateOrder(request); } }需要注意的是使用WithSpan注解需要单独引入opentelemetry-instrumentation-annotations依赖dependency groupIdio.opentelemetry.instrumentation/groupId artifactIdopentelemetry-instrumentation-annotations/artifactId version1.33.0/version /dependency这样写的好处是代码侵入性很小你只需要把注解加到关键的业务方法上观察链路上就能看到这些方法执行的耗时情况非常方便。4.4 部署一个数据接收端链路数据产生之后必须有一个服务端去接收和可视化否则数据只是在本地打转看不到任何效果。最简单的方案是直接部署Jaeger。用Docker可以快速起一个docker run -d --name jaeger \ -e COLLECTOR_OTLP_ENABLEDtrue \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ jaegertracing/all-in-one:latest启动完成后浏览器访问http://localhost:16686就能打开Jaeger的查询界面。之后启动你的Spring Boot项目在界面的Service下拉框里选择你的服务名点Find Traces就能看到实时的链路数据。4.5 自定义服务名默认情况下Spring Boot会使用spring.application.name来作为服务名称这个名称会显示在链路查询界面上。如果你的服务名没有配置建议补上否则链路上会显示一堆unknown_service排查问题的时候非常痛苦spring: application: name: order-service从这一步开始你的项目已经具备了基本的全链路监控能力。接下来我讲讲集成之后怎么用这些数据来解决实际的问题以及我踩过的那些坑。5. 从数据到价值用链路追踪实际解决两个常见问题链路追踪的核心价值在接入之后才开始释放。我拿两个高频场景来说明一下。5.1 定位下游慢服务假设业务方反馈创建订单这个接口越来越慢了以前的做法是我们挨个去查订单服务、库存服务、支付服务的日志看哪个环节出现了慢查询或者长时间等待。现在做的事情很简单到Jaeger界面选择create-order的接口名直接查调用这个Trace的所有Span按耗时排序一眼就能看到哪个Span耗时最长。印象很深的一次我们发现用户点击下单到返回结果总耗时2.8秒链路图上显示网关只花了20毫秒订单服务花了300毫秒而支付服务一个叫checkPaymentStatus的数据库查询Span居然占了1.9秒。拿着这个信息去看代码发现问题很简单这个接口里做了一次数据库全表扫描加了索引之后耗时立刻降到了80毫秒。没有链路数据之前这个定位过程至少要加上两三天的沟通和排查成本。5.2 关联日志与错误定位OpenTelemetry的Trace可以做到让链路上下文贯穿日志框架。简单说就是每一条日志都会自动带上当前Trace ID和Span ID。你把日志和链路数据关联起来之后在系统报错的时候日志系统里搜Trace ID就能拿到整条调用链路的上下文不需要再手动把时间戳对来对去了。具体实现方式是在日志的pattern中添加traceId和spanIdpattern%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n/pattern这里利用的是MDC机制OpenTelemetry的桥接器会自动把Trace ID和Span ID写入到这个上下文中。这样日志文件里的每一行都能看到链路信息配合ELK或者Loki这类日志系统搜索和定位效率会高非常多。6. 进阶玩法与避坑指南这部分内容是实操中趟出来的经验也是我认为这篇文章最值得看的部分。6.1 采样策略不能一刀切很多朋友刚集成OpenTelemetry的第一反应是既然是全链路监控那我对所有请求都埋点采样。理论上没错但实际应用时会发现只要流量稍微上来一点数据量会立刻爆炸。假设你的系统QPS是1000每个请求平均产生30个Span每个Span大概200字节那么一天的链路数据量就是1000乘以30乘以200字节乘以86400秒算下来一天要产生大概518GB的数据。这个存储成本对绝大多数团队来说都是无法接受的。合理的做法是分场景设计采样策略。比如核心交易链路可以用高采样率或者直接全量采样非核心的查询接口可以用10%甚至1%的采样率。OpenTelemetry支持的尾采样Tail Sampling可以根据最终策略来决定链路是否上报比如只采样包含错误的链路或者只采样耗时超过阈值的慢链路。这个机制特别适合对准确性和成本都有要求的场景。6.2 异步和消息队列场景要手动处理上下文如果你在业务中大量使用线程池或者MQ消息上下文传播是一个容易出问题的小坑。OpenTelemetry对常见的多线程场景有自动的包装器但你自己创建的线程或者自定义线程池很可能丢失上下文导致本来是一条完整链路的数据被切成了两截。假如你在一个方法里向线程池提交了一个任务这个任务里发起了外部HTTP调用那么这次HTTP调用生成的Span它的Trace ID很可能不是原来那条链路的因为它并没有自动继承线程外部的Trace上下文。解决办法是在提交任务前手动把当前上下文写入到任务线程中用一个ContextAwareRunnable包装一下public class ContextAwareRunnable implements Runnable { private final Runnable task; private final Context context; public ContextAwareRunnable(Runnable task) { this.task task; this.context Context.current(); } Override public void run() { try (Scope scope context.makeCurrent()) { task.run(); } } }MQ场景也是相似的道理在发送消息时把当前上下文注入到消息头消费时再提取出来。好消息是对于Kafka、RocketMQ这些主流MQOpenTelemetry提供了自动的埋点扩展引入依赖之后大多数情况会自动处理不需要自己写代码。但如果你的MQ中间件比较小众或者做了比较特殊的封装那就要留意这一步。6.3 数据存储的选型思路链路数据有了之后存储选型也是一个值得提前规划的问题。Jaeger默认使用Elasticsearch作为存储数据量大了之后对ES集群的压力不小。我们现在用的是将OTLP数据统一打到自行部署的Collector上然后从Collector同时导出到Prometheus用于指标监控导出到Loki用于日志关联链路数据则导出到Tempo或者ClickHouse这类偏分析型的存储里。Tempo是Grafana开源的分布式追踪后端它的一个很大优势是对象存储即可支撑底层存储成本相对可控查询体验也和Grafana生态无缝集成如果你们公司已经在用Grafana作为统一监控大屏强烈推荐调研一下Tempo。6.4 别忽略指标数据的价值聊链路追踪的时候很多人的关注点都在Trace上但OpenTelemetry其实是一个统一的可观测性框架链路、指标、日志三件事它都在做。实际接入的时候建议顺手把指标也一起采了。你通过OpenTelemetry SDK暴露出来的HTTP服务QPS、P99延迟、JVM内存等信息配合Prometheus和Grafana可以搭建一个比较完整的监控体系。当一个接口变慢了你先通过指标看到P99明显上升再通过链路数据定位到具体是哪个下游调用慢了最后通过日志系统把那个环节的详细堆栈拉出来整个排查路径就非常顺畅。6.5 冗余属性别乱加OpenTelemetry允许你为每个Span附加任意属性很多人容易犯的错误是觉得属性加得越多越好结果每条链路都塞了几十个Key。这么做会带来两个问题一是存储成本大幅上升二是查询界面会非常杂乱反而看不清核心信息。我的经验是每个Span的属性只保留三类就够了第一类是用于定位问题的标识信息比如订单ID、用户ID第二类是影响性能判断的数据比如是否命中缓存、数据量大小第三类是错误信息如异常类型和错误消息。其他的一律不往Span里塞如果确实需要详细数据把对应的日志系统关联起来不放在链路里。7. 关于落地的一点个人体会最后聊点实际的。链路追踪这套东西推行的难点不在于技术本身而在于团队习惯的转变。你辛辛苦苦把OpenTelemetry接入了把Jaeger部署好了如果大家遇到问题还是习惯性地去翻日志人工对时间戳那这套基础设施就只是个摆设白白占用机器资源。所以接入的同时要想办法推动团队所有人真正用起来比如在故障复盘会上强制要求通过链路数据来分析根因甚至可以在上线检查清单里加一项新接口必须有链路埋点慢慢把意识建立起来。另外任何一个可观测性系统的建设都不是一蹴而就的。如果你现在的系统还非常简单单体应用流量也不大那确实没必要搞链路追踪直接用好日志可能就够用了。但只要你动了微服务拆分的念头或者说已经有了多个服务在相互调用、中间还有MQ解耦那么链路追踪就不要再等到事故发生了才去建设。你不需要一开始就追求覆盖所有服务、所有场景先从一个核心链路串起来跑通整个采集、存储、展示、分析闭环再逐步扩展边界这是最务实的做法。作为一个经历过深夜三小时手工排查事故的老后端我对这套方案的评价就是八个字前期投入长期复利。接入那两天花的时间会在之后每一次排查线上问题的时候加倍还给你。
返回列表