ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SkyWalking 7.0 + SpringBoot 自动埋点深度调优与故障排查指南

SkyWalking 7.0 + SpringBoot 自动埋点深度调优与故障排查指南

1. 为什么自动埋点不是“开箱即用”,而是需要亲手调教的精密仪器

“SkyWalking 7.0 + SpringBoot 自动埋点”这个组合,听起来像是一键部署的魔法——加个依赖、配个参数、启动服务,链路图就自动在UI里铺开。我第一次在某电商后台项目里试的时候,也是这么想的。结果服务跑起来了,SkyWalking UI里却只有一片寂静:没有Trace,没有Span,连最基础的HTTP入口都看不到。不是报错,不是崩溃,是彻底的“静默失效”。

这恰恰暴露了自动埋点最核心的误解:它不是“全自动”,而是“免代码侵入式自动”。关键词是“免代码侵入”,不是“免配置、免理解、免调试”。SkyWalking 的 Java Agent 机制,本质是通过字节码增强(Bytecode Instrumentation)在 JVM 启动时,动态织入监控逻辑。它不修改你的源码,但必须精准识别你的类、方法、框架版本,并在正确的生命周期节点插入探针。而 SpringBoot 的生态太庞大了,一个 starter 依赖背后可能藏着十几个间接依赖,版本稍有错位,Agent 就会“视而不见”。

比如,你用的是 SpringBoot 2.7.x,它默认内嵌 Tomcat 9.0.x;但 SkyWalking 7.0 官方文档明确标注,对 Tomcat 9.0.65+ 的支持存在已知兼容性问题。如果你恰好升级了 Tomcat 补丁包,Agent 就会跳过 HTTP Servlet Filter 的增强点,导致所有 Web 请求链路直接丢失。这不是 Bug,是字节码匹配规则的严格性决定的——它宁可不埋点,也不埋错点。

再比如,SpringBoot 的 Actuator 端点(/actuator/health, /actuator/metrics)默认是被 SkyWalking 排除在追踪范围之外的。这是设计使然:健康检查高频、无业务语义,埋点反而污染数据。但如果你没意识到这点,看到这些端点没链路,就会误判为“埋点失败”。

所以,“玩转”的第一个门槛,从来不是技术多难,而是心态切换:从“期待黑盒生效”转向“主动理解探针工作边界”。这决定了你后续是花两小时定位一个版本冲突,还是花两天在日志里大海捞针。我后来把 SkyWalking Agent 比作一个极其严谨的审计员——它不进你的办公室(不改代码),但它要求你把所有门牌号(类名)、开门时间(方法签名)、甚至门锁型号(字节码版本)都提前报备清楚,否则它连楼都不上。

提示:SkyWalking 7.0 的探针日志级别默认是 INFO,大量关键诊断信息(如“Skip class xxx: not matched”、“Enhance class xxx success”)被淹没在常规日志里。不把 agent.log 的日志级别调到 DEBUG,等于蒙着眼睛调试。

2. 埋点生效的三重校验:从 JVM 参数到 Span 上报的完整链路

自动埋点不是“配完就完”,而是一个需要逐层验证的流水线。任何一环断裂,都会导致最终 UI 里空空如也。我把这个过程拆解为三个必须亲自确认的校验层,每层都有其不可替代的验证价值。

2.1 第一层校验:JVM 启动参数是否真正加载了 Agent

这是最基础、也最容易被忽略的一环。很多人复制粘贴-javaagent:/path/to/skywalking-agent.jar,却没确认它是否真的被 JVM 解析并执行。

验证方法不是看应用是否启动成功,而是看 JVM 进程的启动参数本身。在 Linux 下,执行ps -ef | grep java,找到你的 SpringBoot 进程,仔细检查输出中是否完整包含了-javaagent:参数及其路径。常见陷阱包括:

  • 路径中包含空格或中文,未用引号包裹,导致 JVM 只读取了路径的一部分;
  • 使用了相对路径(如./skywalking-agent.jar),而启动脚本的工作目录(PWD)与预期不符;
  • 在 Docker 容器中,Agent jar 包未正确挂载到容器内路径,宿主机路径存在,容器内却是空的。

