【K8S 运维实战】01-K8s架构深度剖析

K8s 架构深度剖析:从组件协作到一次调度的完整数据流

一句话定位:把控制面与数据面的协作拆到字节级,看懂一次 Pod 调度的完整链路。

写在前面

刚上手 K8s 的同学,大多是从 kubectl run 开始的。命令一敲,Pod 就起来了,看起来像魔法。但生产环境从来不给你"魔法"的余地------apiserver 响应变慢、etcd 磁盘打满、scheduler 卡住、kubelet 报 "PLEG is not healthy",这些故障没有一件能用 kubectl run 解决。

我这几年带过不少从传统运维转 K8s 的兄弟,发现一个共同问题:大家能把组件名字背下来,但说不清"一个 Pod 从提交到启动,中间到底发生了什么"。说不清链路,排查就只能靠 Google 报错信息,效率低、容易误判。有一次生产事故,一个 Node 上 Pod 一直 Pending,同事折腾了一小时去查 scheduler 日志,结果问题是 kubelet 静态压力导致节点 NotReady。链路不清楚,定位方向就是错的。

这篇我打算把 K8s 架构掰开揉碎:先讲声明式 API 和 reconcile 模型这个"灵魂",再走一遍 Pod 创建的 11 步数据流,最后把每个组件故障的爆炸半径和关键参数摆出来。看完这篇,你应该能在脑里建立一张"组件协作时序图",下次出问题知道往哪儿看。

版本基线:K8s 1.30 / Helm 3.14,容器运行时 containerd 1.7。

核心问题

  • apiserver、etcd、scheduler、controller-manager、kubelet、kube-proxy 到底怎么协作?
  • 声明式 API 和 reconcile 模型为什么是 K8s 的灵魂?
  • 一次 kubectl apply 创建 Pod,中间经历了多少步?谁在监听谁?
  • 某个组件挂了,爆炸半径到底多大?哪些是单点、哪些有冗余?
  • 各组件关键参数怎么调?生产环境默认值够不够?

一、原理剖析

1.1 控制面与数据面的分工

K8s 是典型的"控制器模式"系统,核心思想:用户声明期望状态(System State Desired),控制器不断观察当前状态,把差异抹平。理解这一点,比记十个组件名字都重要。

整个集群分两块:

  • 控制面(Control Plane):大脑。包含 apiserver、etcd、scheduler、controller-manager(还有 1.25+ 独立出来的 cloud-controller-manager)。负责"决策"。

  • 数据面(Data Plane):手脚。包含 kubelet、kube-proxy、容器运行时(containerd)。负责"执行"。

    ┌─────────────────────────────────────────────────────────────┐
    │ Control Plane │
    │ ┌──────────┐ ┌────────────────┐ ┌──────────────────┐ │
    │ │ apiserver│◄──│ scheduler │ │controller-manager│ │
    │ │ │──►│ │ │ (Deployment/RS/ │ │
    │ │ │ └────────────────┘ │ Node/Endpoint...)│ │
    │ │ │◄───────────────────────┤ │ │
    │ │ │◄──┐ └──────────────────┘ │
    │ └────┬─────┘ │ │
    │ │ │ ┌──────────┐ │
    │ └─────────┴───►│ etcd │(唯一有状态组件) │
    │ └──────────┘ │
    └─────────────────────────────────────────────────────────────┘
    │ watch/list

    ┌─────────────────────────────────────────────────────────────┐
    │ Data Plane │
    │ ┌───────────────────────Node 1──────────────────────────┐ │
    │ │ kubelet ◄─► containerd ◄─► runc ◄─► Pod(container) │ │
    │ │ kube-proxy(iptables/ipvs) │ │
    │ └───────────────────────────────────────────────────────┘ │
    │ ┌───────────────────────Node 2──────────────────────────┐ │
    │ │ kubelet ◄─► containerd ◄─► runc ◄─► Pod(container) │ │
    │ └───────────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────────┘

关键认知:etcd 是唯一有状态组件,所有集群状态都存在这里。apiserver 是唯一读写 etcd 的入口,其他组件都通过 apiserver 的 watch 机制获取变化。这个设计让控制面组件全部无状态,可以水平扩展。

1.2 声明式 API 与 Reconcile 模型

