1. 从一个日常部署场景说起:集群到底在干嘛
我第一次试着在生产环境用Kubernetes(下文直接叫k8s)的时候,脑子里只有一个模糊的印象:它能把一堆服务器我管起来,有问题自动重启,流量大了自动扩展。但真到搭建集群、排查故障的时候,发现自己连"组件之间谁和谁说话、谁负责干什么"都没搞清,出了问题只能靠重启和百度。
如果你也是准备入门k8s的开发或运维,我强烈建议你先别急着写YAML,也别一上来就折腾各种插件。先把集群里都有什么"角色"搞清楚,这一篇就是干这个的。
你可以把k8s理解成一个托管容器的小区物业:你只告诉物业"我这个房里要住3个租客(容器),一个负责做饭,两个负责打扫",后面租客怎么安排、房子怎么分配、谁生病了谁来替补,都归物业管。这个"物业"由好几个部门组成,有的坐镇物业总部(控制面),有的驻在每个楼层(工作节点)。
对应到技术语言:k8s是一种容器编排系统,其核心目标就是让你用声明式的方式描述期望状态,系统持续调和现实状态,直至两者一致。这里的"期望状态"就是"我要3个副本""我要Nginx镜像1.24版"之类;而调和动作,就是集群里各组件分工协作干出来的。
整篇文章我会把组件分成两拨讲:一拨是"控制面成员",负责做决策;另一拨是"工作节点成员",负责干活。最后再用一条"创建Pod的完整旅程"把所有串起来。
2. 控制面四件套:谁在做决策、谁在记账、谁在安排工作
控制面(Control Plane)是集群的大脑。它不一定只部署在一台机器上,通常会有多个副本保证高可用。这里先按单机房、单主节点的场景讲,逻辑清楚后再谈多副本。
2.1 kube-apiserver:所有请求的唯一入口,集群的"前台"
所有和集群有关的操作——不管是kubectl敲入的命令、各组件之间的通信,还是外部系统通过API访问——最终都要经过kube-apiserver,通常简称apiserver。
它有三层职责:
- 认证、授权、准入:先证明"你是谁"(证书/token),再判断"你有没有权限干这事"(RBAC),最后通过准入控制器检查(比如配额够不够、这个标签是否合规)。
- RESTful API转发:把请求转换成etcd能理解的操作,就像前台把客人的需求抄成工单递给档案室。
- 无状态网关:apiserver自己不存业务状态,所有状态都落到etcd里,这也是为什么它可以水平扩展多个实例。
一个容易忽略的点:apiserver是集群里唯一能和etcd直接对话的组件。换句话说,任何人想改集群状态,都得走apiserver这道门。这么设计的好处是"权限集中、审计集中",任何变更都有据可查;坏处是apiserver一旦过载或挂了,整个集群就"聋哑"了——不是业务流量断了,而是所有人都无法查询和修改集群状态了。
实操心得:多主节点环境下,给apiserver前面挂负载均衡时,注意保持长连接参数,比如TCP的keepalive时间、超时时间,不要沿用普通的Web负载均衡配置。我见过太多因为负载均衡空闲超时设得太短,导致apiserver连接频繁断开、kubectl频繁卡顿的情况。
2.2 etcd:群集状态的"档案室",所有真相都在这里
etcd是一个分布式的键值存储,集群里所有对象——节点信息、Pod定义、配置、Secret、Deployment状态——都以键值形式存在这里。它最大的特点是强一致性,也就是说无论集群里有多少个etcd节点,大家读到的最新值都是一样的。
为什么可能需要等一会才能看到"实时状态"?
因为k8s集群本身就是一个基于"期望状态不断调和"的系统,各组件通过apiserver往etcd里写变化,又通过监听机制感知变化。etcd保证的是"一旦写入成功,所有人都能读到这个最终值",但落到各组件自己的内存视图可能稍有延迟,这是最终一致的体现,不是bug。
useful类比:etcd像物业的房屋档案册,它不负责规划、不负责执行,只负责把所有事实准确记下来。如果档案册丢了,物业就失去了一切记忆,所以它需要备份,而且备份要放在独立环境,不能和集群同生共死。
- 生产环境etcd建议单独部署(至少3节点),磁盘尽量用SSD,不要和业务容器混跑。
- 定期做快照。这条我劝你自己动手写个cron任务,别指望云厂商的自动备份一定符合你的RPO要求。
- 删除操作要小心,etcd里的数据被覆盖后很难"找回",这和MySQL里误删还能靠binlog恢复不太一样。
2.3 kube-controller-manager:只盯着"现状改没改到位"的调和器
controller-manager名义上是一个组件,里面其实跑着几十种不同的小控制器(controller)。它们各有分工:
- 节点控制器:检查节点健康,发现节点失联就标记NotReady,等一段时间没恢复就执行Pod驱逐。
- 副本控制器:盯住ReplicaSet,发现副本数少于期望值就补充。
- 部署控制器:盯住Deployment,负责滚动更新的节奏控制、新旧版本Pod的切换。
- 端点控制器:维护Service对应的Pod列表,谁添加、谁删除,都会反映到Endpoints对象里。
这些小控制器的工作模式高度统一,叫Reconcile循环(调和循环):从etcd / 缓存读取当前状态,和期望状态对比,有差异就执行动作,比如创建Pod、删除Pod、更新状态,然后进入下一轮循环。整个k8s的"自动恢复"能力,就是靠这一群永远在对比、永远在纠偏的小循环撑起来的。
有一个非常关键但容易被忽略的事实:controller-manager本身不直接创建容器,它只是调用apiserver的接口去"修改集群对象"。最终创建容器的动作,要等后续章节的工作节点组件接手。理解这一点,你以后排查"为什么Pod没有按预期创建"时,思路就能顺着这条链路一层层往下找。
2.4 kube-scheduler:决定"新Pod放哪台机器"的分配师
调度器(scheduler)负责给还没有分配节点的Pod找到合适的家。它从apiserver拿到"待调度Pod"的队列,然后做两件事:
- 过滤(预测):排除掉不满足条件的节点。比如Pod要求4核,节点只有2核;或者Pod声明要落在有特殊标签的节点上,其他节点直接出局。
- 打分(优选):在合格节点里按资源余量、亲和性、污点容忍等规则打分,选最优者。
选完后,scheduler并不过去物理机上"推一把",而是把结果写成一个对象绑定(Binding),通过apiserver更新到etcd里。后续真正在节点上拉起容器的,是节点上的kubelet看到这个绑定后自己动手的。
调度策略默认是"资源足够就安排",但生产环境常常需要加自定义规则,比如:
- 节点亲和性:把数据库Pod固定到有大磁盘的节点组。
- 污点与容忍:给某些节点打上"这里只为特定业务服务"的污点,其他Pod默认进不来。
- 拓扑分布约束:把副本分散到不同可用区,避免一个机房故障全挂。
注意:scheduler看重的是"调度时刻"的资源情况,但调度完到Pod真正启动还有时间窗口,高并发创建Pod时可能出现"腹泻式调度后节点负载不均"。这是scheduler设计上的固有延迟,高级用法可以用调度插件和Pods Topology Spread来缓解,入门阶段了解即可。
3. 工作节点三件套:真正在机器上干活的组件
控制面做再多的决策,终究要落到具体的服务器上跑容器。每一台工作节点上都有三个常驻组件,它们才是"一线员工"。
3.1 kubelet:节点上的"全权代表",负责容器生老病死
kubelet是每台节点上唯一的"官方管理员",它以Agent形式运行,持续通过apiserver监听(Watch)分配给本节点的Pod需求。一旦发现有新的Pod对象绑到自己身上,kubelet就会接手后续一系列操作:
- 拉起容器(调用容器运行时,比如containerd)
- 挂载存储卷,协调CNI插件给Pod配置网络
- 定期执行存活探针(Liveness Probe)和就绪探针(Readiness Probe)
- 向apiserver上报节点状态、Pod状态、资源使用情况
它和控制面的通信方向需要注意:kubelet是主动向apiserver发起连接的,不是apiserver反向连它。所以kubelet所在节点的网络只要能做到"出去"就行;反过来,如果你的防火墙规则要求apiserver主动找到kubelet,那方向就搞反了,集群一定起不来。
一个常见的坑:kubelet默认每10秒上报一次状态,节点失联后,控制器并不能立刻判定节点宕机,而是要等节点失联宽限期(node-monitor-grace-period,默认40秒)过去,再配合一次Pod驱逐宽限期,才算"确认节点没了"。所以生产上看到节点宕机但Pod没有马上迁移,是正常现象,不要急着怀疑系统坏了。
3.2 kube-proxy:让Service"像一台虚机"的连通工具
每个节点上还有一个叫kube-proxy的进程。它解决的核心问题是:一群Pod的IP会随时变化,你怎么用一个固定的"前端地址"稳定地访问它们?
答案就是Service对象。而Service只是存在etcd里的一个逻辑定义,真正让流量按照Service规则转发到具体Pod的,是每台节点上的kube-proxy。它会把Service和后端Pod的关系转换成节点上的网络规则,常见的模式有:
- iptables模式:成熟稳定,但规则多了之后性能一般。
- ipvs模式:性能更好,支持更多调度算法,也是目前主流推荐模式。
需要理解的是,kube-proxy只负责做规则,真正走流量时数据包的转发是靠Linux内核的netfilter完成的,并不经过kube-proxy进程本身。所以kube-proxy的CPU占用通常不高,但如果节点上Service数量上千、规则极其复杂,iptables模式的效率会明显下滑,切换ipvs模式会有可感知的提升。
3.3 容器运行时:真正"生"出容器的底层引擎
在k8s里,kubelet并不直接调用docker命令,而是通过CRI(Container Runtime Interface)和容器运行时对话。当前事实标准是containerd,早期k8s还内置支持docker,后来通过CRI插件过度,如今docker直接作为k8s运行时已经很少见了。
还有一个概念值得了解:pause容器(也叫基础设施容器)。每个Pod在真正启动业务容器之前,kubelet会先创建一个全新的pause容器,它的唯一作用是"占住"这个Pod的网络命名空间和Linux命名空间,后续所有业务容器都共享这个命名空间。换句话说,Pod里多个容器可以通过localhost互相访问,原因就在pause容器身上。
如果不太理解pause容器,你只需要记住一句话:每个Pod都有一个"隐形大哥",它先占着网络,业务容器才进来一起住。这个设计让Pod成为k8s里最小的调度和资源共享单位,而不是单个容器。
3.4 节点上的常驻服务:不只是三个组件
除了上面三件套,节点上还少不了一些日常插件:
- CoreDNS:集群内部域名解析,默认Service名称和命名空间会自动注册成域名,Pod之间通过域名访问,依赖它的解析。
- CNI网络插件:为每个Pod分配IP、打通跨节点网络,常见有Calico、Cilium、Flannel。
- Metrics Server:提供资源指标数据,是
kubectl top node这类命令的数据来源;没有它,HPA(水平自动扩缩容)也无法工作。
4. 一条请求从进入到落地的完整旅程:把组件串起来看
单独记每个组件的职责容易忘,我们来追踪一条真实请求,把前面所有内容串成一个完整故事。
假设你执行一条命令:
kubectl create deployment nginx-demo --image=nginx:1.24完整链路如下:
第1站:kubectl → apiserver
kubectl先把这条命令转成JSON格式的请求,发给apiserver。apiserver先做身份认证(验证你的kubeconfig里的证书),再做授权检查(确认你有权限在default命名空间创建Deployment),最后通过准入控制器校验合法性。没问题,它就把"用户想创建一个名为nginx-demo的Deployment"这个对象,持久化写入etcd。
第2站:deployment controller → replicaset
deployment控制器一直监听etcd里的Deployment对象。它看到一个新的Deployment出现,期望副本数是1(默认),于是它去创建对应的ReplicaSet对象。ReplicaSet控制器又发现"这个Set目前没有Pod",于是开始创建Pod对象。
第3站:scheduler介入
这个Pod对象创建出来后,它的节点字段是空的。scheduler一直监视这类"待调度Pod",它开始过滤和打分。假设当前集群里node-01资源充足,于是它把这个Pod绑定到node-01,并回写状态到etcd。
第4站:kubelet接手
node-01上的kubelet通过watch感知到"有个新Pod被分配给自己",于是按Pod定义开始干活:先创建pause容器铺好网络命名空间,再从镜像仓库拉取nginx镜像,启动真正的nginx容器,把port、env、volume逐一配置好。Container运行时通过CRI接口执行这些动作,同时把容器的运行状态回传给kubelet。
第5站:网络与服务别名
容器起来了,CNI插件给它分配一个集群内部IP。此时如果你之前已经定义了一个Service指向这个Deployment,kube-proxy会自动把Service的转发规则更新为"流量可以送到这个新Pod的IP",并且当Pod被删除时,这条规则也会被清理,相关Pod从Endpoints中摘除。
整个链路你会发现,apiserver像一条"主干道",几乎每个环节都要经过它;etcd像"记忆库",所有状态都从这里读、写;controller-manager像"监工",不断检查各个环节有没有偏离;scheduler像"分配员",只管决定去哪;kubelet像"一线工人",具体把活干完;kube-proxy和CNI插件则负责把抽象的网络变成能用的现实。
我再补一个真实排查经验:有一次新加的节点始终没有调度上去任何Pod,查看日志发现scheduler一直在报"node is not ready"。当时我第一反应是看kubelet是否正常运行,确实运行正常,后来才定位到是时间同步问题——节点时钟和主节点差异超过1分钟,kubelet上报状态时被apiserver的正常心跳检查逻辑拦了下来。装好NTP服务、校准时钟后,一切恢复正常。这件事给我的教训是:排查集群问题的时候,从"组件之间只要有一个没对齐,整条链路就堵"的角度出发,往往比盯着单个组件的日志更高效。
5. 集群组件部署形态与容量规划的一些参考
知道了每个组件的职责,接下来就是它们应该怎么部署、各给多少资源。这里给出的是中小集群的常见参考,不是绝对标准。
5.1 单机学习 vs 高可用生产
| 形态 | 控制面部署 | 工作节点 | 适用场景 |
|---|---|---|---|
| 单节点/开发 | 控制面和工作节点在同一台机器 | 1台即可 | 学习、联调、Demo |
| 小型集群 | 控制面单独1~3台 | 2~5台 | 中小规模业务 |
| 生产高可用 | 控制面独立部署3台(apiserver/etcd等全部多副本) | 按需扩容 | 线上业务,建议至少3个控制面节点加3个etcd节点 |
控制面节点不建议和工作节点混跑,尤其不要让etcd和业务容器共享磁盘IO,否则一个业务高峰期就能把性能拖垮。
5.2 资源配比的经验值
- 最小单机学习环境:2核4G内存,跑一个控制面加一个工作节点,日常玩转基本够用。
- 小规模生产主节点:建议4核8G起步,apiserver和etcd共享这台机器时,内存最好不低于8G。
- 工作节点:看业务容量,一般4核8G起步,再做垂直扩容。
有一个要反复强调的事:etcd对磁盘延迟极其敏感,如果使用机械硬盘或NoSQL类网络的存储,etcd的性能会直接拖累整个集群的控制面响应,表现为kubectl查询极慢、调度延迟高,但节点上的容器可能还跑得好好的。这种"业务没事、命令卡死"的体验,往往就是etcd在门口堵车。
5.3 常见误区
- 认为"apiserver只是个代理":它要处理认证、准入、序列化、缓存、限流,负载并不低。主节点上apiserver进程CPU飙升时,需要检查是不是有太多的list-watch请求。
- 以为"controller-manager挂了只是控制慢":它挂了之后,虽然Pod本身还能跑,但Deployment的滚动更新、故障副本补充、节点失联后的驱逐都会停摆,等于集群的"自愈能力"失效了。
- 把kube-proxy不当回事:在旧版本k8s里,kube-proxy的iptables规则和Service更新有时会让部分节点上的连接短暂中断。升级到ipvs模式并配合conntrack参数调优后,这类问题会缓解很多。
6. 入门阶段最容易踩到的坑,以及如何避免
这部分是我自己带团队过程中最常被问到的问题,我把它整理成排查清单,初学阶段照着排查能少走很多弯路。
6.1 组件日志要看哪个进程
- 控制面组件要么是systemd服务(比如
kubelet、kube-proxy),要么是容器化Pod(比如apiserver等)。排查时优先看kubelet日志,因为绝大多数下层问题都会传导到它这里。 - 查看Pod起不来的原因,第一优先看
kubectl describe pod,它会把事件历史列出来,比如镜像拉取失败、端口冲突、探针失败。比看容器日志更高效。
6.2 证书和Token是隐性雷区
集群内部的证书有有效期,apiserver的证书过期后,kubelet、kubectl、各个控制器之间都会报certificate has expired or is not yet valid。生产环境建议提前配置自动续期,并监控证书到期时间。
我踩过一个具体案例:某内部集群某天突然大量报x509: certificate has expired,但kubectl访问还是正常的,后来发现是服务账户Token里内置的旧证书过期,Pod访问API时用到旧签发证书就会报错。这类问题只看Pod日志很容易误判成网络问题,实际上换一个重新签发的Token就能解决。
6.3 Node失联后的Eviction时间线
节点宕机,Pod不会瞬间被重新调度。整体时间线大概是:kubelet心跳超时(默认40秒)→ 节点控制器标记NotReady → 再过一段时间(由pod-eviction-timeout决定,默认5分钟)→ 才开始驱逐该节点上的Pod。也就是说,就算你的节点真的挂了,理想情况下也要5分多钟才迁移业务,关键业务必须提前设计多副本和优雅终止,不能指望快速迁移兜底。
6.4 不要忽略网络插件版本和内核参数的匹配
Calico或Cilium这类CNI插件,对Linux内核模块和iptables/netfilter版本有要求。升级内核或切换网络插件大版本时,最好先在一个节点上验证,否则可能出现某些节点上Pod间通信异常,但容器本身跑得好好的诡异问题。
这类问题的排查思路一般是:先看节点上ip a里Pod网卡是否存在,再看CNI插件的日志,最后查内核模块是否加载。
7. 控制面扩容和升级时的三件事
如果你已经能在测试环境里正常创建Pod,下一步基本就是考虑生产化,此时有三件事必须提前规划。
第一,etcd备份是性命攸关的。建议维护一个独立的备份任务,把etcd快照存到对象存储或异地机器。恢复流程也建议在测试环境演练一遍,不要等真正出事了才去翻文档。
第二,版本升级遵循"先控制面、后节点"的顺序。控制面内部依次升级etcd、apiserver、controller-manager、scheduler,然后处理节点的kubelet和kube-proxy。跨小版本升级没问题,但跨大版本(比如从1.24升到1.28)建议逐版本或查阅官方支持链路,不要跳。
第三,组件之间的兼容性要看官方矩阵。kubelet最好和apiserver保持同版本或小版本差,不要差太多。很多人图省事手动装一个最新版kubelet配一个稍老版本apiserver,结果证书、API字段兼容性上问题一堆。
8. 最后再分享一个小技巧:用"角色卡"的方式记忆组件
入门阶段,我不建议靠死记硬背每个组件的标志和端口。我自己的办法是,先建立"谁做决策、谁干活、谁管账"的角色框架,再遇到具体问题的时候追问"这一步是哪个角色在负责"。
比如看到Pod一直Pending,就问:是scheduler没选好节点,还是节点资源不足?这两者的排查路径完全不同,前者看scheduler日志和事件,后者看节点资源和污点。
再比如看到Deployment一直没有新的Pod生成,就问:deployment controller有没有创建ReplicaSet?ReplicaSet有没有创建Pod?还是controller-manager压根没启动?顺着这个思路,排查的速度会比直接乱翻日志快得多。
k8s的组件体系看起来很多,但核心逻辑其实就一条:一个集中式控制面加一群分布式执行者,用声明式期望状态驱动整个系统不断靠拢。把这条主线揪住,后面无论学习Service、Ingress、Helm还是Operator,理解起来都会顺畅很多。