做Java后端的时间长了,线上排查这事儿真的能把人磨到没脾气。CPU飙到100%找不到是谁干的,接口偶发变慢不知道卡在哪个环节,JVM频繁Full GC却只能靠猜,日志、指标、链路各看各的,翻半天才能对上号。OpenTelemetry这套东西,就是奔着把这堆烂账一次性捋清楚去的。它把链路追踪(Traces)、指标(Metrics)、日志(Logs)三大信号统一成一套采集与上报体系,配合Java Agent甚至可以做到代码零侵入启动,直接把监控能力挂到应用上。这篇帖子我结合自己实际接入和排坑的过程,把选型思路、Agent接入、Collector配置、指标与链路关联、常见问题这些事一次说透,适合微服务后端开发、SRE以及刚接触可观测性的Java程序员直接当手册用。
1. 为什么我最终选了OpenTelemetry而不是继续堆组件
1.1 传统监控方案之痛:三套系统三种玩法
早几年做Java应用监控,基本是“三座大山”各管各的。指标用Spring Boot Actuator暴露端点,再让Prometheus去抓;链路追踪单独部署一套SkyWalking或者Zipkin,服务里再塞Agent;日志就更麻烦了,Logback输出文件,Filebeat采集,再进Elasticsearch。单看每一套都挺完善,但放一起就乱了:同一个请求,在Metrics里是一条QPS数据,在Trace里是一个Trace ID,在日志里又是另一套时间戳和上下文,三个系统之间没有任何关联字段,出了事只能人工拿着时间点去猜。
这种方案的维护成本也不低。每接一个新服务,要确认Actuator配置、Prometheus抓取路径、SkyWalking Agent版本、日志采集规则,四套配置各来一遍;每接触一个新技术栈(比如加了个Redis或者MQ),又要去了解它在各个监控体系里该怎么暴露数据。团队小还好,团队一大人一多,光协调这些监控配置就能消耗不少迭代时间。
1.2 OTel用一套SDK统一三大信号
OpenTelemetry(简称OTel)是OpenTracing和OpenCensus两个项目合并后发展出来的,现在的定位就是可观测性领域的事实标准。它最核心的贡献是把Traces、Metrics、Logs三类数据抽象成一套统一的API和SDK,再用统一的数据模型和导出协议(OTLP)把数据送出去。也就是说,你在代码里或者Agent层面只需要面对OTel这一套东西,后面接Jaeger、Zipkin、Prometheus、Grafana还是自家平台,都是换个导出器的事。
打个比方,以前家里装了摄像头、烟感器、门锁报警器,每个都配一个独立App,出了事轮流打开看。OTel相当于把所有设备统一接进一个中控屏,底层品牌换不换,对使用方无感知。对于Java应用来说,这个好处尤其明显,因为Java生态的组件实在太多,如果每个中间件都要单独适配监控SDK,工作量是灾难级的,而Agent自动埋点机制可以一次性覆盖绝大多数主流库。
1.3 链路、指标、日志关联起来才真正值钱
很多团队其实已经上了Prometheus或者SkyWalking,但用了很久还停留在“看个大概”的程度,核心原因就是数据之间没有关联。OTel在数据模型里规范了Trace ID、Span ID、Service Name这些字段,并且通过语义约定(Semantic Conventions)把HTTP请求、数据库操作、消息队列消费等行为都定义了统一的属性名,这样从一条报错日志能直接跳到对应的Trace,从一条Trace又能看到这一次请求的数据库消耗和外部调用时间。
举个例子:线上某个订单接口超时了。以前你得先看日志、再看监控、再猜调用链,现在日志里带着trace_id,拷贝到链路系统里搜一下,几毫秒就能看到这是数据库慢查询还是下游服务拖慢的,同时再看一下这个接口的延迟直方图,确认是偶发还是持续劣化。这个体验一旦用上,基本回不去老方案了。
2. Java接入方式选型:Java Agent还是SDK手动埋点
2.1 Java Agent:零代码侵入的快速方案
先聊最省事的接入方式。OTel官方提供的opentelemetry-javaagent.jar,本质上是一个基于字节码增强技术的Agent,JVM在加载应用类的时候会动态改写字节码,把埋点代码自动织入到HTTP框架、数据库客户端、消息队列客户端等主流组件中。整个过程不需要改业务代码,不需要重新编译,只需要在启动命令里加上-javaagent参数即可。
Java Agent自动支持的组件相当广,基本覆盖了Java后端日常能遇到的场景。我在实际项目里用到过的几类列一下:
| 类别 | 自动埋点覆盖组件 |
|---|---|
| Web框架 | Spring Boot/Spring MVC、Spring WebFlux、JAX-RS、Servlet |
| HTTP客户端 | JDK HttpClient、Apache HttpClient、OkHttp、Spring RestTemplate/WebClient |
| 数据库 | JDBC全系(MySQL、PostgreSQL、Oracle)、HikariCP、MyBatis |
| NoSQL | Redis客户端Jedis/Lettuce、MongoDB驱动、Elasticsearch REST客户端 |
| 消息队列 | Kafka、RabbitMQ、JMS |
| 定时任务 | Quartz、Spring Scheduling |
这意味着大多数常规Java服务,挂上Agent基本就能把链路数据采集起来,不需要任何开发投入。对于历史项目来说,这是成本最低的切入点,也很适合先在某个非核心服务上试点,验证通了再推广。
2.2 SDK手动埋点:业务指标和定制化场景的补充
Agent确实方便,但它有一个弱点:只能采集框架层面的通用数据,业务层面的事情它不知道。比如“用户下单成功率”“购物车结算耗时”这类和业务强相关的指标,Agent不会顺手帮你埋好,这时候就得靠SDK手动埋点。
OTel Java SDK的使用方式也不复杂。先在pom.xml里引入api依赖,然后在代码里拿Meter和Tracer去做自定义指标与链路。一个典型的业务计数器代码大致长这样:
// 引入依赖:io.opentelemetry:opentelemetry-api OpenTelemetry otel = GlobalOpenTelemetry.get(); Meter meter = otel.meterBuilder("order-service") .setInstrumentationVersion("1.0.0") .build(); LongCounter orderCounter = meter .counterBuilder("order.create.total") .setDescription("创建订单总数") .setUnit("1") .build(); // 业务代码里 orderCounter.add(1, Attributes.builder() .put("channel", "app") .put("region", "cn-east") .build());链路这边的自定义埋点则是拿到Tracer创建Span,可以给某个方法单独加一层追踪信息:
Tracer tracer = otel.getTracer("order-service"); Span span = tracer.spanBuilder("processPayment") .setAttribute("order.id", orderId) .startSpan(); try (Scope scope = span.makeCurrent()) { // 业务逻辑 } finally { span.end(); }对于有核心链路监控需求、又想保留业务上下文的项目,SDK埋点是标配选择。
2.3 真实项目里该怎么选
有人会纠结到底用Agent还是SDK,实际经验告诉我,这不是二选一的问题,而是配合使用。我的建议是:默认以Java Agent为底座,先把基础的全链路数据采集起来;如果某些核心链路需要更细的业务语义,再用SDK做定向埋点补充。两者的数据在OTel机制下是天然打通的,不存在Agent和SDK两套数据互斥的问题。
| 对比维度 | Java Agent | SDK手动埋点 |
|---|---|---|
| 接入成本 | 低,加参数即可 | 中,需要引入依赖并编写代码 |
| 代码侵入性 | 零侵入 | 有侵入,但更灵活 |
| 覆盖范围 | 主流框架和中间件 | 完全由自己控制 |
| 业务指标埋点 | 不支持 | 支持 |
| 维护成本 | Agent升级即可 | 代码随版本维护 |
| 适合场景 | 快速全量铺开 | 核心链路精细监控 |
从投入产出比来看,小型项目或者排查类场景用Agent就够了;中大型项目或者公司有统一可观测性平台建设需求时,建议Agent加SDK混合使用。
3. 实操:把Java应用接入OpenTelemetry全流程
3.1 准备阶段:版本和下载
动手之前先把版本理清楚。OTel Java Agent目前的稳定版本已经到2.x,建议直接去GitHub的opentelemetry-java-instrumentation仓库Releases页面拿最新的稳定版,不要用老旧的1.x版本,因为新版本修复了一堆兼容性问题,尤其是对Spring Boot 3.x和JDK 17/21的支持,老Agent版本很容易踩坑。
环境方面,确认JDK版本满足要求即可。OTel Java Agent官方支持JDK 8及以上,但如果你用的是JDK 21这种较新版本,建议用最新Agent,否则可能遇到字节码增强不兼容的问题。下载好之后,把jar包放到一个固定的目录,比如/opt/otel/opentelemetry-javaagent.jar,后续启动命令直接引用。
3.2 最小化启动配置:先让链路跑起来
最基础的接入方式就是修改启动命令。假设你有一个标准的Spring Boot应用,原本启动命令是:
java -jar order-service.jar接入OTel之后改成:
java -javaagent:/opt/otel/opentelemetry-javaagent.jar \ -Dotel.service.name=order-service \ -Dotel.exporter.otlp.endpoint=http://localhost:4317 \ -jar order-service.jar两个参数的作用要搞清楚。-Dotel.service.name用来标识服务名,这个会在后端所有看板里作为第一维度的筛选条件,起名规范一点,比如用“业务域-服务名”格式;-Dotel.exporter.otlp.endpoint是OTel Collector的地址,这里指向localhost:4317,也就是说本地起了Collector在接收数据。
如果现在后端还没准备好,可以先加一个启动参数把数据打到控制台看效果:
-Dotel.traces.exporter=logging -Dotel.metrics.exporter=none -Dotel.logs.exporter=none这样启动后,每接一个请求,控制台就会直接打印生成的Span信息,方便你确认Agent确实生效了。我实际测试时习惯先用logging模式验证,确认链路通了再接正式后端,这个习惯帮我排掉了不少“以为是Agent没生效,其实是后端没起来”的误会。
启动成功后日志里会看到一行类似"OpenTelemetry Javaagent 2.x.x started"的信息,看到这一行就说明Agent加载成功了。
3.3 常用的Agent配置项整理
除了上面的最小配置,实际生产里还会用到不少参数,这里把常用的整理成一个表:
| 配置项 | 作用 | 默认值 |
|---|---|---|
| otel.service.name | 设置服务名 | 未知服务名 |
| otel.exporter.otlp.endpoint | OTLP导出地址 | http://localhost:4317 |
| otel.traces.sampler | 链路采样策略 | parentbased_always_on |
| otel.traces.sampler.arg | 采样率参数 | 无 |
| otel.metrics.exporter | 指标导出器 | otlp |
| otel.logs.exporter | 日志导出器 | otlp |
| otel.javaagent.debug | 是否打印Agent调试日志 | false |
| otel.instrumentation.exclude.classes | 排除特定类不埋点 | 无 |
需要特别提醒采样率这个参数。默认的parentbased_always_on表示每条链路都采集,在高并发场景下全量采集会产生大量数据,后续存储和查询压力都不小。常规做法是设置采样率,比如采用尾部采样或头部采样,按比例采集。设置方式如下:
-Dotel.traces.sampler=parentbased_traceidratio -Dotel.traces.sampler.arg=0.1这个配置表示以10%的比例采样,并且child span会跟随父span的采样决策,避免出现一条链路一半有数据一半没数据的尴尬情况。采样率具体设多少得结合业务量评估,一般小流量服务可以设1,大流量服务建议0.1甚至更低,核心链路可以用额外规则保证高采样。
3.4 用OTel Collector统一接收和转发
Agent把数据发出来之后,最好别直接对接最终存储系统,中间加一层OpenTelemetry Collector更控得住局面。Collector是官方提供的数据接收、处理和转发组件,可以理解为可观测性数据的“消息中间件”,帮你在数据进入存储之前做批量发送、数据过滤、额度控制、多后端分发这些事。
一个最简的Collector配置文件长这样:
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: memory_limiter: check_interval: 1s limit_percentage: 80 spike_limit_percentage: 20 batch: timeout: 5s send_batch_size: 1024 exporters: debug: verbosity: detailed otlp: endpoint: jaeger:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, batch] exporters: [debug, otlp] metrics: receivers: [otlp] processors: [memory_limiter, batch] exporters: [debug, otlp]配置文档里几个组件的作用说明一下。receivers是数据入口,这里开启了4317端口的gRPC和4318端口的HTTP两种协议,对应Agent默认的OTLP导出方式;processors里的memory_limiter控制内存占用,batch负责攒一批数据再发送,减少网络请求次数;exporters是数据出口,debug出口会把数据打到日志里,otlp出口把数据转发给Jaeger或其他后端。
Collector的启动方式很灵活。本地环境可以直接用二进制启动,容器化场景用Docker挂载配置启动更省事:
docker run -d \ -p 4317:4317 -p 4318:4318 \ -v $(pwd)/otel-collector-config.yaml:/etc/otelcol/config.yaml \ otel/opentelemetry-collector-contrib:latest加Collector这一层和Agent直连后端最大的区别在于:连接管理更可控、出现突发流量时有批量和缓冲机制、多个环境的数据可以统一出口。实际接入时我建议不管规模大小,都先把Collector架起来,后面加存储后端只是改配置的事,不用动应用侧。
3.5 数据链路验证:能不能查到一条完整Trace
配置完之后需要验证一下数据链路是否真的通了。我的验证流程一般是三步走。
第一步,确认Agent启动时没有报错。启动日志里出现了"OpenTelemetry Javaagent ... started"基本就能确定Agent层面没问题。
第二步,确认Collector收到了数据。在Collector启动的终端窗口里,如果配置了debug exporter,调用一次业务接口就能看到类似这样的日志输出:
Span #0 Trace ID: 4f3e2a1b8c9d0e1f2a3b4c5d6e7f8a9b Span ID: 1a2b3c4d5e6f7a8b Name: GET /api/order/{id} Attributes: http.route -> /api/order/{id} http.request.method -> GET http.response.status_code -> 200看到这个基本可以断定从Agent到Collector这段是通的。
第三步,在最终观测后端(比如Jaeger或者Grafana Tempo)里搜索刚刚调用的接口名,如果能查到Trace详情,整个接入就大功告成了。我常用的验证组合是“Agent + Collector + Jaeger”,Jaeger不需要太多配置,本地起一个容器就能获得完整的链路查询体验。
4. 监控数据抓什么才有用:指标和链路的落地点
4.1 JVM核心指标:这几个必须盯
JVM指标是可观测性建设的基础。OTel的Java Agent会自动上报一系列JVM运行指标,你不需要额外引入JMX Exporter之类的组件,挂在看板上重点关注以下几项就够日常排查用了:
| 指标名 | 含义 | 常见问题信号 |
|---|---|---|
| jvm.memory.used | JVM已用内存 | 持续居高不下可能内存泄漏 |
| jvm.memory.limit | JVM最大内存 | 和used对比看余量 |
| jvm.gc.duration | GC耗时 | 突刺说明GC频繁或Full GC |
| jvm.gc.count | GC次数 | 次数异常增加说明对象分配压力大 |
| jvm.threads.count | 线程数 | 持续增长可能是线程泄漏 |
| jvm.classes.loaded | 已加载类数 | 异常增长检查动态类生成 |
GC相关的指标尤其值得优先关注。线上出现接口卡顿但CPU并不高时,优先看jvm.gc.duration和jvm.gc.count,如果看到Full GC耗时突刺,基本就能确定问题方向,再配合堆转储去定位对象引用链。
4.2 HTTP调用和数据库链路:瓶颈一眼定位
Agent接入后,每个HTTP请求会自动生成一条Trace,里面会带上http.request.method、http.route、http.response.status_code这些语义属性,方便按接口维度聚合延迟。数据库访问则对应生成db.system、db.statement之类的属性,可以直接看到某条SQL在整条链路里耗时占比。
有次排查一个订单查询接口变慢的问题,从Trace里看到数据库调用占了总耗时的85%,再看db.statement发现是一条带全表扫描的查询,直接在库上优化了索引,接口延迟从800ms降到了50ms。没有链路数据的时候,这种排查至少得靠人肉翻日志加脑补才能定位。
4.3 自定义业务指标:把业务状态纳入监控
业务指标的价值在于,它能把你不能被埋点系统覆盖的业务逻辑变化实时反映出来。比如库存服务扣减失败次数、支付网关超时回调数、下单到支付完成的时长分布,这些指标跟你的业务直接挂钩,Agent不会自动给出,只能手动埋。
SDK方式埋点计数器或者直方图的做法,在2.2节已经给了代码示例。这里补充一个直方图的例子,用于记录业务耗时分布:
DoubleHistogram requestDuration = meter .histogramBuilder("order.checkout.duration") .setDescription("结算耗时分布") .setUnit("ms") .build(); long start = System.currentTimeMillis(); // 业务逻辑 requestDuration.record(System.currentTimeMillis() - start, Attributes.builder().put("channel", "app").build());直方图最终在Prometheus里可以配合histogram_quantile函数计算出P95、P99耗时。有了这些数据,业务侧提“最近支付变慢了”这种模糊问题的时候,你直接甩一张P99趋势图过去,沟通效率完全不一样。
5. 常见问题与排查技巧实录
5.1 NoSuchMethodError和类加载冲突
使用Java Agent时最典型的坑是类加载冲突。Agent做字节码增强时会引入一些自身依赖,如果应用里也加载了相同类库,就可能出现NoSuchMethodError或者ClassCastException。比如一些老项目自己打包了旧版本的guava,和Agent需要的版本不一致,启动时就容易炸。
遇到这种情况,先别慌着排查业务代码,按照这个顺序来:
- 看Agent启动日志里是否有exclude相关的告警提示;
- 如果确认是某个类冲突,通过-Dotel.instrumentation.exclude.classes=com.example.conflict.Class排除掉该类,让Agent不埋这个点;
- 检查应用里是否硬编码了和OTel冲突的依赖版本,比如老旧版本的grpc-netty,尽量升级到和Agent匹配的版本。
如果是Agent的某个自动埋点在特定库版本上不兼容,还可以用-Dotel.instrumentation.exclude-plugins按插件维度关闭,比如关掉对某类中间件的自动埋点,牺牲单个组件换取整体稳定性是值得的。
5.2 链路Span不上报的排查思路
Agent起来了、接口也调用了,但后端就是看不到数据。这类问题我遇到太多次了,排查思路其实很固定。
先确认启动参数里OTEL_EXPORTER_OTLP_ENDPOINT地址对不对。注意Agent默认走的是OTLP gRPC协议,默认端口是4317,如果你写成了http://localhost:4318那是HTTP协议端口,需要额外配置协议参数。最简单的做法是把端点地址直接指向Collector的4317端口。其次看Collector日志有没有收到请求,没有收到就从网络连通性查起,telnet一下端口通不通,别小看防火墙规则,容器环境下端口没映射出来是高频问题。
如果Collector日志显示收到了数据但后端查不到,那问题出在Collector的exporters配置上。把debug exporter打开,如果debug有数据、正式exporter没有,重点检查后端地址和证书配置,尤其是按了TLS时候容易在证书上栽跟头。
5.3 日志里没有trace_id:MDC关联配置没到位
链路数据和日志的关联是很多人最后一步才想起来做的。你希望日志里能带上trace_id和span_id,方便从日志查询直接跳转链路,这需要应用日志框架配合输出MDC字段。
以Logback为例,先把日志pattern改成这样:
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{trace_id} %X{span_id}] - %msg%n</pattern>正常情况下,OTel Agent会自动把trace_id和span_id写入SLF4J的MDC,也就是你不需要写任何业务代码,只要日志pattern里加这两个占位符就能输出关联ID。如果你的Agent版本较老或者应用自己屏蔽了相关instrumentation,可以显式设置-Dotel.instrumentation.logback-mdc.enabled=true试试。
这里有一个我自己踩过的细节:log4j2的使用者要小心,部分版本对MDC自动注入的支持不如Logback稳定,建议在接入初期先确认日志框架版本,或者直接把应用日志框架统一到Logback,省得两套配置来回折腾。
5.4 指标数据量和成本失控怎么办
最后聊一个偏治理的话题。全量采集会带来数据量爆炸的问题,尤其是大促或者高流量时段,Span总量可能是平时的几十倍。合理的做法是分环境配置不同的采样策略:测试环境全量采集,生产环境按比例采样,关键核心交易链路单独设置规则保证全采。
OTel本身支持尾部采样处理器,可以在Collector里按规则决定哪些链路保留。日常用头部采样加固定比例已经能解决大部分问题,如果业务对链路完整性要求极高,再考虑加一套基于规则的过滤,避免一上来就上复杂方案,反而不好维护。
写在最后的一点建议
接入OpenTelemetry这件事,实际动手之后会发现并没有想象中那么复杂,真正需要花时间的其实是把数据规范定好、把观测口径统一。我个人的建议是,新项目直接在起步阶段就接入Agent,别拖到上线后再补。老项目如果暂时没精力全量改造,选一个流量最小的服务先跑通全链路,把日志关联、指标看板、链路查询这些体验完整走一遍,再横向推广。再分享一个小技巧:Agent接入初期一定要先开debug exporter确认数据能出,再接正式Collector和后端,这两步分开验证能替你省掉非常多定位问题的时间。