声明式 vs 命令式,是 K8s 和传统运维最大的思维差异。

  • 命令式:"帮我启动一个 nginx Pod"。系统执行完就完事,Pod 挂了没人管。
  • 声明式:"我要 3 个 nginx 副本"。系统记录这个期望,控制器持续观察,少了补、多了删。

Reconcile(调和)循环是这套模型的实现核心。伪代码长这样:

go 复制代码
for {
    desired := getDesiredState()    // 从 etcd 读 spec
    current := getCurrentState()    // 从集群读 status
    if desired != current {
        actions := computeActions(desired, current)
        execute(actions)            // 通过 apiserver 改状态
    }
    sleep(reconcilePeriod)
}

这套模型有三个特点值得记:

  1. 幂等:控制器跑多少遍结果一样,网络抖动重试不会出问题。
  2. 最终一致:不保证实时一致,但保证最终趋近期望。
  3. 水平触发:不关心"事件发生的时刻",只关心"当前状态对不对"。比边缘触发(Event-driven)健壮得多------丢一个事件不会让系统永久跑偏。

Deployment、ReplicaSet、StatefulSet、DaemonSet、Node Controller、Endpoint Controller......全是 reconcile 循环。理解了这一个模式,就理解了 K8s 一半的设计。

1.3 List-Watch:组件协作的神经传导

K8s 组件之间不直接互相调用,而是通过 apiserver 的 Watch 机制传递变化。这是必须想清楚的点。

  • List:全量拉取某资源。启动时用,或 watch 断开重连时用。
  • Watch:增量推送资源变化事件(ADDED/MODIFIED/DELETED)。长连接,基于 HTTP/2。

每个控制器都 watch 自己关心的资源。scheduler watch Pod(找未调度的);kubelet watch Pod(找分配到本节点的);controller-manager watch Deployment(算 ReplicaSet)、watch Node(处理节点故障)。所有变化都从 etcd → apiserver → watch 推送,组件不直接读 etcd。

复制代码
etcd ──(watch)──► apiserver ──(watch)──┬─► scheduler        (watch Pod,等 spec.nodeName=="")
                                        ├─► kubelet          (watch Pod,等 spec.nodeName==自己)
                                        ├─► kube-proxy       (watch Service/Endpoint)
                                        ├─► deployment ctrl  (watch Deployment)
                                        └─► node ctrl        (watch Node)

这里有个生产细节:apiserver 的 watch 缓存 (也叫 "watch cache")。apiserver 在内存里给每个资源维护一份缓存,watch 请求优先从缓存读,不打 etcd。这就是为什么 etcd 挂了短时间内集群还能查 kubectl get(读走缓存),但写操作会失败。理解这点,排查"apiserver 慢但 etcd 不慢"就有方向了------可能是 watch cache 失效或 cache miss 高。

二、实战操作

2.1 环境准备

我们用一台控制面 + 两台工作节点模拟,系统 Ubuntu 22.04,K8s 1.30.4。

bash 复制代码
# 三台机器都要做(假设 IP:10.0.0.10 master,10.0.0.11/12 worker)
# 1. 关闭 swap
sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab

# 2. 内核模块
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay && sudo modprobe br_netfilter

# 3. 内核参数
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system

# 4. 安装 containerd
sudo apt-get update && sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# 配置 SystemdCgroup(1.30 推荐)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd && sudo systemctl enable containerd

# 5. 安装 kubeadm/kubelet/kubectl
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | \
  sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | \
  sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet=1.30.4-1.1 kubeadm=1.30.4-1.1 kubectl=1.30.4-1.1
sudo apt-mark hold kubelet kubeadm kubectl

2.2 一次 Pod 创建的 11 步数据流

环境就绪后,我们走一遍完整链路。先部署个 nginx Deployment:

bash 复制代码
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80
EOF

