折腾K8s的时间一长,你会发现很多运维问题其实都离不开几个底层逻辑:调度器怎么挑节点,kubelet和API Server怎么保持心跳,流量怎么进Pod,存储怎么挂上去。这套东西想透了,排查问题基本就是顺藤摸瓜。这篇笔记就作为K8s技术笔记的第一篇,把我在实际环境里反复用到的核心机制和操作心得整理出来,既聊原理,也聊踩坑,希望给刚接手K8s集群的同学提供一些能直接上手的东西。
1. 集群核心组件与运行机制解析
K8s集群拆开来看,无非就是控制面加数据面。控制面负责决策,数据面负责执行,两边通过API Server这个唯一入口对话。理解每个组件的职责边界,以及它们之间如何配合,是运维K8s的基本功。
1.1 控制面组件:API Server、etcd、Controller Manager、Scheduler
先说API Server。它是整个集群的交通枢纽,所有组件的读写请求都打到它这里,再由它统一认证、鉴权、限流,然后把数据落进etcd。注意,API Server本身不直接操作节点上的容器,它只负责把期望状态(Desired State)写进etcd,并下发指令给对应的组件。所以集群里哪个组件挂了都不能影响API Server和etcd的正常运行,否则整个控制面就“失明”了。
etcd是K8s的存储底座。集群里所有资源对象,比如Pod、Service、ConfigMap,都以键值对形式保存在etcd里。etcd的性能直接决定集群的响应速度,尤其是大量Pod频繁创建销毁的场景,etcd的磁盘IO会成为瓶颈。我在生产环境里见过因为etcd磁盘是机械盘,导致API Server响应延迟飙升的情况,后来换成SSD后整个集群的稳定性提升了一个级别。
Controller Manager是一堆控制器的集合。比较常见的Deployment控制器负责保证Pod副本数达标,Node控制器负责节点状态监控与驱逐,Namespace控制器负责清理删除后的资源。它的工作方式可以理解成一个“填坑”循环:不断对比当前状态与期望状态,发现差距就去修正。这个修正动作不是主动推,而是被动list-watch加计算,所以它不会像kubelet那样直接跟容器运行时打交道。
Scheduler就是集群里的“房产中介”。它负责为新创建的Pod找到最合适的节点。调度算法分为过滤和打分两步,过滤阶段先剔掉不满足条件的节点,打分阶段再按资源余量、亲和性、污点容忍等因素给候选节点排序,最后把Pod绑定到得分最高的节点上。这个绑定动作是通过创建Pod的Binding资源实现的,并不是直接控制kubelet。
1.2 数据面组件:kubelet、kube-proxy、容器运行时
kubelet是节点上最重要的agent。它从API Server监听该节点需要运行的Pod清单,然后调用容器运行时创建、启停容器,再持续上报节点和Pod状态。如果API Server失联,kubelet会按本地的静态Pod配置继续维持已有容器运行,但无法接收新的调度指令。这也是为什么控制面短暂故障时,业务容器一般不会立刻中断的原因。
强调一个容易忽略的细节:kubelet不仅管容器生命周期,还负责探针检查、Pod垃圾回收、镜像回收、挂载存储卷。探针失败会触发容器重启或摘流,存储卷挂载失败则会让Pod一直处于ContainerCreating状态。所以排查问题不能只盯着容器引擎日志,kubelet的日志信息量更大。
kube-proxy负责实现Service的访问逻辑。它会把ClusterIP和Endpoints的映射关系转换成节点上的转发规则。默认模式下,kube-proxy用iptables实现,Pod数量多到一定程度时,iptables规则链会非常长,导致请求延迟增加。后来ipvs模式成为生产环境的更优选择,因为它的hash查找复杂度远低于iptables线性匹配。我会在第4节详细对比。
容器运行时在下层,负责真正拉起沙箱和容器进程。常见的运行时包括containerd、CRI-O等,通过CRI接口跟kubelet对接。需要注意容器运行时版本与K8s版本的兼容性,比如部分K8s版本对containerd的最低版本有要求,我见过因为containerd版本过老导致Pod反复CrashLoopBackOff的情况,升级运行时后恢复。
1.3 组件之间的一致性保证机制
K8s的一致性靠list-watch机制。API Server维护一个资源版本号sequence,各个组件通过API Server长连接监听资源变化,一旦资源版本递增就触发回调。这个机制比轮询高效得多,但也带来了一个隐患:kubelet或Controller Manager如果断连时间过长,重连后要进行全量list重新同步。
通常建议给各组件设置合理的sync频率,比如kubelet默认每10秒同步一次Pod状态,Controller Manager默认每5秒检查一次Deployment。如果集群规模大、资源对象多,可以适当调低同步频率,减小API Server压力,但不能太低,否则故障恢复时间会拉长。
组件间的心跳,主要通过Lease对象实现。我们可以通过kubectl get lease查看节点心跳记录,如果节点失联,Coordinated Lease的续租时间会超时,Node控制器就会把它标记为NotReady。这个机制我们可以用来做节点故障的自愈判断:心跳超时多久后开始驱逐Pod,给kube-controller-manager的按时间窗参数提权。
2. 工作负载调度原理与关键配置
K8s的调度策略直接决定Pod跑在哪儿。生产环境里经常因为默认调度策略不合适,导致资源分布不均或故障域不隔离,理解调度器的筛选逻辑,能帮我们精准控制Pod位置。
2.1 调度器的工作流程与多队列设计
Scheduler内部不是简单把所有Pod塞进一个队列,而是按优先级分队列。当新的Unscheduled Pod出现,先根据PriorityClass放入不同的队列,然后调度周期开始:从队列取出Pod,执行过滤与打分,最后把绑定结果写入API Server。如果绑定失败,Pod会回到队列重新等待,并触发背压机制。
一个常见的场景是节点资源碎片化。比如一个节点有40GB内存,已分配了35GB,此时一个需要6GB内存的Pod就无法调度到该节点,尽管节点空闲内存足够。因为K8s调度看的是“已分配数量”而不是“实际使用量”,这点在配置时一定要留意。推荐为节点预留一部分系统级余量,比如通过system-reserved参数为kubelet和系统进程预留资源。
调度失败还有一个容易被忽略的点:调度器会检查Pod的端口冲突。如果两个Pod都要求绑定宿主机的同一个端口,即便节点资源充足,也会被过滤掉。这种问题尤其容易出现在使用NodePort类型的Service上,端口规划不好会导致调度不对称。
2.2 节点亲和性与反亲和性配置要点
节点亲和性(Node Affinity)分为软性(preferred)和硬性(required)两种。硬性必须满足,软性尽量满足,当没有符合软性条件的节点时,Pod仍然可以在其他节点启动,只是优先级低。这个设计非常实用。
举例说明,一个监控采集Pod要求必须部署在带有gpu=true标签的节点上,但又不希望该节点跑太多CPU密集型任务:
affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: gpu operator: In values: - "true" preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: workload-type operator: NotIn values: - cpu-heavy注意“IgnoredDuringExecution”这个后缀,意思是运行期间如果节点标签变化,不影响已运行Pod,只会影响新调度任务。所以运维上临时修改节点标签,不会导致已有业务中断,这个特性值得善用。
反亲和性(Pod Anti-Affinity)常用于分散Pod副本。比如希望同一个工作负载的多个副本分布在不同的可用区或物理机,避免单点故障。配置时用topologyKey限定“不能放在同一拓扑域”,可以按主机名、机架标签、可用区标签来选择隔离维度。
用过之后我的体会是:反亲和性会增加调度复杂度,如果Pod副本数过多而可用节点不足,会产生大量Pending。建议只在关键业务容器上启用,且为软性反亲和为主。
2.3 污点与容忍度的实际应用
污点(Taint)是打在节点上的“标记”,表示该节点不接受带有此污点的Pod调度。容忍(Toleration)是Pod上的“许可”,表示Pod愿意运行在被打了该污点的节点上。
典型的场景是把专用节点隔离出来。比如给GPU节点打上gpu=true:NoSchedule的污点,只有声明容忍该污点的GPU任务Pod才能调度上去,其他普通Pod不会误入。这个做法比用节点亲和性更严格,因为即使资源充足,没有容忍Pod也无法调度。
另一个常见用法是驱逐节点做维护。运维人员给节点打一个maintenance=true:NoExecute污点后,该节点上所有不具备对应容忍的Pod会被立即驱逐。注意NoExecute会触发即刻清理,比NoSchedule的“不调度新Pod”更加强硬,维护前务必确认业务能承受重启或迁移。
实操提醒:如果误打了污点导致节点上大量Pod被驱逐,紧急恢复时可以删除污点,但已被驱逐的Pod需要重新创建或由控制器自动拉起。不要在业务高峰期做这类操作,否则可能引发雪崩。
3. 资源模型与QoS保障
K8s里最容易被忽视但又极其重要的就是资源限制。没有资源限制的Pod可以随意吃掉节点所有内存,最终把系统拖垮。我们不仅要懂requests和limits的语义,还要懂得它们背后的QoS分类。
3.1 requests与limits的区别和计算方式
requests是“预定额度”,调度器用它判断节点剩余可分配资源。limits是“硬上限”,运行时通过cgroup强制约束容器用量。配置时我们需要根据实际负载的安全余量来设置,不能拍脑袋。
举例:一个Java应用通过压测发现稳定运行需要2核CPU和2GB内存,瞬时高峰时最多吃到4GB内存。此时可以设置requests为2核2GB,limits为4核4GB。不过这里有个坑:CPU limit超过request额度时,如果节点有富余CPU,Pod可以使用到limit值,但如果节点CPU紧张,会被优先压缩。内存limit超出的部分不会触发压缩,而是OOM Kill。
再举一个实际推算的例子。一个8核32GB内存的节点,假设屏蔽掉系统预留2GB和kubelet预留1GB,可分配内存为29GB左右。如果有30个Pod的requests为1GB,那么至少需要30GB内存,该节点装不下,调度器会过滤掉该节点。我们在规划容量时,一定要以requests累加值为依据,而不是以实际内存使用率为依据。
3.2 QoS等级:Guaranteed、Burstable、BestEffort
K8s根据requests和limits的组合将Pod分成三个服务质量等级:
- Guaranteed:requests等于limits,所有容器都满足时,此类Pod优先级最高,内存不足时最后被驱逐。
- Burstable:requests小于limits,至少有一个容器设置了requests或limits,被挤压时可能被回收。
- BestEffort:没有设置任何requests和limits,内存紧张时最先被驱逐。
这个分级直接影响节点OOM场景下的生存概率。我建议生产环境的关键业务尽量做成Guaranteed,内存吃紧时系统会优先清理BestEffort和Burstable的Pod。如果业务实在没法明确limits,可以用namespace级别的默认值兜底,避免出现大量BestEffort。
配置优先级还可以在Pod上设置PriorityClass,但它跟QoS是两回事。PriorityClass决定调度队列优先级和节点内存不足时驱逐的顺序,但不能替代资源限制。
3.3 通过LimitRange和ResourceQuota控制资源边界
ResourceQuota是namespace级别的资源总量限制,能限制该命名空间下所有Pod的requests和limits总和。LimitRange是对象级别的默认值限制,能限制单个Pod或容器的资源配置范围。
举个实际场景:一个项目组有多个团队共用集群,需要给他们划分不同命名空间。可以为“订单项目”命名空间设置:
apiVersion: v1 kind: ResourceQuota metadata: name: quota namespace: orders spec: hard: requests.cpu: "10" requests.memory: "20Gi" limits.cpu: "20" limits.memory: "40Gi"这样即使某个团队误提交了超大Pod,也会被ResourceQuota拒绝。LimitRange可以配合设置单容器默认requests/limits:
apiVersion: v1 kind: LimitRange metadata: name: default-limit namespace: orders spec: limits: - default: cpu: "1" memory: "512Mi" defaultRequest: cpu: "500m" memory: "256Mi" max: cpu: "4" memory: "4Gi"加了LimitRange之后,里面创建Pod如果没有显式声明资源值,就会采用默认配置,避免散养Pod耗尽集群资源。
4. 网络模型与Service访问原理
K8s的网络是运维和开发都绕不开的话题。一个集群内每个Pod都有独立IP,通讯用CNI插件打通,对外访问靠Service和Ingress。网络链路长,排查起来也比其他部分更繁琐,这一节把核心链路梳理清楚。
4.1 Pod网络模型与CNI插件的作用
K8s规定每个Pod有独立IP,Pod内所有容器共享这个IP。这个IP由CNI插件在Pod创建时分配,并负责设置网络命名空间、路由和iptables规则。常见CNI包括Flannel、Calico、Cilium等。
Flannel实现简单,性能适中,适合中小规模集群。Calico基于BGP和iptables,支持NetworkPolicy做精细隔离,规模上限更高。Cilium基于eBPF,性能最好,但对内核版本有较高要求。选型时不仅要看当前规模,还要考虑未来是否需要网络策略,以及内核版本是否支持eBPF特性。
我从运维角度看,Calico在网络策略方面虽然配置稍复杂,但可观测性和排障能力强于Flannel。如果集群中需要多租户隔离、微服务深度安全策略,优先考虑Calico。如果只是demo或学习环境,Flannel完全够用。
4.2 kube-proxy的三种模式对比
kube-proxy的工作核心是把Service的ClusterIP映射到一组Pod IP。早期版本默认用iptables,每个Service创建时生成一堆规则,Pod数量多后规则链暴涨,连接新建速度下降。后来ipvs模式逐步成为主流,它利用内核IPVS模块做类似负载均衡的处理,性能好且规则数量与Service数量线性相关。
三种模式对比表:
| 模式 | 原理 | 优势 | 劣势 |
|---|---|---|---|
| userspace | 用户态代理转发 | 兼容旧版本,调试简单 | 性能差,已淘汰 |
| iptables | 内核态规则链 | 无需额外模块,通用性强 | 规则多时性能下降 |
| ipvs | 内核态IPVS虚拟服务器 | 高性能,支持更多负载均衡算法 | 依赖ip_vs内核模块 |
切换ipvs模式只需在kube-proxy配置中设置相关项,并确保节点加载ip_vs相关内核模块。切换到ipvs后,可以用ipvsadm -L -n查看转发规则,排障比iptables的层层遍历直观很多。如果节点内核没有加载相关模块,Pod的ClusterIP访问会失败,但Pod间直连不受影响。
4.3 Service类型与外部访问方案
Service有ClusterIP、NodePort、LoadBalancer和ExternalName几种类型。ClusterIP仅集群内部访问;NodePort在每台节点上开放一个端口,外部可以通过任意节点IP加端口访问;LoadBalancer依赖云厂商的负载均衡器;ExternalName则做DNS解析层面的映射。
日常排障时,很容易混淆ClusterIP不通与外网不通的原因。先确认Pod的IP能通,再确认Service的ClusterIP能否通,最后再看外部链路。有个取巧的方法:在集群内起一个临时Pod,用kubectl run带curl镜像进入集群网络空间测试,这样可以区分是集群内部问题还是外部链路问题。
很多运维会在Service后面再加一层Ingress统一入口,做路径转发和TLS终结。Ingress本身只定义规则,实际流量由Ingress Controller承载,常见的有Nginx Ingress、Traefik等。配置Ingress时有一个高频坑:后端Service的targetPort必须指向容器的service端口,而不是容器的启动端口,如果写错会出现502。
5. 存储接入与持久卷设计
有状态应用的存储设计是K8s运维的分水岭。能不能从容应对数据库、消息队列、日志组件的持久化需求,取决于对PV、PVC和StorageClass的理解深度。
5.1 PV、PVC、StorageClass的层级关系
PersistentVolume(PV)是集群里的持久化存储资源,可以理解成一块盘。PersistentVolumeClaim(PVC)是业务对存储资源的申请。StorageClass则是动态创建PV的模板,定义了如何调用底层存储API来做一个新PV。
理解这三个概念的关键在于分层。业务只需要声明PVC,比如“我需要5GB读写多路存储”,至于这5GB是从NFS还是块存储抠出来的,由存储管理员或StorageClass决定。这种解耦对生产和开发都很友好,开发不需要知道底层存储设备是什么。
如果手动创建PV再绑定PVC,是静态供给;通过StorageClass自动创建PV,是动态供给。生产环境建议尽量用动态供给,因为静态PV在一堆存储服务中手工维护,时间和人力成本都很高。
5.2 动态供给与静态供给的选择
动态供给适合按需扩容的云存储,比如云盘、NAS动态卷。PVC创建后,存储控制器会自动调用底层接口创建一块盘并挂载到Pod。优点是省去管理员手工建盘;缺点是需要提前调好存储插件和StorageClass参数。
静态供给适合网络文件系统或本地大块盘。比如机房里有几台存储服务器,把固定大小的NFS共享目录映射成多个PV,每个PV对应一个目录。管理员需要负责PV的回收策略,回收方式(Recycle、Delete、Retain)直接影响后续使用。
我踩过一个坑:使用静态PV时忘记设置回收策略,导致PVC删除后PV仍处于Released状态,需要手工清洗目录并重新置为Available。后来统一配置了Retain策略,减少这类问题。
5.3 本地存储与网络存储的适用场景
本地存储性能好,但无法跨节点迁移。比如给PostgreSQL配置了节点本地盘,节点故障后Pod在新节点重建,但数据还留在原节点的盘里,问题很麻烦。如果用网络存储(NFS、Ceph、云盘),数据可以漂移,但网络延迟和带宽会直接影响性能。
建议有状态服务优先使用分布式或云存储,并配合节点亲和性把Pod固定到指定节点,减少由于网络存储导致的性能波动。如果业务对性能要求极高且能接受故障迁移时的数据恢复,再考虑本地存储方案。
还有一个细节:PVC的访问模式。单个节点读写用ReadWriteOnce,多个节点共享读写用ReadWriteMany。NFS一般支持ReadWriteMany,但块存储如云盘通常只能ReadWriteOnce。如果多个Pod都在同一节点上但每个Pod都想独立写入同一个PVC,仍然需要ReadWriteMany模式。
6. 故障排查与性能优化实录
运维K8s最看重的是快速定位问题。下面把我在实际环境里遇到的高频问题和排查思路整理成笔记,算是一份速查手册。
6.1 Pod调度失败的常见原因与定位步骤
调度失败最典型的表现就是Pod一直Pending。先执行kubectl describe pod <pod名>查看Events,常见错误提示有:
- 0/x nodes available: 资源不足
- 0/x nodes available: node(s) had taint
- pod has unbound immediate PersistentVolumeClaims
- 节点亲和性规则不匹配
资源不足时,需要检查节点实际可分配资源与待调度Pod的需求。塔台问题大多来自requests设置过高,而不是节点真的没资源。可以直接执行kubectl describe node <节点名>查看每个节点的Allocatable,再比对所有Pod的requests总和。
污点问题则要先列出节点污点:
kubectl describe node <节点名> | grep -i taint如果发现节点上有污点而Pod没有相应容忍,需要给Pod加上tolerations配置,或者确认污点是否为误打。PVC未绑定问题则要检查存储类是否存在,以及存储插件是否有权限创建卷。
6.2 节点NotReady的排查思路
节点NotReady通常是kubelet心跳超时导致的。首先检查节点上的kubelet进程是否存活:
systemctl status kubelet如果kubelet退出了,直接看journal日志确认原因,端口占用、证书过期都是常见原因。如果kubelet在,但节点状态还是NotReady,可能是容器运行时失联,kubelet无法正常上报状态。检查containerd或CRI-O的状态,并用crictl ps测试运行时连接。
另一个场景是节点磁盘压力过大,K8s会自动打上DiskPressure或MemoryPressure污点,导致新Pod无法调度。执行kubectl describe node可以看到Conditions里对应的压力状态,清理过期镜像和日志文件后即可恢复。
6.3 高频问题排查记录表
| 故障现象 | 常见原因 | 排查命令与手段 | 恢复建议 |
|---|---|---|---|
| Pod Pending | 资源不足/污点/亲和性 | kubectl describe pod/node | 调整requests、加容忍、清理 |
| Pod ImagePullBackOff | 镜像不存在或仓库认证失败 | kubectl describe pod查看Events | 检查镜像名、增加imagePullSecret |
| Pod CrashLoopBackOff | 进程启动失败或探针失败 | kubectl logs --previous | 检查代码或降低探针阈值 |
| Service无法访问 | Kube-proxy规则异常 | ipvsadm -L -n、systemctl status kube-proxy | 重启kube-proxy、检查内核模块 |
| Node NotReady | Kubelet心跳超时 | systemctl status kubelet、journalctl | 重启/修复kubelet,检查证书 |
排查时尽量先用describe和logs拿到第一手事件信息,再用sysdig或crictl进入容器内部观察网络、进程和挂载点。我见过不少同事一上来就重启节点,结果问题反复出现,最后发现是证书过期,这类系统性排障思路很重要。
性能优化方面,核心指标是API Server延迟、etcd存储延迟、kubelet同步进度。如果API Server延迟高,优先看etcd磁盘IO和CPU;如果kubelet同步滞后,看是否Pod数量过大导致大量list请求;如果大量Pod集中在少数节点,适当放开反亲和性让副本分散。
7. 运维实战中的经验沉淀
写这篇笔记的过程中,几件实际问题突然浮现出来。第一件事发生在某个常规升级日,集群有几十个Pod突然从Running变成Terminating。排查后确认是某位同事在维护节点时误打了NoExecute污点,而没有提前加上对应的容忍。恢复的方法是紧急删除污点,让控制器重新调度,但已经重启的Pod还是造成了几分钟的业务中断。这件事之后,我要求所有节点维护操作必须写明维护原因和时间窗,且提前确认业务可容忍中断。
第二件事是关于资源配额。有次一个测试团队反馈Pod无法创建,报错信息没有任何有效提示,最后定位到是测试命名空间里ResourceQuota的CPU额度用完了。虽然这只是测试环境,但它提醒了我,ResourceQuota不仅能限制总量,更能在业务失控时充当“刹车”。现在每上一个大项目,我都会先在命名空间里规划好资源配额,再让开发自由使用。
第三件事是镜像仓库的认证配置。内部环境偶尔会遇到ImagePullBackOff,往往不是镜像不存在,而是镜像模板里的imagePullSecrets缺失或过期。建议在命名空间里默认配置imagePullSecret,并在镜像更新时校验收件签名,这样可以避免很多权限层面的意外。
最后想分享一个我个人的操作习惯:每次执行kubectl命令前,先确认当前上下文是不是预期集群。kubectl config get-contexts可以快速查看集群环境,避免手滑在错误环境里删资源。另外,尽量给每个集群打上不同的kubeconfig标识,并配合prompt显示当前上下文,降低误操作概率。
这篇笔记主要是把K8s运维里最核心的组件协作、调度逻辑、资源模型、网络与存储要点做了系统性的梳理。后续我还会继续整理更多实际案例,包括集群升级、备份恢复、网络排障等方向,有兴趣的可以关注这个系列。