错误追踪、业务信号与可观测成本:怎样诊断而不过载
错误平台中的一个问题,可能聚合了数万条异常事件;其中一部分来自同一请求的重试,另一部分可能属于不同用户。修复优先级需要知道受影响的操作、持续时间和业务后果,不能只按异常条数排序。
遥测数据承担不同任务:指标描述整体变化,Trace 展开一次调用,日志保存具体事件,错误分组把重复现象归到可处理的问题。它们通过服务、版本和关联标识连接起来,各自仍保留自己的计数单位。
把重复错误归成可以处理的问题
事件、尝试、操作与问题
一个业务操作 operation
├─ attempt 1 → 异常事件 → 问题组 A
├─ attempt 2 → 同类异常 → 问题组 A
└─ attempt 3 → 成功
最终操作结果:success这个操作产生两个错误事件,包含三次尝试,最后完成一次成功。若对每一层都记录相同异常,还可能出现更多日志和错误事件。排查依赖不稳定时需要看尝试失败率;衡量用户最终是否完成操作时,需要按最终结果计数。
issue 是平台按规则形成的问题组。默认分组可能参考异常类型、堆栈和消息,也可以通过 fingerprint 规则调整;不同平台和版本的算法不同。Sentry 的项目分组配置就分别提供 grouping、stack enhancement 和 fingerprint 配置,不应把某个本地 Hash 算法当成所有错误平台的标准。
稳定指纹保留哪些差异
| 输入 | 是否适合直接参与问题标识 | 原因 |
|---|---|---|
| 稳定错误代码 | 通常适合 | 描述同一类失败原因 |
| 依赖名称、操作类别 | 视诊断职责加入 | 区分不同依赖或业务动作 |
| 异常类、关键业务栈帧 | 适合经规则选择 | 区分执行路径 |
| 动态订单号、请求ID | 不适合 | 每次不同,会把同类问题拆散 |
| 完整异常message | 需规范化 | 可能含参数、路径和敏感数据 |
| 发布版本 | 通常作为查询维度 | 每次发布换组会丢掉问题的连续历史 |
分组太细时,一个缺陷分成许多小问题;分组太粗时,不同故障混在一起。调整指纹后,用一组已知错误同时验证“应该合并的仍合并”和“应该分开的仍分开”,并在平台保留算法版本或迁移记录。
实验使用受控错误代码、有限依赖名和异常类组合出身份,再 Hash 为短字符串。FailureFingerprint.java 不读取动态 message:
String identity = "v1|" + stableCode + "|" + dependency
+ "|" + failure.getClass().getName();
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(identity.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest, 0, 12);这些输入来自应用定义的值,不接受用户自由拼接。Hash 只缩短标识,不提供匿名化保证,也无法绝对消除碰撞。完整错误类型和脱敏上下文仍应保留在事件中,以便重新归组。
异常堆栈也需要选择记录位置。底层捕获后原样抛出,入口再记录同一条异常时,两份堆栈通常不能提供两倍信息。可在负责最终处理的层记录完整异常,其他层补充有意义的阶段事件或字段;如果底层已经恢复,则在恢复层说明结果。不要为减少重复而把关键 cause 或 suppressed 异常全部删掉。
用业务结果衡量影响
系统失败、业务拒绝和结果未知
HTTP 状态、异常类型和业务结果并非一一映射。输入不合法可能返回400;余额不足可能返回业务拒绝;下游不可用可能返回503;客户端超时则只说明它未在期限内拿到结果,写操作可能仍在服务端继续。
| 结果类别 | 观测点 | 后续处理 |
|---|---|---|
| 成功完成 | 已满足业务完成条件 | 记录完成次数与耗时 |
| 明确拒绝 | 已知规则或权限拒绝 | 保留原因,按产品目标判断是否影响成功率 |
| 系统错误 | 服务未能按契约完成 | 关联错误组、依赖和发布版本 |
| 结果未知 | 调用方等待终止,终态未确认 | 按 operationId 查询或对账,避免盲目重试写入 |
异步任务接收成功后,还要单独测量终态完成率、最老未完成任务年龄和处理延迟。入口一直返回202,后台却停止消费时,请求成功率会掩盖实际故障。查询式指标适合反映当前积压,累计指标适合计算新接纳、完成和失败的速率。
对用户影响去重,应采用稳定的操作标识或经授权处理的用户维度,在业务分析存储中计算。不要把每个 operationId 变成 Prometheus 标签。通过 Trace 采样保留下来的错误数也无法直接替代总体分母,尤其尾采样优先保留错误时,保留数据中的错误比例会被主动放大。
RED、USE 与具体问题
RED关注请求速率、错误和持续时间,适合从服务外部看处理效果。USE关注资源利用、饱和与错误,适合检查CPU、线程池、连接池、磁盘等瓶颈。两者可以连接:请求尾延迟增长后,查看连接等待是否同步上升,再用Trace和线程栈定位等待对象。
整体CPU正常而一个租户持续失败,需要按有限业务维度展开;错误率正常而长任务越来越旧,需要检查积压年龄。指标选择从实际故障形态出发。方法和常见指标可查 Google SRE 监控实践。
SLI、SLO 与错误预算
SLI 是具体测量,例如“在5秒内成功完成的合格操作数 / 全部合格操作数”。SLO 为它设定目标和窗口,例如30天达到99.9%。合格操作的定义要提前确定:是否包含主动取消、无效输入、健康检查、内部重试,都影响分子分母。
按事件计算时:
允许失败比例 = 1 - SLO
错误预算数量 = 窗口内合格操作总数 × 允许失败比例
预算消耗速率 burn rate = 当前失败比例 / 允许失败比例若SLO为99.9%,当前失败比例为1%,burn rate就是10。它说明当前消耗速度为允许平均速度的十倍;只有假定这一速率持续,才能外推多久耗尽预算。按请求计数的99.9%与按时间统计的99.9%有不同分母,不能直接互换成“每月允许停机多少分钟”。SLI选择与目标建立见 SRE SLO 实施方法。
告警既要尽快发现大故障,也要减少瞬时抖动。短窗口响应快,长窗口能确认问题持续;多窗口燃烧率告警同时要求两者越线,可以兼顾响应和噪声。低流量服务还需考虑样本量,一次失败就得到100%并不自动意味着应立即唤醒值班人员。窗口、阈值与处置等级的推导见 SRE 基于SLO告警。
运行请求并检验告警规则
一组有明确结果的请求
下载错误与遥测实验包,解压进入 observability-signals-lab。实验使用 Spring Boot 4.1.1 管理 Micrometer 1.17.1、OTel 1.62.0,Java 编译目标17;Prometheus 3.5.1运行规则,Collector 0.148.0把收到的Span写到本地文件。
Linux宿主需要 Docker、Compose V2、Bash、curl、jq、unzip。操作者为有Docker权限的普通用户,构建容器显式指定宿主UID/GID;这个设置不改变daemon权限。项目目录、缓存和out须可写:
mkdir -p .m2 out
docker run --rm --user "$(id -u):$(id -g)" \
--entrypoint mvn -e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
-v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 -B -ntp \
-Dmaven.repo.local=/m2 -Duser.home=/tmp clean verify
export LAB_UID=$(id -u) LAB_GID=$(id -g)
docker compose -p ob17-signals up -d
bash scripts/verify-http.sh构建应有21个测试通过。HTTP脚本等待健康,再发ok、retry、decline、error四次请求;它禁用curlrc与代理,分别断言传输、精确HTTP状态和响应体。首次执行且应用尚无其他请求时:
| 请求模式 | HTTP与最终结果 | 尝试次数 | Collector收到的Span数 |
|---|---|---|---|
| ok | 200 / success | 1 | 2 |
| retry | 200 / success | 3 | 4 |
| decline | 422 / declined | 0 | 1 |
| error | 503 / error | 1 | 2 |
retry在前两次尝试主动抛出受控异常,第三次成功;error只尝试一次。由此得到operation成功2、拒绝1、系统错误1;attempt成功2、错误3;请求结束后active=0。整体系统错误比例为1/4,尝试错误比例为3/5,二者描述不同对象。
脚本重复运行会增加累计量,但每次都按新响应的Trace ID检查Span。网络或服务启动失败先看 docker compose -p ob17-signals ps 和对应容器日志;指标暂时为空则检查Prometheus抓取状态。缺少JAR时先完成构建,Collector文件不可写时修复out的宿主权限。依赖下载使用可信私服或准备好的离线缓存,不跳过测试来掩盖下载失败。
从查询表达式到持续告警
rules.yaml中的示范告警为:
- alert: OperationFailureRate
expr: |
(sum(rate(lab_operation_duration_seconds_count{outcome="error"}[5m]))
/ sum(rate(lab_operation_duration_seconds_count[5m])) > 0.1)
and sum(increase(lab_operation_duration_seconds_count[5m])) >= 20
for: 2m
labels:
severity: warning每条实例序列先rate再汇总,处理进程重启产生的counter reset;错误比例超过10%,且5分钟估算操作数不少于20,表达式才成立。for要求这组告警标签对应的条件持续满足2分钟后进入firing,期间条件不成立会重置等待。这个等待过程检查条件是否持续成立,不额外对错误率求平均。Prometheus 告警规则解释了pending与firing状态。
这些数字用于低成本演示,未从某个生产SLO推导。正式接入时按服务保留必要标签,避免把多个独立服务汇总成一个总数;同时设计通知路由、负责人和可执行处理入口。若需要按多个窗口判断燃烧率,依据前面的SLO计算设置表达式,而不是把10%直接当万能阈值。
规则还区分两种没有业务读数的情况:
up{job="signals-app"} == 0
absent_over_time(up{job="signals-app"}[2m])第一条表示已知target抓取失败,第二条表示该job在最近窗口没有样本。正常抓取但没有业务请求则是另一种状态。up=1 只描述采集请求成功,数据内容和业务行为仍需独立检查。
实际执行规则测试:
docker run --rm --entrypoint /bin/promtool \
-v "$PWD:/work:ro" -w /work prom/prometheus:v3.5.1 \
test rules rules-test.yaml正常输出SUCCESS。五组固定输入分别覆盖:足够流量下持续20%错误触发;少量请求即使高错误比例也不触发;抓取失败触发独立告警;目标消失触发missing;实例重启后先rate/increase再sum仍按重置处理。测试使用真实PromQL引擎,规则输入格式见 Prometheus 规则单元测试。
四次HTTP请求本身不足以触发上述20次阈值,也没有等待完整生产告警窗口。单元测试验证表达式和时间条件,HTTP实验验证真实应用到采集器的信号;本机没有配置短信、邮件或值班平台,不会发出外部通知。
告警如何连接具体事件
告警先保留服务、环境、结果类别和查询窗口。进入同一时间范围的错误分组,再按发布版本和依赖筛选;获得一个Trace ID后,展开父子Span确认哪一次attempt失败、后续是否恢复,最后查对应日志的业务操作标识。
在实验文件中查看错误Span的稳定指纹:
jq -s \
'[.[].resourceSpans[].scopeSpans[].spans[] |
select(.status.code == 2) |
{name,traceId,attributes}]' out/traces.jsonl错误attempt的attributes包含 lab.error.fingerprint。其值来自前面的稳定字段组合;FingerprintTests验证动态消息变化仍归同组,不同代码或依赖分开。单凭两条事件有同样指纹,仍需检查实际类型和调用位置,防止分组规则合并过度。
控制采集开销并保留诊断能力
三种信号按各自单位计算
日志:事件数/秒 × 平均编码字节 → 写出与传输量
追踪:请求数/秒 × 平均每请求Span数 × 头采样比例 → 估算导出Span量
指标:实际标签组合 × 每种Meter展开序列数 → 活跃序列追踪公式适用于采样选择与请求类型基本独立的比例头采样,使用每请求Span数的平均值。尾采样若偏向错误或重试较多的长链,请求保留比例与Span保留比例就会不同,应直接统计实际保留的Span速率。日志容量还受异常栈长度、压缩、索引和保留期影响。尾采样之前已经支付的SDK、网络和Collector缓冲开销,也不会因为后端只保留10%而全部缩小十倍。指标按抓取周期不断存样本,活跃序列相同但保留时间加倍,历史存储通常还会增加。
本实验中,可直接数实际抓取样本和本地文件字节:
curl -q --noproxy '*' --fail-with-body -sS \
http://127.0.0.1:17781/actuator/prometheus -o out/metrics.txt
grep -c '^lab_' out/metrics.txt
wc -c out/traces.jsonl
docker compose -p ob17-signals logs --no-log-prefix app \
> out/app.log 2>&1
wc -c out/app.log当前配置下lab_前缀有24个样本:三个outcome,每个Timer展开4个累积桶(含+Inf)、count、sum、max,共21条,再加2条attempt Counter与1条Gauge。这个数只覆盖lab_指标,未包括JVM、HTTP与Collector自身指标,也不是后端保留期内的历史序列总量。
文件字节包含已经积累的全部数据。估算一段操作的新增量,应比较执行前后差值;若目录中有其他测试流量,不能把总文件大小直接除以本轮4次请求。容器日志文件还包含框架启动信息,本实验使用Boot扁平JSON字段,可先过滤非JSON行和其他事件:
jq -Rc 'fromjson? | select(.event == "operation.completed")' \
out/app.log > out/operation-events.jsonl
wc -l -c out/operation-events.jsonl输出行数为匹配事件数,字节数为筛选后重新编码的JSON大小;它不是原始网络传输字节。Boot的event与Logback原生JsonEncoder的kvpList结构不同,其他编码方式的查询见结构化日志。
在数据产生处限制增长
日志按事件职责设定级别,减少重复堆栈,控制消息和请求载荷长度。指标采用路由模板和有限结果集合,注册前限制标签;Trace选择有诊断意义的操作,设置采样、SpanLimits与有界导出队列。具体指标配置见Actuator与Micrometer,采样损失与队列恢复见OpenTelemetry追踪。
对某个故障临时提高日志级别或采样比例时,限定服务、实例或路由,并设置结束时间与容量观察。全站长期DEBUG、每个方法创建Span、把用户ID加入所有指标,都会让遥测成本随业务增长失控。
脱敏应尽量发生在进入异步队列前。后端索引删除字段,无法消除它已在网络、采集器磁盘或另一个出口出现的事实。账户凭据、Cookie、个人信息和业务机密按字段白名单处理,保留期限和查阅权限也要匹配用途。OWASP日志实践给出了敏感信息处理与日志保护建议。
遥测自身故障时如何继续诊断
| 情况 | 当下检查 | 恢复后补查 |
|---|---|---|
| 指标突然全绿且请求量归零 | target、up、样本缺失、入口实际流量 | 新请求是否进入指标,旧告警是否被错误抑制 |
| 日志停止但业务仍有请求 | Appender、队列、磁盘、采集驱动 | 新事件到达,停机区间是否有丢失 |
| Collector队列持续上涨 | 接收率、输出错误、后端限额和磁盘 | 队列排空速度、新旧数据时延 |
| Trace只剩随机片段 | 父采样、SDK队列、头转发和尾采样路由 | 固定新请求的父子关系及Span数量 |
| 错误数下降但用户仍失败 | 操作终态、拒绝比例、积压年龄 | 真实业务结果与数据状态 |
Collector存活、Exporter发送成功、后端可查询分别位于不同阶段。发送失败计数也可能包括后来重试成功的尝试,不能直接作为永久丢失数。采集服务自身的接收、拒绝、发送和队列指标应按版本确认,见 Collector内部遥测。
业务故障与遥测故障同时出现时,保留低成本的独立入口,例如受控健康探测、宿主资源读数与必要的本地日志。遥测链不能无限占用业务线程等待后端恢复;有界缓冲和明确丢弃策略至少能限制它对应用的影响。业务持久事实仍由数据库、事务日志或专门审计系统保存。
CPU持续繁忙而Trace里主要是一个长片段时,可进一步使用JFR或profiler定位热点;它们按运行栈和采样事件回答“时间消耗在哪里”,与一次请求的Span拓扑互补。性能诊断的时间口径见延迟与排队。
确认修复后先验证新请求结果和信号到达,再检查积压、未确认操作和采集缺口。恢复采集不意味着业务数据已经补齐;业务回查与对账需要按稳定operationId进行。实验结束执行 docker compose -p ob17-signals down,仅停止本项目容器与网络,out中的诊断输出保留。
权威资料与规范地址
错误分类与监控方法
- Sentry项目分组配置:https://docs.sentry.io/api/projects/update-a-project/
- Google SRE分布式系统监控:https://sre.google/sre-book/monitoring-distributed-systems/
- RED方法:https://grafana.com/blog/the-red-method-how-to-instrument-your-services/
- USE方法:https://www.brendangregg.com/usemethod.html
- OWASP日志实践:https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
SLO与告警
- SLO实施:https://sre.google/workbook/implementing-slos/
- 基于SLO的告警:https://sre.google/workbook/alerting-on-slos/
- Prometheus告警规则:https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
- Prometheus规则测试:https://prometheus.io/docs/prometheus/latest/configuration/unit_testing_rules/
- Collector内部遥测:https://opentelemetry.io/docs/collector/internal-telemetry/