更可靠的验证方式,是在应用启动日志的最开头查找 SkyWalking Agent 的初始化日志。正常情况下,你会看到类似这样的几行:

[INFO] 2023-10-15 14:22:33:218 main SkyWalkingAgent - SkyWalking Agent v7.0.0 started successfully. [INFO] 2023-10-15 14:22:33:220 main SkyWalkingAgent - Installed plugin [spring-webmvc-5.x-plugin]. [INFO] 2023-10-15 14:22:33:221 main SkyWalkingAgent - Installed plugin [tomcat-7.x-plugin].

注意,这里的关键是Installed plugin。它表明 Agent 不仅加载了,还成功识别并激活了针对你所用框架的插件。如果只看到started successfully,但没有Installed plugin,说明 Agent 加载了,但插件扫描失败——大概率是你的 SpringBoot 版本与插件支持范围不匹配。

2.2 第二层校验:Agent 是否成功增强了目标类

这一层深入到了字节码层面。即使 Agent 加载了,它也必须找到你的 Controller 类、Service 方法,并在其中插入Tracer.createEntrySpan()和Tracer.captureExitSpan()这样的调用。这个过程叫“类增强(Class Enhancement)”。

最直接的验证方式,是查看agent/logs/agent.log文件。将config/agent.config中的agent.log.level=DEBUG,重启应用,然后搜索关键词Enhance class。你会看到大量类似这样的日志:

[DEBUG] 2023-10-15 14:23:05:112 SkyWalkingAgent-1-Thread-1 ClassEnhancePluginDefine - Enhance class org.springframework.web.servlet.DispatcherServlet by org.apache.skywalking.apm.plugin.spring.mvc.v5.define.DispatcherServletInstrumentation. [DEBUG] 2023-10-15 14:23:05:115 SkyWalkingAgent-1-Thread-1 ClassEnhancePluginDefine - Enhance class com.example.demo.controller.UserController by org.apache.skywalking.apm.plugin.spring.mvc.v5.define.ControllerInstrumentation.

这两条日志意味着:Agent 成功找到了 SpringMVC 的核心分发器DispatcherServlet,以及你自定义的UserController,并为其应用了对应的插件逻辑。如果UserController没有出现在日志里,那你的链路就注定断在第一跳。

为什么你的 Controller 会被跳过?最常见的原因是类加载器(ClassLoader)隔离。SpringBoot 的spring-boot-devtools会启用一个特殊的RestartClassLoader,而 SkyWalking Agent 默认只监控AppClassLoader。解决方案很简单:在agent/config/agent.config中添加一行:

plugin.spring.mvc.class_loader_injector = true

这个参数会强制 Agent 注入到所有类加载器中,确保 devtools 环境下也能正常增强。

2.3 第三层校验:Span 数据是否成功上报至 OAP 服务

前两层都 OK,不代表链路就一定能在 UI 里显示。数据必须从你的应用进程,通过 gRPC 或 HTTP 协议,完整、无损地送达后端的 OAP(Observability Analysis Platform)服务。

验证此环节,不能只看 UI,而要看应用端的上报日志和 OAP 端的接收日志。在应用端agent.log中,搜索reporter或send,你会看到类似:

[DEBUG] 2023-10-15 14:24:12:889 SkyWalkingAgent-1-Thread-3 GRPCReporter - Send 12 spans to collector. [INFO] 2023-10-15 14:24:12:891 SkyWalkingAgent-1-Thread-3 GRPCReporter - Report span success.

这表示数据已发出且收到 OAP 的 ACK 响应。但如果看到Report span failed,就要立刻检查网络连通性和 OAP 配置。

OAP 端的验证同样关键。查看 OAP 的logs/oap-server.log,搜索SegmentRecord或receive。一个健康的上报会显示:

[INFO] 2023-10-15 14:24:12:895 grpc-server-worker-1 SegmentRecord - Receive segment from service: user-service, instance: user-service@10.0.1.12.

