听到“K8S是用来解决什么问题的?”这种问题,第一反应通常是先立正,因为这问题看着基础,但真能一句话讲清楚的人并不多。网上铺天盖地都是安装部署教程、面试题、operator案例,反而把最核心的“它到底为什么存在”给说糊了。我大概从2018年开始用K8S,从单机Docker一路折腾到几百个Node的生产集群,踩过的坑可以写一本书。今天不打算重复那些搭建步骤,我想用最直白的方式,把K8S存在的理由、它解决的核心问题、以及它实际改变了什么,一次性说透。
一句话先放在这里:Docker解决的是“应用怎么打包和运行”,K8S解决的是“一批打好的应用,在几十台机器上怎么配合、怎么存活、怎么扩容、怎么升级”。后者才是生产环境真正要命的地方。这篇文章适合两类人看:一类是刚接触K8S、被各种概念绕晕的新手,另一类是已经会用但总是“照着抄配置,出了问题抓瞎”的运维或开发。看完你会明白,K8S不是一套为了复杂而复杂的工具,它是在容器化走到一定规模之后,被现实逼出来的产物。
1. 单机容器时代:为什么非要多一个编排层
先回到没有K8S的日子。你在自己电脑上用Docker跑一个MySQL,命令很简单,一条docker run就起来了,映射个端口,数据挂个卷,完事。但随着服务数量变多、机器数量变多,事情开始不对劲。
1.1 没有K8S时的日常:三座大山
第一座山是部署靠人肉。假设你有二十个服务,每台机器上部署哪几个服务、端口怎么分配、环境变量怎么传、配置怎么同步,全靠人记。今天A同学改了一个服务的镜像标签,明天线上部署的时候用错了版本,没人知道是谁改的。我见过不少团队,真正的服务部署清单存在某位同事的笔记本里,他休假的时候没人敢动生产。
第二座山是扩容靠手速。某个服务流量涨了三倍,理想情况是加两台机器、或者在这个服务后面把副本从2个拉到6个。但现实是:你得先登录服务器,看看有没有空闲资源,然后手动把新容器拉起来、挂到负载均衡后面、再把旧的摘掉。等这一套操作做完,高峰期早就过去了。流量是动态的,人肉是静态的,这两件事天然冲突。
第三座山最要命,故障靠人品。容器本身会崩、机器会宕、进程会OOM。如果哪天凌晨三点某个节点死了,上面跑了五个服务实例,你需要一台一台检查哪些服务受影响,然后把它们在其他机器上重新拉起。运气好十分钟搞定,运气不好到天亮还在对着日志怀疑人生。这不是运维能力的问题,这是单机思维面对分布式环境的必然结果。
1.2 K8S到底在管理什么:从“机器”到“集群”
K8S的思路是对上面三座山做了一次整体推倒重来。它不再让你和一台具体机器打交道,而是把一批机器抽象成一个“资源池”。你只需要告诉这个池子:我要跑什么镜像、要几个副本、需要多少CPU和内存、需要对外提供什么端口。至于这些容器具体落在哪台机器上、机器挂了怎么办、副本多了少了怎么办,交给K8S去决定。
用个生活化的类比:容器是快递盒,Docker是打包机,而K8S是一个智能快递柜管理系统。你作为寄件人,只需要写清楚包裹数量和规格,系统自己会找空柜口塞进去;哪个柜门坏了,它自动把包裹挪到另一个柜子;柜子不够用了,它还能通知人加柜。整个过程你不需要知道包裹在哪个具体柜子的第几排——这就是“面向集群”和“面向单机”的本质区别。
一个很关键的设计是,K8S把“期望状态”这个概念变成了操作核心。你在YAML文件里声明“我要三个副本”,K8S就会持续确保集群里始终有三个副本,哪怕其中一个挂了,它也会立刻重新拉一个补上。运营这套系统的思路从“手动修”,变成了“声明目标、系统自愈”。这也是为什么上了K8S之后,运维团队的核心工作不再是“救火”,而是“设计规则”。
2. 核心原理:调度、调谐与声明式API怎么落地
理解了K8S解决什么问题,再来看它是怎么做到的。很多人被K8S的组件名字劝退,什么apiserver、scheduler、controller-manager、etcd、kubelet,其实整套架构可以用一个很朴素的模型串起来。
2.1 一次Pod调度背后发生了什么
当你执行kubectl apply -f deployment.yaml之后,事件链条是这样的:apiserver先收到这个请求,它是整个集群的“门卫”,所有的读写操作都必须经过它;然后apiserver把你要创建的Deployment存进etcd,etcd是集群的“账本”,所有状态都记在这里;接着controller-manager里的Deployment控制器发现“当前副本数是0,期望副本数是3”,于是创建对应的Pod对象;scheduler这个“大脑”开始干活,它会综合每台Node的资源余量、标签约束、污点容忍等因素,把Pod分配给最合适的一台机器;最后,目标机器上的kubelet这个“手脚”收到指令,调用容器运行时把Pod真正拉起来。
我经常把这套链路简化成一句话:apiserver负责接待,etcd负责记账,controller负责盯数,scheduler负责分配,kubelet负责执行。很多人觉得这些组件复杂,但如果你带着“每个组件只干一件事”的预期去理解,K8S源码级别的复杂度反而没那么可怕。
2.2 声明式API:别告诉我怎么干,告诉我想要什么
K8S最反直觉也最精髓的设计,是它的API不是命令式的,而是声明式的。传统运维方式是什么?命令式的。比如“执行这个脚本启动服务”“把服务B停了”“把配置改成这个”。每一步都是具体动作,执行顺序错了、某一步失败了,整个状态就乱了。而K8S的YAML描述的是“最终长什么样”,不是“怎么变成这样”。
举个例子。我想让某个Web服务运行6个副本,只需要在Deployment里写replicas: 6。我不需要关心如果第3个挂了,是应该启动一个新的、还是把请求切到别的副本上——这些都是系统自己推演出来的。这就像你跟装修公司说“我要一个三室一厅的房子”,而不是说“第一步砌墙、第二步铺电线、第三步装门窗”。后者一旦中间漏了一步,后面全乱;前者不管过程如何曲折,系统始终朝着你描述的目标收敛。
这里有个实战中很容易体会到的好处:配置即代码,代码可审查。以前谁改了线上配置,要翻操作记录才能找到;现在所有期望状态都躺在Git仓库里,每次变更都是一次代码提交,出问题可以回滚,可以diff,可以走审批流。这个价值在出生产事故的时候,比任何高可用架构都救命。
2.3 控制循环:K8S的“监工”如何维持集群稳定
声明式API的背后是一套“控制循环”机制,这是K8S维持稳定状态的引擎。它的逻辑说起来很笨:不断对比“当前状态”和“期望状态”,如果不一样,就执行动作让它们一样。这个循环永远在跑,就像家里装了个监工,每隔几秒检查一次玻璃杯是不是放在指定的位置上,不在就立刻挪回去。
K8S内部有大量的控制器,都遵循这个模式。Deployment控制器看副本数够不够,HPA控制器看流量高了要不要自动扩容,Node控制器看机器失联了要不要把上面的Pod标记为异常。这也解释了为什么K8S里有些现象看起来像“自己好了”——其实是控制循环在背后默默纠偏。我踩过一个坑:某次集群里一个Pod一直CrashLoopBackOff,我以为主动删掉它就会清掉,结果刚删完又拉了一个新的起来。为什么?因为ReplicaSet还在,期望副本数是2,我删了一个,控制器就立刻给我补了一个。想改行为,就改期望状态,不要试图和控制器对着干。
3. 网络与存储:Service、外部访问和持久化
有了调度和自愈,容器能跑起来了,但生产环境还有两个更务实的问题:一是Pod的IP随时会变,互相之间怎么找到对方;二是外部流量怎么进来;三是数据都存在容器里,容器一换代数据就没了怎么办。K8S对这三个问题的解法,是它最值钱的部分。
3.1 Pod IP天生会漂:Service如何解决服务发现
Pod在K8S里是“朝生暮死”的。滚动更新时旧Pod下线新Pod上线,故障时Pod会被调度到另一台机器,每次IP都不一样。如果你让服务之间通过Pod IP直接通信,那就等于通讯录里的号码每天换一次,谁也受不了。
Service就是为这个问题设计的稳定锚点。你可以把Service理解成一个虚拟IP加一组后端Pod的映射表。Service的IP不会变,Pod的IP变了,Service会自动把流量转发给新的地址。服务A访问服务B,只需要访问Service的名字或者虚拟IP,完全不用关心B后面到底有几个Pod、它们在哪台机器上。这套机制解决了两个问题:服务发现和负载均衡。
还有个配合机制叫EndpointSlice。Service不是凭空知道要转发给谁的,它靠标签选择器去匹配Pod。Controller会实时维护这个映射关系,Pod一变化,映射就更新。这里最常见的问题是标签写错。我见过有人把app: nginx写成app: nginx后面多了个空格,结果Service匹配不到任何Pod,对外表现就是502。排查的时候第一反应先看Service的Endpoints是否为空,这一步能排除掉一大半问题。
3.2 外部流量怎么打进集群:externalIPs之外的选择
Pod之间的通信解决了,外部用户要访问你的服务怎么办?K8S提供了几种方式,热度比较高的externalIPs其实只是其中最直白的一种——给Service分配一个外部IP,直接把流量引到Service上。它的优点是简单,缺点是灵活性和高可用能力有限,不适合大规模入口。
更主流的分层是NodePort、LoadBalancer和Ingress。NodePort的原理是:每个节点都开放一个固定端口,流量打到任何一台机器的这个端口都会被转发到对应Service。适合测试环境,小规模够用。LoadBalancer对接公有云的负载均衡产品(阿里云SLB、AWS ELB等),是云环境的标准做法。Ingress则更靠应用层,相当于集群内的“智能路由器”,按域名、按路径把流量分发到不同Service,很多生产环境用它来做统一入口。
流量进集群这件事,我建议这样配合用:云上环境优先LoadBalancer+Ingress,物理机房考虑Keepalived/自建负载均衡+NodePort,别把externalIPs当高可用方案。还有个很容易忽略的细节,Pod访问Service时,如果源IP被SNAT了,后端服务拿到的客户端真实IP会不对。需要开启externalTrafficPolicy: Local来保留原IP,但代价是流量只会转发到本节点的Pod,跨节点转发的场景要额外考虑。
3.3 存储怎么挂在Pod上:PV与PVC的职责分离
容器本身是无状态的,数据放在容器里,容器一重建数据就没了。生产环境的数据库、文件服务、消息队列都有持久化要求,K8S的答案是PV(PersistentVolume)和PVC(PersistentVolumeClaim)。
PV是集群里的存储资源,由管理员提前准备,可以对接云硬盘、NFS、Ceph、本地盘等。PVC是用户对存储的申请单,你只需要声明“我要10G的读写存储”,K8S就会找到一个匹配的PV并绑定。这套设计把“存储长什么样”和“存储怎么用”分开了:管理员关心硬件的池化,应用开发者只关心容量和性能要求。
实际使用中几个高频坑要记住:一是PV的回收策略,Retain、Delete还是Recycle,选错了会直接导致删除PVC后底层数据被干掉;二是存储分配的伸缩能力很重要,K8S虽然支持卷扩容,但不是所有存储类都支持,EBS、PersistentDisk这类块存储要确认是否开启动态扩容;三是有状态应用建议用StatefulSet而不是Deployment去挂PV,因为StatefulSet才保证稳定的网络标识和固定的存储绑定关系,这也是它比Deployment多出来的核心价值。
4. 生产环境实操:GPU调用、故障排查与常见问题速查
理论讲清楚了,还得在真实环境里能落地。这章挑两个大家问得最多的问题展开:K8S怎么把GPU拿去给任务用,以及那串“API server not healthy”的初始化报错到底怎么解。
4.1 K8S如何把GPU调度给任务
AI和大模型火了之后,申请不到GPU成了很多团队的日常,而k8s调用GPU的问题也随之变成面试和实战的高频点。K8S本身对GPU的感知能力是有限的,它只把GPU当成一种可以调度的资源,至于底层的驱动、容器怎么访问GPU,需要外部插件来打通。
流程是这样的:首先你的节点上要装好NVIDIA驱动,以及nvidia-container-runtime;然后部署NVIDIA的device plugin,它会把节点上的GPU数量汇报给K8S,让K8S知道这台机器有几个GPU可用;之后你在Pod的resources里声明nvidia.com/gpu: 1,调度器就会把Pod安排到有GPU且余量足够的节点上。这一步做完,容器内就能使用nvidia-smi看到设备了。
最容易翻车的地方有三个:第一是device plugin没安装或者版本和驱动不匹配,导致节点虽然插着GPU但K8S上报的资源数是0;第二是没给Pod声明GPU资源,某个程序内存显存泄露把同一物理卡上的其他任务拖死;第三是GPU显存不能被K8S直接感知,K8S只能按卡分配,不能切分,所以大任务和小任务混跑同一个GPU设备时资源争抢会很激烈。更高级的需求比如GPU虚拟化(MIG、vGPU),需要额外方案,K8S原生能力覆盖不到。另外,在声明CPU和GPU的limits时,需要确保limits值与requests值一致,否则K8S的QoS等级发生变化,OOM时会先杀掉你的Pod。
4.2 master初始化报错API server not healthy的排查实录
这个报错算是kubeadm初始化集群时最让人心态爆炸的一个了。每次新手跑到这一步卡住,都会产生“我离成功只差一步”的错觉。我在生产环境里遇到这个报错,解决率最高的一条路径是:先把系统组件状态查一遍,再定位到具体日志。
先解释一下这个报错是什么意思。kubeadm init执行完以后,它会等待apiserver进入健康状态,等了4分钟左右没等到,就会报出the API server is not healthy after 4m0.00747357s。根源不在kubeadm本身,而是apiserver这个Pod没起来。大多数时候是下面四个原因:
- 镜像拉不下来。kubeadm会把etcd、apiserver、controller-manager、scheduler等组件容器化,如果你在init时指定的imageRepository在本地网络访问不了,组件容器会一直处于ImagePullBackOff。
- 容器运行时没配置对。如果你的K8S版本要求使用CRI(比如containerd),但默认还是用docker引擎,kubelet会一直报找不到CRI socket。需要确认
/etc/containerd/config.toml中CRI路径和kubelet的启动参数能对上号。 - 系统资源不足或swap没关。kubeadm在preflight阶段就会查swap,但偶尔有机器配置了swapfile,导致初始化进行到一半就报错。
- etcd没有成功启动。apiserver启动时高度依赖etcd,etcd没就绪的话apiserver必然起不来。这个问题在CPU核数很少、内存不足的云服务器上很常见,我遇到过一台2C2G的机器装K8S全家桶,etcd直接OOM。
排查的正确姿势是:先systemctl status kubelet看kubelet服务状态,然后journalctl -u kubelet -f追踪日志,再用crictl ps -a(或docker)看apiserver容器是否处于Running状态。如果容器不存在,优先查kubelet日志里有没有报镜像拉取失败或CRI连接失败;如果容器CrashLoop,就去看apiserver的日志,通常会有更具体的错误,比如etcd连接超时或证书路径不对。
这里有一个我个人的建议:尽量把apiserver、etcd等控制面组件的资源限制设置充足,特别是etcd对磁盘IO和内存比较敏感。在低配机器上学习和生产环境是两码事,踩过一次坑就知道,初始化报错十有八九不是命令错了,而是基础条件没满足。
4.3 生产环境故障类型速查表
整理了这几类生产环境的K8S故障,这可不只是应付面试的,而是运维真正会遇到的事:
| 故障现象 | 可能原因 | 首选排查动作 |
|---|---|---|
| 节点一直NotReady | kubelet停了、CNI插件挂了 | 查看节点事件和kubelet日志 |
| 大量Pod处于Pending | 资源不足或调度约束冲突 | describe pod看事件,查资源余量 |
| Service无法访问,Endpoints为空 | 标签选择器匹配不到Pod | 核对Service和Pod的label |
| 滚动更新一直不结束 | readinessProbe配置过长或失败 | 检查探针配置和后端响应时间 |
| Pod反复重启 | 探针失败触发重启、容器OOM | 看Pod的status和容器的exit code |
| etcd报磁盘空间预警 | etcd compaction未开启或保留旧版本过多 | 检查etcd配置和监控指标 |
| 磁盘满导致Evicted | 节点磁盘压力 | 清理日志和镜像缓存,并设置资源限制 |
我巡生产集群这几年来,最深的感受是故障本身不可怕,可怕的是没有可观测性。很多看起来玄乎的故障,只要先把metrics、logs、events这三类数据配好,定位时间能从小时级降到分钟级。尤其是Events,kubectl describe pod给出的最后几行事件,往往直接指向根因。别一上来就重启节点,那叫碰运气,不叫排查。
5. 面试高频题与Operator案例:从背答案到建模型
K8S相关的面试题多得可以出书了,但很多答案是背出来的,换了换场景就不会用。这一章想提供一个更接近本质的思路:高手是怎么组织自己对K8S的认知的。
5.1 几道高频K8S面试题的真正考点
给你几道常见面试题,我不按标准答案一条条写,直接说它们背后在考察什么:
Deployment和StatefulSet的区别——这道题表面考API对象,实际考你有没有真正管理过有状态服务。Deployment管理的Pod是无状态的,名字随机、存储不固定、可以随意调位置;StatefulSet的Pod有稳定的网络标识和稳定的存储,按顺序部署,适合做数据库、Redis、Zookeeper这类有状态组件。面试官想要听到的是“我知道有状态和无状态的边界在哪里,知道数据库不该随随便便用Deployment”。
Pod的调度流程是怎样的——这道题考的是你对控制面组件的梳理,apiserver、etcd、scheduler、controller-manager、kubelet之间的协作讲清楚,比背过任何命令都加分。
Service和Ingress有什么区别——很多新人会答反。Service是集群内的负载均衡,提供稳定的虚拟IP和DNS入口,解决Pod漂移问题;Ingress是集群入口的方向,做七层路由,把外部请求按域名路径转发到不同的Service。两者是互补关系,不是替代关系。我通常会再加一句:Ingress本身也需要一个Controller(nginx/treafik)才能真正跑起来,说的越细越好。
K8S集群里一个Pod一直处于Pending状态,怎么排查——故障排查类问题是真正的分水岭。标准路径:kubectl describe pod查看事件,看是否资源不足、污点无法容忍、存储卷绑定失败、调度器问题。这些步骤实际是考你有没有真正处理过生产问题,而不是只会背yaml。
5.2 Operator案例:把人工运维流程固化成代码
很多人听过operator这个词,但不知道这货到底解决什么。举个例子,你运行了一套自建的MySQL集群,日常需要做备份、主从切换、扩副本。这些操作如果靠人手工执行,最大的问题是:每个人切换到凌晨操作的手法都不一样,且容易出错。K8S的operator模式就是把这个运维逻辑写成一个控制器,这个控制器盯着你的MySQL自定义资源,一旦主节点挂了,它自动完成切换;达到一定时间点,它自动触发备份。人工运维的规则被固化成代码,整个流程像一个无限循环的运维机器人。
operator的技术基础是CRD(自定义资源定义)+ Controller,核心是“扩展K8S API,让它认识你的业务对象”。如果你所在团队有频繁操作数据库、消息队列、AI训练任务的需求,尝试写一写operator很值得,甚至比自己手动维护一堆开源框架脚本更可控。我见过一个典型的失败案例是:公司自己用Ansible脚本组了一个MySQL主从集群,后来因某次机器IP变动脚本大规模跑挂,导致任务失败。之后他们把这一整块迁移到K8S的operator模式,一切都声明式化了,团队加班时间直接砍半。
5.3 学习路线建议:从会用到会用对
最后聊聊怎么学。网上很多人问“k8s权威指南第五版有没有pdf”,看书当然有帮助,但K8S这种工具类的东西,一味看书效果真的有限,必须操作。我建议的顺序是:先在本地用minikube或Kind把核心概念跑通,比如Deployment、Service、ConfigMap、Volume;然后去云上购买便宜的低配机器,或者直接用云厂商托管的K8S集群,试一次从零到一的部署;然后可以尝试把之前手动部署过的服务,直接改造成K8S的YAML清单版本。
生产环境还有一个“小号”的摸索过程,K3S是一个不错的选择,对机器配置要求低,想自己上手安装集群的话可以先用它模拟生产拓扑,等理解好了再上kubeadm。遇到问题,不要满足于“重启就好了”,试着看一遍Pod的事件日志、看kubelet的日志,把根因找出来。把上面的高频问题都自己实际排查过一遍,你对K8S的理解可以远超大部分背面试题的人。
我个人在实际操作中的体会是,K8S不是一个用完就能立刻见效的工具,它更像是一款中长期投资。刚上手会觉得学习曲线陡峭、配置复杂,但一旦跑过半年你会发现,基础设施真正变成了“平台”,应用发布从“提心吊胆”变成“流水线一样自然”,这种转变值得投入时间。最后分享一个小技巧:生产环境里每次变更前,把当前期望状态单独打一个标签(比如commit号),出问题时候能迅速定位到是哪一次变更引发的,这是我在线上踩过多次坑之后养成的习惯,对大多数人和团队都会很有帮助。