大多数架构图都止步于服务本身。遥测数据的路径往往是事后才画的,而且通常是在故障期间,由那个正在追查为什么一半的链路数据丢了、为什么日志账单翻了一倍的人来画。到了那个时候,答案取决于一些从没人写下来的细节:哪个组件在做采样,哪个组件添加了 Pod 名称,后端变慢时又会发生什么。
可观测性架构图展示日志、指标和链路追踪如何离开每个服务、在哪里被补充信息、被采样和过滤,最终又落到哪里。本文围绕 Kubernetes 上的 OpenTelemetry 绘制这样一张图,讲解背后的 Agent 和 Gateway 模式,并列出应该标注哪些内容,好让这张图在下一次故障时依然有用。
三类信号,三个问题
| 信号 | 回答的问题 | 典型来源 | 通常去向 |
|---|---|---|---|
| 指标 | 出问题了吗?有多严重? | SDK 的计数器和直方图、kubelet 和节点统计 | 时序数据库,比如兼容 Prometheus 的存储 |
| 链路追踪 | 问题出在请求路径的哪一段? | 来自 SDK 或自动埋点的 Span | 链路存储,比如 Jaeger 或 Tempo |
| 日志 | 到底发生了什么? | stdout 和 stderr、日志文件、SDK 日志记录 | 日志存储,比如 Loki 或 OpenSearch |
这三类信号只有共享上下文时才能配合使用。日志行里的 trace ID,加上每类信号都带有的相同 service.name 和 Pod 名称,能让你从一条告警跳到那个慢 Span,再跳到解释原因的那行日志。架构必须完整地保留这些上下文。
基本组件
OTLP 是 OpenTelemetry 用于三类信号的协议。规范规定了默认端口:基于 gRPC 的 OTLP 用 4317,基于 HTTP 的 OTLP 用 4318。
SDK 和自动埋点运行在每个服务内部,并导出 OTLP。让它们指向本地的 Collector,而不是直接指向厂商,这样调整路由时就不需要重新部署。
Collector 是一个单独的二进制程序,每类信号都有自己的管道:receiver 接收数据,processor 处理数据,exporter 把数据继续发送出去。部署文档介绍了两种主要的运行方式:
- Agent。 Collector 运行在应用旁边,可以是 sidecar,也可以是每个节点一个 Pod 的 DaemonSet。在 Kubernetes 上,Agent 也是 Kubernetes 组件发挥作用的地方:Kubernetes Attributes Processor 添加 Pod、命名空间和 Deployment 名称,Filelog Receiver 跟踪容器的 stdout 和 stderr,Kubeletstats Receiver 采集节点和 Pod 的指标。
- Gateway。 一个或多个 Collector 作为独立服务运行,通常每个集群、数据中心或区域一个端点。Gateway 适合放那些只想做一次的决定:采样、过滤敏感属性、保管后端凭据,以及发送到多个后端。
头部采样还是尾部采样
链路追踪是成本最高的信号,所以大多数团队都会采样。头部采样在链路开始时就做决定,比如保留 10%。它很便宜,却无法因为某条链路后来失败了而保留它。尾部采样在看到一条链路的全部或大部分 Span 之后再做决定,所以能保留每一个错误和每一个慢请求。
尾部采样有结构上的代价:一条链路的所有 Span 必须到达同一个 Collector。放在轮询式负载均衡器后面,这一点就做不到。Gateway 模式用两层来解决:第一层运行以 trace ID 为路由键的 load-balancing exporter,第二层运行 tail sampling processor。在图上这是两个方框,值得把两个都画出来。
示例:Kubernetes 上的网店
这个网店在 shop 命名空间里运行三个 Deployment:web、checkout-api 和 payments-worker,每个都集成了 OpenTelemetry SDK。observability 命名空间承载整个管道:一个 otel-agent DaemonSet,一个按 trace ID 路由 Span 的 otel-lb Deployment,以及一个执行尾部采样策略的 otel-sampler Deployment。指标、链路追踪和日志各自进入自己的存储,告警规则基于指标运行。
图表内容
- shop
- web
- checkout-api
- payments-worker
- observability
- otel-agent
- otel-lb
- otel-sampler
- Trace store
- Metrics store
- Log store
- Alerting
- OTLP/gRPC 4317
- stdout logs (filelog)
- traces
- by trace ID
- kept traces
- metrics
- logs
- alert rules
这张图的读法如下:
- 左侧,服务。 每个 Deployment 通过 OTLP/gRPC 的 4317 端口,把链路、指标和日志导出到所在节点的 Agent。虚线连线是容器的 stdout,Agent 直接从节点读取,而不是通过网络接收。
- 中部,Agent。 Agent 为所有数据添加 Kubernetes 元数据,然后把指标和日志直接发往各自的存储,把链路数据发往 Gateway。
- 中部,Gateway。
otel-lb按 trace ID 路由每个 Span,因此同一条链路的所有 Span 都会到达同一个otel-samplerPod,由它保留错误和慢链路,并对其余部分采样。 - 右侧,存储与告警。 每类信号都有自己的后端。告警规则读取指标,并通知当班的值班人员。
- Markdown 块写明了采样策略,以及每条管道都要遵守的一条规则:把 memory limiter 放在最前面。
需要标注什么
- 每一跳的协议和端口。 是 OTLP/gRPC 4317 还是 OTLP/HTTP 4318,以及发往每个后端的导出格式。
- 每条连线上传输哪些信号。 一条管道很少以同样的方式传输全部三类信号。
- 在哪里采样,策略是什么。 写出具体数字;只写“已采样”容易招来错误的假设。
- 数据在哪里被补充信息或脱敏。 Kubernetes 元数据在 Agent 中添加,令牌和个人数据在 Gateway 中移除。
- 每个后端的保留期限。 它决定了成本,也决定了排查时还能找到什么。
- 故障时的行为。 队列、重试,以及后端宕机时会丢弃什么。
常见错误
- SDK 直接导出到厂商。 每次调整路由或凭据都要重新部署,而且数据离开之前没有任何地方可以过滤。
- 在普通负载均衡器后面做尾部采样。 同一条链路的 Span 落到不同的 Collector 上,采样决定只能基于碎片做出。
- 没有 memory limiter。 配置文档中提供了
memory_limiterprocessor;把它放在每条管道的第一位,这样流量突增时 Collector 会拒绝数据,而不是耗尽内存。 - 高基数的指标标签。 把用户 ID 或请求 ID 作为指标标签,会让时间序列成倍增长。应该把它们放在 Span 和日志里。
- 不监控管道本身。 正如内部遥测文档所述,Collector 默认在 8888 端口暴露自身的指标。为导出失败设置告警,否则你只会在恰好需要这些数据的那次故障中,才发现数据丢了。
关于集群本身,Kubernetes 架构图介绍了控制平面、节点和工作负载;微服务架构图则介绍了这条管道所观测的服务。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 从 Kubernetes 节点包中添加两个 Namespace 框,分别是
shop和observability。在shop中为每个服务放一个 Deployment。 - 在
observability中为 Agent 添加一个 DaemonSet,再为负载均衡层和采样层添加两个 Deployment 节点。 - 从 DevOps 节点包中为各个后端添加 指标、链路追踪 和 日志 节点,并为告警添加一个 告警 节点。
- 把它们连接起来,并在每条连线上标注信号和协议,比如“OTLP/gRPC 4317”。把容器日志那条连线设为虚线。
- 添加一个基础节点包中的 Markdown 块,写明采样策略。
这个示例共用 13 个图表节点,在 Free 套餐的限制之内。如果你的服务已经运行在 Kubernetes 上,可以用 Kubernetes YAML 转图表工具把清单文件转换成左半部分的初稿。
常见问题
需要 Gateway 模式的 Collector 吗,还是 Agent 就够了? 小规模部署只用 Agent 就够了。当你需要尾部采样、需要把凭据和过滤集中在一处,或者需要多个后端时,再加上 Gateway;这时 Agent 会把数据转发给它。
日志也应该经过 Collector 吗? 可以。Agent 中的 Filelog Receiver 读取容器日志,添加与链路和指标相同的 Kubernetes 元数据,并允许你在存储前过滤。很多团队在迁移完链路和指标之后,最后才把日志迁到 Collector 上。
头部采样什么时候就够用了? 流量较低,或者所有链路看起来都差不多时就够用。如果你需要保留每一个错误和每一个慢请求,就用尾部采样,并画出它所需的两层 Gateway。
仪表盘和告警应该画在图上的哪里? 画在它们所查询的存储旁边。基于指标的告警规则是架构的一部分:它们决定了谁会被叫醒。
怎样让这张图保持最新? 在修改 Collector 配置的同一个拉取请求里修改这张图,并在运行手册中链接它。参见不会过时的架构图。