ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K8s架构拆解:从控制面到Pod调度的全链路解析

K8s架构拆解:从控制面到Pod调度的全链路解析 一提到 Kubernetes新同事的第一反应往往是复杂。几十个节点几百个 Pod谁也说不清一个请求到底先经过哪里。我见过太多人把精力花在背命令上结果一遇到 Pod 调度失败、节点 NotReady 就直接抓瞎。这篇文章想换个角度直接从 Kubernetes 的分布式架构入手把节点、Pod、控制面之间的关系一层层拆开讲透。不绕理论不堆概念最终落到一条部署命令背后完整走一遍从提交到运行的全链路。读完之后你应该能回答三个问题整个集群靠什么保持一致一台 Pod 是怎么被安顿下来的出了问题该查谁先去哪儿看日志。无论你是刚开始摸集群的运维还是写应用但从来没关心过容器落在哪里的开发按这个思路去理都比背手册快得多。1. 先看整体控制面与节点到底怎么分工的1.1 为什么非要把集群拆成两层如果你第一次接触 Kubernetes最容易被绕晕的就是怎么有这么多组件。事实上整个架构只有两个逻辑层面控制面Control Plane和工作节点Worker Node。控制面负责决策节点负责执行这是一切分工的起点。这种拆分的核心逻辑和公司里董事会定方向、部门干执行是一模一样的。如果没有控制面每个节点都自己想办法、自己决定怎么干活那集群一多必然出现各弹各的调的乱象。Kubernetes 的做法非常朴素所有关于集群应该长什么样的信息都集中到控制面节点只能按照控制面下发的指令干活不能自己发挥。控制面里面又包括五大件kube-apiserverAPI 网关、etcd状态存储、kube-controller-manager控制器管家、kube-scheduler调度器以及承载其余控制逻辑的 cloud-controller-manager通常云环境才用。其中最核心、最容易被误解的一点是节点之间不会直接互相通信来协商业务所有状态变更都要经过 API Server。实际排查问题的时候这个分层意识能帮你省下很多时间。比如集群出现某个 Pod 一直 Pending你首先不应该登录到节点上翻日志而是先去看控制面的调度记录反过来如果节点上的 Pod 已经跑起来了但访问不通你才需要去节点上看 kube-proxy 和网络插件。把控制面和数据面分开是所有分布式问题排查的第一步。1.2 控制面组件之间的协作关系控制面并不是五件套各干各的它们之间有一条清晰的协作链条。外部用户通过 kubectl 把我要创建 3 个副本的 Deployment发给 API ServerAPI Server 做完身份认证、权限校验和准入控制之后把这份期望状态写入 etcd此时 Deployment 控制器通过 API Server 的 Watch 机制发现当前集群里副本数是 0期望是 3于是创建 ReplicaSet由 ReplicaSet 控制器继续补齐 Pod 对象。到这里集群只是决定要有 3 个 Pod但还不知道这 3 个 Pod 放在哪台机器上。scheduler 会通过 API Server Watch 到这批未调度的 Pod经过预选、优选、打分之后把Pod 应该运行在 node-01这个绑定决定写回 API Server。节点上的 kubelet 一直在 Watch API Server发现自己这台机器被安排了新 Pod就去找 Docker/containerd 真正把容器拉起来。这条链路里藏着分布式系统最本质的一个词状态。Kubernetes 里的一切资源Node、Pod、Deployment、Service 等都只是 etcd 里的一份数据各种控制器都是看着这份数据、把现实改成期望状态的执行者。理解了这条链路你再看任何 K8s 问题都会清晰很多用户要什么控制面记录什么节点执行什么三者永远在对账。2. 节点拆开看kubelet、运行时、kube-proxy 各在忙什么2.1 节点是如何注册进集群的一个普通的物理机或虚拟机要从裸机变成集群里的工作节点靠的是 kubelet。kubelet 启动时会自动向 API Server 发起注册创建属于这台机器的 Node 对象并持续上报状态。这也是为什么新节点加入集群往往只需要把 kubelet 配好、把证书拿到手就能被集群发现不需要你手工在集群里 create 一个 Node 资源。这里经常有人踩坑如果你在云平台上新买了一台机器费劲把 kubelet 装好了却在kubectl get nodes里看不到它。绝大多数情况是以下三种原因kubelet 证书没配好API Server 拒绝它的注册请求节点上的容器运行时没起来导致 kubelet 健康检查失败或者安全组/网络策略挡住了 kubelet 与 API Server 之间的 6443 端口通信。节点正式加入集群后kubelet 会通过 Lease 机制定期续约心跳。默认情况下如果控制面超过一定时间没收到某节点的心跳就会把节点标记为 NotReady最终触发 Pod 驱逐迁移。这个机制保证了某个节点突然宕机这种分布式故障不会导致服务永远瘫痪但也需要你理解一个前提标记 NotReady 和真正驱逐 Pod 之间有时间差生产环境不要指望它秒级反应。2.2 kubelet 才是节点上真正的主角很多人以为节点上最核心的是 Docker 或 containerd其实不对。kubelet 才是真正理解Kubernetes 语义的组件容器运行时只负责把容器跑起来。kubelet 干的事情远不止拉镜像、起容器这么简单。它持续 Watch API Server找到分配给本节点的 Pod 对象然后对比本地容器当前状态和Pod 期望状态之间的差异不断调谐。同时它还要负责上报节点状态、管理节点上的存储卷Volume挂载、执行存活探针LivenessProbe和就绪探针ReadinessProbe、在节点资源不足时执行驱逐Eviction。这里有一个非常关键的设计kubelet 并不是API Server 叫我干嘛我就干嘛而是主动去 Watch、去发现、去对账。即使 API Server 和 kubelet 之间的网络临时抖动kubelet 也会继续维持本机已有容器运行不会因为和领导失联就立刻杀掉工作负载。这种本地自治的能力是 K8s 能够在分布式环境里容忍部分故障的重要原因。实操上我建议你养成两个习惯第一节点上的问题日志优先看journalctl -u kubelet而不是先看容器日志第二遇到节点显示 NotReady时先在对应节点上执行kubectl describe node 节点名看 Conditions 里的具体原因再决定是网络问题、资源问题还是 Docker/containerd 假死。2.3 容器运行时和 pause 容器节点上的另一个关键角色是容器运行时Container RuntimeKubernetes 通过 CRIContainer Runtime Interface接口与它通信。你装集群时大概率会碰到 containerd、CRI-O以及曾经的 Docker。这些运行时本质上负责三件事拉取镜像、启停容器、管理容器生命周期。有个概念值得单独拿出来讲每个 Pod 启动时kubelet 首先会让运行时创建一个 pause 容器也被称为 inf 容器然后再把业务容器加进同一个 Pod 的命名空间里。pause 容器本身不跑任何业务逻辑它只是提前把 Network Namespace、PID Namespace、IPC 等基础设施创建好让同一个 Pod 里的所有容器住在同一个屋檐下。这也是为什么同一个 Pod 里的容器可以通过 localhost 互相访问的根本原因。实话说第一次听到这个设计的时候我也觉得绕K8s 为什么不直接把业务容器堆在一起非要加一个透明容器垫底实际运维过就会发现pause 容器的存在让 Pod 的生命周期管理简单得多。比如 Pod 需要重启业务容器不需要重建网络栈需要为整个 Pod 挂网络只需要操作 pause 容器。这个细节也直接体现了Pod 是 K8s 最小调度单位的含义——你调度、伸缩、重启的都是整个 Pod而不是其中一个容器。3. 一台 Pod 是怎么被安顿下来的调度器运行内幕3.1 调度器给节点打分的过程scheduler 的工作远比找一台空闲机器复杂。Kubernetes 默认调度器把调度流程抽象成了两个阶段Filter预选和 Score优选。Filter 阶段只做排除把不满足硬性条件的节点剔除。比如 Pod 声明了nodeSelector那没有对应标签的节点直接淘汰Pod 要求 8Gi 内存那可用内存不足 8Gi 的节点直接淘汰节点上有和 Pod 端口冲突的容器也淘汰。经过 Filter 之后剩下的节点才进入 Score 阶段。Score 阶段会对每个剩余节点打分考量因素包括节点已有的资源分配率、Pod 之间是否存在亲和/反亲和、节点当前运行了多少 Pod、镜像是否已经在本机等。之前有朋友问我K8s 为什么不能哪里资源多就放哪里。这个说法只对了一半因为只盯着资源剩余量会出现两台节点都空着 60% 的 CPU其中一台已经有 20 个 Pod另一台只有 2 个 Pod的情况平均分配反而更有利于应对突发流量。所以 scheduler 的打分规则里会综合节点上已用资源的均衡度和 Pod 分布的均衡度而不是单纯找一台上限最宽的机器。实际排查Pod 一直 Pending时完整命令永远是kubectl describe pod pod名看 Events 里 scheduler 输出的原因。最多的两类结果是0/5 nodes are available所有节点都过不了预选以及node(s) had taint节点打了污点Pod 没对应容忍。定位到这两条信息再看具体是资源不足、端口冲突还是亲和性要求满足不了比在节点上瞎翻要高效得多。3.2 资源请求与节点可分配量的博弈提到调度必须讲透 requests 和 limits这是新手最容易忽略、生产环境最容易出问题的配置。简单来说requests 是给调度器看的最低保证limits 是给运行时看的上限约束。调度器判断一台节点能不能放得下这个 Pod比较的是这台节点的可分配量Allocatable里当前已经 allocated 了多少 requests还剩下多少。注意调度器并不计算节点上实际的瞬时 CPU 使用率它只看过去累计声明的 requests。所以说如果你从来不给 Pod 写 requests调度器没有任何依据只能当它不需要资源最终结果就是 Pod 全被命中最空闲的节点而其他节点空闲。我见过很多线上事故都是从压榨 requests开始的。比如开发觉得我的容器内存实际上只用 500Milimits 写 2Gi 就够了但 K8s 是按照 requests 来调度和判断驱逐的。假如你在 8Gi 内存的节点上部署了 10 个 requests 为 1Gi 的 Pod表面看刚好 10Gi 超出调度器会把第 11 个 Pod 挡在外面但如果里面某个 Pod 内存飙到 6Gi超过 limit 之前整台节点就一起陪着等 OOM。给 requests 和 limits 留合理余量并且让同一台节点上的 Pod 总量略小于 Allocatable是分布式系统稳定性的基本盘。3.3 用亲和性和污点控制 Pod 去向默认调度器只能保证资源够、约束满足但没办法理解你运维层面的诉求比如数据库的 Pod 必须和缓存服务在同一个节点或者GPU 任务别凑在普通业务节点上。这类需求靠的就是亲和性Affinity和污点Taint/Toleration。污点的逻辑很像现实里的隔离区节点可以打上 Taint比如node-role.kubernetes.io/control-plane:NoSchedule普通 Pod 由于没有对应 Toleration就不会被调度到控制面节点。如果你坚持要把某个 Pod 放到特殊节点上就得给 Pod 加对应的 Toleration明说我愿意被调度到这个有污点的机器。这套机制非常利于多租户或混合负载场景GPU 节点打上只有GPU工作负载才能上来的污点比手动维护一个调度名单要可靠得多。亲和性则分成 nodeAffinity节点亲和和 podAffinityPod 亲和/反亲和。前者是把 Pod 往某类节点上拉比如通过 label 选择带有disktypessd标签的节点后者是把多个 Pod 拉在一起或者强制分散开例如让同一个 Deployment 的多个副本尽量分布到不同节点避免单点故障。注意 Pod 反亲和有preferredDuringSchedulingIgnoredDuringExecution和requiredDuringSchedulingIgnoredDuringExecution之分前者是尽量满足、不满足也没关系后者是硬条件不满足就调度不上去。很多新手把反亲和写成 required结果节点不够时整个 Pod 一直 Pending想半天不明白为什么。4. 集群状态存在哪etcd 与控制循环4.1 etcd 在架构里的位置如果说节点是乐手、Pod 是乐器上正在演奏的旋律那 etcd 就是整个乐团房间里唯一一份总谱。所有集群的期望状态、实际状态、配置信息最终都以键值对的形式存在 etcd 里而且只有 API Server 能直接访问 etcd其他组件一律只能通过 API Server 间接读写。etcd 是基于 Raft 算法实现的分布式键值存储它的核心价值在于保证多副本之间的强一致性。生产环境通常部署三副本或五副本每次写入要超过半数节点确认才会真正生效。这就回答了一个经典的疑问为什么 Kubernetes 控制面推荐用奇数节点因为 Raft 需要多数派才能维持写入能力三副本挂一个还能跑挂两个就只能读五副本挂两个还能继续服务挂三个就瘫痪。奇数副本在同样容忍一定节点故障的前提下投入产出比最高。我在做环境压测时曾遇到过 etcd 性能瓶颈当时的现象是kubectl get nodes卡住、APIServer 端口大量超时、节点大量报错。真正原因不是 API Server 本身代码出问题而是 etcd 所在那台机器磁盘 IOPS 被打满fsync 延迟飙升导致每次写入都要等很久。所以如果你负责生产集群请把 etcd 放在独立的、用 SSD 的机器上千万不要和其他高负载组件共享磁盘。etcd 慢API Server 就慢所有控制器和 kubelet 都会跟着喘不上气。4.2 控制器模式是什么Kubernetes 里最容易被误解的概念之一就是 Controller控制器。很多人以为它是一个单一进程在后台循环看门实际上控制器是一整套期望状态与实际状态不断对账的设计范式。举个最直观的例子你创建了 Deployment{replicas: 3}控制器看到当前 ReplicaSet 只有 2 个 Pod它就去创建一个新 Pod过一会儿另一个 Pod 因为节点故障死了控制器又看到当前 2 个期望 3 个于是再创建一个。整个过程中没有人命令它这么做它只是每时每刻在问自己期望态和实际态一致吗不一致就调一致就等着。这也是自愈两个字背后真正的含义。这种机制的最大优点是即使在分布式环境下某段时间出现了多个控制器同时操作同一个资源也能靠最终一致性收敛回来。因为你改了 ReplicaSet 的副本数控制器下一轮对账就会把多余 Pod 删掉或者补齐缺失 Pod不会因为某一次异常中断导致集群永远卡在错误状态。理解了控制器模式你就理解了很多 K8s 特性自动扩缩容、滚动更新、故障自愈的底层原理——它们全部建立在状态驱动而不是事件驱动之上。5. 网络与服务发现Pod 之间怎么互相通信5.1 CNI 网络模型集群网络恐怕是很多人最头疼的部分因为 Pod 的 IP 是动态的每次重建都可能变。Kubernetes 的网络模型定了一个核心目标每个 Pod 都有独立的 IP且任意两个 Pod 之间可以直接通信不需要 NAT无论它们在不在同一个节点上。实现这个目标的抽象层就是 CNIContainer Network Interface。以最常见的 Flannel 和 Calico 为例它们做的事本质上是为每个 Pod 在节点上创建 veth 虚拟网卡一端连着 Pod 的网络命名空间一端连着所在节点的 cni0 网桥或路由同时为节点规划网段比如 node-01 负责 10.244.1.0/24node-02 负责 10.244.2.0/24并配置路由协议让流量能跨节点转发。Flannel 默认走 VXLAN 隧道或 host-gwCalico 则可以走 BGP 或 IPIP 隧道。运维上有个非常经典的坑装完集群一切正常但不同节点上的 Pod 互相 ping 不通。90% 的原因是 CNI 插件没有被正确安装或者各节点的网段分配冲突。Kubernetes 本身根本不管网络通不通它只负责创建 Pod网络是你自己装的插件的事。所以遇到网络不通不要先翻 K8s 文档先查 CNI 组件的 Pod 是不是 Running再到节点上看路由表ip route最后再判断是不是防火墙规则拦截。5.2 Service 和 kube-proxy 是怎么做负载均衡的Pod IP 会变因此业务访问不能直接写死 Pod IPKubernetes 给出的抽象是 Service。Service 有一个稳定的虚拟 IPClusterIP并通过 selector 选定一组 Pod。但它自己并不承担流量转发工作真正干活的是每个节点上的 kube-proxy。kube-proxy 通过 Watch API Server拿到 Service 和 Endpoints 的变动然后把负载均衡规则写到本机的 iptables 或 IPVS 里。当一个请求访问 ClusterIP 时流量会在内核层面被 DNAT 转发到某个真实 Pod 的 IP 上。这就是为什么Service 没法 ping 通——ClusterIP 是虚拟 IP它只存在于防火墙规则里并不对应任何一块网卡。这里有两个实践要点。第一如果你在集群里抓包排障别在 Service IP 上抓要抓就抓 Pod 的真实 IP或者抓 kube-proxy 写入的 iptables 规则。第二当业务 Pod 数量极少比如只有一个副本时Service 依然能正常访问因为它会把流量转发给唯一那个 Pod但如果 Endpoints 里是 0 个后端Service 一样会丢包此时优先排查 selector 到底匹配没匹配上 Pod 的标签。5.3 DNS 和集群内寻址Kubernetes 集群里自带 DNS 服务默认是 CoreDNS这让 Pod 之间可以通过服务名互相访问。同一个命名空间里你直接访问service-name就能解析到 ClusterIP跨命名空间则用service-name.namespace.svc.cluster.local这个完整域名。这套命名规则解决了一个非常实际的分布式问题服务实例的位置不停地变但服务的逻辑名能保持稳定。DNS 排障也有个常见坑应用日志里报 DNS 解析失败却不一定是 CoreDNS 挂了。你需要在业务 Pod 里手动nslookup或者getent hosts试一下解析同样的域名如果本机能通应用却报错多半是应用的 DNS 配置resolv.conf被覆盖了比如使用了hostNetwork: true或者自定义 dnsPolicy 导致没有走系统 DNS。另一种情况是 CoreDNS 自身副本太少请求一多就超时表现也是间歇性解析失败。这时候看 CoreDNS 的 CPU 和日志通常比看业务日志更容易定位到根因。6. 实操视角一条 deploy 命令背后的完整旅程6.1 从 kubectl apply 到 Pod 运行的全链路理论讲了这么多我们用最小化集群走一遍实际流程。假设你在开发环境用 kubeadm 初始化了一个三节点集群一个控制面、一个工作节点然后在终端执行kubectl apply -f nginx-deployment.yaml输入 nginx-deployment.yaml 内容大致是apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80这一条命令背后至少发生了下面这些事kubectl 把 YAML 解析成 API 对象向 API Server 发起 POST 请求API Server 先做认证确认你是合法用户再通过 RBAC 检查你有没有权限创建 Deployment接着进入准入控制阶段比如检查资源配额最后把这份期望状态写入 etcd。到这里控制面只确认了用户希望 3 个 nginx 副本但它还没创建任何容器。接下来进入控制器作用域。Deployment 控制器发现期望副本数是 3而集群里完全没有相关副本就去创建 ReplicaSetReplicaSet 控制器发现自己管理的 Pod 数量是 0就开始逐个创建 Pod 对象。注意这一步 Pod 仍然只是对象还没有绑定到任何节点。此时 scheduler 介入经过 Filter 和 Score 后给这三个 Pod 分别挑选节点并把绑定记录通过 API Server 写回 etcd。最后一个环节才是节点上的动作。node-01 的 kubelet Watch 到有一个 Pod 被分配给自己于是通知 containerd 拉取 nginx 镜像、先创建 pause 容器、再启动 nginx 容器同时配置本节点 CNI 网络、挂载存储卷。容器起来之后kubelet 运行各种探针检查它是否健康探针通过Pod 状态变为 Running并且 StatefulSet 等控制器也拿到反馈。整条链路到这里才算走完。6.2 常见卡点和排查顺序实际操作中很可能卡在某一步。最常见的现象是kubectl get pods一直显示 Pending 或 ImagePullBackOff。先看 Pending。Pending 说明 Pod 还没被绑定到节点原因大概率在调度环节。执行kubectl describe pod看最后一行 Events 里的调度错误通常能直接看到0/3 nodes are available。这时再逐个看被排除的原因可能节点 CPU requests 不够了、可能是 PVC 没有绑定、可能是污点未容忍。记住一个原则调度问题先查 events不要一上来就去节点上翻 kubelet 日志。ImagePullBackOff 则说明调度已经成功但镜像没拉下来。最常见的是镜像名称写错、私有仓库认证失败、或者镜像在对应架构上不存在比如在 ARM 节点上拉了 x86 镜像。按顺序排查kubectl describe pod里容器状态描述再去节点上试手拉镜像通常就能找到答案。拿到这条命令链你等于掌握了一条 deploy 命令的分布式交响曲里所有重要参与者API Server 是唯一的指挥接口etcd 是乐谱控制器和调度器是排练指挥kubelet 是乐手containerd 是乐器本身而 Pod 是被演奏出来的作品。7. 高发故障排查速查表7.1 高发问题速查把常见问题整理成速查表遇到问题先对照分类再按顺序排查能少走很多弯路。现象最可能的原因第一排查动作Pod 一直 Pending资源不足、节点污点、PVC 未绑定kubectl describe pod看 Events节点 NotReadykubelet 假死、网络断连、运行时异常节点上journalctl -u kubelet -fImagePullBackOff镜像名/标签错、私有仓库认证失败kubectl describe pod看 Image 状态Service 访问不通selector 没匹配上 Pod、kube-proxy 异常kubectl get endpoints、检查 iptablesDNS 解析失败CoreDNS 副本不足、业务 dnsPolicy 异常看 CoreDNS 日志与 CPU 指标CrashLoopBackOff业务启动即退出、探针失败、配置错误kubectl logs看业务日志节点维护后一直 NotReady只删了节点没清理 kubelet 证书重装 kubelet 并清理 /etc/kubernetes7.2 我踩过的坑和一些习惯最后分享几个个人经验不一定写在官方文档里但能实打实帮你少折腾。第一生产集群里删节点一定要按顺序来先kubectl cordon node让新 Pod 不调度上去再kubectl drain node --ignore-daemonsets把已有 Pod 迁移走最后才kubectl delete node node。很多人贪快直接 delete node结果节点上遗留的 Pod 没人接管业务直接中断。drain 之前还要确认 DaemonSet 这类不能随便驱逐的组件否则连网络插件都被赶跑了。第二etcd 备份永远别等到出事了才想起来。Kubernetes 所有状态都在 etcd 里控制面的任何误操作比如误删命名空间都会被控制器真的执行掉。我自己习惯每天定时做 etcd snapshot并且把快照复制到集群外部。别嫌麻烦出过一次事故就知道这步值多少钱。第三看日志要先看组件自己的日志再看业务日志。节点上 kubelet 日志是journalctl -u kubelet容器业务日志是kubectl logs两者不要搞混。遇到偶发问题优先看组件日志的时间线和警告信息往往问题根源藏在组件之间的交互而不是业务代码里。第四要给关键命名空间加资源配额ResourceQuota否则一个应用里面写了个死循环疯狂创建 Pod整个集群都可能被拖垮。这类问题在分布式系统里尤其隐蔽因为故障的表现往往是所有 Pod 都很慢但根源只是某一个工作负载太贪心。我对 Kubernetes 架构最深的一点体会是它所有的魔力都建立在状态驱动和对账循环上。理解这个思路之后Kubernetes 不再是一堆组件名的堆砌而是一套有逻辑的分布式机器每个组件都在自己的位置上循环着同一件事看看现实和期望差多少想办法把它补回来。这也解释了我为什么在诊断问题时总喜欢先问谁负责这块、它看见的状态是什么、它想要的期望是什么——沿着这个思路走下去绝大多数复杂故障都能被一步步拆开。
返回列表