kubectl apply 回车到 Pod Running,中间发生了 11 步。这是这篇的重头戏,我画个时序图:
containerd kubelet scheduler ReplicaSet Ctrl Deployment Ctrl etcd apiserver kubectl containerd kubelet scheduler ReplicaSet Ctrl Deployment Ctrl etcd apiserver kubectl #mermaid-svg-fkBr9MCIgIKtUJh8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fkBr9MCIgIKtUJh8 .error-icon{fill:#552222;}#mermaid-svg-fkBr9MCIgIKtUJh8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fkBr9MCIgIKtUJh8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fkBr9MCIgIKtUJh8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fkBr9MCIgIKtUJh8 .marker.cross{stroke:#333333;}#mermaid-svg-fkBr9MCIgIKtUJh8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fkBr9MCIgIKtUJh8 p{margin:0;}#mermaid-svg-fkBr9MCIgIKtUJh8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fkBr9MCIgIKtUJh8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fkBr9MCIgIKtUJh8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fkBr9MCIgIKtUJh8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fkBr9MCIgIKtUJh8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fkBr9MCIgIKtUJh8 .sequenceNumber{fill:white;}#mermaid-svg-fkBr9MCIgIKtUJh8 #sequencenumber{fill:#333;}#mermaid-svg-fkBr9MCIgIKtUJh8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fkBr9MCIgIKtUJh8 .messageText{fill:#333;stroke:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fkBr9MCIgIKtUJh8 .labelText,#mermaid-svg-fkBr9MCIgIKtUJh8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .loopText,#mermaid-svg-fkBr9MCIgIKtUJh8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fkBr9MCIgIKtUJh8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fkBr9MCIgIKtUJh8 .noteText,#mermaid-svg-fkBr9MCIgIKtUJh8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fkBr9MCIgIKtUJh8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fkBr9MCIgIKtUJh8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fkBr9MCIgIKtUJh8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fkBr9MCIgIKtUJh8 .actorPopupMenu{position:absolute;}#mermaid-svg-fkBr9MCIgIKtUJh8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fkBr9MCIgIKtUJh8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fkBr9MCIgIKtUJh8 .actor-man circle,#mermaid-svg-fkBr9MCIgIKtUJh8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fkBr9MCIgIKtUJh8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上面是同步返回,下面是异步 reconcile 1. POST /apis/apps/v1/deployments (含 HTTP Authn)2. Authn(证书) + Authz(RBAC) + Admission(Validating/Mutating)3. 写入 Deployment 对象写入成功,返回带 resourceVersion 的对象4. 201 Created (kubectl 打印 deployed)5. watch 收到 Deployment ADDED 事件6. 创建 ReplicaSet(满足 replicas=2)写入 ReplicaSet7. watch 收到 ReplicaSet ADDED8. 创建 2 个 Pod(spec.nodeName 空)写入 Pod x29. watch 收到 Pod(Phase=Pending, nodeName="")过滤(Filter:资源/taint/affinity)+ 打分(Score)+ 选节点10. POST /pods/<name>/binding 绑定到 Node更新 Pod.spec.nodeName11. watch 收到绑定到本节点的 Pod调 CRI containerd 拉镜像、创建容器更新 Pod.status.phase=Running, containerStatuses写入 Pod status

逐步拆解:

第 1-4 步:apiserver 的请求处理 。kubectl 把 YAML 转 JSON,带 kubeconfig 里的客户端证书发 HTTPS 请求。apiserver 经过三道关卡:认证(Authentication,你是谁)→ 授权(Authorization,你能干啥)→ 准入(Admission,你干的事合不合规) 。准入分 Mutating(改对象,如注入 sidecar)和 Validating(校验,如 OPA Gatekeeper)。通过后写 etcd,返回带 resourceVersion 的对象。注意:到这里 Pod 还没创建,只是 Deployment 对象入库了。

第 5-6 步:Deployment Controller 工作流 。controller-manager 里的 Deployment Controller 通过 watch 收到新 Deployment,根据 spec.template 创建 ReplicaSet(名字是 nginx-demo-<rs-hash>)。这一步只是把"我要 2 副本"翻译成"创建一个 RS,副本数 2"。

第 7-8 步:ReplicaSet Controller 工作流 。RS Controller watch 到新 RS,发现实际 Pod 数 0 ≠ 期望 2,创建 2 个 Pod。注意此时 Pod 的 spec.nodeName 是空的,状态 Pending。

第 9-10 步:scheduler 调度 。scheduler watch 到 spec.nodeName == "" 的 Pod,开始调度。两阶段:Filter (预选,排除资源不足、taint 不容忍、亲和性不满足的节点)+ Score (优选,对剩余节点打分,如 LeastRequestedStrategy、AntiAffinity)。选出节点后,通过 POST /pods/{name}/binding 把节点名写回 Pod。

