Appearance
观测性三件套:Metrics / Logging / Tracing
看在前面
观测性(Observability)题在面试里的标准形态:"线上一个接口变慢,你怎么定位?"——答案的第一句永远是"三件套各回答一个问题,先问哪个"。这篇按五块写。包括:三支柱的分工边界、Prometheus 拉模型与 PromQL 要点、结构化日志与 trace_id 贯穿、OpenTelemetry 统一采集架构、四大黄金信号。
与同目录方法论互链:拿到信号之后的排查动作见 问题定位步骤。按 trace 定位到瓶颈后的优化见 性能优化方法论。JVM 层指标暴露的 Jolokia 实践见 JMX在Springboot的使用。
一、三支柱:各回答一个问题,别混用
text
┌──────────────────────────────────────────────┐
用户请求 ──►│ Metrics "有没有问题?在哪?"(聚合、低成本) │
│ _rate(errors[5m]) > 0.1 ? 报警 : - │
├──────────────────────────────────────────────┤
│ Tracing "问题在哪一段?"(跨服务、因果链) │
│ gateway 230ms ─ serviceA 210ms │
│ └─ db query 180ms ◄── 真凶在这
├──────────────────────────────────────────────┤
│ Logging "当时到底发生了什么?"(个体、全量) │
│ trace_id=abc ORDER_123 超时 timeout │
└──────────────────────────────────────────────┘
发现问题(Metrics)→ 定位链路(Tracing)→ 还原现场(Logging)| 支柱 | 回答 | 数据形态 | 成本模型 | 反模式 |
|---|---|---|---|---|
| Metrics | 系统现在健康吗 | 时序数值(预聚合) | 极低(每指标一串数) | 拿指标做个体排查(没有个体) |
| Logging | 具体这次发生了什么 | 离散事件文本 | 高(全量存储) | 什么级别都 INFO、无结构化 |
| Tracing | 慢/错在哪一段 | span 树(有因果关系) | 中(采样) | 采样率一刀切 100% 或 0.1% |
边界感是第一考点:指标是"面",链路是"线",日志是"点"。面用于报警与容量,线用于跨服务归因,点用于最终现场还原。用日志做监控(肉眼 grep)和用指标做排查(看不出单次请求),都是支柱错用。
二、Metrics:Prometheus 拉模型与 PromQL
text
拉模型(Pull):Prometheus 周期性抓取目标的 /actuator/prometheus(metrics 端点)
Prometheus ──scrape(15s)──► 应用A :8080/actuator/prometheus
──scrape(15s)──► 应用B │ # HELP http_server_requests_seconds ...
──scrape(15s)──► 中间件 exporter(mysqld_exporter/node_exporter...)
为什么拉不推:
✔ 抓取间隔/目标清单集中在服务端可控(扩缩容改配置即生效)
✔ 目标挂了 Prometheus 直接知道(推模型是"没消息=不知道挂没挂")
✔ 应用侧零依赖(只暴露文本端点),天然配合服务发现
短生命周期任务(批处理)推不了 → Push Gateway 中转,是拉模型的补丁不是主流
存储与查询:时序 = {__name__="http_requests_total", method="POST", uri="/api/order",
status="500"} @ timestamp → VTSDB(本地)/ 远端存储PromQL 四个必会要点(面试写得出一条才算会):
promql
# 1. Counter 必须求率:QPS 与错误率
rate(http_server_requests_seconds_count[5m]) # 每秒速率
sum(rate(errors_total[5m])) / sum(rate(requests_total[5m])) # 错误率(除法防各实例分母不一)
# 2. P95/P99 延迟:histogram_quantile + 聚合
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
# 3. by 聚合维度决定报警的"分辨率":by (service) 看整体,by (instance) 找单机
# 4. 阈值要有窗口语义:x > 0.9 for 2m(持续 2 分钟才报,防毛刺)四种指标类型一句话。Counter 只增(求 rate)、Gauge 可增减(内存/连接数直读)、Histogram 分桶(可算分位数)、Summary 服务端预算分位数(不可跨实例聚合)。聚合分位数必须用 Histogram,这是选型题的标准答案。
三、Logging:结构化 + trace_id 贯穿
text
非结构化:2026-10-17 14:23:01 INFO OrderService 下单成功 12345
→ grep 12345 能找到这一条,找不到"这次请求的其他日志"
结构化(JSON):
{"ts":"...","level":"INFO","service":"order","trace_id":"abc123","span_id":"d4",
"user_id":42,"order_id":12345,"msg":"order created","cost_ms":18}
→ ELK/Loki 里按 trace_id 一键拉出跨服务全部日志,按字段过滤聚合工程要点四条:MDC(Mapped Diagnostic Context)注入 trace_id——入口 filter 生成(或从 W3C traceparent 头提取)放进 MDC。日志框架自动带出,跨服务由 trace context 透传续上。级别纪律——ERROR 要"有人被叫起来",WARN 是"可自动恢复但要留痕",INFO 是业务主线事件,DEBUG 生产默认关。日志要能在三件套之间导航——trace_id 就是日志与链路的连接键,缺了它日志是孤岛。采集——Filebeat/Vector 采集 → Kafka 缓冲 → Loki/ES 存储,应用只管写本地文件/标准输出。
四、Tracing 与 OpenTelemetry:一套 SDK 采三样
text
┌─────────────────────────────┐
应用(OTel SDK) │ OTel Collector │
┌───────────────┐ │ ┌─────────┐ ┌───────────┐ │ ┌────────────┐
│ OTel API │ batch │ │ receive │─►│ process │──┼──►│ Prometheus │
│ (Trace/Metric │────►│ │ (OTLP) │ │ 过滤/改写/ │ │ ├────────────┤
│ /Logs 统一) │ │ └─────────┘ │ 采样/脱敏 │ │ │ Jaeger/ │
└───────────────┘ │ └─────┬─────┘ │ │ Tempo │
埋点写 API, │ │批处理重试 │ ├────────────┤
实现可换 (noop/ │ ▼ │ │ Loki/ES │
OTLP/厂商) │ export(多后端) │ └────────────┘
└─────────────────────────────┘
span 传播:traceparent: 00-{trace_id}-{parent_span_id}-{flags} ← W3C 标准头
serviceA ──HTTP──► serviceB ──MQ──► serviceC
span1(200ms) span2(150ms) span3(40ms) 同一 trace_id 串成树OTel 的价值主张一句话:API 与 SDK 分离 + Collector 中间层,后端(Prometheus/Jaeger/Loki)可替换。埋点代码只写一次,换监控栈不动业务代码。三个实践要点:采样策略分层(头部采样按比例 1%~10% 打底,尾部采样保错误与慢请求 100% 落盘)。错误请求必须留样本。跨异步传递(线程池要包装 TaskDecorator、MQ 要把 context 塞进消息头,否则链路在异步边界断掉)。span 命名要稳定(ORDER_CREATE 而非带 id 的动态串,否则聚合无意义)。
五、四大黄金信号:SRE 的报警四问
Google SRE 四信号,任何服务的健康盘面就这四个:
| 信号 | 问的问题 | 典型来源 | 常见病根 |
|---|---|---|---|
| 延迟 Latency | 请求多快(要区分成功/失败的延迟) | Histogram P95/P99 | 下游慢、锁竞争、GC |
| 流量 Traffic | 扛了多少请求 | QPS / 并发 / 消息速率 | 突发、攻击、重试风暴 |
| 错误 Errors | 多少请求失败 | 5xx 率 / 业务错误码 | 依赖故障、脏数据、发布 |
| 饱和度 Saturation | 资源到顶了吗 | CPU / 内存 / 连接池 / 队列堆积 | 容量不足、泄漏、慢消费 |
延迟信号最容易答错的细节:P99 必须与错误率一起看。故障时快失败(超时熔断)会把 P99"变好"。只看延迟会得出系统变健康的荒谬结论。所以延迟要按成功/失败分桶统计。
追问链
面试官:为什么 Prometheus 选拉模型,推模型(如 StatsD)差在哪?
拉模型把控制权收敛在服务端:目标清单、抓取间隔、健康状态(scrape 失败即目标异常信号)一处可见。应用只暴露文本端点零依赖,配合 K8s 服务发现扩缩容免配置。推模型应用侧要配 agent、网络策略要开洞、且"没推上来"无法区分"没数据"和"挂了"。短任务推不了是拉模型的盲区,Push Gateway 补位。
面试官:trace_id 是怎么跨服务传的?异步/线程池里丢了怎么办?
同步 HTTP:入口从 W3C traceparent 头提取(没有则生成),出口把当前 context 注入请求头,下游 SDK 自动接续。异步两处断点:线程池直接执行 Runnable 会丢 ThreadLocal 上下文——要包装(Spring 的 TaskDecorator 或 OTel 的 ContextWrappingExecutorService)。MQ 消息要显式把 context 编码进消息属性,消费端提取续链。链路的完整性取决于最弱的一段边界处理。
面试官:日志采样还是全量?tracing 采样率怎么定?
日志:ERROR/WARN 全量,INFO 按服务分级(交易类全量、边缘服务可采样),DEBUG 默认关。tracing:分层数字比单一百分比科学——头部采样 5%~10% 打底保流量画像。尾部采样规则兜底(error=true 100% 留、latency > 1s 100% 留),既控成本又保证"出问题的请求一定有完整链路"。
面试官:Histogram 和 Summary 都能算分位数,用哪个?
Summary 在客户端预算分位数(不可聚合:实例 A 的 P95 和实例 B 的 P95 无法合并成全局 P95)。Histogram 存桶分布,服务端 histogram_quantile 可跨实例聚合。多实例微服务是常态,所以默认 Histogram。桶边界要按业务延迟量级设计(默认桶对毫秒级接口全挤在一桶,P95 算出来是废数)。
面试官:指标报警了但 tracing 采样没采到这条慢请求,怎么办?
两招配合:其一,尾部采样规则保证慢请求全留(阈值与报警阈值联动)。其二,指标报警信息里带上维度(哪个 service/uri/instance)。即使无 span,也能按时间窗 + 维度从日志(trace_id 在结构化日志里)反查个体。所以三件套互为索引的设计(指标维度 ↔ span 标签 ↔ 日志字段)不是优雅问题,是可运维性问题。
面试官视角小结
- 三支柱分工:Metrics 面(健康/报警)、Tracing 线(跨服务归因)、Logging 点(现场还原)。"有没有问题→在哪一段→当时发生了什么"是标准动作顺序。支柱错用(grep 做监控、指标做排查)是主要扣分点。
- Prometheus:拉模型三理由(控制权收敛/目标健康可见/应用零依赖)。Push Gateway 是短任务补丁。PromQL 四要点(rate 求率、histogram_quantile 分位数、by 定分辨率、for 防毛刺)。聚合分位数必须 Histogram。
- 日志:结构化 JSON + MDC 注入 trace_id 贯穿——trace_id 是三件套的连接键。级别纪律与采集链路(file → agent → 缓冲 → 存储)。
- OTel:API/SDK 分离 + Collector 中间层换后端不动代码。头部+尾部分层采样(错误与慢请求必留)。异步边界(线程池/MQ)显式续 context,span 命名要稳定。
- 黄金信号:延迟(成功失败分桶)、流量、错误、饱和度。P99 与错误率必须联看,快失败会"美化"延迟。
- 联动:拿到信号后的完整排查见问题定位步骤。定位到瓶颈后的优化菜单见性能优化方法论。
2026-10 新增(Round1 补缺)。