谁在干活:kubectl 命令背后的组件分工

系列前十几篇,集群一直当"黑盒"用—— kubectl apply 一个清单,应用就起来了,至于是谁把这件事做完的,没拆开看过。对兼职运维来说,这个黑盒必须拆开:排障的第一问不是"怎么修",而是"哪一环出了问题、该看谁"——Pod 一直 Pending 是调度的问题还是资源的问题?探针失败是应用的问题还是 kubelet 的问题?服务访问不通是 Service 配置还是网络插件?组件职责 = 排障归属地图

这篇用一条生产里每天都在用的命令( kubectl apply )当主线案例,把控制面四个组件和节点四个组件的职责、工作流程讲清楚,并用 kind 集群上的实测事件证明"谁在干活"。理论向,不做源码级剖析——兼职运维只需要知道"每个组件干什么、出了事找谁"。

1. 组件全景:控制面管"想",节点管"做"

K8s 的所有组件分成两组,职责边界非常清晰:

  • 控制面(control plane):负责决策——存状态、做调度、收敛声明,全在控制面;
  • 节点(node):负责执行——拉镜像、起容器、转发流量、管网络,全在节点。
%% K8s 组件全景: 控制面 4 件套 + 节点 4 件套
flowchart TD

    subgraph CP["控制面(决策)"]
        API["kube-apiserver\n唯一入口 + 收费站"]
        ETCD[("etcd\n唯一真相存储")]
        SCH["kube-scheduler\n给新 Pod 找节点"]
        CM["kube-controller-manager\n控制器集合(把声明收敛成动作)"]
    end
    subgraph N1["节点 learn-worker"]
        KL1["kubelet\nPod 生命周期 + 探针"]
        KP1["kube-proxy\nService 转发规则"]
        CT1["containerd\n容器运行时"]
        CNI1["kindnet\nPod 网络 + IP"]
    end
    subgraph N2["节点 learn-worker2"]
        KL2["kubelet"]
        KP2["kube-proxy"]
        CT2["containerd"]
        CNI2["kindnet"]
    end

    API --> ETCD
    API <-->|"watch/上报"| KL1
    API <-->|"watch/上报"| KL2
    API <-->|"watch"| KP1
    API <-->|"watch"| KP2
    API <-->|"watch"| SCH
    API <-->|"watch"| CM
    style API fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style SCH fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style CM fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style ETCD fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold
    style KL1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style KL2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style KP1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style KP2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style CT1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style CT2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style CNI1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style CNI2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff

在 kind 里这些组件都是真实运行的 Pod(生产 K8s 同款,只是 kind 把它们跑在 Docker 里)。看它们:

kubectl get pods -n kube-system

我集群上的实际输出(组件清单):

etcd-learn-control-plane                    Running   ← 控制面
kube-apiserver-learn-control-plane          Running   ← 控制面
kube-controller-manager-learn-control-plane Running   ← 控制面
kube-scheduler-learn-control-plane          Running   ← 控制面
kube-proxy-9wt6c / -c8cxk / -f4rp4          Running   ← 每节点一个
kindnet-4bc92 / -9mmgq / -bpnbc             Running   ← 每节点一个(CNI)
coredns-589f44dc88-fnj77 / -l6rjv           Running   ← 集群 DNS

1.1 控制面四组件:一句话职责

组件一句话职责对应命令/现象
kube-apiserverK8s 的唯一入口:所有读写请求都过它,负责认证(你是谁)、授权(你能干什么)、准入(合不合规),然后读写 etcdkubectl 的任何命令第一站都是它
etcd唯一真相:集群所有状态(资源对象、配置、期望状态)的键值存储,带 watch 能力集群状态持久化都在这
kube-scheduler调度器:给新 Pod 挑一个合适的节点(看资源余量、污点、亲和性),只决定"放哪",不负责"启动"Pod 一直 Pending → 找它
kube-controller-manager控制器集合:Deployment 控制器、ReplicaSet 控制器、Node 控制器……每个控制器一个控制回路,把"声明"收敛成"动作"kubectl scale 、自愈、滚动全是它

📌 前置知识:apiserver 是"收费站",etcd 是"账本"——收费站把所有请求登记进账本,其他组件(scheduler/controller/kubelet)全都通过 watch 账本变化来感知"该干活了",干完活再写回账本。所有组件不直接对话,只通过 apiserver + etcd 间接协作——这是理解一切工作流程的关键。

1.2 节点四组件:一句话职责

