Kubernetes 生产环境排障手册:从节点异常到 Pod 驱逐的诊断链路

一、K8s 排障的"黑箱迷雾":多层抽象下的故障定位困境

Kubernetes 的多层抽象是排障的最大障碍。一个简单的"服务 503"报错,可能的原因链路从上到下跨越五层:Ingress 规则配置错误 → Service 选择器不匹配 → Pod 处于 CrashLoopBackOff → 容器内应用 OOM → 节点内存压力触发驱逐。每一层都有自己的状态和事件,但层与层之间的关联并不直观。某团队在排查"前端无法访问后端 API"问题时,花了 2 小时逐层检查 Ingress、Service、Pod、容器日志,最终发现是节点 NotReady 导致 Pod 被驱逐,而节点 NotReady 的原因是 kubelet 磁盘满。

更棘手的是"间歇性故障"——Pod 偶尔重启、请求偶尔超时、延迟偶尔飙升。这类故障在排查时往往已经恢复,现场证据消失。传统的 kubectl describekubectl logs 只能看到当前状态,无法回溯故障发生时的上下文。

系统化的排障手册需要解决两个问题:一是建立标准化的诊断链路,从症状出发逐层缩小排查范围;二是构建故障现场保留机制,在故障发生时自动采集关键信息,避免事后无据可查。

二、排障诊断链路:从症状到根因的五层下钻模型

flowchart TB
    subgraph L1["第一层:入口层<br/>Ingress / Gateway"]
        A["症状:503/504/连接超时"] --> B["检查项<br/>• Ingress 规则<br/>• TLS 证书<br/>• 后端健康检查"]
    end

    subgraph L2["第二层:服务层<br/>Service / Endpoints"]
        B --> C["检查项<br/>• Service 选择器匹配<br/>• Endpoints 是否为空<br/>• 端口映射是否正确"]
    end

    subgraph L3["第三层:Pod 层<br/>Pod 状态与事件"]
        C --> D["检查项<br/>• Pod Phase/Conditions<br/>• 最近 Events<br/>• 重启次数与原因"]
    end

    subgraph L4["第四层:容器层<br/>容器运行时与日志"]
        D --> E["检查项<br/>• 容器退出码<br/>• OOMKilled 标记<br/>• 应用日志与堆栈"]
    end

    subgraph L5["第五层:节点层<br/>Node 状态与资源"]
        E --> F["检查项<br/>• Node Conditions<br/>• 资源使用率<br/>• 驱逐事件与磁盘压力"]
    end

    F -->|"节点异常"| G["根因定位<br/>磁盘满/内存压力/网络分区"]
    E -->|"应用异常"| H["根因定位<br/>OOM/配置错误/依赖超时"]

    style A fill:#f96,stroke:#333
    style G fill:#9f9,stroke:#333
    style H fill:#9f9,stroke:#333

五层下钻模型的诊断逻辑:

第一层:入口层。当用户报告"服务不可用"时,首先检查 Ingress 控制器。常见问题包括:Ingress 规则的 path 配置错误(如 /api/v1 写成 /api/v1/,末尾斜杠导致路由不匹配)、TLS 证书过期、后端健康检查配置不当导致 Ingress 认为后端不可用。诊断命令:kubectl get ingresskubectl describe ingress <name>、检查 Ingress 控制器日志。

第二层:服务层。Ingress 正常但请求无法到达 Pod,问题通常在 Service 层。最常见的是 Service 选择器与 Pod 标签不匹配——这在滚动更新后尤其容易发生,新版本 Pod 的标签变了但 Service 选择器没更新。另一个常见问题是 Endpoints 为空——Service 找不到匹配的 Pod。诊断命令:kubectl get endpoints <service-name>kubectl describe service <name>

第三层:Pod 层。Service 有 Endpoints 但请求仍失败,需要检查 Pod 状态。关键信息在 Events 中——kubectl describe pod <name> 的 Events 部分记录了 Pod 的完整生命周期事件:调度失败、镜像拉取失败、健康检查失败、被驱逐等。Pod 重启次数和上次终止原因(Last State)是定位问题的关键线索。

第四层:容器层。Pod 状态异常但原因不明,需要深入容器内部。退出码是最直接的线索:137 表示 OOMKilled(内存不足被杀),1 表示应用错误退出,137+128=信号终止。容器日志(kubectl logs --previous)记录了崩溃前的最后输出。对于 Java 应用,JVM 堆转储(Heap Dump)是分析 OOM 的关键证据。

第五层:节点层。多个 Pod 同时异常,问题可能在节点层面。Node Conditions 中的 MemoryPressureDiskPressureNetworkUnavailable 是关键信号。节点驱逐(Eviction)事件记录在 kubelet 日志中。磁盘满是最常见的节点级故障——容器日志、镜像层、临时文件持续增长,最终导致节点不可用。

三、自动化排障脚本的代码实现

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import subprocess
import json