如果 OAP 日志里完全没有这条记录,而应用端日志显示success,那问题几乎可以锁定在 OAP 的 gRPC 端口(默认 11800)被防火墙拦截,或者 OAP 的core.default配置中receiver-sharing-server的host地址写成了localhost,导致外部应用无法连接。

注意:SkyWalking 7.0 的 OAP 默认使用H2内存数据库做存储,仅用于演示。生产环境必须切换为 Elasticsearch 或 MySQL。如果忘记切换,OAP 启动时会报H2 is not supported for production,但进程不会退出,只是所有数据写入失败,导致 UI 一片空白。这个错误日志非常隐蔽,藏在oap-server.log的启动初期,很容易被忽略。

3. SpringBoot 特定场景下的埋点失效深度排查

SpringBoot 的“约定优于配置”哲学,在监控领域有时会变成“约定掩盖真相”。很多埋点失效,根源不在 SkyWalking,而在 SpringBoot 自身的特性与 SkyWalking 插件的交互逻辑。以下是我在多个项目中反复踩坑、最终形成标准化排查清单的三大典型场景。

3.1 场景一:WebFlux 应用的“零链路”之谜

当你的 SpringBoot 项目基于 WebFlux(响应式编程)而非传统的 WebMvc 时,SkyWalking 7.0 的默认行为会让你大吃一惊:所有链路都消失了,UI 里只有孤零零的unknown服务。

原因在于,WebFlux 的核心是Mono和Flux,其执行是异步、非阻塞、由事件循环驱动的。传统的基于ThreadLocal的链路上下文传递机制(SkyWalking WebMvc 插件所依赖的)在这里完全失效。ThreadLocal在事件循环线程池中无法跨subscribe边界传递上下文。

解决方案不是换插件,而是启用 SkyWalking 的响应式支持。在agent/config/agent.config中,必须显式开启:

plugin.spring.webflux.enabled = true plugin.spring.webflux.context_propagation_enabled = true

第二行context_propagation_enabled是关键。它会启用 SkyWalking 自研的ReactorContext传播机制,将TraceContext注入到 Reactor 的Context中,从而在flatMap、map等操作符内部也能正确延续链路。如果不开启,你只会看到WebFlux DispatcherHandler这一个 Span,后面所有的业务逻辑都“脱链”。

实操心得:WebFlux 的链路跨度(Span Duration)计算与 WebMvc 不同。WebMvc 的 Span 从请求进入 Servlet 到响应写出结束;而 WebFlux 的 Span 会一直持续到Mono/Flux的整个订阅流完成。这意味着,一个看似简单的Mono.delayElement(Duration.ofSeconds(5)),会让整个 Span 持续 5 秒。这不是 Bug,是响应式模型的固有特性,需要你在分析性能瓶颈时特别注意。

3.2 场景二:Feign Client 的“出站链路丢失”

在微服务架构中,一个 SpringBoot 服务通过 Feign Client 调用另一个服务,这是再常见不过的场景。但你会发现,调用方的链路图里,只有一个FeignClient的 Span,而这个 Span 的peer字段(目标服务地址)是空的,下游服务也收不到任何链路数据。

根本原因在于,Feign 的默认实现(Default)是一个纯 HTTP 客户端,它不携带任何 SkyWalking 的上下文头(如sw8)。SkyWalking 的 Feign 插件,只对feign-core的SynchronousMethodHandler进行增强,但前提是 Feign 的Client实现必须是 SkyWalking 支持的类型。

标准解法是强制 Feign 使用OkHttpClient或ApacheHttpClient,并确保它们的依赖版本与 SkyWalking 插件兼容。在pom.xml中,排除掉默认的URLConnection,引入okhttp:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 排除默认 client --> <exclusions> <exclusion> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>

同时,在application.yml中配置:

feign: okhttp: enabled: true

这样,SkyWalking 的feign-okhttp-plugin才能生效,自动在 HTTP Header 中注入sw8上下文,下游服务的 Agent 才能解析并延续链路。

