LabHub

博客

可观测性完全指南:日志、链路追踪与 LLM 监控

한국어English日本語中文

引言 — 什么是可观测性

系统变慢了。用户说"付款付不出去"。可服务器运行正常,CPU 也没问题,错误日志也看不出什么名堂。问题到底出在哪里?能回答这个问题的能力,正是可观测性(observability)。

可观测性常常和监控被混为一谈。监控看的是"预先设定好的指标是否落在正常范围内",可观测性看的是"能否用系统内部状态,回答从系统外部抛出的任意问题"。前者盯着已知的未知(known unknowns),后者深入挖掘未知的未知(unknown unknowns)。

构成这种能力的素材,通常被称为三大支柱(three pillars):日志、指标、链路追踪。本文会逐一梳理这三大支柱,对比日志存储(Loki vs OpenSearch)和分布式链路追踪(OpenTelemetry、Jaeger、Tempo)的选型,再延伸到近来新兴的LLM 可观测性(Langfuse 等)。

三大支柱 — 日志、指标、链路追踪

这三种信号从不同角度记录同一件事。

最近,持续性能剖析(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 } }
      ]
    }
  }
}

代价是"重"。因为什么都要索引,存储空间和内存消耗都大,集群运维(分片、堆内存、再均衡)也很费手。日志量越大,基础设施成本涨得越陡。

该怎么选

结构化日志与日志级别

不论选哪种存储,都有一条共同的原则。日志最好不要写成给人读的句子,而应该记录为给机器解析的结构化数据(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)

链路追踪的基本概念是这些。

上下文传播 — 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 里集中管理。

埋点有两种方式。

一个最小化的 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

收集到的链路数据要存储、检索,这就要用到链路追踪后端。

采样 — head 与 tail

如果把链路 100% 全部存下来,数据量和成本都扛不住,所以要做采样

LLM 可观测性 — 为什么需要专门的工具

接下来转向最近迅速崛起的新领域。LLM 应用(聊天机器人、RAG、智能体)光靠传统可观测性工具是不够的。原因很明确。

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 提供的核心功能是这些。

其他选择 — LangSmith、Helicone、Phoenix

不是只有 Langfuse 一家。

选型标准大致如此。以 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,也能和普通链路连起来。

常踩的坑 — 成本与基数

决策清单

结语

可观测性不是靠一个工具就能搞定的事,而是把日志、指标、链路追踪这三大支柱(以及正在崛起的性能剖析)用 trace_id 串起来,打造一个"能回答任意问题的系统"的工作。在日志这一环,Loki 和 OpenSearch 构成了成本与检索能力的两极;在链路追踪这一环,OpenTelemetry 统一了标准,Jaeger、Tempo 承接落地;再往上,Langfuse 这类工具正在填补 LLM 这个新的层次。

关键不在于"哪个最好",而在于"什么适合这套系统",尤其是"这些信号彼此之间是否连接在一起"。把三大支柱各自立好,再用 trace_id 牢牢串起来,下一次凌晨三点的故障,你就不用靠肉眼在三个窗口之间来回比对,而是可以一路点下去,直达根因。

参考资料

评论

还没有评论。

登录后即可发表评论