背景与问题
很多团队上 Kubernetes,第一阶段往往只关注“能跑起来”,第二阶段才会真正遇到架构问题:
- 控制平面单点,
kube-apiserver一挂,全员失联 - etcd 抖动,集群看起来“活着”,但所有变更都卡住
- 某个 noisy neighbor 工作负载把节点 CPU、内存、IO 打满,别的业务一起陪葬
- 一个命名空间里的错误配置,把整个集群调度和网络都拖慢
- Pod 频繁重建,但业务方只看到一句“服务偶发超时”
我自己第一次接手生产集群时,最痛的不是某一个组件坏了,而是系统没有隔离层次:控制平面和业务平面互相影响、业务之间互相争抢、排障路径也没有固定套路。结果就是一旦出问题,大家都在猜。
这篇文章不从“组件百科”讲起,而是站在 troubleshooting(故障排查) 的角度,带你把两个关键问题串起来:
- 控制平面怎么做高可用,才能在故障时不失控?
- 工作负载怎么做故障隔离,才能把事故影响面收小?
如果你已经有基础的 Kubernetes 使用经验,这篇文章会更适合你。
典型故障现象
在进入设计前,先看几类常见现象。很多线上事故,其实都能映射到下面几种模式。
现象 1:kubectl 偶发超时或完全不可用
表现:
kubectl get pods -A卡住- CI/CD 发版失败
- 控制器无法更新资源状态
- 新 Pod 一直 Pending,但节点其实有资源
这类问题往往指向:
kube-apiserver单点或负载均衡异常- etcd 延迟高
- 控制平面节点资源被抢占
- 网络策略或证书问题
现象 2:节点没挂,但业务开始大面积超时
表现:
- 某些服务 RT 飙升
- Pod 重启变多
- 节点
MemoryPressure/DiskPressure - kubelet 日志里出现 eviction
这通常不是“集群挂了”,而是隔离失效:
- 没有 requests/limits
- 没有 QoS 区分
- 没有 PodDisruptionBudget
- 没有节点污点和业务分层
- 核心系统组件和普通业务混跑
现象 3:某个业务故障扩散成全集群事故
表现:
- 一个批处理任务把整个节点池打爆
- 某租户命名空间里的 Pod 数暴涨
- 大量短生命周期 Job 导致 apiserver、controller-manager 压力飙升
这时候你会发现:故障不怕局部爆发,怕没有边界。
核心原理
这一部分我们只讲排障和落地最相关的原理,不追求面面俱到。
1. 控制平面高可用的本质
Kubernetes 控制平面高可用,核心有三层:
kube-apiserver多副本- etcd 高可用
- 稳定的访问入口(LB/VIP)
可以把它理解成:
apiserver是集群入口- etcd 是状态真相
- 负载均衡器是入口门面
只要这三层里有任意一层设计不稳,集群就会进入“看起来有些组件活着,但实际上无法治理”的状态。
flowchart TD
U[运维/控制器/kubectl] --> LB[负载均衡器 VIP/LB]
LB --> A1[kube-apiserver-1]
LB --> A2[kube-apiserver-2]
LB --> A3[kube-apiserver-3]
A1 --> E1[etcd-1]
A2 --> E2[etcd-2]
A3 --> E3[etcd-3]
CM1[kube-controller-manager] --> LB
SCH1[kube-scheduler] --> LB
subgraph Worker Nodes
P1[业务 Pod]
P2[系统 Pod]
end
P1 --> LB
P2 --> LB
为什么 apiserver 要多副本?
因为所有控制动作几乎都经过它:
- 创建/删除 Pod
- 调度结果写入
- 控制器更新状态
- kubelet 上报 Node/Pod 状态
单实例时,一旦宕机,已有 Pod 可能还能跑,但整个集群会进入不可变更状态。
为什么 etcd 是关键中的关键?
因为 etcd 保存集群状态。如果 etcd 不可写或延迟很高:
- apiserver 响应会变慢
- 控制器同步会卡住
- 调度器失去最新视图
- 资源对象可能出现“读得到旧状态,写不进去新状态”
etcd 不是普通数据库替代品,它更像是控制平面的“共识底座”。
2. 工作负载故障隔离的本质
隔离不是一个功能点,而是多层机制叠加出来的结果:
- 资源隔离:requests/limits、LimitRange、ResourceQuota
- 调度隔离:taint/toleration、nodeSelector、affinity
- 生命周期隔离:PDB、优雅终止、探针
- 网络隔离:NetworkPolicy
- 权限隔离:RBAC、命名空间、ServiceAccount
- 故障域隔离:拓扑分布、跨可用区、副本反亲和
flowchart LR
A[故障隔离] --> B[资源隔离]
A --> C[调度隔离]
A --> D[网络隔离]
A --> E[权限隔离]
A --> F[故障域隔离]
B --> B1[requests/limits]
B --> B2[Quota/LimitRange]
C --> C1[taint/toleration]
C --> C2[affinity/anti-affinity]
D --> D1[NetworkPolicy]
E --> E1[RBAC]
F --> F1[topologySpreadConstraints]
3. 为什么“高可用”和“隔离”必须一起设计?
一个很现实的问题:
如果控制平面高可用做了,但业务工作负载没隔离,那么在资源争抢时,控制平面仍然可能被业务拖死。
反过来也一样:
如果工作负载隔离做得不错,但控制平面单点,平台治理能力一断,修复动作就很慢。
所以我的经验是:先保控制平面可治理,再保业务故障有边界。
方案设计:从止血到长期架构
这里给一个适合中小到中大型集群的实战思路。
控制平面建议架构
- 3 个控制平面节点
- 每个节点运行:
kube-apiserverkube-controller-managerkube-scheduler- etcd(stacked etcd 方案)
- 前置一个 LB 或 VIP
- 控制平面节点打污点,不跑普通业务
什么时候选 stacked etcd?
适合:
- 集群规模中等
- 运维团队不想单独维护 etcd 集群
- 想降低部署复杂度
什么时候考虑 external etcd?
适合:
- 大规模集群
- etcd 需要独立扩容、独立运维
- 对控制平面和存储平面隔离要求更高
工作负载分层建议
建议至少分三层:
- 系统层
- CoreDNS、CNI、metrics-server、ingress controller
- 关键业务层
- 订单、支付、用户核心链路
- 普通业务/批处理层
- 报表、离线任务、临时任务
落地方式:
- 系统层节点池
- 关键业务节点池
- 批处理节点池
再通过:
- taint/toleration
- node affinity
- resource quota
- priority class
把边界真正“钉住”。
实战代码(可运行)
下面这部分我尽量给出可以直接应用或最少改动即可运行的示例。
1. 使用 kubeadm 初始化高可用控制平面
kubeadm-config.yaml
说明:这是一个简化可运行示例,适合实验或标准化部署模板起点。请将 IP、域名、版本替换成你的环境值。
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.28.0
controlPlaneEndpoint: "k8s-api.example.com:6443"
networking:
podSubnet: "10.244.0.0/16"
apiServer:
certSANs:
- "k8s-api.example.com"
- "10.0.0.10"
controllerManager: {}
scheduler: {}
etcd:
local:
dataDir: /var/lib/etcd
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: "10.0.0.11"
bindPort: 6443
nodeRegistration:
name: "cp-1"
criSocket: "unix:///run/containerd/containerd.sock"
taints:
- key: "node-role.kubernetes.io/control-plane"
effect: "NoSchedule"
初始化:
kubeadm init --config kubeadm-config.yaml --upload-certs
把输出中的 join 命令保存好,用于其他控制平面节点加入。
其他控制平面节点加入
kubeadm join k8s-api.example.com:6443 \
--token <your-token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--control-plane \
--certificate-key <certificate-key>
工作节点加入
kubeadm join k8s-api.example.com:6443 \
--token <your-token> \
--discovery-token-ca-cert-hash sha256:<hash>
2. HAProxy 作为 apiserver 前置负载均衡
如果你没有云 LB,可以先用 HAProxy。
haproxy.cfg
global
log /dev/log local0
maxconn 2000
defaults
log global
mode tcp
timeout connect 10s
timeout client 1m
timeout server 1m
frontend k8s_api
bind *:6443
default_backend k8s_api_backend
backend k8s_api_backend
option tcp-check
balance roundrobin
server cp1 10.0.0.11:6443 check
server cp2 10.0.0.12:6443 check
server cp3 10.0.0.13:6443 check
如果用 Docker 快速启动:
docker run -d --name haproxy \
-p 6443:6443 \
-v $(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro \
haproxy:2.8
3. 给控制平面节点加隔离,避免普通业务调度上来
先看节点污点:
kubectl describe node cp-1 | grep Taints -A2
如果没有,可手动添加:
kubectl taint nodes cp-1 node-role.kubernetes.io/control-plane=:NoSchedule
kubectl taint nodes cp-2 node-role.kubernetes.io/control-plane=:NoSchedule
kubectl taint nodes cp-3 node-role.kubernetes.io/control-plane=:NoSchedule
4. 对普通业务启用 requests/limits 和配额
namespace-quota.yaml
apiVersion: v1
kind: Namespace
metadata:
name: team-a
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-a
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "200m"
memory: "256Mi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "30"
应用:
kubectl apply -f namespace-quota.yaml
5. 用节点池和污点做业务分层
假设我们有一组批处理节点,标签为 workload=batch,并打上污点。
kubectl label nodes worker-3 workload=batch
kubectl label nodes worker-4 workload=batch
kubectl taint nodes worker-3 workload=batch:NoSchedule
kubectl taint nodes worker-4 workload=batch:NoSchedule
批处理任务 Deployment:
batch-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-app
namespace: team-a
spec:
replicas: 2
selector:
matchLabels:
app: batch-app
template:
metadata:
labels:
app: batch-app
spec:
tolerations:
- key: "workload"
operator: "Equal"
value: "batch"
effect: "NoSchedule"
nodeSelector:
workload: batch
containers:
- name: app
image: nginx:1.25
resources:
requests:
cpu: "300m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
ports:
- containerPort: 80
应用:
kubectl apply -f batch-app.yaml
6. 用反亲和和拓扑分布避免副本扎堆
critical-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app
namespace: team-a
spec:
replicas: 3
selector:
matchLabels:
app: critical-app
template:
metadata:
labels:
app: critical-app
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- critical-app
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: critical-app
containers:
- name: app
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
7. 用 PDB 防止运维动作把服务一起赶下线
pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: critical-app-pdb
namespace: team-a
spec:
minAvailable: 2
selector:
matchLabels:
app: critical-app
应用:
kubectl apply -f critical-app.yaml
kubectl apply -f pdb.yaml
8. 最小化 NetworkPolicy,先实现命名空间间隔离
前提:你的 CNI 支持 NetworkPolicy,例如 Calico、Cilium。
default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: team-a
spec:
podSelector: {}
policyTypes:
- Ingress
allow-same-namespace.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: team-a
spec:
podSelector: {}
ingress:
- from:
- podSelector: {}
应用:
kubectl apply -f default-deny.yaml
kubectl apply -f allow-same-namespace.yaml
现象复现:模拟一个“资源争抢导致故障扩散”的场景
为了让排障更具体,我们模拟一个 CPU 打满的工作负载。
cpu-burn.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cpu-burn
namespace: team-a
spec:
replicas: 4
selector:
matchLabels:
app: cpu-burn
template:
metadata:
labels:
app: cpu-burn
spec:
containers:
- name: stress
image: polinux/stress
args: ["--cpu", "2"]
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "2"
memory: "128Mi"
部署:
kubectl apply -f cpu-burn.yaml
观察节点资源:
kubectl top nodes
kubectl top pods -n team-a
如果你的集群没有做分层和配额,很容易看到某些节点被迅速打满,进而影响同节点其他业务。
定位路径:出了问题先看哪里
这一节是 troubleshooting 的重点。我建议排障时不要一上来就“到处翻日志”,而是按层次走。
sequenceDiagram
participant User as 用户/业务
participant SVC as Service/Ingress
participant Pod as Pod
participant Node as Node/Kubelet
participant API as kube-apiserver
participant ETCD as etcd
User->>SVC: 请求超时/失败
SVC->>Pod: 转发失败或无可用后端
Pod-->>Node: Pod 重启/探针失败/被驱逐
Node-->>API: 状态上报异常或延迟
API-->>ETCD: 读写慢/失败
ETCD-->>API: 高延迟
第一步:先分清是控制平面问题还是数据平面问题
快速判断命令
kubectl get --raw='/readyz?verbose'
kubectl get componentstatuses
kubectl get nodes
kubectl get pods -A
注意:
componentstatuses已不推荐长期依赖,但在某些环境里还可以做粗略观察- 更可靠的是直接看
readyz、Pod 状态和组件日志
如果 kubectl 都连不上,优先检查:
- LB/VIP 是否通
6443端口是否可达- apiserver 进程是否存活
- 证书是否过期
第二步:控制平面排查路径
检查 apiserver
kubectl -n kube-system get pods -l component=kube-apiserver -o wide
kubectl -n kube-system logs kube-apiserver-cp-1 --tail=100
看这些关键词:
etcdserver: request timed outcontext deadline exceededx509: certificate has expiredtoo many open files
检查 etcd
如果是 kubeadm stacked etcd:
kubectl -n kube-system get pods -l component=etcd -o wide
kubectl -n kube-system logs etcd-cp-1 --tail=100
常见异常:
- leader 频繁切换
- 磁盘延迟高
- WAL/fsync 慢
- 空间不足触发告警
如果节点上有 etcdctl,可以直接健康检查:
ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health
检查 LB
nc -vz k8s-api.example.com 6443
curl -k https://k8s-api.example.com:6443/readyz
如果 LB 不健康,往往表现为:
- 某个 apiserver 已挂,但 LB 还在转发
- TCP 通,但后端 TLS 或 HTTP readyz 不正常
- 健康检查策略过于简单
我踩过一个坑:LB 只做 TCP 端口探测,结果后端 apiserver 进程还在,但实际上已经无法正常处理请求,最终表现就是“偶发卡死”。
第三步:工作负载排查路径
看 Pod 为什么不可用
kubectl describe pod <pod-name> -n team-a
kubectl logs <pod-name> -n team-a --previous
重点看:
OOMKilledCrashLoopBackOffReadiness probe failedEvictedInsufficient cpuInsufficient memory
看节点压力
kubectl describe node <node-name>
kubectl top node <node-name>
journalctl -u kubelet -n 100 --no-pager
观察是否有:
MemoryPressureDiskPressure- eviction 相关日志
- 镜像拉取卡顿
- 容器运行时异常
看调度是否出问题
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
常见调度事件:
- 0/6 nodes are available
- node(s) had taint that the pod didn’t tolerate
- node(s) didn’t match Pod’s node affinity
- insufficient cpu/memory
这些信息非常关键,因为它告诉你:是资源不够,还是规则把自己卡住了。
常见坑与排查
这一节我列一些线上最常见、也最容易被忽略的问题。
坑 1:控制平面高可用了,但 etcd 仍然是隐形单点
现象
- apiserver 有多个副本
- 但一旦某个控制平面节点磁盘异常,整个集群变慢
原因
- stacked etcd 虽然是多节点,但磁盘性能差、网络延迟高,仍然会导致整体不可用
- etcd 对磁盘 fsync 很敏感,不能拿普通低性能盘硬扛
排查
kubectl -n kube-system logs etcd-cp-1 --tail=200 | grep -Ei "took too long|leader|timeout|slow"
止血方案
- 先限制大规模对象变更操作
- 暂停批量发布、批量 Job 创建
- 检查磁盘 IO 和容量
- 必要时迁移 etcd 到更稳定的存储节点
坑 2:节点资源没设 requests,调度看起来“很空”,运行时却被打爆
现象
- Pod 都能调度成功
- 运行一会儿后节点高负载、频繁 OOM
原因
调度器依据 requests 做资源预估,不是依据 limits,更不是依据“程序真实可能吃掉多少”。
排查
kubectl get pod -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{range .spec.containers[*]}{.name}{": reqCPU="}{.resources.requests.cpu}{", reqMem="}{.resources.requests.memory}{"; "}{end}{"\n"}{end}'
止血方案
- 立即通过 LimitRange 给默认 requests/limits
- 对高风险命名空间加 ResourceQuota
- 关键业务单独节点池
坑 3:只配了 liveness probe,没配 readiness probe
现象
- Pod 已 Running,但服务大量 502/超时
- 重启反而加剧问题
原因
- liveness 只负责“活没活”
- readiness 才负责“能不能接流量”
止血方案
- 关键服务至少配置 readiness
- 启动慢的应用加 startupProbe
- 探针阈值不要照抄模板
坑 4:PDB 配太死,节点维护时 drain 不动
现象
kubectl drain <node-name> --ignore-daemonsets
卡住很久。
原因
minAvailable过高- 副本数本来就不够
- 单副本服务还配置了严格 PDB
排查
kubectl get pdb -A
kubectl describe pdb critical-app-pdb -n team-a
边界条件
PDB 是防止“主动驱逐”过度,不防节点突然宕机。不要把它理解成“高可用保险”。
坑 5:NetworkPolicy 一上来就全拒绝,结果 DNS 先挂了
现象
- Pod 之间互通失败
- 外部依赖访问异常
- 应用报“无法解析域名”
原因
- 默认拒绝后,没有放行到 CoreDNS 的流量
止血方案
补允许 DNS 的策略,例如:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: team-a
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
安全/性能最佳实践
这一节不追求大全,但都是能实打实减少事故概率的点。
安全最佳实践
1. 控制平面节点禁止跑普通业务
这是最基本、也最容易被忽视的一条。
建议:
- 保留控制平面污点
- 仅允许必要系统组件容忍该污点
- 不要为了“资源利用率”把普通业务塞进去
2. 最小权限原则
- 不要给默认 ServiceAccount 过高权限
- 每个系统组件独立 RBAC
- 租户隔离场景下,按命名空间细分 Role/RoleBinding
快速查看高风险绑定:
kubectl get clusterrolebinding
kubectl get rolebinding -A
3. 证书和密钥轮换要有台账
生产里经常不是组件挂了,而是证书过期了。
检查 kubeadm 证书:
kubeadm certs check-expiration
4. 审计日志要开,但不要无限制落盘
审计日志很有用,尤其是排查“谁改了配置”。
但如果不控制策略和存储,很容易变成 IO 压力源。
性能最佳实践
1. etcd 优先保证稳定磁盘和低延迟网络
如果预算有限,先别急着上更多插件,先把 etcd 所在节点硬件打稳。
重点关注:
- 磁盘延迟
- 空间碎片
- leader 稳定性
- 备份恢复演练
2. 限制对象风暴
会给控制平面施压的常见行为:
- 短时间创建大量 Job/Pod
- 频繁更新 ConfigMap/Secret
- 控制器重试风暴
- 大量 watch client
建议:
- 对批处理任务做速率限制
- 控制部署波次
- 避免把 Kubernetes 当高频配置数据库
3. 系统组件资源要保底
例如 CoreDNS、CNI、ingress controller,建议明确 requests/limits。
否则业务波峰一来,最先被挤压的往往是这些“底座服务”。
4. 关键业务使用 PriorityClass,但不要滥用
如果所有业务都说自己“关键”,那实际上谁都不关键。
一个简化示例:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-workload
value: 100000
globalDefault: false
description: "用于核心业务工作负载"
应用后在 Pod 中引用:
priorityClassName: critical-workload
一套实用的止血方案
当线上已经出问题时,不要一上来就大改架构。我的建议是按下面顺序止血。
场景 1:apiserver 变慢或不稳定
优先级:
- 检查 LB 是否把流量发往不健康节点
- 检查 etcd 健康和延迟
- 停止大批量发布/批量 Job
- 检查控制平面节点是否被资源打满
- 查看证书、磁盘、文件句柄等基础问题
场景 2:节点资源争抢严重
优先级:
- 暂停高消耗批处理任务
- 驱逐或缩容异常工作负载
- 对命名空间补 ResourceQuota / LimitRange
- 给关键业务迁移到专用节点池
- 为系统组件补资源保底
场景 3:某租户影响全局
优先级:
- 用 Quota 限流该租户对象数和资源量
- 加默认 deny 网络策略,阻止横向扩散
- 拆分节点池,避免与关键业务同池
- 复盘该租户的发布和权限边界
总结
Kubernetes 集群架构做得好不好,不是看图画得多漂亮,而是看故障来了以后:
- 控制平面是不是还能治理
- 故障是不是能被限制在局部
- 排障是不是有稳定路径
- 止血动作是不是能快速执行
如果你希望把这篇文章里的内容真正落地,我建议按这个顺序推进:
- 先做控制平面高可用
- 3 控制平面节点
- 稳定 LB/VIP
- etcd 健康检查和备份
- 再做最基本的工作负载隔离
- requests/limits
- LimitRange / ResourceQuota
- 控制平面和普通业务分离
- 然后做关键业务保护
- 独立节点池
- PDB
- 反亲和和拓扑分布
- 最后补网络和权限边界
- NetworkPolicy
- 最小 RBAC
- 审计和证书轮换
边界条件也要说清楚:
- 如果你的集群规模很小,别一上来把架构搞得过重,先守住最关键的高可用和隔离底线
- 如果你的集群已经进入多租户、多环境共用阶段,那“靠约定隔离”基本不够,必须制度化、模板化、平台化
- 如果 etcd 和控制平面节点底层硬件不稳,再多 YAML 也救不了根问题
一句话收尾:
高可用保证你“还能管”,故障隔离保证你“不会一起炸”。这两件事,是 Kubernetes 生产架构最该优先做的基本功。