class FaultLayer(Enum):
    INGRESS = "ingress"
    SERVICE = "service"
    POD = "pod"
    CONTAINER = "container"
    NODE = "node"

@dataclass
class DiagnosisResult:
    """诊断结果"""
    layer: FaultLayer
    resource_name: str
    status: str                  # healthy/warning/critical
    issues: list[str] = field(default_factory=list)
    suggestions: list[str] = field(default_factory=list)
    raw_output: str = ""

class K8sTroubleshooter:
    """Kubernetes 自动化排障工具"""

    def __init__(self, namespace: str = "default"):
        self.namespace = namespace

    def _kubectl(self, args: str) -> str:
        """执行 kubectl 命令并返回输出"""
        cmd = f"kubectl -n {self.namespace} {args}"
        try:
            result = subprocess.run(
                cmd, shell=True, capture_output=True, text=True, timeout=30
            )
            return result.stdout + result.stderr
        except subprocess.TimeoutExpired:
            return "ERROR: command timed out"

    def diagnose_service(
        self, service_name: str
    ) -> list[DiagnosisResult]:
        """对指定服务执行全链路诊断"""
        results = []

        # 第一层:Ingress 检查
        results.append(self._check_ingress(service_name))

        # 第二层:Service 检查
        results.append(self._check_service(service_name))

        # 第三层:Pod 检查
        pod_results = self._check_pods(service_name)
        results.extend(pod_results)

        # 第五层:节点检查(如果 Pod 有问题)
        for r in pod_results:
            if r.status != "healthy":
                node_name = self._get_pod_node(r.resource_name)
                if node_name:
                    results.append(self._check_node(node_name))
                break

        return results

    def _check_ingress(
        self, service_name: str
    ) -> DiagnosisResult:
        """检查 Ingress 配置"""
        output = self._kubectl(
            f"get ingress -o json"
        )
        result = DiagnosisResult(
            layer=FaultLayer.INGRESS,
            resource_name=service_name,
            status="healthy",
            raw_output=output[:500],
        )

        try:
            ingresses = json.loads(output)
            for ingress in ingresses.get("items", []):
                for rule in ingress.get("spec", {}).get("rules", []):
                    for path in rule.get("http", {}).get("paths", []):
                        backend = path.get("backend", {})
                        svc_name = backend.get("service", {}).get("name", "")
                        if svc_name == service_name:
                            # 检查路径配置
                            path_value = path.get("path", "")
                            if path_value.endswith("/") and len(path_value) > 1:
                                result.issues.append(
                                    f"路径 {path_value} 以斜杠结尾,"
                                    f"可能导致路由不匹配"
                                )
                                result.suggestions.append(
                                    "检查 Ingress path 配置,"
                                    "确认末尾斜杠是否必要"
                                )
                                result.status = "warning"
        except json.JSONDecodeError:
            result.status = "critical"
            result.issues.append("无法解析 Ingress 配置")

        return result

    def _check_service(
        self, service_name: str
    ) -> DiagnosisResult:
        """检查 Service 和 Endpoints"""
        result = DiagnosisResult(
            layer=FaultLayer.SERVICE,
            resource_name=service_name,
            status="healthy",
        )

        # 检查 Endpoints
        endpoints_output = self._kubectl(
            f"get endpoints {service_name} -o json"
        )
        try:
            endpoints = json.loads(endpoints_output)
            addresses = []
            for subset in endpoints.get("subsets", []):
                addresses.extend(subset.get("addresses", []))

            if not addresses:
                result.status = "critical"
                result.issues.append(
                    "Endpoints 为空,Service 找不到匹配的 Pod"
                )
                result.suggestions.append(
                    "检查 Service selector 是否与 Pod labels 匹配"
                )
        except json.JSONDecodeError:
            result.issues.append("无法获取 Endpoints")

        return result

    def _check_pods(
        self, service_name: str
    ) -> list[DiagnosisResult]:
        """检查 Pod 状态"""
        output = self._kubectl(
            f"get pods -l app={service_name} -o json"
        )
        results = []

        try:
            pods = json.loads(output)
            for pod in pods.get("items", []):
                pod_name = pod["metadata"]["name"]
                result = DiagnosisResult(
                    layer=FaultLayer.POD,
                    resource_name=pod_name,
                    status="healthy",
                )

                # 检查 Pod Phase
                phase = pod.get("status", {}).get("phase", "")
                if phase != "Running":
                    result.status = "critical"
                    result.issues.append(f"Pod Phase: {phase}")

                # 检查容器状态
                for container_status in pod.get("status", {}).get(
                    "containerStatuses", []
                ):
                    restart_count = container_status.get("restartCount", 0)
                    if restart_count > 3:
                        result.status = "warning"
                        result.issues.append(
                            f"容器 {container_status['name']} "
                            f"已重启 {restart_count} 次"
                        )

                    # 检查 OOMKilled
                    last_state = container_status.get("lastState", {})
                    terminated = last_state.get("terminated", {})
                    if terminated.get("reason") == "OOMKilled":
                        result.status = "critical"
                        result.issues.append(
                            f"容器 {container_status['name']} OOMKilled"
                        )
                        result.suggestions.append(
                            "增加内存 Limit 或排查内存泄漏"
                        )

                # 检查 Conditions
                for condition in pod.get("status", {}).get("conditions", []):
                    if condition.get("type") == "Ready" and \
                       condition.get("status") != "True":
                        result.status = "warning"
                        result.issues.append("Pod Not Ready")

                results.append(result)
        except json.JSONDecodeError:
            results.append(DiagnosisResult(
                layer=FaultLayer.POD,
                resource_name=service_name,
                status="critical",
                issues=["无法获取 Pod 列表"],
            ))

        return results

    def _check_node(self, node_name: str) -> DiagnosisResult:
        """检查节点状态"""
        output = self._kubectl(
            f"get node {node_name} -o json"
        )
        result = DiagnosisResult(
            layer=FaultLayer.NODE,
            resource_name=node_name,
            status="healthy",
        )

        try:
            node = json.loads(output)
            for condition in node.get("status", {}).get("conditions", []):
                ctype = condition.get("type", "")
                status = condition.get("status", "")

                if ctype == "Ready" and status != "True":
                    result.status = "critical"
                    result.issues.append("节点 NotReady")
                elif ctype == "MemoryPressure" and status == "True":
                    result.status = "critical"
                    result.issues.append("节点内存压力")
                    result.suggestions.append(
                        "检查节点内存使用,考虑驱逐低优先级 Pod"
                    )
                elif ctype == "DiskPressure" and status == "True":
                    result.status = "critical"
                    result.issues.append("节点磁盘压力")
                    result.suggestions.append(
                        "清理容器日志和未使用的镜像"
                    )
        except json.JSONDecodeError:
            result.issues.append("无法获取节点状态")

        return result

    def _get_pod_node(self, pod_name: str) -> Optional[str]:
        """获取 Pod 所在节点"""
        output = self._kubectl(
            f"get pod {pod_name} -o jsonpath='{{.spec.nodeName}}'"
        )
        return output.strip("'") if output else None

    def print_report(self, results: list[DiagnosisResult]):
        """打印诊断报告"""
        for r in results:
            icon = {"healthy": "✓", "warning": "⚠", "critical": "✗"}
            print(f"[{icon.get(r.status, '?')}] "
                  f"{r.layer.value}/{r.resource_name}: {r.status}")
            for issue in r.issues:
                print(f"    - {issue}")
            for suggestion in r.suggestions:
                print(f"    → {suggestion}")
            print()