提示:如果你的项目已经用了RestTemplate,它的埋点是开箱即用的,因为 SkyWalking 的spring-rest-template-plugin对RestTemplate的execute方法有原生支持。但WebClient(WebFlux 的客户端)则需要额外配置webclient-plugin,逻辑与 WebFlux 类似。

3.3 场景三:自定义 Starter 中的“埋点盲区”

很多团队会封装自己的 SpringBoot Starter,将通用功能(如统一日志、权限校验、缓存工具)打包成一个my-common-starter。当这个 Starter 里的某个工具类(比如CacheUtil)被业务 Controller 直接调用时,你可能会发现,CacheUtil的方法调用并没有生成独立的 Span,它只是作为ControllerSpan 的一个子节点(Sub-Span)存在,甚至被合并掉了。

这是因为 SkyWalking 的插件体系,是按“框架”而非“业务包”来组织的。CacheUtil属于你的业务代码,不在任何官方插件的增强列表里。Agent 默认只会增强org.springframework.cache.*这样的 Spring Cache 包,而不会去增强com.mycompany.util.*。

要让CacheUtil也拥有独立的、可追踪的 Span,你需要编写一个自定义插件。这并非遥不可及。SkyWalking 提供了非常清晰的插件开发指南。核心步骤如下:

  1. 创建一个新模块skywalking-custom-plugin,依赖apm-sniffer/apm-agent-core;
  2. 编写一个CacheUtilInstrumentation类,继承ClassInstanceMethodsEnhancePluginDefine;
  3. 在enhanceClass方法中,返回com.mycompany.util.CacheUtil;
  4. 在define方法中,指定要增强的方法(如get,put)和对应的Interceptor;
  5. 将编译好的插件 jar 包,放入agent/plugins/目录下。

这个过程,本质上就是告诉 SkyWalking Agent:“请特别关注CacheUtil这个类,当它的get方法被调用时,请执行我提供的CacheGetInterceptor,在这个 Interceptor 里,我手动创建一个新的 EntrySpan。”

经验总结:自定义插件不是银弹,它会带来额外的维护成本。对于大多数场景,更推荐的做法是:在业务 Controller 中,对关键的、耗时长的工具方法调用,手动使用 SkyWalking 的TracerAPI 进行埋点。例如:

