一句话速览:可观测性的三大支柱各回答一个问题——Logging 回答”发生了什么”,Metrics 回答”系统整体是否健康”,Tracing 回答”这一次请求卡在哪一环”。三者互补不可替代;同时要记住:可观测性工具本身也是跑在你环境里的软件,有兼容性和资源成本,不是配完就万事大吉

涉及原始记录:07-面试问题/开发问题/01-依赖与配置问题.md P0-02;
07-面试问题/验证问题/03-基础设施与中间件问题.md P2-01

最近修订:2026-08-07

目录


技术点 1:SkyWalking Agent —— 无侵入式链路追踪的原理与代价

难度:⭐⭐ | 掌握要求:Java Agent 字节码增强原理、JDK 强封装与 --add-opens

核心概念

SkyWalking Agent 的”无侵入”是通过 Java Agent 机制(-javaagent 参数)实现的:JVM 启动时先加载 Agent,Agent 用字节码增强(instrumentation)的方式在运行时修改目标类的字节码,在方法调用前后插入埋点逻辑(记录 traceId、耗时等),业务代码完全不需要手写任何埋点代码。这个机制天然要求 Agent 能够用反射去访问、修改目标类的内部结构,包括很多 JDK 内部类。

原理深挖:一条 Trace 是怎么串起来的

链路追踪的数据模型很简单:Trace(一次完整请求链路)由若干 Span(一个环节,如一次 HTTP 调用、一次 DB 查询)组成,Span 之间用 parent-spanId 连成树。难点在跨进程传播:Gateway 收到请求生成 traceId 后,必须在调下游服务时把它带过去——SkyWalking Agent 会自动在 HTTP 请求头里注入 sw8 头(携带 traceId、父 spanId 等),下游服务的 Agent 解析这个头把新 Span 挂到同一棵树上。跨进程上下文传播全靠这个头,这和 Feign 拦截器透传 xid(Seata)、透传用户 Header(鉴权)是完全相同的设计思想——链路上下文必须显式在线程间/进程间搬运,不会自动跟随。

sequenceDiagram
    participant C as 客户端
    participant G as Gateway
    participant L as lease服务
    participant H as house服务
    participant S as SkyWalking OAP

    C->>G: 请求(无 trace 上下文)
    Note over G: Agent 创建 Trace
traceId=T1, span0 G->>L: Feign/HTTP 调用
Header 注入 sw8(T1, span0) Note over L: Agent 创建子 Span
(T1, span1, parent=span0) L->>H: 调用 house
sw8(T1, span1) Note over H: Agent 创建子 Span
(T1, span2, parent=span1) G-->>S: 各 Agent 异步上报 Span 数据 L-->>S: OAP 按 traceId 归并 H-->>S: 还原完整调用链

我们踩过的坑

P0-02:JDK 21 的强封装机制拦住了 Agent 的反射操作

JDK 9 引入的模块系统(Jigsaw)默认对 JDK 内部包做了”强封装”(Strong Encapsulation),未经显式声明,外部代码不能通过反射访问这些内部类的非 public 成员。SkyWalking 9.4.0 版本的部分插件实现是在 JDK 更早期版本的开放策略下写的,没有预料到新版本 JDK 的限制会更严格,遇到 JDK 21 时触发 InaccessibleObjectException,或者导致 Agent 初始化阶段卡顿。

解决方式是在 JVM 启动参数里显式声明”开放”这些内部模块,允许反射访问:

1
2
3
4
JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens java.base/java.lang.module=ALL-UNNAMED \
-javaagent:$BASE_DIR/deploy/skywalking/skywalking-agent/skywalking-agent.jar"

--add-opens <module>/<package>=<目标模块> 的含义是”把这个包对指定的目标模块开放,允许深度反射访问”,ALL-UNNAMED 表示对所有未命名模块(也就是传统的 classpath 应用)开放。

