在 EKS 上搭建一套 Metrics、Logs、Traces 一体化可观测平台
一篇关于 Prometheus、OpenTelemetry、Loki、Tempo、Grafana 和 Nightingale 的生产实践笔记
配置快照:2026-09
本文已脱敏,账号、集群、域名、CIDR、ARN、bucket 和业务名称均使用占位符
写在前面
最早搭监控时,我们通常从 Prometheus 开始;遇到线上问题以后,又补一个 Loki;再往后为了分析跨服务调用,继续部署 Tempo 或 Jaeger。三个系统都有了,但数据还是割裂的:指标告诉我们“出问题了”,日志告诉我们“报了什么错”,Trace 才能解释“请求到底在哪一跳变慢”。
这次改造的目标不是简单把三个开源组件安装进 Kubernetes,而是把它们连接成一条完整的排障链路:
告警发现异常
-> 指标确定受影响服务和时间窗口
-> Trace 找到慢调用或错误调用
-> trace_id 跳转到对应日志
-> 回到 Pod、节点和 Kubernetes 事件定位根因
最终采用的核心组件是:
- Prometheus:Kubernetes 与应用指标存储和 PromQL 查询;
- OpenTelemetry Operator/Collector:Trace、日志采集、尾部采样和 Span Metrics;
- Loki:容器日志存储和 LogQL 查询;
- Tempo:分布式 Trace 存储和查询;
- Grafana:Metrics、Logs、Traces 的统一查询与关联;
- Nightingale:告警规则评估、事件管理和通知。
这篇笔记重点记录四件事:部署过程、架构设计、关键参数,以及系统上线以后仍需要继续优化的部分。
一、先看最终架构
这里有三个比较重要的设计点。
第一,应用只需要认一个 OTel Gateway,不直接感知 Tempo、采样器或指标后端。以后更换存储,应用不需要跟着改。
第二,Span Metrics 必须在尾部采样之前生成。正常 Trace 最终只保留 5%,但请求量、错误率和延迟指标不能也只剩 5%,否则告警会严重失真。
第三,日志 Agent 必须是 DaemonSet。容器日志文件在节点本地,中心化 Deployment 看不到所有节点的 /var/log/pods。
二、当前生产组件和规模
为了让后面的参数有上下文,先列一下脱敏后的生产快照。
| 组件 | 版本 | 部署形态 |
|---|---|---|
| kube-prometheus-stack | 86.2.3 | Helm |
| Prometheus | 3.12.0 | StatefulSet,当前 1 副本 |
| Prometheus Operator | 0.91.0 | Deployment,1 副本 |
| OpenTelemetry Operator | 0.156.0 | Deployment,1 副本 |
| OTel Gateway | Collector Contrib 0.156.0 | Deployment,2 副本 |
| OTel Tail Sampling | Collector K8s 0.156.0 | StatefulSet,2 副本 |
| OTel Logs Agent | Collector Contrib 0.156.0 | DaemonSet,每节点 1 个 |
| Loki | 3.7.3 | Monolithic,2 副本 |
| Tempo | 2.10.7 | Distributed |
| Nightingale | 8.5.1 | 独立告警平台 |
当前 Prometheus 大约维护 59.8 万条活跃序列,写入速率约 3.91 万 samples/s。容器日志 Agent 覆盖 17 个节点。这个量级还没有逼迫 Loki 和 Tempo 做极限横向扩展,但已经足以暴露单点、基数和故障域问 题。
三、部署前准备
1. 统一 namespace 和节点池
所有可观测性组件放在独立 namespace:
kubectl --context <PROD_CONTEXT> create namespace <OBS_NAMESPACE>
核心控制面组件优先放到 <SYSTEM_NODE_POOL>,业务节点只运行 node-exporter 和日志 Agent 等 node-local workload。
这里有一个容易忽略的问题:节点选择器并不等于高可用。如果 system 节点池只有两台机器,即便每个组件都设置了两个副本,所有组件仍然集中在两个故障点上。因此部署前应同时确认:
- 节点数量;
- 可用区分布;
- requests 之后的剩余容量;
- anti-affinity 和 topology spread 是否真的渲染到 workload。
2. 准备 gp3 StorageClass
Prometheus、Loki 和 Tempo Ingester 都需要 RWO 持久卷。EBS 是可用区级资源,所以每个 StatefulSet 副本必须拥有自己的 PVC,而不是让多个副本共享同一个卷。
建议开启:
volumeBindingMode: WaitForFirstConsumer;allowVolumeExpansion: true;- gp3,并根据实际吞吐调整 IOPS/throughput。
PVC 只能扩容,不能缩容。仓库 values 与现场容量不一致会导致新副本拿到错误大小的盘,后面会再次提到这个问题。
3. 对象存储与 IRSA
Loki 和 Tempo 分别使用独立 S3 bucket:
<LOG_BUCKET>
<TRACE_BUCKET>
基础要求:
- Block Public Access;
- 默认加密;
- versioning;
- 生命周期与组件 retention 不冲突;
- 使用 IRSA,不在 values 中保存 Access Key/Secret Key;
- 日志、Trace 和未来 Metrics bucket 分离,便于权限、成本和生命周期治理。
脱敏后的 ServiceAccount 形式如下:
apiVersion: v1
kind: ServiceAccount
metadata:
name: <COMPONENT_SERVICE_ACCOUNT>
namespace: <OBS_NAMESPACE>
annotations:
eks.amazonaws.com/role-arn: <IRSA_ROLE_ARN>
四、第一步:部署 Prometheus
Prometheus 使用 kube-prometheus-stack,Grafana 和 Alertmanager 不在这个 release 中部署:
grafana:
enabled: false
alertmanager:
enabled: false
prometheus:
prometheusSpec:
scrapeInterval: 15s
retention: 30d
enableRemoteWriteReceiver: true
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 300Gi
安装命令:
helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
--kube-context <PROD_CONTEXT> \
--namespace <OBS_NAMESPACE> \
--version 86.2.3 \
--values prom-stack/values.yaml \
--wait \
--timeout 15m
当前关键参数
| 参数 | 当前值 | 说明 |
|---|---|---|
| scrape interval | 15s | Kubernetes 和应用监控的统一默认周期 |
| retention | 30d | 当前全部指标保存在本地 PVC |
| PVC | 现场 300Gi | 仓库/CR 曾仍声明 200Gi,属于配置漂移 |
| remote-write receiver | 开启 | 当前有真实写入流量,不能直接关闭 |
| replicas | 1 | 当前最大单点 |
这里踩过的坑:扩盘不等于改了 Git
现场 PVC 已经从 200Gi 扩到 300Gi,但 Prometheus CR 和仓库 values 仍然是 200Gi。现有卷不会自动缩回去,看起来“一切正常”;一旦增加第二个 Prometheus 副本,新 PVC 就会按 200Gi 创建。
所以扩容完成以后必须同步修改 Git 声明,并通过 Helm diff 或 GitOps 检查漂移。
另一个坑:remote-write 和双副本不是一回事
两个 Prometheus 独立抓取同一批 targets,可以实现 scrape HA。但把 remote-write Service 后端从一个 Pod 改成两个 Pod,只会让写请求被随机拆分,每个 Prometheus 收到一部分数据,并不会自动复制。
因此后续 Prometheus HA 改造必须拆成两部分:
- 两个 Prometheus + Thanos Query,解决 scrape 和查询 HA;
- Thanos Receive、Mimir 或托管 Prometheus,解决 remote-write 复制。