- 引言 — 什么是可观测性
- 三大支柱 — 日志、指标、链路追踪
- 信号如何相连 — trace_id 与 exemplar
- 日志 — Loki vs OpenSearch
- 分布式链路追踪 — OpenTelemetry 成为标准
- LLM 可观测性 — 为什么需要专门的工具
- 合而为一 — 以 OpenTelemetry 为中心的技术栈
- 结语
- 参考资料
引言 — 什么是可观测性
系统变慢了。用户说"付款付不出去"。可服务器运行正常,CPU 也没问题,错误日志也看不出什么名堂。问题到底出在哪里?能回答这个问题的能力,正是可观测性(observability)。
可观测性常常和监控被混为一谈。监控看的是"预先设定好的指标是否落在正常范围内",可观测性看的是"能否用系统内部状态,回答从系统外部抛出的任意问题"。前者盯着已知的未知(known unknowns),后者深入挖掘未知的未知(unknown unknowns)。
构成这种能力的素材,通常被称为三大支柱(three pillars):日志、指标、链路追踪。本文会逐一梳理这三大支柱,对比日志存储(Loki vs OpenSearch)和分布式链路追踪(OpenTelemetry、Jaeger、Tempo)的选型,再延伸到近来新兴的LLM 可观测性(Langfuse 等)。
三大支柱 — 日志、指标、链路追踪
这三种信号从不同角度记录同一件事。
- 日志(logs):特定时间点发生的单个事件记录。"12:03:11,订单 4821 付款失败,原因:银行卡被拒。"最详细,但量很容易爆炸。
- 指标(metrics):按时间聚合的数值。"每秒请求数""p99 延迟""错误率"。存储成本低,适合仪表盘和告警,但没有单个事件的上下文。
- 链路追踪(traces):一次请求跨越多个服务的完整旅程。从 API 网关出发,经过订单服务、支付服务、数据库,展示每一段各花了多久。
最近,持续性能剖析(continuous profiling)常被拿来当作第四大支柱。它在生产环境里以函数为单位,持续采样 CPU、内存,能在比链路追踪更深的代码层级回答"哪个函数在吃 CPU"这个问题。Grafana Pyroscope、Parca 是这类工具的代表。
关键不在于分别去看这三种信号,而在于把它们串联起来看。在指标里发现"错误率飙升了",跳到那个时间点的链路追踪确认"支付服务变慢了",再看挂在那条链路上的日志读出原因:"银行卡发卡方超时"。这条链路正是可观测性的精髓所在。
信号如何相连 — trace_id 与 exemplar
把三种信号粘合起来的,是关联标识符(correlation ID),其中最核心的就是 trace_id。
最实用的连接方式是把 trace_id 埋进日志里。如果结构化日志把 trace_id 作为字段一并记录下来,就能在查看某条链路时,过滤出"只属于这次请求"的日志。反过来,看到一条错误日志时,也能用它的 trace_id 打开完整的链路。
{
"timestamp": "2026-07-03T12:03:11.482Z",
"level": "ERROR",
"service": "payment-service",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"message": "card declined",
"order_id": 4821
}
指标和链路追踪通过 exemplar(样本)相连。exemplar 是给指标数据点附上一个代表性 trace_id 的技巧。比如给"p99 延迟"直方图里那个慢的桶,附上一个真实的慢请求的 trace_id,就能在仪表盘上点击图表里冒出来的那个尖峰,直接跳转到那条链路。它是从指标(发生了什么)通往链路追踪(为什么会这样)的一座桥。
让这种连接成立,是可观测性设计的一半工作。信号再多,只要彼此没有连起来,遇到问题时就只能靠肉眼在三个分离的窗口之间来回比对,苦不堪言。
日志 — Loki vs OpenSearch
日志存在哪里、怎么存,会大幅左右成本和检索体验。两大代表阵营是 Grafana Loki 和 OpenSearch(前身是 Elasticsearch 系)。
Loki — "日志界的 Prometheus"
Grafana Loki 的哲学,用一句话概括就是"不对日志正文建索引"。Loki 不会对日志全文建立倒排索引,而是只对标签(label)建索引。日志正文本身经压缩后,原样丢进廉价的对象存储(S3、GCS 等)里。所以就像 Prometheus 用标签来处理指标一样,Loki 也用标签来处理日志。"日志界的 Prometheus"这个绰号正是由此而来。
它的查询语言是 LogQL。先用标签缩小日志流的范围,再在其中过滤文本。
{app="payment-service", level="error"} |= "card declined" | json | order_id="4821"
这里最前面的花括号部分是标签选择器(建了索引的那部分),从 |= 往后则是正文过滤器。得益于这种结构,索引很小,存储成本低,运维也轻量。但如果没能靠标签充分收窄范围,就对海量时间段做全文搜索,速度会变慢。标签设计决定成败。
要留意的陷阱是高基数(high cardinality)标签。如果把 user_id、trace_id 这类取值种类会爆炸的东西当作标签,日志流会被切分成数百万条,Loki 会急剧变慢。这类值应该放进日志正文,靠过滤器去查,而不是当作标签。
OpenSearch — 强大的全文检索
OpenSearch(以及它的源头 Elasticsearch)走的是完全相反的路线。它几乎对日志的每一个字段都建立倒排索引。这样一来,基于任意字段做复杂的全文检索、聚合、分面分析都很快,还配有 Kibana / OpenSearch Dashboards 这种强大的检索 UI。
同样的查询用 OpenSearch 查询 DSL 写出来是这样的。
{
"query": {
"bool": {
"must": [
{ "match": { "service": "payment-service" } },
{ "match": { "message": "card declined" } },
{ "term": { "order_id": 4821 } }
]
}
}
}
代价是"重"。因为什么都要索引,存储空间和内存消耗都大,集群运维(分片、堆内存、再均衡)也很费手。日志量越大,基础设施成本涨得越陡。
该怎么选
- Loki:如果对成本敏感,日志的用法主要是"靠标签缩小范围,扫最近的时间段",而且已经身处 Grafana、Prometheus 生态,这个选择很自然。
- OpenSearch:如果经常需要对任意字段做强大的全文检索和复杂的聚合分析(安全日志分析、审计、SIEM 等),并且能承担相应的基础设施成本,它会很强大。
结构化日志与日志级别
不论选哪种存储,都有一条共同的原则。日志最好不要写成给人读的句子,而应该记录为给机器解析的结构化数据(JSON)。相比 "2026-07-03 payment failed for order 4821" 这样的字符串,像上面那样字段分开的 JSON,在过滤、聚合上占绝对优势。
日志级别(DEBUG、INFO、WARN、ERROR)是控制噪音的第一个旋钮。而对于流量大的服务,采样是必需的。成功请求的 INFO 日志没必要 100% 保留。比如可以只对正常请求采样 1%,错误则 100% 保留,这样在成本和信息量之间取得平衡。
分布式链路追踪 — OpenTelemetry 成为标准
在微服务里,一次用户请求会经过无数个服务。重建这段旅程的工作就是分布式链路追踪,而这个领域事实上的标准是 OpenTelemetry(OTel)。
为什么是 OpenTelemetry
OpenTelemetry 是 CNCF 旗下的厂商中立标准。过去每个可观测性工具都有各自独立的埋点 SDK,换后端就意味着要把代码里的埋点全部重来一遍。OTel 把这层埋点标准化了。代码只需要用 OTel 埋点一次,数据要发到哪里(Jaeger、Tempo、商业厂商等)以后只需改导出器(exporter)配置就能定,这正是为了解除锁定(lock-in)。
链路追踪的基本概念是这些。
- 链路(trace):一次请求的完整旅程。由唯一的 trace_id 标识。
- 跨度(span):构成一条链路的一个工作单元(例如"数据库查询""调用支付 API")。有起始和结束时间。
- 父子关系:跨度是嵌套的。"处理订单"这个跨度里,"检查库存""支付"这些跨度作为子级嵌入其中。这棵树构成了链路的形状。
- 属性(attributes)与事件(events):附在跨度上的键值元数据(例如 http.method、db.system),以及跨度生命周期中发生的带时间戳的记录。
上下文传播 — traceparent 头部
当服务 A 调用服务 B 时,要让两个服务的跨度归入同一条链路,trace_id 就必须传递过去。这种上下文传播(context propagation)的标准是 W3C Trace Context,也就是搭载在 HTTP 请求上的 traceparent 头部。它的值大致是版本、trace_id、父级 span_id、标志位用连字符连起来的形式。
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
版本 trace_id (128位) 父级 span_id 标志位(已采样)
接收方服务读取这个头部,把自己的跨度作为子级挂在同一个 trace_id 下面。就这样,链路能跨越服务边界连成一体。
OTel Collector 与埋点方式
OTel Collector 是放在应用和后端之间的一个独立进程。它接收(receive)多个服务发来的遥测数据,进行加工(process:批处理、采样、编辑属性),再导出(export)到想要的后端。应用只需要认识 Collector 一个,后端切换、多路发送、采样策略都能在 Collector 里集中管理。
埋点有两种方式。
- 自动埋点(auto-instrumentation):语言代理自动挂钩常用库(HTTP 服务器、数据库驱动、gRPC 等)并生成跨度。几乎不用改代码,能快速上手。
- 手动埋点(manual instrumentation):用 SDK 亲自打开跨度、附加属性。要精细捕捉业务逻辑中有意义的区间时需要用它。
一个最小化的 OTel SDK 初始化大致是这样(以 Node.js 为例)。
const { NodeSDK } = require("@opentelemetry/sdk-node");
const {
getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-http");
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "http://otel-collector:4318/v1/traces",
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
后端 — Jaeger 与 Tempo
收集到的链路数据要存储、检索,这就要用到链路追踪后端。
- Jaeger:发源于 Uber 的成熟开源链路追踪系统。自带 UI,提供链路检索、依赖关系图、时间线。
- Grafana Tempo:相当于 Loki 在链路追踪领域的对应物。它不对链路建索引,而是整条整条地存进廉价对象存储,大幅降低成本。默认按 trace_id 查询,和 Loki、Prometheus 在 Grafana 里能无缝联动。
采样 — head 与 tail
如果把链路 100% 全部存下来,数据量和成本都扛不住,所以要做采样。
- 头部采样(head-based):在链路开始的那一刻就立即决定"这条留,那条丢"。比如"只留 1%"。简单又便宜,但可能恰好把有问题(慢或出错)的链路丢掉。
- 尾部采样(tail-based):等链路结束后,看完整体再做决定。可以设置"有错误、或者比 p99 还慢的链路一律保留"这样的规则,把正常流量丢掉、把有问题的流量确实留下来。代价是必须缓冲到链路结束为止,给 Collector 带来更多负载和内存开销。
LLM 可观测性 — 为什么需要专门的工具
接下来转向最近迅速崛起的新领域。LLM 应用(聊天机器人、RAG、智能体)光靠传统可观测性工具是不够的。原因很明确。
- 非确定性:同样的输入,每次输出都不一样。"成功/失败"这种二元判断抓不住质量问题。
- 提示词与版本漂移:改一行提示词,质量就崩了的情况很常见。必须追踪是哪个提示词版本产生了哪个结果。
- 成本:LLM 调用按 token 计费。要看清是哪个请求、哪个链路步骤在吃多少 token,才能控制成本。
- 质量评估:哪怕延迟正常,答案也可能是错的,或者是幻觉(hallucination)编造出来的。正确与否需要单独做评估(eval)。
- RAG 调试:答案不对劲时,得分清楚原因是检索(retrieval)找错了文档,还是生成(generation)无视了文档。
Langfuse — LLM 调用的链路追踪
Langfuse 是一款正面应对这个问题的开源 LLM 可观测性平台。从概念上讲,它和分布式链路追踪很像。把一次用户交互作为一条链路,把其中每个步骤记录为嵌套的跨度。只不过跨度的内容是针对 LLM 特化的。每次 LLM 调用都会记录提示词、生成结果、token 用量、成本、延迟,链、智能体、检索这些步骤则以父子关系嵌套起来。
一个 RAG 流水线的链路,极度简化后大致是这样的形态。
{
"trace_id": "t_9f2c",
"name": "rag-chat",
"input": "退款政策是什么?",
"spans": [
{
"name": "retrieval",
"type": "retriever",
"input": "退款政策",
"output": ["doc-12", "doc-45"],
"latency_ms": 82
},
{
"name": "llm-answer",
"type": "generation",
"model": "claude-x",
"prompt_tokens": 1240,
"completion_tokens": 180,
"cost_usd": 0.0042,
"latency_ms": 1310
}
],
"scores": [{ "name": "helpfulness", "value": 0.9 }]
}
Langfuse 提供的核心功能是这些。
- 嵌套链路:把链、智能体、工具调用、检索可视化成层级结构。智能体调用了哪些工具、调用了几次,一目了然。
- 提示词版本管理:按版本存储、发布提示词,并把哪个版本产生了哪条链路关联起来。
- 评估与打分(evals/scores):给链路附加分数。LLM-as-a-judge 自动评估、基于规则的打分、人工标注,都能装进去。
- 用户反馈:把终端用户的点赞/点踩之类的反馈,和对应的链路关联起来,作为真实的质量信号。
其他选择 — LangSmith、Helicone、Phoenix
不是只有 Langfuse 一家。
- LangSmith:LangChain 团队做的商业平台。和 LangChain/LangGraph 紧密集成,链路追踪、评估、数据集管理都很细致。
- Helicone:特点是代理(proxy)方式。以代理的形式挂在 LLM API 前面,能在几乎不改代码的情况下加上日志、缓存、成本追踪。
- Arize Phoenix:开源,而且基于 OpenTelemetry(OpenInference)是它的强项。除了链路追踪和评估之外,在嵌入、漂移分析这类 ML 可观测性功能上也很强。
选型标准大致如此。以 LangChain 为中心就选 LangSmith,想不改代码快速接入就选 Helicone,看重开源和 OTel 标准就选 Phoenix 或 Langfuse。
合而为一 — 以 OpenTelemetry 为中心的技术栈
现在把整体串成一张图。近来常见的架构是把 OpenTelemetry 当作枢纽。
应用(各服务) ── OTel SDK ──▶ [ OTel Collector ]
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
[ Loki (日志) ] [ Tempo (链路追踪) ] [ Prometheus (指标) ]
└───────────── 在 Grafana 中统一查询 ──────────────┘
LLM 调用 ─────────────────▶ [ Langfuse (LLM 链路·成本·评估) ]
核心原理正是前面强调过的 trace_id 关联。应用用 OTel 埋点,发送给 Collector,Collector 把日志分给 Loki、链路分给 Tempo、指标分给 Prometheus。在 Grafana 里把三者放在同一个画面里,靠 trace_id 在日志和链路之间往返,靠 exemplar 从指标跳到链路。LLM 部分由 Langfuse 单独承接,只要带上同一个 trace_id,也能和普通链路连起来。
常踩的坑 — 成本与基数
- 基数爆炸:如果在指标标签或 Loki 标签里放进 user_id、request_id、trace_id 这类取值会无限增长的东西,时间序列、日志流就会暴增,存储会崩掉。高基数的值应该放进日志正文或链路属性里,标签只用取值种类有限的东西(服务名、地区、状态码等)。
- 不加节制地 100% 全量存储:把全部日志、链路都全量存下来,成本扛不住。要早早定好策略——正常的做采样,有问题的(错误、慢请求)则确实保留。
- 没有连起来的信号:如果不把 trace_id 埋进日志里,三大支柱就会各玩各的。一定要在埋点的早期阶段就注入 trace_id。
- 放任不管的 LLM 成本:提示词变长、重试变多时,token 成本会悄悄暴涨。要用 Langfuse 这类工具持续观察每个请求、每个步骤的成本。
决策清单
- 日志存储:以成本、标签为中心就选 Loki,需要强大的全文检索、分析就选 OpenSearch。
- 埋点:如果是新起步,从一开始就用 OpenTelemetry。能避开厂商锁定。
- 链路后端:低成本、身处 Grafana 生态就选 Tempo,需要独立、成熟的 UI 就选 Jaeger。
- 采样:不想漏掉有问题的链路就用尾部采样,追求简单、低成本就用头部采样。
- LLM:如果在运营 LLM 应用,一定要在 Langfuse/LangSmith/Helicone/Phoenix 中引入一个。
- 关联:不管用什么,都要把 trace_id 埋进每一条日志,把信号连起来。
结语
可观测性不是靠一个工具就能搞定的事,而是把日志、指标、链路追踪这三大支柱(以及正在崛起的性能剖析)用 trace_id 串起来,打造一个"能回答任意问题的系统"的工作。在日志这一环,Loki 和 OpenSearch 构成了成本与检索能力的两极;在链路追踪这一环,OpenTelemetry 统一了标准,Jaeger、Tempo 承接落地;再往上,Langfuse 这类工具正在填补 LLM 这个新的层次。
关键不在于"哪个最好",而在于"什么适合这套系统",尤其是"这些信号彼此之间是否连接在一起"。把三大支柱各自立好,再用 trace_id 牢牢串起来,下一次凌晨三点的故障,你就不用靠肉眼在三个窗口之间来回比对,而是可以一路点下去,直达根因。
参考资料
- OpenTelemetry 官方文档:https://opentelemetry.io/docs/
- W3C Trace Context 标准:https://www.w3.org/TR/trace-context/
- Grafana Loki 文档:https://grafana.com/docs/loki/latest/
- Grafana Tempo 文档:https://grafana.com/docs/tempo/latest/
- Jaeger 官方网站:https://www.jaegertracing.io/
- OpenSearch 文档:https://opensearch.org/docs/latest/
- Langfuse 文档:https://langfuse.com/docs
- Arize Phoenix:https://docs.arize.com/phoenix
현재 단락 (1/154)
系统变慢了。用户说"付款付不出去"。可服务器运行正常,CPU 也没问题,错误日志也看不出什么名堂。问题到底出在哪里?能回答这个问题的能力,正是**可观测性**(observability)。