CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
可观测性入门/课时/插桩与埋点
RawBlame
课时目录12 节

可观测性入门

  1. 01可观测性三支柱
  2. 02指标与 SLI
  3. 03结构化日志
  4. 04分布式链路追踪
  5. 05插桩与埋点
  6. 06监控与告警
  7. 07遥测与 OpenTelemetry
  8. 08Spring 可观测性
  9. 09SLO 与错误预算
  10. 10仪表盘设计
  11. 11故障排查流程
  12. 12综合练习

插桩与埋点

自动/手动 instrumentation。

30 分钟

插桩与埋点

这一课你将能做到

  1. 区分 自动插桩 与 手动埋点 的职责边界,说清判题链路上哪些观测点必须靠框架、哪些必须自己写
  2. 为 POST /api/judge/submissions 设计最少必要的手动埋点(Counter / Timer / Span),并给出标签白名单
  3. 评估插桩对 性能 与 标签基数 的影响,能讲清「指标爆炸」是怎么把自己放倒的

为什么要学

可观测性不会从空气里长出来:要么框架和 agent 替你注入,要么你在业务代码里显式声明。插桩(Instrumentation) 就是把 Metrics / Logs / Traces 信号「种」进系统的过程。种少了,故障时两眼一抹黑;种多了、标签乱了,时序库和账单先把你放倒——典型翻车:有人给每个 REST 路径的每个查询参数组合建指标,一周后 Prometheus 内存爆了,监控先死于自己的观测。

判题链路横跨 浏览器 → Nginx → Spring Boot → Kafka → 判题 worker → MySQL/Redis,任何一个环节没有信号,排障就断在哪一环。面试里「自动插桩和手动埋点怎么分工」「你们怎么控制指标基数」都是高频题,本课给答案。

先建立图像

方式来源覆盖什么注意
自动插桩OTel Java Agent、Micrometer 的 MVC/JDBC 插桩HTTP 入口、JDBC、HTTP client、JVM零侵入打底,标签仍要治理
手动埋点业务代码里的 registry.counter()、@Timed、自定义 Span业务结果:提交量、判题通过/拒绝、队列投递只埋业务决策点,控制基数
一次判题提交的信号来源:

浏览器 → Nginx → Spring Boot → Kafka → worker → MySQL/Redis
           │         │          │       │         │
           │         │          │       │         └─ 自动:JDBC Span + 慢 SQL 日志
           │         │          │       └─ 手动:judge_results_total{result="accepted"}
           │         │          └─ 自动:Kafka producer Span + 手动:队列投递 Counter
           │         └─ 自动:http_server_requests Timer + MVC Span
           └─ 手动:access log(含 traceId)

原则:自动打底(框架已知边界),手动补「业务结果与决策」(框架不知道的部分)

跟着做

步骤 1:给判题链路列一张「谁负责」清单

在记事本填表,每个观测点标注 自动/手动:

1. 接口 QPS、4xx/5xx、延迟      → 自动(Micrometer MVC 插桩)
2. JDBC 调用、连接池占用         → 自动(DataSource 插桩)
3. Kafka 投递成功/失败           → 自动(producer 插桩)+ 手动业务标签
4. 判题结果分布(AC/WA/TLE/RE)  → 手动:框架不知道什么叫「判题通过」
5. 提交语言分布                  → 手动 Counter,标签 language(枚举可控)
6. traceId 全链传播              → 自动(agent),异步线程池需验证

应得到的核心结论:超过八成观测点应该自动完成,手动只补框架不知道的业务语义。

步骤 2:写一个低基数的判题结果 Counter

worker 判完题后记录结果:

@Service
public class JudgeResultRecorder {

    private final MeterRegistry registry;

    public JudgeResultRecorder(MeterRegistry registry) {
        this.registry = registry;
    }

    public void record(String language, String result) {
        // 标签只用有界枚举:language 3~5 种,result 是 AC/WA/TLE/RE/MLE 之一
        registry.counter("judge_results_total",
                "language", language,
                "result", result)
            .increment();
    }
}