关键设计决策:排障脚本采用五层下钻模型,从入口层到节点层逐层检查,每层有明确的检查项和判断标准。诊断结果分为三级——healthy(正常)、warning(需关注)、critical(需立即处理)。Pod 检查特别关注重启次数和 OOMKilled 标记,这是生产环境最常见的故障模式。节点检查关注 MemoryPressure 和 DiskPressure,这两个信号是节点级故障的前兆。

四、排障方案的边界与权衡

自动化排障的局限:脚本只能检查"已知模式"的故障——Ingress 配置错误、Endpoints 为空、Pod 重启、OOMKilled、节点压力等。对于应用逻辑层面的故障(如数据库死锁、缓存穿透、业务逻辑错误),脚本无法诊断。这类问题需要结合应用日志和分布式追踪系统。

权限要求:排障脚本需要集群的读取权限(get/list/watch),在生产环境中通常需要通过 RBAC 配置专用的 ServiceAccount。如果集群开启了 Pod Security Standards,某些诊断命令可能被限制。

大规模集群的性能:当集群包含数千个 Pod 和数百个节点时,全量诊断的执行时间可能超过 5 分钟。实践中应先通过告警定位故障范围(如特定命名空间、特定节点),再对故障范围执行精细化诊断。

间歇性故障的捕获:当前脚本是"按需执行"的,无法捕获间歇性故障的现场。需要配合事件记录系统——在 Pod 重启、节点驱逐等事件发生时自动触发诊断脚本,将现场信息保存到对象存储,供事后分析。

五、总结

Kubernetes 生产环境排障手册通过五层下钻模型——入口层、服务层、Pod 层、容器层、节点层——建立了从症状到根因的系统化诊断链路。每层有明确的检查项和判断标准,自动化脚本将人工逐条执行 kubectl 命令的流程编码为可复用的诊断工具。落地时需注意三点:一是自动化排障只能覆盖已知故障模式,应用逻辑故障需结合日志和追踪;二是大规模集群应先定位故障范围再精细化诊断;三是间歇性故障需要事件驱动的自动诊断机制,而非按需执行。排障的本质是"缩小搜索空间"——从集群级别的模糊症状,逐层下钻到具体的配置错误或资源瓶颈。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