第 11 步:kubelet 启动容器。目标节点的 kubelet watch 到绑定给自己的 Pod,开始干活:

  1. 调 CRI(containerd)拉镜像、创建 PodSandbox(pause 容器,持住 network namespace)。
  2. 调 CNI 给 Pod 配网络 IP。
  3. 按顺序启动 init 容器(若有),再启动业务容器。
  4. 调 CRI 启动容器,持续通过 PLEG(Pod Lifecycle Event Generator)感知容器状态。
  5. 把容器状态写回 Pod.status,apiserver 存 etcd。

到这里,kubectl get pods 才能看到 Running。

一个常见误区:很多人以为 scheduler 直接命令 kubelet 启动容器。错。scheduler 只做"选节点 + 绑定",剩下全靠 kubelet 自己 watch。这种完全解耦的设计让每个组件可以独立扩展和故障隔离。

2.3 验证数据流的实战命令

光看图不过瘾,实际操作一遍能加深印象。

bash 复制代码
# 1. 看 apiserver 审计日志(如果开了 audit),确认请求经过的链路
# 默认 kubeadm 装的集群 audit log 在 /var/log/kubernetes/audit/audit.log
sudo tail -f /var/log/kubernetes/audit/audit.log | jq '.'

# 2. 实时观察事件流,这是定位"卡在哪一步"最直接的工具
kubectl get events -n default --sort-by='.lastTimestamp' --watch

# 3. 跟踪单个 Pod 的调度过程
kubectl get pod <pod-name> -o wide --watch
# Pending → (scheduler 绑定) → ContainerCreating → Running

# 4. 看 scheduler 决策(1.30 默认开启 scheduler 调度结果日志)
kubectl -n kube-system logs kube-scheduler-<master> | grep <pod-name>
# 能看到 "Successfully bound pod to node xxx" 和被排除的节点原因

# 5. 看 kubelet 启动日志
sudo journalctl -u kubelet -f | grep <pod-name>

# 6. 用 kubectl debug 看 apiserver 各阶段耗时
kubectl get --raw '/metrics' | grep apiserver_request_duration_seconds

# 7. 看 watch 是否健康(apiserver 视角)
kubectl get --raw='/metrics' | grep watch_cache

我强烈建议读者按上面命令在测试环境跑一遍,尤其是 events 的 watch。生产排查 80% 的"Pod 起不来"问题,events 都能直接告诉你答案(SchedulerError、FailedScheduling、FailedMount、FailedSandbox......)。

三、踩坑与排查

坑 1:apiserver 响应慢,整个集群"卡住"

现象 :kubectl get pod 要等 10+ 秒,有时直接超时。但 docker ps/crictl ps 在节点上正常,Pod 业务正常。

原因:apiserver 是所有请求的入口,它慢整个集群就慢。常见原因:

  • etcd 慢(磁盘 IO 高、fsync 延迟大)。
  • watch cache 失效,大量请求穿透到 etcd。
  • 大量 LIST 请求(如某些监控 agent 全量 list Pod)。

排查与解决:

bash 复制代码
# 1. 看 etcd 健康度(关键指标:commit/apply/fsync 延迟)
sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  endpoint status -w table

# 2. 看 etcd 磁盘 fsync 延迟(应 < 10ms)
kubectl -n kube-system exec etcd-<master> -- sh -c \
  'ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
   --cacert=/etc/kubernetes/pki/etcd/ca.crt \
   --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
   --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
   endpoint status -w json' | jq '.[].Status.dbSize'

# 3. 看 apiserver watch cache 命中情况
kubectl get --raw='/metrics' | grep -E 'etcd_request_cache_gets|watch_cache'

# 4. 找出谁在疯狂 LIST
kubectl get --raw='/metrics' | grep apiserver_request_ | sort -t' ' -k2 -n -r | head -20

我遇到过一次生产事故,某个日志采集 agent 配置错了一直全量 LIST Pod(每 30 秒一次,集群 5 万 Pod),把 apiserver 打到 100% CPU。解决:让该 agent 改用 watch + informer 缓存,LIST 频率从 30 秒降到 5 分钟一次。

根治 :etcd 用独立 SSD(db size 超过 2GB 就要警惕);给 apiserver 加 --max-requests-inflight=400 --max-mutating-requests-inflight=200 限流;开启 audit 但只记录 metadata 级别(记录 RequestResponse 会爆)。

坑 2:kubelet 报 "PLEG is not healthy",Pod 状态卡住

