凌晨三点,急诊室大门被推开,一位病人被推进抢救室,主诉是“系统崩溃”。作为值班医生,你面前摆着三样东西:监护仪上跳动的生命体征曲线、半尺厚的病历本、流调员递来的行动轨迹图。只看监护仪,你知道病人心率140很危险,却不知道为什么;翻完病历本,你了解了病人有高血压史,但不清楚今天发生了什么;端详轨迹图,你发现他刚从健身房出来,却无法判断身体内部出了什么状况。只有把三份材料拼在一起,你才能快速整合出结论:剧烈运动诱发心梗,马上送导管室。 这就是云原生系统的日常。微服务架构让一个用户请求要穿过十几个服务,就像病人要在医院里经历挂号、抽血、CT、专家门诊。系统的“生命体征”就是指标(Metrics)——周期性采样的数值,回答“系统现在健康吗”;“病历记录”就是日志(Logs)——每行带时间戳的事件描述,回答“刚才发生了什么”;“行动轨迹”就是链路追踪(Traces)——一个请求经过每个服务的完整路径,回答“为什么变成这样”。三者合在一起,构成可观测性的三支柱。本篇我们将延续这个“急诊科诊断”的比喻,把三支柱的架构、协同方式和工程落地讲清楚。
可观测性三支柱是 AI 工程团队的“急诊诊断能力”——掌握它,你就能在系统故障时用最短时间从海量数据中定位根因,把平均故障恢复时间(MTTR)缩短一个量级。
目录
可视化演示
演示操作系统进程的五种基本状态及转换条件:New→Ready→Running→Waiting/Terminated,通过步进控制逐个展示状态转换过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
进程状态转换 五种基本状态:New/Ready/Run/Wait/Exit 调度器控制Ready与Running之间的转换 I/O等待导致Running转入Waiting状态
三支柱概述
想象你是一家三甲医院的急诊科主任。凌晨三点,一个病人被推进抢救室,主诉是“系统崩溃”。你面前摆着三样东西:生命体征仪上跳动的心率曲线、桌上半尺厚的病历本、流调员递来的行动轨迹图。单独看任何一样,你都只能得到片面结论——心率快可能是发烧也可能是恐慌;病历厚说明病史长但不说明当下危机;轨迹图能看出去了哪里却看不出身体内部出了什么问题。只有三样齐备,你才能判断:这是急性心梗,马上送导管室。
云原生系统的可观测性,就是为每套系统配备这“急诊三件套”。指标(Metrics)是系统的生命体征——每秒请求数、CPU 使用率、错误率,以数值形式周期性采集,回答“系统现在健康吗”。日志(Logs)是系统的病历本——每一行带时间戳的文本记录,回答“系统刚才发生了什么”。链路追踪(Traces)是系统的行动轨迹——一个请求从入口到每个微服务的完整路径,回答“一次请求具体经过了哪里、耗时多久”。
这三者不是三个独立工具,而是一套协同诊断体系。监控告诉你“出问题了”,可观测性告诉你“为什么出问题”。三支柱的价值在于:指标报警时,你能顺着日志找到具体错误,再沿链路追踪定位到是哪个服务的哪个环节拖慢了整个请求。
指标监控:生命体征仪表盘
急诊室里的心电监护仪从不关心病人昨晚吃了什么,它只以固定频率捕捉心率、血压、血氧饱和度这些关键数值。一旦数值越界,立刻响起刺耳警报。指标监控就是系统的监护仪:用数值描述系统状态,以固定时间间隔采样,用最少的存储换来最实时的健康感知。
一个指标由三要素构成:指标名称(如 http_requests_total)、标签(如 method="GET"、status="500")以及采样值。这就是指标的数据模型——把标签想象成监护仪上的不同导联:心电图导联贴胸口,血氧夹夹手指,每个标签都从不同维度切分同一个指标。计数器只增不减(总请求数),仪表盘可升可降(当前连接数),直方图记录分布(请求耗时分布)——这对应监护仪上不同的波形模式。
采集方式上,系统通过 Prometheus 这类工具定期“拉取”(Pull)各服务的指标端点,或由服务主动“推送”(Push)到网关。配上告警规则,当指标超过阈值(如 P99 延迟超过 500ms),就触发告警通知值班工程师。
指标监控的局限也很明显:它只告诉你“心率 140”,但不告诉你“为什么 140”。这时就需要翻开病历本——日志。
- 错误请求数(分子)统计周期内返回状态码为 5xx 或业务定义失败的请求总数,代表“病人出现不良反应”的次数。
- 总请求数(分母)同一周期内所有进入系统的请求数量,构成判断比例的基数,对应“所有就诊人次”。
- 乘以 100%将比值转换为百分比形式,方便与告警阈值(如 1%、5%)直接比较。
日志管理:病历记录本
每个病人都有一本厚厚的病历,医生每次查房都会在上面写一行:“体温 38.5℃,主诉头痛,开药布洛芬”。日志就是系统的病历本——每一行日志记录一个离散事件,包含时间戳、级别、来源和详细描述。它最贴近“发生了什么”的原始事实,因此排障时工程师的第一反应永远是“去看看日志”。
但病历本有个麻烦:它太厚了。一个中等规模的系统每天产生几十 GB 甚至 TB 级日志,像病历堆满整间档案室。如果每行日志都是非结构化自由文本(“user 123 visit failed“),事后检索就像在病历堆里找某一句话——大海捞针。结构化日志为此而生:用 JSON 等格式固定字段,让每行日志成为可被机器解析的”结构化病历”。
日志的采集通常通过 Filebeat/Fluentd 等代理读取日志文件或监听标准输出,解析后送入 Elasticsearch、Loki 等存储。为提高检索效率,日志系统通常建立倒排索引,支持按关键字、时间范围、字段值组合查询。
但病历本只记录“出现过什么”,记录不了“一件事如何在科室之间流转”。要追踪流程路径,需要第三份材料——行动轨迹图。
- timestamp(时间戳)记录事件发生的精确时间,精确到毫秒甚至纳秒,是日志关联分析的核心排序键。
- level(级别)标识日志的严重程度,常见 DEBUG/INFO/WARN/ERROR 四级,类似病历上的“注意”“紧急”标签。
- trace\_id(追踪号)把同一请求的所有日志串联起来的身份证号,这是三支柱关联的“病例编号”。
链路追踪:病毒传播链流调
疫情期间,流调员的职责是画出一条传播链:0 号病人去了超市,接触了收银员,收银员又接触了保安……每一个接触点、每一次停留时长都记录在案。链路追踪干的就是这件事:针对一个用户请求,记录它穿过的每一个服务、每个服务内的操作及耗时,形成一棵“调用树”。
链路追踪的核心概念是 Span(追踪片段)。一个 Span 代表一次带时间范围的操作,比如“订单服务调用支付服务”这一过程。多个 Span 通过父子关系组成一棵树,树的根就是用户请求的入口,整棵树称为一个 Trace。每个 Span 都携带 trace\_id(整棵树的唯一编号)、span\_id(自己的编号)、parent\_span\_id(父节点编号)和时间戳。
由树形结构,我们可以快速计算每个环节的耗时占比——比如发现整个请求 2 秒里 1.5 秒耗在数据库查询上。但要让 Span 跨服务传递,请求每次调用下游服务时,都必须把当前的 trace\_id、span\_id 打包进 HTTP 头(如 traceparent 头),下游服务收到后继续沿这条链记录。这个过程叫上下文传播。
有了三份报告,下一步的关键是:如何把它们拼成一份完整的诊断书?
- Trace(整条链)一次外部请求引发的所有 Span 的集合,对应完整传播链。
- Span(单个接触记录)一次具体的操作片段,包含开始时间、结束时间和操作名,对应流调表上的一行。
- parent 关系(接触关系)每个 Span 指向其父 Span,形成树形结构,对应流调中“谁接触了谁”。
关联分析:三种报告合体诊断
急诊医生从不只看一张单子:心电图的异常 ST 段(指标)提示心梗,化验单的肌钙蛋白升高(日志)印证诊断,家属描述的“从健身房出来就倒地”(轨迹)则解释了诱因。在可观测性里,三种数据的关联分析,才是从“发现异常”走向“定位根因”的桥梁。
关联的枢纽是一个公共字段——trace\_id。当请求穿过订单服务时,订单服务的日志会带上 trace\_id;支付服务被调用时,也会生成带同一 trace\_id 的日志和 Span;整个过程中产生的指标同样可以用标签标记路由维度。这样,当某条链路追踪变慢时,我们可以带着 trace\_id 去日志系统搜出这条链路上的所有日志,再结合指标的全局画像,快速锁定根因。
- trace\_id 关联日志与追踪日志行中记录 trace\_id,查询时以该 id 作为过滤条件,得到同一请求的全部日志。
- 服务名+时间窗关联指标链路中指出性能瓶颈的服务名和时段,再用该服务名和相近时间窗检索指标曲线,观察资源水位。
- 三向汇总形成结论将三类证据汇总,得到“哪个服务、什么时间段、因什么资源问题导致请求失败”的完整结论。
OpenTelemetry:统一体检标准
过去的问题是:医院里每台设备都用自己的数据格式——心电图仪输出的纸带、化验室的表格、流调系统的 GIS 图,想合并分析必须先手工誊抄。微服务架构早期也一样:Prometheus、ELK、Jaeger 各说各话,工程师被迫在三个工具间反复切换对齐。
OpenTelemetry(简称 OTel)是 CNCF 推出的统一可观测性标准,相当于给整个行业规定了“电子病历”的格式。它提供一套 API 和 SDK,让应用只需接一次 SDK,就能同时生成指标、日志、追踪三种数据,并通过统一的导出协议(OTLP)发送到任何后端——无论是开源的 Prometheus/Jaeger,还是商业的 Datadog/阿里云 ARMS。
OTel 的目标不是取代 Prometheus 或 ELK,而是做“设备与系统之间的翻译官”——让各种后端消费统一格式的数据。这大大降低了团队切换工具栈的门槛,也使得“一次埋点,多处消费”成为可能。
标准统一之后,现实中的落地还面临一个残酷问题:数据采集是要花钱的。
- App 接入 OTel SDK应用代码中引入 SDK,创建 TracerProvider、MeterProvider 和 LoggerProvider 三个入口。
- SDK 统一处理三种信号SDK 内部负责采样、上下文注入、批量导出,应用代码与具体后端解耦。
- OTLP 导出到后端通过 gRPC/HTTP 将打包好的数据传给任意接收 OTLP 的后端,更换后端无需改业务代码。
落地实践:体检套餐的性价比设计
体检中心不会建议客户做“全身体检每天一次”——太贵且没必要。可观测性落地最大的坑,恰恰是“全量采集、全量存储、全量保留”。高基数的指标(每个用户一个标签)、全量日志、100% 采样率的链路追踪,会让存储和计算成本随业务流量线性爆炸。因此,落地三支柱的第一课是学会做减法。
成本优化有三板斧。第一板斧:指标降基数——把 user\_id 这类高基数标签从指标中剥离,只保留 service、api 等低基数标签;第二板斧:日志分层——热日志存高性能存储 7 天,冷日志压缩转储到对象存储 90 天,甚至只保留 ERROR 级别以上的完整上下文;第三板斧:采样策略——这是链路追踪成本控制的核心手段。
除了采样,更聪明的做法是场景驱动的采集策略:核心交易链路全量采样(如支付、下单),非核心链路(如商品浏览)10% 采样;错误请求全量保留,因为排障价值最高。最后,把三支柱的接入纳入 CI/CD 流水线,让每个新服务在发布前必须通过“可观测性体检”(埋点覆盖率检查),才算真正把三支柱落地成工程体系。
- 目标每秒 Trace 数(分子)根据可接受的存储成本设定每秒最多入库的 Trace 数,如 100 个/秒。
- 实际每秒 Trace 数(分母)系统当前真实的请求量,即流量峰值。当流量超过目标,采样率自动调低。
- min 限幅流量低时采样率也不会超过 100%,即不会放大数据量。
AI 应用场景
场景一:AI 大模型服务的故障根因定位
阶段:模型服务上线后的运维监控
解决的问题:大模型推理服务链路长(网关→鉴权→推理→向量库→缓存),一次超时可能由任意一环引起。传统方式下工程师逐台查日志、对时间线,效率极低。借助三支柱,指标告警(首 token 延迟升高)触发后,通过 trace\_id 在日志中拉取该请求完整链路,直接定位是向量库查询超时还是 GPU 排队。
具体案例:某 AI 应用在晚高峰出现“回答变慢”投诉。指标面板显示 P99 延迟从 1.2s 飙到 4s;链路追踪发现 80% 耗时集中在 embedding 服务调用向量数据库;日志显示该时段向量库连接池被慢查询占满。三份证据合体后,运维将连接池扩容并加了限流,故障 15 分钟内恢复。
场景二:AI 训练任务的资源性能优化
阶段:分布式训练的资源分析与调优
解决的问题:大模型分布式训练中,GPU 利用率低往往源于数据加载瓶颈或通信阻塞。通过指标监控(GPU 利用率、网络吞吐)发现异常,通过链路追踪分析数据加载流水线中每个阶段的耗时占比,结合结构化日志(如数据迭代器报错)定位根因。
具体案例:训练团队发现 GPU 利用率只有 40%。链路追踪显示 DataLoader 的 prefetch 阶段耗时占 55%,指标显示 host CPU 已打满。团队据此将数据预处理改为异步多进程 + 缓存,GPU 利用率提升到 85%,整体训练时间缩短 30%。
Python 演示
"""链路追踪模拟:展示 Trace/Span 与三支柱协同使用 OpenTelemetry 风格的简化实现"""import timeimport uuidimport jsonfrom collections import defaultdict class Span: def __init__(self, name, trace_id, span_id, parent_id=None): self.name = name self.trace_id = trace_id self.span_id = span_id self.parent_id = parent_id self.start_time = time.time() self.end_time = None self.attributes = {} self.children = [] def finish(self): self.end_time = time.time() self.attributes["duration_ms"] = round( (self.end_time - self.start_time) * 1000, 2 ) return self.attributes["duration_ms"] def trace_exporter(root_span): """导出 span 树为字典列表""" lines = [] def walk(span, depth): dur = span.attributes.get("duration_ms", 0) prefix = " " * depth lines.append( f"{prefix}span: {span.name} ms={dur} " f"trace={span.trace_id[:8]} parent={span.parent_id}" ) for child in span.children: walk(child, depth + 1) walk(root_span, 0) return "\n".join(lines) def simulate_request(order_id): """模拟一次订单请求:订单->支付->库存""" trace_id = uuid.uuid4().hex root = Span("POST /order", trace_id, uuid.uuid4().hex) db_span = Span("cache.order_cache", trace_id, uuid.uuid4().hex, root.span_id) time.sleep(0.05) cache_log = {"level": "INFO", "service": "order", "trace_id": trace_id} db_span.finish() root.children.append(db_span) pay_span = Span("payment.charge", trace_id, uuid.uuid4().hex, root.span_id) time.sleep(0.12) if order_id % 5 == 0: pay_span.attributes["error"] = "insufficient_balance" err_log = {"level": "ERROR", "service": "payment", "trace_id": trace_id} else: err_log = None pay_span.finish() root.children.append(pay_span) inv_span = Span("inventory.check", trace_id, uuid.uuid4().hex, root.span_id) time.sleep(0.03) inv_span.finish() root.children.append(inv_span) root.finish() logs = [cache_log, err_log] if err_log else [cache_log] return root, logs def main(): fake_metrics = defaultdict(int) print("=== 可观测性三支柱模拟 ===") for order_id in range(1, 8): root, logs = simulate_request(order_id) fake_metrics["total_orders"] += 1 has_error = any(l["level"] == "ERROR" for l in logs) if has_error: fake_metrics["error_orders"] += 1 print(f"\n--- 请求 #{order_id} ---") print("【链路追踪 Trace 树】:") print(trace_exporter(root)) print("【结构化日志】:") for log in logs: print(" " + json.dumps(log, ensure_ascii=False)) error_rate = fake_metrics["error_orders"] / fake_metrics["total_orders"] * 100 print("\n【指标监控】") print(f" 总订单数 = {fake_metrics['total_orders']}, " f"错误率 = {error_rate:.1f}%") if __name__ == "__main__": main()
class Span每个 Span 记录操作名、trace_id、span_id、父 id 和起止时间,对应追踪概念的单条“流调记录”。simulate_request(order_id)模拟一次完整请求,依次调用三个服务并生成子 Span,相当于让请求穿过三个“科室”。trace_exporter(root_span)递归遍历 Span 树并格式化输出,展示请求的完整调用链。cache_log` / `err_log每个服务产生结构化日志并携带 trace\_id,这是后续关联分析的“住院号”。fake_metrics["total_orders"]记录总请求数和错误数,最后计算错误率,对应指标监控的“生命体征”。
常见误区
误区我只要把日志打印得足够详细,出问题翻日志就能解决,指标和链路追踪没必要搞那么复杂。
正解日志是病历本,但病历本回答不了“病人现在心率多少”和“病毒是怎么传播的”。一个请求跨 10 个服务,每处都打印日志,想串起来得手动搜索 trace\_id,而链路追踪直接给出调用树。三支柱各司其职:指标发现异常,追踪定位环节,日志补充细节,缺一不可。
为何常见:排障时第一个动作通常是看日志,且日志信息最直观、最容易理解,容易让人产生“只要话说得够多就一定能找到原因”的错觉。
误区给指标加上 user\_id、order\_id 这类高基数标签,排查时就能按用户维度精确查询了,多方便。
正解高基数标签(每个用户/订单一个值)会让指标基数爆炸——每个唯一组合都是一条时间序列,存储和查询成本飙升,Prometheus 这类 TSDB 几乎无法支撑。指标设计应遵循“维度有限、基数可控”原则,高基数信息交给日志和追踪保存。
为何常见:人脑天然希望数据维度越细越能定位问题,没意识到指标系统的高基数代价与其他系统完全不同。
误区给服务加个 SkyWalking Agent 就算做链路追踪了,三支柱也就齐了。
正解追踪不只是“接入一个工具”,而是要求请求在跨服务时通过 HTTP 头传递 trace 上下文,否则每个服务各自生成的 Span 无法串成 Trace 树。同时要设计好 Span 的命名、属性和采样策略,否则会产生大量无意义的 Span 数据。
为何常见:工具接入是可见的动作,而上下文传播是隐性的架构设计,初学者往往只看到前者,误以为“装了工具=完成了追踪”。
误区可观测性就得指标、日志、追踪全部搭建完成才算数,缺一样就是不合格。
正解落地是渐进式的。存量系统可以先做指标+日志,后续逐步接入追踪;对单体应用,追踪的收益不如微服务大。关键是根据系统形态和排障场景决定优先级,而非机械追求三件套齐全。
为何常见:概念宣传中“三支柱”是并列关系,容易让新手误以为是必须同时满足的硬性要求,忽略了落地的成本与场景适配。
面试题
Q1. 可观测性三支柱指的是哪三样数据?
- A.指标、日志、链路追踪← 正确答案
- B.CPU、内存、磁盘
- C.告警、监控、可视化
- D.请求、响应、错误码
答案:A。三支柱是 Metrics(指标)、Logs(日志)、Traces(链路追踪),对应系统的生命体征、病历和行动轨迹。B 是典型的资源指标,C 是可观测性的应用层能力,D 是单次请求的数据维度,都不构成三支柱的概念结构。
Q2. “服务每秒请求数持续上升但错误率为 0”,用三支柱中最适合回答“系统现在健康吗”的是?
- A.日志
- B.指标← 正确答案
- C.链路追踪
- D.告警规则
答案:B。指标以周期性数值描述系统状态的健康度,相当于生命体征仪,能直接回答“现在健康吗”。日志和追踪回答“发生了什么、为什么发生”,告警是对指标阈值的主观设定,自身不产生可观测性数据。
Q3. 关于指标数据模型,以下哪个说法正确?
- A.指标由名称、标签、采样值组成← 正确答案
- B.指标标签基数越高越好
- C.计数器可以随时减少
- D.指标是唯一能定位根因的数据
答案:A。指标模型是“名称 + 标签 + 值”,标签需控制基数避免存储爆炸,计数器只增不减。定位根因需要三支柱协同,单靠指标只能发现问题而非根因。
Q4. 结构化日志相比非结构化日志的优势有哪些?
- A.便于机器解析和检索← 正确答案
- B.支持按字段值过滤查询← 正确答案
- C.可以完全替代链路追踪
- D.降低日志对时序数据库的依赖
答案:AB。结构化日志以固定字段(JSON)存储,支持字段级检索,优势是“好查”。但结构化只是格式改进,替代不了追踪的树状结构,也与时序数据库无关。
Q5. 在链路追踪中,Span 的 parent\_span\_id 字段起到什么作用?
- A.记录服务所在主机的 IP 地址
- B.标识该 Span 的父节点,构成调用树结构← 正确答案
- C.记录 CPU 消耗的关联进程号
- D.标记该 Span 所在的服务名称
答案:B。parent\_span\_id 指向父 Span,将零散的 Span 串成 Trace 树,类似流调中“谁接触了谁”的关系记录。主机 IP 和进程号是资源标签,服务名称由 Span 的 attributes 标记,都不是 parent 字段的作用。
Q6. trace\_id 在可观测性关联分析中扮演什么角色?
- A.唯一标识一次外部请求,关联日志与追踪数据← 正确答案
- B.表示服务启动的唯一编号
- C.是告警规则的主要编号
- D.用于负载均衡的会话标识
答案:A。trace\_id 是每次请求全局唯一的“住院号”,日志、指标、追踪都在记录时携带它,查询时用该字段关联三者。它不是服务编号、告警编号或会话标识,后两者与请求级关联无关。
Q7. 关于 OpenTelemetry 的描述,正确的有哪些?
- A.它统一了指标、日志、追踪的生成 API← 正确答案
- B.它取代了 Prometheus 和 Jaeger 成为唯一后端
- C.应用接一次 SDK 即可导出三种信号← 正确答案
- D.它要求后端必须是云厂商的托管服务
答案:AC。OTel 提供统一 API/SDK 产生三类信号,并用 OTLP 导出到任意后端。它是“翻译官”而非“整套医院”,Prometheus/Jaeger 依然是常见后端,也不限制云厂商;B、D 与 OTel 的定位相悖。
Q8. 为控制链路追踪存储成本,最核心的手段是?
- A.将日志全部删除只留追踪
- B.对 Trace 设置合理的采样策略← 正确答案
- C.用 MySQL 代替时序数据库
- D.关闭所有服务的告警
答案:B。采样策略(如按流量比例、按错误全采)是控制 Trace 量级的主要手段,对应“只对代表性病例做流调”。删日志会破坏完整性,换数据库不解决数据量问题,关告警则失去监控能力。
Q9. 某请求 P99 延迟从 1s 升到 5s,工程师想确认瓶颈是数据库还是下游服务,最直接的证据来自哪里?
- A.该请求的 Trace 瀑布图中各 Span 的耗时占比← 正确答案
- B.聚合 CPU 使用率曲线
- C.服务进程的启动参数
- D.代码仓库的最近提交记录
答案:A。Trace 瀑布图逐段展示请求在数据库、下游服务各环节的耗时,一眼看出哪个 Span 占比最大——这正是“行动轨迹”的用途。CPU 曲线只看整体资源,启动参数和代码提交不能定位运行时的耗时分布。
Q10. 在真实微服务系统中通过三支柱定位“订单服务频繁超时”根因时,有效的协同排查路径有哪些?
- A.从链路追踪确定超时集中在下游支付服务← 正确答案
- B.用 trace\_id 检索日志确认支付服务的具体错误← 正确答案
- C.用指标时间窗比对支付服务所在节点的 CPU 负载← 正确答案
- D.直接删除订单服务以消除故障
答案:ABC。三支柱协同的经典路径是:追踪定位环节 → 日志查细节 → 指标看资源水位,三者通过 trace\_id 和服务+时间窗关联,逐步收窄根因。D 是极端且错误的处置,不属于可观测性流程。