组件一句话职责对应命令/现象
kubelet节点代理(agent):管理分配给本节点的 Pod(创建/销毁容器)、执行探针、向 apiserver 上报节点和 Pod 状态探针失败、节点 NotReady → 找它
kube-proxy把 Service 的虚拟 IP 翻译成转发规则(iptables/ipvs),实现负载均衡服务访问不通 → 找它
containerd容器运行时:真正拉镜像、创建/启动/停止容器镜像拉取失败 → 找它
kindnet(CNI)给每个 Pod 分配 IP、打通节点间 Pod 通信Pod 网络不通 → 找它

生产里 CNI 通常是 Calico/Cilium/Flannel,kind 用的是简化版 kindnet——职责相同,实现不同

2. 案例主线: kubectl apply 的完整旅程

现在把一条日常命令拆开,看它背后控制面和节点怎么接力。用 kubectl apply -f deploy.yaml (创建 Deployment)为例:

%% kubectl apply 的 10 步旅程: 谁在每一步干什么
flowchart TD

    S1["① kubectl 发送请求\nPOST Deployment 清单"]
    S2["② apiserver\n认证 → 授权 → 准入 → 写入 etcd"]
    S3["③ Deployment 控制器\nwatch 到新对象, 发现 期望2/实际0, 创建 ReplicaSet"]
    S4["④ ReplicaSet 控制器\n创建 2 个 Pod 对象(状态 Pending)"]
    S5["⑤ scheduler\n为 Pending Pod 选节点, 写入 nodeName"]
    S6["⑥ kubelet\nwatch 到分配给自己的 Pod, 调 containerd 拉镜像建容器"]
    S7["⑦ kindnet\n给 Pod 分配 IP, 接入 Pod 网络"]
    S8["⑧ kubelet\n执行探针, 上报就绪状态"]
    S9["⑨ Endpoint 控制器\n把 Pod IP 加进 Service endpoints"]
    S10["⑩ kube-proxy\nwatch 到 endpoints 变化, 更新 iptables 规则"]

    S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> S10
    style S1 fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style S2 fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style S5 fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style S3 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style S4 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style S9 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style S6 fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold
    style S7 fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold
    style S8 fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold
    style S10 fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold

每一步的主角(控制面 3 个 + 节点 2 个接力):

主角在做什么
①②kubectl → apiserver请求进收费站:你是谁(认证)、能干什么(授权)、合不合规(准入)→ 落库 etcd
Deployment 控制器watch 到新 Deployment,算账:期望 2 副本、实际 0 → 创建 ReplicaSet(控制器回路的活)
ReplicaSet 控制器继续算账:RS 期望 2、实际 0 → 创建 2 个 Pod 对象
scheduler看到 Pending 的 Pod → 按资源/污点挑节点 → 把 nodeName 写进 Pod
kubelet看到"分配给本节点的 Pod" → 调 containerd 拉镜像、创建容器
kindnet给 Pod 发 IP、接进网络(CNI 的活)
kubelet探针(startup/readiness)通过 → 上报 Pod Ready
⑨⑩Endpoint 控制器 + kube-proxyPod IP 进 endpoints → iptables 规则更新 → 流量可达

注意第⑤步的细节:scheduler 只是"在账本上写了个 nodeName"——真正把容器拉起来的是第⑥步的 kubelet。决策和执行严格分离,这就是为什么 Pod Pending 时你该先查调度(⑤),Pod 起不来时该查节点侧(⑥⑦⑧)。

3. 实测证据:events 里写着"谁在干活"

理论说完,上实证。在 kind 集群创建一个 Deployment,然后看它的 Event 对象——每个事件的 reportingComponent 字段就是干活的组件名:

kubectl create deployment event-demo --image=nginx:1.25 --replicas=1
kubectl get events -o jsonpath='{range .items[*]}{.reason} ← {.reportingComponent}{"\n"}{end}'

实测输出(精简):

ScalingReplicaSet ← deployment-controller     # 控制面: Deployment 控制器发现期望1实际0
SuccessfulCreate  ← replicaset-controller     # 控制面: RS 控制器创建了 Pod
Scheduled         ← default-scheduler          # 控制面: 调度器把 Pod 分给 learn-worker2
Pulled            ← kubelet                   # 节点: kubelet 让 containerd 拉镜像
Created           ← kubelet                   # 节点: 容器创建
Started           ← kubelet                   # 节点: 容器启动

一条命令,四个组件接力,每个事件都带着签名——这就是"谁在干活"的铁证。以后排查时 kubectl get events 永远是你判断"卡在哪一环"的第一工具。