现象 :节点上的 Pod 状态长期不更新,kubectl describe node 看到 PLEG 警告,kubelet 日志循环报 PLEG is not healthy: pleg was last seen active 3m5s ago

原因 :PLEG(Pod Lifecycle Event Generator)是 kubelet 用 CRI 查询容器状态的循环。它卡住通常是容器运行时(containerd)出问题:runtime 卡死、Docker daemon 卡死(老版本)、节点上容器太多导致 ListPodSandbox 超时。

排查与解决:

bash 复制代码
# 1. 看 kubelet 日志
sudo journalctl -u kubelet --since "10 min ago" | grep -i pleg

# 2. 测试 containerd 响应(如果这步慢,就是 runtime 问题)
sudo crictl ps -a | head
time sudo crictl ps

# 3. 看 containerd 是否卡死
sudo systemctl status containerd
sudo journalctl -u containerd --since "10 min ago" | tail -50

# 4. 重启 containerd(谨慎,会重启所有 Pod)
# sudo systemctl restart containerd

预防 :别在单节点塞太多 Pod(建议 < 110);containerd 配置调 max_concurrent_downloads、清理镜像;监控 PLEG 延迟指标 kubelet_pleg_relist_duration_seconds

坑 3:scheduler "选不出节点",但节点明明有资源

现象 :Pod 一直 Pending,events 报 0/3 nodes are available: 3 Insufficient memory。但 kubectl describe node 看着内存还够。

原因 :这个坑新手特别容易踩。describe nodeAllocatable 才是真正可分配的,Capacity 是物理总量。而且 kubelet 会预留一部分给系统(--kube-reserved--system-reserved),还有 eviction-threshold。所以"看着够"≠"调度够"。

排查与解决:

bash 复制代码
# 看节点真实可分配
kubectl describe node <node> | grep -A5 "Allocated resources"

# 算一下:Allocatable - 所有 Pod requests - 预留 >= 新 Pod request?
# 如果 Pod 没设 request,默认 scheduler 看不到资源占用,会一直能调度
# 但一旦某个大 Pod 设了大 request,容易卡

# 解决:给所有 Pod 都设 requests,别让 scheduler 瞎调度
# 必要时上 LimitRange 强制 namespace 内必须设

坑 4:controller-manager "看不到" Deployment 变化

现象:改了 Deployment 镜像,但 Pod 不滚动更新。

原因:controller-manager 和 apiserver 之间的 watch 断了(网络抖动、apiserver 重启)。watch 断开后会触发 list 重新建立缓存,但极端情况下重新 list 失败会卡住。

排查:

bash 复制代码
kubectl -n kube-system logs kube-controller-manager-<master> | grep -i "watch"
# 看是否有 "watch connection closed" 或 "too old resource version"

# 看控制器的工作队列是否堆积
kubectl -n kube-system exec kube-controller-manager-<master> -- \
  ps aux | grep kube-controller

通常重启 controller-manager(static pod 会自动拉起)能临时解决,根治要查网络稳定性和 apiserver 的 --default-watch-cache-size

四、最佳实践

生产环境组件参数调优清单,可以直接作为团队基线:

  • etcd:

    • 独立 SSD(强烈建议 NVMe),--quota-backend-bytes=8GB(默认 2GB 容易触发告警)。
    • --auto-compaction-retention=5(小时),开 --auto-compaction-mode=periodic
    • 定期 etcdctl defrag(在线 defrag,3.4+ 安全)。
    • 监控 etcd_server_proposals_failed_totaletcd_disk_wal_fsync_duration_seconds
  • apiserver:

    • --max-requests-inflight=400 --max-mutating-requests-inflight=200(默认 400/200,大集群可调高但要评估内存)。
    • --default-watch-cache-size=100(默认 100,大集群 Pod/Service 调到 500-1000)。
    • 开启 audit log,级别用 Metadata(不要 RequestResponse,会爆)。
    • 多实例 + LB,至少 2 个 apiserver。
  • scheduler:

    • 多实例(默认多副本,通过 leader election 选主)。
    • --percentage-of-nodes-to-score=50(默认 50,大集群可调,小集群用 100 全打分)。
    • 监控 scheduler_e2e_scheduling_duration_secondsscheduler_pending_pods
  • controller-manager:

    • 多实例 + leader election。
    • --node-monitor-period=5s--node-monitor-grace-period=40s(默认值,根据网络调整)。
    • --horizontal-pod-autoscaler-sync-period=15s--horizontal-pod-autoscaler-tolerance=0.1
  • kubelet:

    • --max-pods=110(默认 110,生产别超 250,容器多 PLEG 会慢)。
    • --kube-reserved=cpu=500m,memory=1Gi,ephemeral-storage=1Gi
    • --system-reserved=cpu=500m,memory=1Gi
    • --eviction-hard=memory.available<500Mi,nodefs.available<10%
    • --image-gc-high-threshold=85 --image-gc-low-threshold=80
  • kube-proxy:

    • 大集群用 ipvs 模式(mode: "ipvs"),iptables 模式规则多了之后性能急剧下降。
    • --conntrack-max-per-core=32768(默认 32768,高并发可调大)。
    • 监控 sync_proxy_rules_latency
  • 通用:

    • 控制面组件全部静态 Pod(kubeadm 默认),自动重启。
    • 监控必备指标:apiserver_request_totaletcd_request_duration_secondsscheduler_e2e_scheduling_duration_secondskubelet_pleg_relist_duration_seconds
    • 告警阈值:apiserver 5xx > 1%、etcd fsync p99 > 25ms、scheduler pending pods > 50 持续 5min。