应得到:能解释为什么 language、result 可以做标签,而 userId、submissionId 绝对不行——前者取值个位数,后者百万级,每个组合都是一条新时序。

步骤 3:给关键业务步骤加手动 Span

「创建提交记录」这种跨库+发消息的决策点值得一个业务 Span:

import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

public SubmissionDto submit(SubmitRequest req) {
    Span span = tracer.spanBuilder("judge.createSubmission")
        .setAttribute("judge.language", req.getLanguage())
        .setAttribute("judge.lessonId", req.getLessonId())
        .startSpan();
    try (Scope scope = span.makeCurrent()) {
        SubmissionDto dto = doSubmit(req);   // 写库 + 投递 Kafka
        span.setAttribute("judge.submissionId", dto.getId()); // Span 属性无基数问题
        return dto;
    } catch (Exception e) {
        span.recordException(e);
        throw e;
    } finally {
        span.end();
    }
}

注意对照:Span 的 attribute 可以挂高基数值(submissionId),指标标签不行。这是两种信号的关键差异——Trace 是单次事件的详情,指标是聚合统计。

步骤 4:验证异步链路的 context 传播

worker 从 Kafka 消费消息处理判题,属于跨进程异步。发一条提交,然后在日志里检查:

# producer 端和 consumer 端应出现同一个 traceId
grep "judge" app.log | grep "traceId=4bf92f3577b34da6a3ce929d0e0e4736"

如果 consumer 端 traceId 变了或为空,说明 Kafka header 传播没生效——OTel agent 对主流 Kafka client 默认支持,自研封装层需检查是否在发消息时丢了 header。

步骤 5:做一次「指标爆炸」预演

假设有人提议:「给每个 lessonId 建一个提交量指标,方便看每课的提交趋势」。

写下拒绝理由和替代方案:

拒绝:lessonId 会随内容增长,每个 ID 一条时序,指标无限膨胀。
替代:Counter 只带 language 等枚举标签;
      「每课提交趋势」用日志字段 lessonId + 离线聚合,或业务库 GROUP BY 出报表。

讲清楚要点

  1. 框架已知 HTTP/DB/Kafka 边界,所以这些优先自动插桩——手写重复且容易与框架口径不一致。
  2. 框架不知道「判题通过」是什么,所以业务结果、领域状态变更必须手动埋——这是手动埋点存在的唯一理由。
  3. 每个标签组合都是一条新时序,所以埋点设计首先是基数设计——先定标签白名单,再写代码。
  4. 插桩本身消耗 CPU 和内存分配,所以热路径避免同步远程调用、避免挂巨型 attribute——SDK 默认批量异步导出,理解并信任这个默认。
  5. Span 属性无基数约束、指标标签有,所以高基数详情进 Trace 和日志,聚合统计进指标——三条信号各司其职。
  6. 异步和线程池是 context 丢失重灾区,所以埋点验收必须包含「跨边界 traceId 连续」这一项。
  7. 降级、熔断触发也是关键事件,所以可靠性动作要有指标或结构化日志——否则故障时你分不清「系统自己降级了」还是「系统坏了」。

你可能会懵的地方

  • MVC 自动插桩已有 HTTP 耗时,还要 @Timed 吗? 自动计的是入口总耗时;@Timed 可以给业务方法一个语义化的名字和标签。如果完全重复且没有新语义,不加——双份指标等于双倍基数。

  • OTel Java Agent 会不会明显拖慢服务? 常规负载下开销通常在个位数百分比,但必须用预发压测实测对比。可以按模块关闭用不到的 instrumentation(如某些老库的插桩),进一步降低开销。

  • 「最少必要」怎么判断? 反着问:没有这条信号,故障时能不能定位?SLO/告警是否需要它?如果答案是「只是好奇」,砍掉。观测成本是持续的存储和查询成本。

  • 日志算不算插桩? 广义上算——日志也是一种遥测信号的生产。实践里 Metrics/Logs/Traces 三条线一起规划:指标管聚合,日志管详情,Trace 管因果。

  • agent 自动插桩和手写 Micrometer 会双计吗? 会,如果不管。约定分工:agent/SDK 管 traces,Micrometer 管 metrics,日志库管 logs;同一信号只允许一个生产者,避免一条请求两条 Span 或两份计数。