必须记住的结论

  • “无侵入式”监控工具的代价是它依赖大量反射/字节码增强能力,JDK 版本升级带来的模块化限制会直接影响它的可用性。引入这类 Agent 工具、同时使用较新的 JDK 版本时,要有意识地检查该工具官方是否明确支持这个 JDK 版本,版本适配问题(尤其是新旧 JDK 的兼容性)几乎是这类工具的常见坑。
  • --add-opens 是绕过 JDK 强封装限制的标准手段,遇到第三方库反射访问 JDK 内部类报 InaccessibleObjectException 时,思路都是类似的:找到具体报错涉及的模块和包名,加对应的 --add-opens 参数,而不是降级 JDK 版本(降级往往有更多连锁影响)。
  • 日志噪音问题(P2-01):Agent 默认日志级别是 INFO,会输出大量插件加载相关的日志,跟业务日志混在一起影响阅读。这是一个小但常见的运维细节:接入任何 Agent/中间件类工具后,第一件事应该顺手检查它的默认日志级别和输出路径,通过配置把它和业务日志分开(比如这里配置 Agent 单独写到 skywalking-api.log),避免污染主日志。
  • 链路追踪跨进程传播的机制(sw8 头)和 Feign 拦截器透传是同一思想:上下文不会自动跟随请求,必须显式注入和解析

一个更宏观的认识:可观测性工具本身也是有”使用成本”的软件

这条记录虽然只对应两个具体问题,但值得提炼一个更普遍的认知:监控/追踪类工具虽然定位是”辅助排障”,但它本身也是运行在同一个 JVM/环境里的软件,会有自己的兼容性问题、资源开销、配置复杂度。接入可观测性工具不是”配完就一定好用”,同样需要像对待业务依赖一样去验证它在当前技术栈组合(这里是 JDK 21)下能不能正常工作。这也是为什么 Sentinel Dashboard 在 JDK 21 下有已知 bug(见 01-微服务治理.md)——中间件生态对最新 JDK 版本的适配,往往会落后于 JDK 本身的发布节奏,引入新 JDK 大版本时要对整条工具链做一次兼容性摸底,而不是只关心业务框架本身。


技术点 2:可观测性三大支柱与 Prometheus 指标监控

难度:⭐⭐ | 掌握要求:三支柱分工、Prometheus 拉模型、四种指标类型

三大支柱的分工

支柱 回答的问题 本项目实现 典型使用时机
Logging(日志) 发生了什么(离散事件的细节) Logback + 文件输出 已知大概位置,要看具体参数/异常栈
Metrics(指标) 系统整体是否健康(聚合数值趋势) Micrometer + Prometheus + Grafana 巡检、告警、容量评估、”是不是变慢了”
Tracing(链路) 这一次请求卡在哪一环(调用链拓扑+耗时) SkyWalking 微服务间互相调用,定位慢/错发生在哪个服务

三者的排查顺序通常是:Metrics 发现异常(QPS 暴跌/错误率上升)→ Tracing 定位到具体服务和环节 → Logging 在该服务里看细节。只依赖日志排障,在微服务架构下等于在十几个服务的日志文件里大海捞针。

Prometheus 的工作模型

Prometheus 与”应用主动上报”的监控方案不同,它是拉(Pull)模型:Prometheus Server 按配置周期(本项目 scrape_interval: 15s)主动抓取每个目标的 /actuator/prometheus 端点。本项目 deploy/docker/prometheus/prometheus.yml 为每个微服务配了一个 job,指标暴露靠的是 Spring Boot Actuator + Micrometer(micrometer-registry-prometheus 依赖),业务代码零侵入。

flowchart LR
    subgraph 微服务
        S1[livin-gateway
/actuator/prometheus] S2[livin-house
/actuator/prometheus] S3[其他服务...] end P[Prometheus Server
每15s拉取] --> S1 & S2 & S3 P --> TS[(内置时序数据库
按指标名+标签存储)] TS --> G[Grafana
PromQL 查询出图] TS --> A[Alertmanager
告警规则触发通知]

四种指标类型是必背概念:

类型 语义 典型例子
Counter 只增不减的计数器 请求总数、异常总数(配 rate() 算 QPS)
Gauge 可增可减的瞬时值 当前线程数、内存占用、连接池活跃连接
Histogram 分桶统计分布 请求耗时分布(算 P95/P99 延迟)
Summary 客户端侧算分位数 类似 Histogram,但分位数在客户端算,无法跨实例聚合