五、小结

K8s 的架构说白了就一句话:所有状态存 etcd,所有组件通过 apiserver watch 状态变化,各自 reconcile 把状态推向期望。理解了 List-Watch + Reconcile,就理解了 K8s 90% 的行为逻辑。

一次 Pod 创建的 11 步链路,记不住没关系,但要知道:apiserver 处理请求 → 写 etcd → Deployment Controller 创建 RS → RS Controller 创建 Pod → scheduler 选节点绑定 → kubelet 拉镜像起容器。每一步都可能卡,排查思路就是顺着链路找断点。

生产排障的黄金法则:kubectl get events,再 kubectl describe,最后才看具体组件日志。events 是 apiserver 视角的"事件流水",80% 的问题它直接告诉你答案。

各组件故障爆炸半径记牢:etcd 挂=集群瘫痪(不可写)、apiserver 全挂=集群瘫痪(不可读写)、scheduler 挂=新 Pod 调不出来但存量正常、controller-manager 挂=自愈失效但存量正常、kubelet 挂=单节点故障、kube-proxy 挂=单节点网络异常。这个分级决定了你的 SLO 和故障演练优先级。

思考题

  1. 如果 etcd 突然挂了,集群还能对外服务多久?哪些操作会立刻失败、哪些还能撑一段时间?基于这个,你会怎么设计 etcd 的故障演练和恢复预案?
  2. 一个 Deployment 从 replicas=2 改到 replicas=5,你能完整描述中间 controller-manager、scheduler、kubelet 的工作过程吗?哪些步骤是串行、哪些是并行的?

延伸阅读

相关推荐
极客先躯7 小时前
高级java每日一道面试题-2026年04月18日-实战篇[Docker]-如何处理金融行业的时序数据容器化?
java·运维·docker·容器·金融·时序数据·高级面试
HackTwoHub8 小时前
等级保护现场测评系统重磅更新,支持 AI 接入,可录入全品类资产清单,自动化巡检核查,批量导出测评归档文件
运维·人工智能·安全·web安全·网络安全·自动化·系统安全
XUHUOJUN8 小时前
Azure Stack Hub 容量规划指南(上篇:Microsoft 软件层 + 架构原理)
架构·azure stack
seacracker8 小时前
HPC存储I/O抖动无解?PowerFS基于CRDT弱一致架构,彻底实现前台零抖动
架构·分布式存储·powerfs·hpc存储·i/o零抖动
解局易否结局8 小时前
ArkTS 设计模式与架构进阶:构建可维护的 HarmonyOS 应用
华为·设计模式·架构·harmonyos
2401_858286118 小时前
OS79.【Linux】POSIX信号量
linux·运维·服务器
公众号:fuwuqiBMC8 小时前
(转自“服务器BMC”)服务器BMC功能——DDR故障预测
运维·服务器·人工智能
会助力智能会务8 小时前
会务系统供应商怎么选?会助力智能会务系统,一站式数字化会务服务商
大数据·运维·人工智能
CodingPioneer9 小时前
03零刻Pro-LS2K3000-Loongnix 20.7.rc1硬盘分区、格式化、挂载与卸载
linux·运维·服务器·硬盘挂载