背景与问题
很多团队把微服务跑上 Kubernetes 之后,第一阶段的目标通常是“先跑起来”。但一旦进入真实生产环境,问题很快就会出现:
- 服务明明
Deployment已经 ready,接口却时好时坏 - 某个接口 RT 飙高,但不知道是应用慢、数据库慢,还是网关限流
- Pod 频繁重启,日志里只有一行
OOMKilled - 业务反馈“偶发超时”,可我们打开监控面板时,一切又恢复正常了
我自己做这类系统时,最深的感受是:Kubernetes 解决了部署编排问题,但不会自动帮你把“为什么坏了”解释清楚。
真正能把问题闭环的,是一套从源码理解到落地实践都说得通的可观测方案。
这篇文章不打算泛泛而谈“可观测性三件套”,而是站在排障视角,围绕 Kubernetes 生态里的几个典型开源项目来做一条完整链路:
- Metrics Server / kube-state-metrics / Prometheus:采集资源与业务指标
- Loki / Promtail:聚合日志
- Jaeger / OpenTelemetry:串起调用链
- kubectl / API Server / Probe 机制:理解 Kubernetes 原生状态与故障信号
文章重点是:怎么从源码和运行机制出发,构建一套能真正用于故障排查的微服务部署方案。
背景与问题
先看一个典型场景。
某个订单服务 order-service 在发布新版本后,出现以下现象:
kubectl get pods显示 Pod 运行正常- Ingress 偶发返回 502
- CPU 不高,但接口超时明显增加
- 日志里看不到明显错误
- 回滚后问题缓解,但并未完全消失
这类问题最难的点,不是“没有工具”,而是信号太多但无法关联。
你会发现:
- K8s 状态告诉你“Pod 活着”
- 指标告诉你“请求变慢了”
- 日志告诉你“下游偶尔超时”
- Trace 告诉你“瓶颈在库存服务”
- 但你仍然需要回答:到底是部署问题、代码问题、依赖问题,还是资源问题?
所以我们需要一套分层模型。
flowchart TD
A[用户请求异常] --> B[入口层 Ingress/Service]
B --> C[应用层 Deployment/Pod]
C --> D[运行时层 容器/Probe/重启]
D --> E[资源层 CPU/内存/网络]
C --> F[日志 Logs]
C --> G[指标 Metrics]
C --> H[链路 Trace]
F --> I[故障定位结论]
G --> I
H --> I
E --> I
这个图表达的是一个核心思路:
故障排查不是先看哪个工具,而是先判断问题落在哪一层,再用对应信号交叉验证。
核心原理
1. Kubernetes 中“可观测”到底在观察什么
在 Kubernetes 里,可观测通常不是单一数据源,而是 4 类信号叠加:
-
资源状态
- Pod 是否 Ready
- 重启次数
- 节点资源压力
- 调度是否成功
-
指标
- QPS、错误率、延迟
- 容器 CPU / Memory
- JVM / Go runtime 指标
- 自定义业务指标
-
日志
- 应用日志
- 容器 stdout/stderr
- Event
- Ingress / Sidecar / Runtime 日志
-
调用链
- 单次请求经过了哪些服务
- 哪个 span 最慢
- 上下游错误传播路径
如果你只采集其中一种,排障时一定会有“证据断层”。
2. 从源码角度理解 Kubernetes 的故障信号
很多排障效率低,是因为对 Kubernetes 的“状态来源”理解不够。
以 Pod Ready 为例,它并不等于“业务可用”。
2.1 Readiness Probe 的本质
Kubernetes 中 kubelet 会周期性执行 readiness 探针。探针成功,Pod 才会被加入 Service Endpoints。
也就是说:
- 容器进程活着 ≠ 服务就绪
- Pod Running ≠ 流量一定能正常进入
- Probe 设计不合理,会制造大量假健康
一个很常见的坑是:应用启动时 HTTP 端口先起来了,但数据库连接池、缓存预热、下游依赖还没准备好,此时 /healthz 返回 200,流量进来就超时。
2.2 Event 和 Status 不是同一类信息
status:当前状态快照event:状态变化过程中的事件流
比如:
status.phase=Running- 但
events中可能连续出现Back-off restarting failed container
如果你只看快照,很容易误判。
2.3 指标采集链路
Prometheus 采集并不是 Kubernetes 自带黑盒,它依赖:
- Service Discovery 发现抓取目标
/metrics暴露指标- 定时 scrape
- TSDB 存储时间序列
- Alertmanager 做告警聚合
所以当你说“Prometheus 没采到数据”,可能是以下任何一环出了问题:
- ServiceMonitor 标签不匹配
- Pod 没暴露 metrics 端口
- RBAC 权限不足
- 网络策略拦截
- 应用根本没注册指标
3. 一个排障友好的开源方案组合
这里我建议中级团队优先采用一套“足够稳、足够简单”的组合:
- Prometheus Operator:管理 Prometheus、ServiceMonitor、Alertmanager
- kube-state-metrics:补足 K8s 资源对象状态
- node-exporter:节点级指标
- Loki + Promtail:日志集中采集,部署成本比 ELK 轻
- OpenTelemetry SDK + Jaeger:采样追踪关键请求
- Grafana:统一可视化
它们关系如下:
flowchart LR
A[应用 Pod] -->|/metrics| B[Prometheus]
A -->|stdout/stderr| C[Promtail]
C --> D[Loki]
A -->|OTLP Trace| E[OpenTelemetry Collector]
E --> F[Jaeger]
G[kube-state-metrics] --> B
H[node-exporter] --> B
B --> I[Grafana]
D --> I
F --> I
这个组合有两个优点:
- 与 Kubernetes 原生生态兼容度高
- 排障时可以把状态、指标、日志、链路放到一个视图里做关联
现象复现
为了避免文章太空,我用一个可运行的小型 Spring Boot 风格替代例子,实际代码用 Python Flask,方便你本地快速验证。
我们模拟一个 order-service:
- 提供
/healthz健康检查 - 提供
/metrics指标 - 提供
/order接口 - 随机制造慢请求和错误
- 暴露 Prometheus 指标
应用代码
from flask import Flask, jsonify
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
import random
import time
import os
app = Flask(__name__)
REQUEST_COUNT = Counter(
'order_request_total',
'Total order requests',
['endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'order_request_latency_seconds',
'Order request latency',
['endpoint']
)
@app.route('/healthz')
def healthz():
# 模拟启动后依赖未就绪的情况,可通过环境变量切换
dependency_ready = os.getenv("DEPENDENCY_READY", "true").lower() == "true"
if dependency_ready:
return jsonify({"status": "ok"}), 200
return jsonify({"status": "degraded"}), 503
@app.route('/order')
def order():
start = time.time()
try:
r = random.random()
# 10% 错误,20% 慢请求
if r < 0.1:
time.sleep(0.2)
REQUEST_COUNT.labels(endpoint='/order', status='500').inc()
return jsonify({"error": "inventory timeout"}), 500
if r < 0.3:
time.sleep(2.5)
else:
time.sleep(0.1)
REQUEST_COUNT.labels(endpoint='/order', status='200').inc()
return jsonify({"orderId": "od-123", "status": "created"}), 200
finally:
REQUEST_LATENCY.labels(endpoint='/order').observe(time.time() - start)
@app.route('/metrics')
def metrics():
return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
依赖文件
flask==2.0.3
prometheus-client==0.14.1
werkzeug==2.0.3
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
实战代码(可运行)
下面给出一套最小化 Kubernetes 部署示例,直接可以用来做实验。
1. Deployment 与 Service
注意这里我特意把 readiness probe 配置为 /healthz,这样你能直观看到“探针是否合理”对流量的影响。
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: your-registry/order-service:latest
imagePullPolicy: IfNotPresent
env:
- name: DEPENDENCY_READY
value: "true"
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 2
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: order-service
labels:
app: order-service
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 8080
2. ServiceMonitor
如果你使用 Prometheus Operator,记得加上 ServiceMonitor。
很多人卡在这里,不是 Prometheus 坏了,而是它根本没发现目标。
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: order-service-monitor
labels:
release: prometheus
spec:
selector:
matchLabels:
app: order-service
endpoints:
- port: http
path: /metrics
interval: 15s
3. Loki + Promtail 最小采集思路
完整安装建议用 Helm,这里给最核心的日志标签配置示意:
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
有了这些标签后,Grafana 里你就可以按:
namespaceapppod
快速定位日志,不用在一堆容器输出里“盲翻”。
4. OpenTelemetry 接入示例
如果你想把 trace 也串起来,可以在应用中加入 OpenTelemetry。下面给出 Python 的最小示例:
from flask import Flask
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
resource = Resource.create({"service.name": "order-service"})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
在接口中包一层 span:
@app.route('/order')
def order():
with tracer.start_as_current_span("create-order"):
time.sleep(0.1)
return {"status": "ok"}
定位路径
这是 troubleshooting 文章最重要的部分。
面对故障,不要上来就看日志。我的建议顺序是:
第一步:先确认是“个体问题”还是“普遍问题”
先看 Pod 分布和状态:
kubectl get pods -l app=order-service -o wide
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp
重点看:
- 是否集中在某个节点
- 是否有频繁重启
- 是否有
Readiness probe failed - 是否存在镜像拉取、调度失败、卷挂载失败
如果只有某个 Pod 异常,优先怀疑:
- 节点问题
- Pod 启动环境差异
- 单副本探针失败
- 热点流量集中
如果全部副本异常,优先怀疑:
- 新版本代码
- 公共依赖
- 配置错误
- 集群层面资源不足
第二步:用指标判断“慢在哪里”
几个常见 PromQL:
查看 5xx 错误率
sum(rate(order_request_total{status="500"}[5m]))
/
sum(rate(order_request_total[5m]))
查看 P95 延迟
histogram_quantile(
0.95,
sum(rate(order_request_latency_seconds_bucket[5m])) by (le)
)
查看容器重启次数
increase(kube_pod_container_status_restarts_total{pod=~"order-service-.*"}[30m])
查看内存接近 limit 的情况
sum(container_memory_working_set_bytes{pod=~"order-service-.*", container="order-service"})
/
sum(kube_pod_container_resource_limits_memory_bytes{pod=~"order-service-.*", container="order-service"})
如果你看到:
- 错误率升高,但 CPU 不高:优先怀疑下游超时、线程池阻塞、连接池耗尽
- 延迟升高,同时内存逼近 limit:优先怀疑 GC 抖动、缓存膨胀、OOM 前兆
- 只有单个 Pod 延迟高:优先怀疑节点抖动或 Pod 局部异常
第三步:回到日志做“证据确认”
日志不要全量看,先带条件过滤:
- 同一时间窗
- 同一个 Pod
- 同一个 traceId / requestId
- 同一种错误关键词
例如 Loki 查询思路:
{app="order-service", namespace="default"} |= "timeout"
如果有 traceId:
{app="order-service"} |= "trace_id=abc123"
这里我踩过的坑是:日志没有结构化字段,最终只能靠模糊搜索。
所以哪怕你不想上复杂日志平台,也至少要让日志输出:
- timestamp
- level
- service
- pod
- trace_id
- error_code
第四步:用 Trace 判断瓶颈在本服务还是下游
假设你发现 /order 接口慢,trace 能帮助回答两个关键问题:
- 时间都耗在哪个 span 上?
- 是应用内部逻辑慢,还是等待外部依赖慢?
sequenceDiagram
participant U as User
participant G as Gateway
participant O as order-service
participant I as inventory-service
participant D as DB
U->>G: POST /order
G->>O: create order
O->>I: reserve inventory
I->>D: update stock
D-->>I: slow response
I-->>O: timeout/retry
O-->>G: 500
G-->>U: 502/500
如果 trace 显示:
order-service内部 span 很短inventory-servicespan 很长- DB span 最长
那么你就不用一直盯着 order-service 容器资源了,问题大概率在库存链路。
常见坑与排查
下面这些坑,在 Kubernetes 可观测建设里出现频率非常高。
1. Probe 写得太“乐观”
现象
- Pod Ready 很快
- 但刚发布就出现大量超时或 502
原因
健康检查只检查 HTTP 端口活着,没有验证依赖可用性。
排查
kubectl describe pod <pod-name>
kubectl logs <pod-name>
对比:
- Pod 进入 Ready 的时间
- 应用真正完成初始化的时间
止血方案
- readiness 检查依赖就绪
- startupProbe 单独处理慢启动场景
- 发布时配合滚动更新和最小可用副本
2. Prometheus 没抓到指标
现象
- Grafana 面板空白
- 目标服务其实是好的
常见原因
ServiceMonitor标签和 Prometheus selector 不匹配- Service 端口名错误
/metrics路径不对- Namespace 不在抓取范围
排查
kubectl get servicemonitor -A
kubectl get svc order-service -o yaml
kubectl port-forward svc/order-service 8080:80
curl http://127.0.0.1:8080/metrics
止血方案
- 先手动
curl /metrics - 再检查 Service 和 ServiceMonitor 对应关系
- 最后进入 Prometheus Targets 页面确认发现链路
3. 日志很多,但查不到关键请求
现象
- 日志平台里一堆数据
- 但很难定位一次失败请求
原因
- 没有 requestId / traceId
- 日志没有结构化字段
- 标签维度设计混乱
止血方案
统一日志最少字段:
{
"timestamp": "2021-11-28T17:03:55Z",
"level": "ERROR",
"service": "order-service",
"trace_id": "abc123",
"message": "inventory timeout"
}
4. OOMKilled 后只盯着应用代码
现象
- Pod 重启
- 错误看起来很随机
排查路径
- 看
kubectl describe pod - 看
lastState - 看容器 limit 设置
- 看内存曲线是否持续逼近 limit
- 看应用是否有突发缓存、批量加载、日志暴涨
止血方案
- 临时提高 memory limit
- 降低批量处理大小
- 把大对象缓存迁移到外部组件
- 增加副本平摊压力
5. 只看应用指标,不看 kube-state-metrics
现象
- 业务指标正常,但服务还是不可用
原因
应用层指标看不到:
- Pod 是否被驱逐
- Deployment 是否滚动失败
- PVC 是否挂载异常
- HPA 是否频繁抖动
建议
业务指标和 Kubernetes 对象状态必须同时看,不然你会误以为“应用没问题”。
安全/性能最佳实践
可观测系统本身也会影响集群稳定性,这点很容易被忽略。
1. 不要无限制抓指标
Prometheus 最怕高基数标签。
例如把 user_id、order_id、trace_id 直接做成 metrics label,时间序列数量会爆炸。
建议
只保留稳定、低基数标签,例如:
serviceendpointmethodstatus
不要在指标 label 中放:
- 用户 ID
- 请求 ID
- 动态 URL 参数
2. 日志要采样和分级
不是所有 INFO 都值得长期保留。
建议
- 生产默认 INFO,问题时临时切 DEBUG
- 错误日志必须结构化
- 高流量服务对访问日志做采样
- 对日志平台设置保留周期
3. Trace 不必全量采集
全量 trace 对存储和网络开销都不小。
建议
- 默认 1%~10% 采样
- 对错误请求、慢请求做尾部采样
- 核心交易链路单独提高采样率
4. 最小权限访问监控面
Prometheus、Grafana、Loki、Jaeger 往往能看到大量系统内部信息。
建议
- 配置 RBAC
- 面板接入统一认证
- 避免将监控服务直接暴露公网
- 敏感日志字段脱敏,如手机号、Token、身份证号
5. 资源隔离要提前做
可观测组件不是“附属品”,它们也会吃资源。
建议
- 给 Prometheus/Loki 设置独立 requests/limits
- 存储容量提前估算
- 大集群按 namespace / team 做指标和日志隔离
- 关键监控组件尽量避免与高波动业务混部
一套可执行的排障清单
如果线上告警来了,我建议按下面这个顺序走,一般 10~20 分钟内能把方向收敛下来。
flowchart TD
A[收到告警] --> B{是全局还是局部}
B -->|局部| C[看 Pod/Event/重启]
B -->|全局| D[看发布记录/公共依赖]
C --> E[查资源指标]
D --> E
E --> F{延迟高还是错误多}
F -->|延迟高| G[看 Trace 找慢点]
F -->|错误多| H[看日志和下游异常]
G --> I[确认根因]
H --> I
I --> J[先止血再修复]
对应命令与动作可以固定成 Runbook:
K8s 状态
kubectl get pods -l app=order-service
kubectl describe deploy order-service
kubectl get events --sort-by=.lastTimestamp | tail -n 20
日志
kubectl logs -l app=order-service --tail=100
kubectl logs <pod-name> --previous
连通性
kubectl exec -it <pod-name> -- sh
curl http://inventory-service/healthz
回滚止血
kubectl rollout undo deployment/order-service
kubectl rollout status deployment/order-service
这套流程的价值不在于命令多高级,而在于:每一步都在缩小问题范围,而不是盲猜。
总结
从 Kubernetes 开源项目出发构建可观测体系,关键不在“装了多少组件”,而在于你是否建立了一条能支持故障排查的证据链:
- 用 Kubernetes 状态 看资源对象是否健康
- 用 Prometheus 指标 看错误率、延迟和资源瓶颈
- 用 Loki 日志 确认失败上下文
- 用 Trace 找到真实的慢点和错误传播路径
如果你现在还没开始搭这套体系,我建议按这个优先级推进:
- 先把健康检查、结构化日志、基础指标补齐
- 再引入 Prometheus + Grafana 做统一观测
- 日志集中采集到 Loki
- 最后在核心链路接入 OpenTelemetry 和 Jaeger
边界条件也要说清楚:
- 小团队、低流量系统,不一定需要一开始就全量上 trace
- 高合规场景下,日志和调用链要优先考虑脱敏与权限隔离
- 超大规模集群需要额外考虑 Prometheus 分片、Loki 存储成本、指标基数治理
一句话收尾:
可观测性的目标不是“看见很多数据”,而是在线上出问题时,能又快又准地回答——到底哪里坏了,先怎么止血,后怎么修。