4. 读路径:get / describe / logs 是谁在响应

写路径(apply)是接力赛,读路径同样分工明确:

命令请求路径谁在响应
kubectl get podskubectl → apiserver → 从 etcd/缓存读apiserver(状态是各 kubelet 持续上报的)
kubectl describe pod同上 + 关联 eventsapiserver 聚合状态与事件
kubectl logskubectl → apiserver → 转发给目标节点的 kubelet → containerd 取日志kubelet(所以节点挂了 logs 也会失败)
kubectl exec同上kubelet → containerd → 容器内执行
%% 读路径: logs 命令的完整链路
flowchart LR

    K["kubectl logs"]
    API["apiserver\n(找到 Pod 在哪个节点)"]
    KL["kubelet\n(目标节点)"]
    CT["containerd\n(容器运行时)"]

    K -->|"请求"| API
    API -->|"转发"| KL
    KL -->|"取日志"| CT
    style K fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold
    style API fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style KL fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff
    style CT fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold

排障启示kubectl logs 失败但应用活着 → 大概率是 kubelet 或网络的问题(不是应用的问题); get 能看到但 logs 看不到 → 节点侧出事了。

5. 排障归属地图:症状 → 先看哪个组件

这是组件知识最实用的落点——遇到症状,先定位是哪一环:

症状该看谁常用命令
Pod 一直 Pendingscheduler(选不上节点)kubectl describe pod 的 Events(调度器会把拒绝原因写进去)
Pod CrashLoopBackOffkubelet + 探针配置kubectl get pods 看 RESTARTS、 logs 看应用
CreateContainerConfigErrorkubelet(配置引用缺失)describe 看具体缺哪个 ConfigMap/Secret
ImagePullBackOffkubelet + containerd(拉镜像)describe 看拉取错误、检查镜像名/tag/凭证
节点 NotReadykubelet(节点代理失联)kubectl get node -o wide 、SSH 上节点看 kubelet 服务
服务访问不通但 Pod 正常kube-proxy / Service 配置 / endpointskubectl get endpointsdescribe svc 看 selector
Pod 间网络不通CNI(kindnet/Calico)看 CNI Pod 状态、Pod IP 是否分配
kubectl 命令全部报错apiserver(收费站挂了)kubectl cluster-info 、看 apiserver Pod

一句话记忆:资源层面的事找控制面(scheduler/controller),运行时的事找节点(kubelet/containerd),流量的事找 kube-proxy,网络的事找 CNI。

6. kind 与生产、ACK 的差异

kind 里这些组件和真实 K8s 几乎一致(kubeadm 式部署,控制面组件以静态 Pod 跑在 control-plane 节点上),所以学习完全有效。但上云后有一个重要变化:

场景控制面组件节点组件
kind自己跑(可看可改)自己跑
自建生产(kubeadm)自己运维(升级/备份/HA)自己运维
ACK 托管版平台托管,看不到 Pod 也不需要管还是你的(kubelet/kube-proxy/CNI 在节点上)

对兼职运维的启示:ACK 上你接触最多的节点组件是 kubelet 和 CNI(节点 NotReady、Pod 卡在某节点、镜像拉取失败这些还是要自己看),控制面组件(apiserver/etcd/scheduler)懂原理即可—— kubectl 命令照样走 apiserver,调度逻辑还是那套,只是"修 apiserver"这件事从你的世界消失了(这正是系列《云 K8s 替你承担了什么》说的:控制面托管)。

7. 总结

把组件拆开看,K8s 的架构其实一句话:所有组件不直接对话,只通过 apiserver + etcd 间接协作——apiserver 是收费站,etcd 是账本,scheduler 负责"想清楚放哪",controller-manager 负责"发现差距就动手",kubelet 负责"动手落地",kube-proxy 和 CNI 负责"让流量和网络成立"。

三个记忆点:

  1. 写路径是接力赛:apply → apiserver → Deployment/RS 控制器 → scheduler → kubelet → containerd/CNI → 就绪,每个事件都带组件签名( reportingComponent );
  2. 排障先定位归属:Pending 找 scheduler、起不来找 kubelet、流量不通找 kube-proxy、网络不通找 CNI——组件职责就是排障地图;
  3. 上云后一半消失一半保留:ACK 托管了控制面(不用管 apiserver/etcd),但节点侧的 kubelet/CNI 还是兼职运维的活——懂原理才能用好托管