public String getUser(String id) { // 手动创建一个 ExitSpan,代表对 CacheUtil 的调用 AbstractSpan span = Tracer.createExitSpan("CacheUtil.get", "cache-service"); try { return cacheUtil.get(id); } finally { span.stop(); } }

这种方式侵入性略高,但胜在简单、可控、无需额外构建和部署插件。

4. 从“能用”到“好用”:生产级埋点的精细化配置与调优

当你的 SkyWalking 7.0 + SpringBoot 应用终于能在 UI 上看到五彩斑斓的链路图时,真正的挑战才刚刚开始。默认配置是为“演示”设计的,生产环境需要一套精细的“手术刀式”配置,以平衡监控精度、系统开销和数据价值。以下是我基于多个高并发项目沉淀下来的六项核心调优策略。

4.1 策略一:采样率(Sample Rate)的动态分级

SkyWalking 默认采样率是1.0(100%),即每一个请求都生成完整的 Trace。这对于 QPS 几十的后台管理服务没问题,但对于一个峰值 QPS 5000 的用户中心服务,这会产生海量的 Span 数据,不仅压垮 OAP 的存储和查询,还会给应用本身带来可观的 CPU 开销(序列化、网络发送)。

生产实践:采用“分级采样”策略。在agent/config/agent.config中,不使用全局sample_rate,而是利用 SkyWalking 的Dynamic Configuration功能,结合业务重要性进行动态控制。

首先,在agent.config中启用动态配置:

agent.namespace = user-service-prod collector.backend_service = oap-service:11800 # 关闭全局采样,交由动态配置管理 sample_n_per_3_secs = -1

然后,通过 SkyWalking UI 的“动态配置”页面,为不同服务、不同接口设置不同的采样规则。例如:

规则 Key规则 Value说明
default_sample_rate0.1全局默认采样率 10%
service.user-service-prod.endpoint.GET:/api/user/{id}1.0核心用户查询接口,100% 采样
service.user-service-prod.endpoint.POST:/api/user/login0.5登录接口,50% 采样,兼顾安全审计与性能
service.user-service-prod.endpoint.GET:/actuator/**0.0Actuator 端点,0% 采样,彻底排除

这种配置方式,让你既能保证核心链路的“全量可观测”,又能将非核心流量的监控开销降到最低。更重要的是,它可以在不重启应用的情况下实时生效。

4.2 策略二:Span 名称的语义化重写

默认情况下,SkyWalking 为 SpringBoot Controller 生成的 Span 名称是GET:/api/user/{id}。这个名称对机器友好,但对人不友好。当你在 UI 里看到几十个GET:/api/xxx时,很难一眼分辨哪个是“获取用户详情”,哪个是“获取用户订单列表”。

解决方案是使用span_name_rewrite插件。SkyWalking 7.0 内置了该插件,只需在agent.config中开启并配置映射规则:

plugin.span_name_rewrite.enabled = true plugin.span_name_rewrite.rules_file = span-name-rules.yml

然后,在agent/config/span-name-rules.yml中编写规则:

rules: - pattern: "GET:/api/user/{id}" name: "UserDetailQuery" - pattern: "POST:/api/order" name: "OrderCreate" - pattern: "GET:/api/product/search" name: "ProductSearch"

这样,UI 中显示的 Span 名称就变成了清晰的业务语义名。这不仅提升了排查效率,也为后续的 APM(Application Performance Management)告警和报表提供了高质量的维度标签。

4.3 策略三:敏感数据的自动脱敏

在链路追踪中,HTTP 请求体(Request Body)和响应体(Response Body)是极有价值的诊断信息。但其中往往包含用户的手机号、身份证号、银行卡号等 PII(个人身份信息)。将这些数据明文上报到 SkyWalking,是严重的安全合规风险。

SkyWalking 7.0 提供了强大的body插件,支持对特定字段进行正则匹配和脱敏。在agent/config/agent.config中:

plugin.http.http_client_body_capture_enabled = true plugin.http.http_server_body_capture_enabled = true plugin.http.body_rules_file = body-rules.yml

body-rules.yml示例:

rules: - type: "request" url_pattern: "/api/user/login" fields: - "password" - "captcha" - type: "response" url_pattern: "/api/user/profile" fields: - "phone" - "idCard" - type: "all" fields: - "token" - "accessToken"

配置后,所有匹配到的字段值,在 SkyWalking UI 的Body标签页中,都会被替换为***。这个过程发生在 Agent 端,数据在离开应用进程前就已经被处理,从根本上杜绝了敏感信息泄露的风险。

注意:body插件会对 JSON 和 Form 表单格式进行解析,但对二进制数据(如文件上传)无效。对于这类场景,建议在业务代码中,通过Tracer的tag方法,只上报必要的、非敏感的元数据(如文件大小、文件类型),而不是整个 Body。

4.4 策略四:异步任务的链路延续

SpringBoot 中大量使用@Async注解来执行异步任务(如发邮件、写日志、更新缓存)。默认情况下,这些异步方法的执行是完全脱离主线程链路的,它们会生成全新的、孤立的 Trace,导致业务逻辑的因果关系断裂。

要实现异步链路的延续,必须启用async插件并配合@Async的自定义配置。在agent.config中:

plugin.spring.async.enabled = true

但这还不够。SpringBoot 的@Async默认使用SimpleAsyncTaskExecutor,它每次都会创建一个新线程,ThreadLocal上下文无法传递。因此,你必须在@Configuration类中,定义一个使用ThreadPoolTaskExecutor的 Bean,并确保它启用了InheritableThreadLocal:

@Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("async-pool-"); // 关键:启用可继承的 ThreadLocal executor.setTaskDecorator(runnable -> { final Map<String, String> context = ContextManager.getRuntimeContext().getCorrelationContext(); return () -> { ContextManager.getRuntimeContext().getCorrelationContext().putAll(context); runnable.run(); }; }); executor.initialize(); return executor; } }

这样配置后,@Async方法的执行,就能正确继承主线程的 Trace ID 和 Span ID,成为主链路的一个子分支,而不是一个全新的、无关的链路。

4.5 策略五:JVM 指标与业务指标的融合

SkyWalking 的强大之处,不仅在于分布式追踪,更在于它能将 JVM 指标(GC 时间、内存使用率、线程数)与业务链路(Trace、Span)进行关联分析。例如,你可以筛选出所有在 Full GC 期间发生的慢请求,或者找出那些导致线程池耗尽的特定业务接口。

要实现这种融合,关键在于jvm插件的正确配置和service维度的统一。在agent.config中:

plugin.jvm.enabled = true # 必须与你的服务名一致,否则指标无法关联 agent.service_name = user-service-prod

然后,在 SkyWalking UI 的“仪表盘”中,你可以创建一个复合图表:X 轴是时间,Y 轴左侧是JVM.Memory.HeapUsed,右侧是Service.SLA(服务可用率)。当两条曲线出现强相关性(如 HeapUsed 骤升时 SLA 骤降),就立刻指向了内存泄漏或 GC 问题。

更进一步,你可以将自定义业务指标(如订单创建成功率、支付回调延迟)通过metrics插件上报。SkyWalking 提供了MeterAPI,允许你以 Prometheus 格式定义和上报指标。这些指标会与你的服务名绑定,最终在 UI 的“服务概览”中,与 JVM 指标、链路指标一同展示,形成一个完整的、立体的可观测性视图。

4.6 策略六:告警规则的“业务化”设计

SkyWalking 的告警功能,默认提供了一套基于技术指标的规则(如service_resp_time > 1000)。但在实际运维中,单纯的技术阈值告警,往往缺乏业务上下文,导致告警疲劳。

我的做法是,将告警规则与业务 KPI 深度绑定。例如,对于一个电商下单服务,核心 KPI 是“下单成功率”。我们定义:

  • 黄金指标(Golden Signal):service_sla(服务可用率)
  • 业务规则:当service_sla在过去 5 分钟内低于 99.5%,且service_cpm(每分钟调用量)大于 100 时,触发告警。

这个规则的意义在于:它过滤掉了低流量时段的偶发抖动(CPM < 100),只在高负载、高影响的场景下才告警,极大提升了告警的准确率和可操作性。

在oap-server/config/alarm-settings.yml中,可以这样配置:

rules: # 业务下单成功率告警 - name: business-order-sla-rule metrics-name: service_sla op: "<" threshold: 99.5 period: 5 count: 3 silence-period: 30 message: 订单服务SLA低于99.5%,当前值: ${VALUE}%, 服务: ${SERVICE_NAME} tags: business: "order" priority: "P0"

通过tags字段,还可以将告警路由到不同的值班群,实现真正的“业务分级告警”。

5. 故障复盘:一次典型的“埋点消失”事件全过程还原

理论讲得再多,不如一次真实的故障复盘来得深刻。下面我将完整还原一个发生在某金融风控系统的线上事故,它完美涵盖了前面提到的所有知识点,也是我“玩转” SkyWalking 过程中最关键的一课。

5.1 故障现象与初步排查

时间:某周三下午 14:30
现象:风控核心服务risk-engine的 SkyWalking UI 中,所有链路图突然消失,只剩下几个孤立的unknown服务。但服务本身运行正常,HTTP 接口响应迅速,日志无任何 ERROR。

第一反应(错误):“OAP 服务挂了?” —— 立刻登录 OAP 服务器,systemctl status oap-server显示 active (running),curl http://localhost:12800返回正常 HTML。排除 OAP。

第二反应(错误):“网络不通?” —— 在risk-engine服务器上telnet oap-service 11800,连接成功。排除网络。

第三反应(正确):“Agent 没加载?” ——ps -ef | grep java,发现-javaagent参数赫然在列。但agent/logs/agent.log里,最后一条日志停留在 14:20,之后再无任何输出。这很反常,因为 Agent 应该每 30 秒就上报一次心跳。

5.2 深度日志分析与根因定位

将agent.log的日志级别临时改为DEBUG,重启服务,复现问题。这一次,日志里出现了一行被之前忽略的 WARN:

[WARN] 2023-10-15 14:35:22:101 SkyWalkingAgent-1-Thread-2 GRPCChannelManager - Channel to collector is not ready, will retry later.

这行日志指向了 gRPC 通道问题。但telnet是通的,为什么通道不 Ready?继续往下翻日志,发现了关键线索:

[ERROR] 2023-10-15 14:35:22:105 SkyWalkingAgent-1-Thread-2 GRPCChannelManager - Failed to create channel to oap-service:11800, cause: io.grpc.StatusRuntimeException: UNAVAILABLE: io exception Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

SSL 握手失败!原来,OAP 服务在当天上午进行了 TLS 证书轮换,新证书由一个新的中间 CA 签发,而risk-engine服务器的 JVM 信任库(cacerts)里没有这个新的中间 CA 证书。

根因:SkyWalking Agent 的 gRPC 客户端,使用的是 JVM 的默认 SSLContext,它依赖于$JAVA_HOME/jre/lib/security/cacerts。证书更新后,Agent 无法建立安全连接,gRPC 通道一直处于IDLE状态,导致所有 Span 数据积压在内存队列中,最终因超时而丢弃。这就是为什么 UI 里“什么都没有”。

5.3 解决方案与长效治理

紧急修复:将新的中间 CA 证书导入到risk-engine服务器的 JVMcacerts中:

keytool -import -trustcacerts -file intermediate-ca.crt -alias intermediate-ca -keystore $JAVA_HOME/jre/lib/security/cacerts

重启服务,agent.log中立刻出现了GRPCChannelManager - Channel is READY,5 分钟后,UI 中的链路图恢复正常。

长效治理:这次事故暴露了基础设施配置的脆弱性。我们立即推动了两项改进:

  1. 自动化证书同步:在 CI/CD 流水线中,增加一个步骤,每当 OAP 的 TLS 证书更新时,自动将新的ca-bundle.crt推送到所有应用服务器,并更新 JVM 的cacerts。
  2. Agent 级别证书配置:修改agent/config/agent.config,不再依赖 JVM 全局信任库,而是为 Agent 单独指定一个信任库:
collector.ssl_trust_store_path = /path/to/risk-engine-truststore.jks collector.ssl_trust_store_password = changeit

这样,Agent 的证书管理就与 JVM 解耦,未来证书更新只需更新这个独立的信任库,不影响其他任何 Java 应用。

5.4 经验教训与认知升级

这次故障,让我对“自动埋点”的理解发生了质的飞跃:

  • “自动”不等于“无感”:Agent 作为一个运行在 JVM 内部的“幽灵进程”,它的健康状态必须被纳入日常监控。我们随后在 Prometheus 中增加了对agent.log中UNAVAILABLE、failed等关键词的文本日志监控,并配置了企业微信告警。
  • 基础设施变更即监控变更:一次简单的证书轮换,就能让整套可观测性系统瘫痪。这意味着,运维团队和 SRE 团队必须建立一个联合的“可观测性基础设施变更清单”,任何影响 Agent 运行环境的变更(JVM 版本、OS 内核、网络策略、TLS 证书),都必须提前通知并协同验证。
  • 日志是唯一的真相来源:在分布式系统中,UI 是表象,日志才是真相。放弃一切“应该没问题”的假设,养成第一时间查阅agent.log和oap-server.log的肌肉记忆,是每个 SRE 的基本功。

我在实际使用中发现,最有效的故障预防,不是写更多的监控脚本,而是把每一次故障的根因、排查路径、解决方案,都固化成一个 CheckList,并嵌入到团队的 Onboarding 文档和日常巡检 SOP 中。比如,现在我们新同事入职的第一周任务,就是独立完成一次从“UI 空白”到“定位证书问题”的全流程复现。只有亲手走过一遍,才能真正理解“玩转”二字的分量。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进