跳转到内容
123xiao | 无名键客

《从源码到实践:基于 Kubernetes 开源项目构建可观测的微服务部署与故障排查方案》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队把微服务跑上 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 在发布新版本后,出现以下现象:

  1. kubectl get pods 显示 Pod 运行正常
  2. Ingress 偶发返回 502
  3. CPU 不高,但接口超时明显增加
  4. 日志里看不到明显错误
  5. 回滚后问题缓解,但并未完全消失

这类问题最难的点,不是“没有工具”,而是信号太多但无法关联
你会发现:

  • 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 类信号叠加:

  1. 资源状态

    • Pod 是否 Ready
    • 重启次数
    • 节点资源压力
    • 调度是否成功
  2. 指标

    • QPS、错误率、延迟
    • 容器 CPU / Memory
    • JVM / Go runtime 指标
    • 自定义业务指标
  3. 日志

    • 应用日志
    • 容器 stdout/stderr
    • Event
    • Ingress / Sidecar / Runtime 日志
  4. 调用链

    • 单次请求经过了哪些服务
    • 哪个 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

这个组合有两个优点:

  1. 与 Kubernetes 原生生态兼容度高
  2. 排障时可以把状态、指标、日志、链路放到一个视图里做关联

现象复现

为了避免文章太空,我用一个可运行的小型 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 里你就可以按:

  • namespace
  • app
  • pod

快速定位日志,不用在一堆容器输出里“盲翻”。

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 能帮助回答两个关键问题:

  1. 时间都耗在哪个 span 上?
  2. 是应用内部逻辑慢,还是等待外部依赖慢?
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-service span 很长
  • 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 重启
  • 错误看起来很随机

排查路径

  1. kubectl describe pod
  2. lastState
  3. 看容器 limit 设置
  4. 看内存曲线是否持续逼近 limit
  5. 看应用是否有突发缓存、批量加载、日志暴涨

止血方案

  • 临时提高 memory limit
  • 降低批量处理大小
  • 把大对象缓存迁移到外部组件
  • 增加副本平摊压力

5. 只看应用指标,不看 kube-state-metrics

现象

  • 业务指标正常,但服务还是不可用

原因

应用层指标看不到:

  • Pod 是否被驱逐
  • Deployment 是否滚动失败
  • PVC 是否挂载异常
  • HPA 是否频繁抖动

建议

业务指标和 Kubernetes 对象状态必须同时看,不然你会误以为“应用没问题”。


安全/性能最佳实践

可观测系统本身也会影响集群稳定性,这点很容易被忽略。

1. 不要无限制抓指标

Prometheus 最怕高基数标签。
例如把 user_idorder_idtrace_id 直接做成 metrics label,时间序列数量会爆炸。

建议

只保留稳定、低基数标签,例如:

  • service
  • endpoint
  • method
  • status

不要在指标 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 找到真实的慢点和错误传播路径

如果你现在还没开始搭这套体系,我建议按这个优先级推进:

  1. 先把健康检查、结构化日志、基础指标补齐
  2. 再引入 Prometheus + Grafana 做统一观测
  3. 日志集中采集到 Loki
  4. 最后在核心链路接入 OpenTelemetry 和 Jaeger

边界条件也要说清楚:

  • 小团队、低流量系统,不一定需要一开始就全量上 trace
  • 高合规场景下,日志和调用链要优先考虑脱敏与权限隔离
  • 超大规模集群需要额外考虑 Prometheus 分片、Loki 存储成本、指标基数治理

一句话收尾:
可观测性的目标不是“看见很多数据”,而是在线上出问题时,能又快又准地回答——到底哪里坏了,先怎么止血,后怎么修。


分享到:

上一篇
《大模型应用落地指南:从RAG检索增强到Agent编排的关键技术与实践陷阱》
下一篇
《分布式架构中基于一致性哈希与服务发现的高可用流量调度实战》