错法对照

错法问题正解
给每个内部私有方法加 Timer指标爆炸、噪音淹没关键信号框架自动打底,手动只埋业务决策点
指标标签用 userId / submissionId时序数量随业务量爆炸低基数枚举标签;详情进日志和 Span 属性
手工再造一套 HTTP 耗时指标与框架口径不一致、双份存储沿用 Micrometer/OTel 语义约定,补业务标签
只埋成功路径失败、超时、降级完全不可见失败与降级同样要 Counter/事件
异步任务不处理 context链路在 Kafka/线程池处断成两截agent 支持的组件直接用,自研封装显式传 context

卡点急救

  1. 手动 Counter 在 /actuator/prometheus 里搜不到:Micrometer 是惰性注册——Counter 第一次 increment() 后才出现。先发一次真实请求再查;另外 Micrometer 导出名会自动加 _total 后缀,别搜错。
  2. 加了 OTel agent 后启动报错或行为异常:先 -Dotel.instrumentation.xxx.enabled=false 逐个排除问题模块,再查 agent 版本与 Spring Boot 大版本的兼容矩阵。
  3. Kafka consumer 端 traceId 全新:检查是否绕过了原生 producer(如自己拼字节数组发送),或消费端用 new Thread 裸跑丢了 context——改用 agent 识别的 KafkaTemplate/Listener,或显式 Context.wrap()。

面试常问

  • 自动插桩和手动埋点怎么分工? 背这个版本:自动插桩靠 agent 或框架集成,覆盖框架已知边界的通用观测点——HTTP 入口出口、JDBC、消息队列收发,零侵入、口径统一,应该作为底座。手动埋点解决框架不知道的业务语义:判题结果分布、转化漏斗、领域状态变更,用业务指标和自定义 Span 表达。分工原则一句话:自动打底保证覆盖,手动补义保证业务可解释;同时同一信号只能有一个生产者,避免双计。

  • 怎么防止指标基数爆炸? 基数 = 各标签取值数的乘积,每个组合一条时序。治理靠三招:标签白名单制度,取值必须是有界枚举;uri 用路由模板不用原始 URL;高基数字段(用户 ID、订单号、异常文本)只进日志字段和 Trace attribute,永远不进指标标签。上线前用 /actuator/metrics 或 TSDB 的 cardinality 统计做体检。

  • 插桩对性能有影响吗?怎么控制? 有,来自三部分:内存分配(每次观测创建对象)、CPU(聚合计算)、导出 IO。控制手段:用 SDK 默认的批量异步导出;关闭用不到的自动插桩模块;手动埋点只放在业务决策点而非每个函数;预发压测对比开关 agent 的吞吐和延迟,拿数据说话。

自测三问

  1. 判题链路上举出三个「必须自动、不必手动」的观测点,以及两个「必须手动」的观测点。
  2. 设计一个判题结果 Counter,写出它的标签并证明每个标签都是有界枚举。
  3. 为什么 Span attribute 可以挂 submissionId,而指标标签不行?

小结一句话

自动打底、手动补义、标签白名单——插桩是为排障和 SLI 服务,不是刷指标数量。

延伸

  • OpenTelemetry:Instrumentation 概念
  • Micrometer:Concepts
  • OTel Java Agent 支持的库与开关
  • 下一课《监控与告警》:信号采齐了,怎么做到「该醒时醒、不该吵时不吵」。
加载本节练习…
← 上一课分布式链路追踪下一课 →监控与告警