Kubernetes 生产环境运维与排障实战:那些年踩过的坑与填平的路
Kubernetes 生产环境运维与排障实战:那些年踩过的坑与填平的路
一、集群又挂了:K8s 运维的真实战场
Kubernetes 在生产环境中跑起来不难,难的是稳定运行。Pod 无限重启、节点 NotReady、Service 端点丢失、PVC 无法挂载……这些问题在测试环境可能永远不会出现,一到生产环境就频繁上演。原因很简单:生产环境的规模、负载和复杂度,是测试环境无法模拟的。
K8s 运维的难点在于故障表象和根因往往不在同一层。Pod 启动失败,可能是镜像拉不下来,也可能是节点资源不足,还可能是网络策略拦截。排查时需要逐层剥开,从应用层到容器运行时,再到内核和网络,每一层都可能是故障点。
本文不谈概念,只谈实战。从最常见的故障场景出发,给出系统化的排查思路和可复用的诊断脚本。
二、故障传播链:K8s 问题的分层定位模型
K8s 的故障排查,需要建立分层思维。不同层的问题有不同的排查工具和方法论。
graph TD subgraph 应用层 A1[Pod CrashLoopBackOff] A2[应用 OOMKilled] A3[健康检查失败] end subgraph 调度层 B1[Pod Pending] B2[节点资源不足] B3[亲和性/污点约束] end subgraph 网络层 C1[Service 无端点] C2[DNS 解析失败] C3[网络策略拦截] end subgraph 存储层 D1[PVC Pending] D2[PV 绑定失败] D3[IO 性能劣化] end subgraph 节点层 E1[节点 NotReady] E2[Kubelet 异常] E3[容器运行时故障] end A1 --> B2 A3 --> C1 B1 --> B2 B1 --> B3 C1 --> C2 C1 --> C3 D1 --> D2 E1 --> E2 E2 --> E3 style A1 fill:#fce4ec style E1 fill:#fce4ec style C2 fill:#fff3e0自上而下排查法是最高效的策略。先确认 Pod 状态,再检查事件和日志,最后深入节点和网络。大多数问题在前两层就能定位,不需要 SSH 到节点上排查。
事件(Events)是排查的金钥匙。K8s 的 Event 对象记录了集群中所有状态变更的详细信息,包括调度决策、镜像拉取、健康检查等。kubectl describe输出末尾的 Events 部分,往往直接告诉你问题出在哪里。
三、排障工具箱:K8s 常见故障的诊断脚本
以下脚本覆盖了 K8s 运维中最常见的五类故障场景,每个脚本都包含详细的排查逻辑和输出解读。
#!/bin/bash # k8s-troubleshoot.sh — K8s 生产环境一键排障工具箱 # 使用方式: ./k8s-troubleshoot.sh <namespace> <pod-name> set -euo pipefail NAMESPACE="${1:-default}" POD_NAME="${2:-}" # 颜色定义,便于区分输出级别 RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # ======================================== # 模块一:集群健康总览 # 快速判断问题范围是单个 Pod 还是整个集群 # ======================================== check_cluster_health() { log_info "===== 集群健康总览 =====" # 检查节点状态,NotReady 的节点是很多问题的根源 local not_ready_nodes not_ready_nodes=$(kubectl get nodes --no-headers | grep -v "Ready" || true) if [[ -n "$not_ready_nodes" ]]; then log_error "发现 NotReady 节点:" echo "$not_ready_nodes" # 进一步检查 NotReady 节点的原因 for node in $(echo "$not_ready_nodes" | awk '{print $1}'); do log_warn "节点 $node 的 Conditions:" kubectl get node "$node" -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}: {.message}){"\n"}{end}' done else log_info "所有节点状态正常" fi # 检查关键组件的 Pod 状态 log_info "核心组件状态:" kubectl get pods -n kube-system -o wide --no-headers | \ awk '{if ($3 != "Running" && $3 != "Completed") print}' | \ while read -r line; do log_error "$line" done # 统计各命名空间的异常 Pod 数量 log_info "各命名空间异常 Pod 统计:" kubectl get pods -A --no-headers | \ awk '{if ($4 != "Running" && $4 != "Completed" && $4 != "Succeeded") print $1}' | \ sort | uniq -c | sort -rn | \ while read -r count ns; do log_warn "命名空间 $ns: $count 个异常 Pod" done } # ======================================== # 模块二:Pod 故障诊断 # 针对 CrashLoopBackOff、ImagePullBackOff 等常见状态 # ======================================== diagnose_pod() { local ns="$1" local pod="$2" log_info "===== Pod 诊断: $ns/$pod =====" # 获取 Pod 状态和容器状态 local pod_status pod_status=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.status.phase}') log_info "Pod 状态: $pod_status" # 检查容器状态,区分 Waiting/Running/Terminated local container_statuses container_statuses=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.status.containerStatuses[*]}' 2>/dev/null || true) if [[ -z "$container_statuses" ]]; then log_warn "无法获取容器状态,可能是 Init 容器阶段" # 检查 Init 容器状态 kubectl get pod "$pod" -n "$ns" -o jsonpath='{range .status.initContainerStatuses[*]}{.name}: {.state}{"\n"}{end}' 2>/dev/null return fi # 提取 Waiting 状态的容器及原因 kubectl get pod "$pod" -n "$ns" -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.state.waiting.reason}{"\t"}{.state.waiting.message}{"\n"}{end}' 2>/dev/null | \ while IFS=$'\t' read -r name reason message; do if [[ -n "$reason" ]]; then log_error "容器 $name 处于 Waiting 状态: $reason" [[ -n "$message" ]] && log_error " 原因: $message" # 针对常见 Waiting 原因给出具体建议 case "$reason" in CrashLoopBackOff) log_warn " 建议: kubectl logs -n $ns $pod -c $name --previous" log_warn " 建议: kubectl describe pod -n $ns $pod | tail -20" ;; ImagePullBackOff|ErrImagePull) log_warn " 建议: 检查镜像地址和拉取凭据" log_warn " kubectl get secret -n $ns" ;; OOMKilled) log_warn " 建议: 增加内存限制或排查内存泄漏" log_warn " 当前限制: kubectl get pod $pod -n $ns -o jsonpath='{.spec.containers[*].resources.limits}'" ;; esac fi done # 检查重启次数,频繁重启意味着持续故障 local restart_count restart_count=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.status.containerStatuses[0].restartCount}' 2>/dev/null || echo "0") if [[ "$restart_count" -gt 3 ]]; then log_warn "容器已重启 $restart_count 次,建议查看历史日志" fi # 输出最近的 Events,这是排障最关键的信息 log_info "最近事件:" kubectl get events -n "$ns" --field-selector involvedObject.name="$pod" --sort-by='.lastTimestamp' 2>/dev/null | tail -10 } # ======================================== # 模块三:网络连通性诊断 # 排查 Service 无端点、DNS 解析失败等问题 # ======================================== diagnose_network() { local ns="$1" local pod="$2" log_info "===== 网络诊断: $ns/$pod =====" # 获取 Pod IP local pod_ip pod_ip=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.status.podIP}' 2>/dev/null || true) if [[ -z "$pod_ip" ]]; then log_error "Pod 无 IP 地址,可能尚未调度或网络插件异常" return fi log_info "Pod IP: $pod_ip" # 检查 Pod 所在 Service 的端点 local labels labels=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.metadata.labels}' 2>/dev/null || true) if [[ -n "$labels" ]]; then log_info "查找匹配的 Service 端点..." kubectl get svc -n "$ns" -o name | while read -r svc; do local endpoints endpoints=$(kubectl get "$svc" -n "$ns" -o jsonpath='{.spec.selector}' 2>/dev/null || true) if [[ -n "$endpoints" ]]; then local svc_name svc_name=$(echo "$svc" | cut -d'/' -f2) local ep_count ep_count=$(kubectl get endpoints "$svc_name" -n "$ns" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | wc -w || echo "0") if [[ "$ep_count" -eq 0 ]]; then log_warn "Service $svc_name 无可用端点" else log_info "Service $svc_name 有 $ep_count 个端点" fi fi done fi # DNS 解析测试 log_info "DNS 解析测试:" kubectl exec -n "$ns" "$pod" -- nslookup kubernetes.default.svc.cluster.local 2>/dev/null && \ log_info "集群内部 DNS 正常" || \ log_error "集群内部 DNS 解析失败,检查 CoreDNS" } # ======================================== # 模块四:资源压力诊断 # 检查节点资源使用和 Pod 资源限制 # ======================================== diagnose_resources() { local ns="$1" local pod="$2" log_info "===== 资源诊断: $ns/$pod =====" # 获取 Pod 所在节点 local node_name node_name=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.spec.nodeName}' 2>/dev/null || true) if [[ -z "$node_name" ]]; then log_error "Pod 未调度到任何节点" return fi log_info "所在节点: $node_name" # 检查节点资源压力条件 log_info "节点资源压力:" kubectl get node "$node_name" -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}' | \ grep -E "MemoryPressure|DiskPressure|PIDPressure" | \ while read -r line; do if echo "$line" | grep -q "True"; then log_error "$line" else log_info "$line" fi done # 检查 Pod 的资源请求和限制 log_info "Pod 资源配置:" kubectl get pod "$pod" -n "$ns" -o jsonpath='{range .spec.containers[*]}{.name}{"\n Requests: "}{.resources.requests}{"\n Limits: "}{.resources.limits}{"\n"}{end}' 2>/dev/null # 检查实际资源使用 log_info "实际资源使用:" kubectl top pod "$pod" -n "$ns" --containers 2>/dev/null || \ log_warn "metrics-server 未安装,无法获取资源使用数据" } # ======================================== # 模块五:存储诊断 # 排查 PVC 挂载失败和存储类问题 # ======================================== diagnose_storage() { local ns="$1" local pod="$2" log_info "===== 存储诊断: $ns/$pod =====" # 获取 Pod 使用的 PVC local pvcs pvcs=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{range .spec.volumes[*]}{.persistentVolumeClaim.claimName}{"\n"}{end}' 2>/dev/null | grep -v "^$" || true) if [[ -z "$pvcs" ]]; then log_info "Pod 未使用 PVC" return fi for pvc in $pvcs; do log_info "检查 PVC: $pvc" local pvc_status pvc_status=$(kubectl get pvc "$pvc" -n "$ns" -o jsonpath='{.status.phase}' 2>/dev/null || true) case "$pvc_status" in Bound) log_info "PVC $pvc 状态: Bound(正常)" # 检查 PV 的回收策略,避免数据意外丢失 local pv_name pv_name=$(kubectl get pvc "$pvc" -n "$ns" -o jsonpath='{.spec.volumeName}' 2>/dev/null || true) local reclaim_policy reclaim_policy=$(kubectl get pv "$pv_name" -o jsonpath='{.spec.persistentVolumeReclaimPolicy}' 2>/dev/null || true) if [[ "$reclaim_policy" == "Delete" ]]; then log_warn "PV $pv_name 回收策略为 Delete,PVC 删除后数据将丢失" fi ;; Pending) log_error "PVC $pvc 状态: Pending(未绑定)" # 检查 PVC 事件,找出绑定失败的原因 kubectl get events -n "$ns" --field-selector involvedObject.name="$pvc" --sort-by='.lastTimestamp' 2>/dev/null | tail -5 ;; Lost) log_error "PVC $pvc 状态: Lost(底层 PV 丢失)" ;; *) log_warn "PVC $pvc 状态: $pvc_status" ;; esac done } # ======================================== # 主流程 # ======================================== main() { log_info "K8s 排障工具箱启动" log_info "命名空间: $NAMESPACE" check_cluster_health if [[ -n "$POD_NAME" ]]; then diagnose_pod "$NAMESPACE" "$POD_NAME" diagnose_network "$NAMESPACE" "$POD_NAME" diagnose_resources "$NAMESPACE" "$POD_NAME" diagnose_storage "$NAMESPACE" "$POD_NAME" else log_info "未指定 Pod 名称,仅执行集群级别检查" log_info "使用方式: $0 <namespace> <pod-name> 进行详细诊断" fi log_info "===== 诊断完成 =====" } main脚本设计思路:五个模块覆盖了 K8s 故障的五个核心维度——集群健康、Pod 状态、网络连通、资源压力和存储挂载。每个模块独立运行,可以单独调用,也可以一键全量扫描。输出使用颜色区分级别,便于快速定位关键信息。
四、K8s 运维的架构权衡:没有放之四海皆准的方案
资源限制的设置困境。Requests 设低了,Pod 可能被调度到资源不足的节点;设高了,集群资源利用率低,成本浪费。Limits 设低了,容易被 OOMKilled;设高了,一个失控的 Pod 可能吃掉整个节点的资源。生产环境建议 Requests 按 P50 实际使用设置,Limits 按 P95 设置,并配合 LimitRange 和 ResourceQuota 做全局管控。
节点维护的停机策略。节点需要重启内核补丁时,直接重启会导致 Pod 中断。正确的做法是先kubectl cordon阻止新 Pod 调度,再kubectl drain驱逐现有 Pod,确认所有 Pod 迁移完成后再维护。但 drain 操作本身可能导致有状态服务的数据不一致,需要根据业务特性评估。
多集群管理的复杂度。单集群规模超过 5000 节点后,etcd 和 API Server 的性能会明显下降。多集群是必然选择,但跨集群的服务发现、流量管理和统一监控,又引入了新的复杂度。联邦集群方案目前还不够成熟,多数团队选择自建多集群管理平台。
版本升级的风险。K8s 每年发布三个小版本,每个版本只维护 14 个月。升级是必须的,但每次升级都可能引入 API 变更和行为差异。建议在预发环境完整验证后再升级生产环境,并保留回滚方案。
五、总结
K8s 生产环境运维的核心能力,不是记住所有命令,而是建立系统化的排查思维。从集群到节点,从 Pod 到容器,从网络到存储,逐层定位、逐层排除。事件和日志是排障的指南针,比任何经验都可靠。
排障工具箱的价值在于标准化——将运维人员的排查经验固化为可复用的脚本,减少人为遗漏,提高排障效率。但工具只是手段,理解 K8s 的架构原理和故障传播机制,才是快速定位问题的根本。
守护系统稳定运行,需要的不仅是技术,还有耐心和系统化的思维。就像在山野中辨认方向,地图和指南针是工具,但真正让你走出困境的,是对地形的理解和冷静的判断。