两个实践要点:① “平均值延迟”具有欺骗性——1 个 10 秒的请求和 99 个 10 毫秒的请求平均下来很正常,看 P95/P99 才能发现长尾;② Prometheus 的标签(label,如本项目给每个 job 打的 application 标签)是做维度下钻的关键——“全系统错误率”按 application 一切开,立刻知道是哪个服务的问题。

Grafana 的角色

Grafana 本身不存数据,是可视化 + 告警入口:配置 Prometheus 为数据源,用 PromQL 写查询出图。本项目的 dashboard 配置在 deploy/docker/grafana/dashboards/。学会读三个黄金指标图就能覆盖 80% 的巡检场景:流量(QPS)、错误率、延迟(P95)——这就是 Google SRE 的”四个黄金信号”(再加一个饱和度 Saturation)。


技术点 3:日志体系 —— 结构化输出与 traceId 透传

难度:⭐⭐ | 掌握要求:日志级别语义、MDC 原理、异步日志

核心概念

日志是三大支柱里最”古老”的,但微服务下有两个必须主动解决的问题:

① 日志级别语义要统一ERROR 表示”需要人工介入的异常”、WARN 表示”异常但系统已自愈”(如重试成功)、INFO 是关键业务节点、DEBUG 只在排查时打开。最常见的问题是 ERROR 滥用(业务校验失败也打 ERROR),导致真正的故障淹没在噪音里——ERROR 日志应该是”每条都值得看一眼”的,否则告警无法基于日志搭建。

② 跨线程/跨服务的日志串联:一个请求在 Gateway → lease → house 三个服务各打了若干行日志,没有关联标识就无法拼出完整链路。解法是把 traceId 打进每条日志:

  • MDC(Mapped Diagnostic Context):SLF4J 提供的基于 ThreadLocal 的日志上下文。在请求入口(过滤器/拦截器)把 traceId 塞进 MDC.put("traceId", ...),日志 Pattern 里加 %X{traceId},这个线程后续打的每一行日志都会自动带上它。请求结束必须 MDC.clear()——MDC 底层是 ThreadLocal,线程池复用线程时不清会串数据(ThreadLocal 的坑详见 10-JVM与并发编程.md)。
  • 异步场景要显式传递:MDC 绑在当前线程上,一旦逻辑切到线程池/MQ 消费者/CompletableFuture,traceId 就断了,需要用装饰的线程池(如 TtlExecutors)或在异步入口重新 set。接入了 SkyWalking 的话,其 Agent 的 logback 插件可以自动把 traceId 注入 MDC,免去手写。
  • 异步追加器:高吞吐服务用 Logback 的 AsyncAppender 把”日志 IO”挪到后台线程,避免业务线程阻塞在磁盘写;代价是 JVM 崩溃时队列里未来得及落盘的日志会丢——核心审计类日志不适合走异步。

与三支柱的呼应

日志带上 traceId 之后,三大支柱就真正打通了:Grafana 看板发现错误率突增 → SkyWalking 按时间找到失败 Trace 拿到 traceId → 用 traceId 去日志里精确捞出这次请求在所有服务里的每一行日志。这套”指标 → 链路 → 日志”的下钻路径,是微服务排障的标准动作。


术语表

术语 含义
Java Agent / instrumentation JVM 启动期加载的字节码增强机制,无侵入埋点的实现基础
Trace / Span 一次完整请求链路 / 链路中的一个环节(一次调用、一次查询)
sw8 头 SkyWalking 跨进程传播链路上下文的 HTTP 请求头
三大支柱 Logging(事件细节)/ Metrics(聚合趋势)/ Tracing(调用链)
拉模型(Pull) Prometheus 主动周期抓取目标指标端点的采集方式
Micrometer / Actuator Spring Boot 的指标门面与端点暴露组件,/actuator/prometheus 由此而来
Counter / Gauge / Histogram / Summary Prometheus 四种指标类型:计数/瞬时值/分桶分布/客户端分位数
P95 / P99 95%/99% 分位延迟,发现长尾慢请求的关键指标
四个黄金信号 延迟、流量、错误率、饱和度(Google SRE 提出的监控核心维度)
MDC SLF4J 基于 ThreadLocal 的日志上下文,用于给日志自动附带 traceId
AsyncAppender Logback 异步日志追加器,日志 IO 后台化,崩溃时可能丢尾部日志