配置和优化k8s,大概是很多运维和开发同学又爱又恨的事。爱的是它把容器调度、服务发现、自动伸缩这些复杂问题抽象成了几个对象和一堆yaml;恨的是,照着文档敲完命令,集群不一定起来,起来了也不一定稳,稳了也不一定快。我自己从裸机部署到生产集群维护踩了三四年坑,今天就把k8s配置和优化这块,从集群初始化、核心组件参数、网络暴露方式,到生产故障排查和GPU这类异构资源调度,按实战路径捋一遍。
1. 部署与初始化:先把集群“立”起来
1.1 部署方案怎么选:kubeadm、二进制还是发行版
很多新手上来就问“用哪个方式装k8s最省事”,我的回答通常是:先别看省事,看你要在什么环境里跑多久。
kubeadm是目前社区最主流的部署方式,它的优势在于把控制平面组件的静态Pod配置、证书生成、引导令牌这些脏活自动化了。你只要把crictl、kubelet、kubeadm三个二进制装好,一条kubeadm init就能拉起一个可用集群。但它也有自己的脾气:对容器运行时版本要求严格,镜像源在国内经常拉不动,版本升级时如果跨了小版本还好,跨大版本必须按官方文档的顺序一步步来。
二进制部署听起来更“原始”,但很多人不知道,它是理解k8s组件协作关系的最快路径。你自己把kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、etcd一个个启动参数写清楚,之后出任何问题,你对“哪个组件负责什么”心里都有数。我陪朋友排查过一个诡异故障:Pod调度成功但一直ContainerCreating,他用kubeadm装的,直接傻眼;我用二进制部署排查过类似的,三分钟就定位到是kubelet的cgroup驱动和容器运行时不一致。
还有一个选择是直接用开源发行版,比如kubespray、k0s、k3s。k3s适合边缘和资源受限环境,kubespray适合批量部署和生产级调优。不过发行版会封装很多细节,如果你本身对k8s还不够熟,建议别有“用发行版就万事大吉”的想法,出问题时你反而更难定位。
1.2 逃不掉的初始化故障:apiserver is not healthy
如果你搜索过k8s部署相关的内容,肯定见过这个报错:The API server is not healthy after 4m0.00747357s。我在一台新买的裸金属服务器上用kubeadm初始化时第一次见到它,心态直接崩了——所有Pod都在CrashLoopBackOff,kubelet日志刷屏,整个集群像没通电的机器。
这个报错的本质是kubeadm在等待apiserver就绪,但apiserver本身起不来或起得很慢。最常见的原因有四个:第一个是镜像拉不下来,apiserver、etcd、controller-manager这些镜像都放在registry.k8s.io,国内网络基本别想;第二个是证书或token过期,特别是你之前init过一次没成功,又没做kubeadm reset,残留的etcd数据会让新集群起不来;第三个是kubelet的cgroup驱动和容器运行时不统一,一般建议都用systemd;第四个是资源不足,kubeadm对控制平面节点的最低要求是2核4G,但你要是真的只给4G内存,初始化的过程会慢到让人怀疑人生。
我当时的排查顺序很简单:先journalctl -u kubelet -f看kubelet日志,确认是不是镜像问题;再用crictl ps -a看静态Pod容器的状态;然后检查/etc/kubernetes/manifests下的apiserver.yaml配置,看有没有端口冲突或者参数写错。如果是镜像问题,先把registry.k8s.io换成镜像加速域名,或者提前用ctr images pull把镜像拉到节点上,再重新kubeadm init。
这里要提醒一个细节:重新初始化之前,一定要kubeadm reset并清理/etc/kubernetes、/var/lib/etcd、/var/lib/kubelet下的残留数据。我见过有人没清理,重置了三次都不健康,最后发现是旧证书和旧etcd数据一直在干扰。reset之后,kubeadm init基本一次过。
2. 核心组件的配置细节与参数调优
2.1 kube-apiserver:集群的“总闸门”
kube-apiserver是整个集群唯一和etcd通信的组件,是所有请求的入口。你敲的每个kubectl get pods,底层都是走apiserver。所以apiserver的配置和性能上限,决定了你的集群能撑多大、多稳。
先说最容易被忽略的准入控制插件。默认的--enable-admission-plugins里包含NodeRestriction、LimitRanger、ResourceQuota,但很多人不会刻意去检查它。我建议至少在测试环境里开启NamespaceLifecycle和DefaultStorageClass,生产环境要评估PodSecurity。这些插件的作用是在请求进etcd之前就拦掉不合法配置,比如你忘了给负载均衡服务加externalTrafficPolicy,集群不会报错,但流量转发行为会很怪。
重点说性能调优。apiserver有几个关键参数直接关系到你能扛多少并发请求:
--max-requests-inflight=400 --max-mutating-requests-inflight=200这两个参数控制并发读和写请求数。默认值分别是400和200,如果你的集群节点超过100台,或者经常有大批量Job同时创建,很容易触顶。触顶的后果不是拒绝请求那么简单,而是客户端会不断重试,反而把apiserver压得更死。
另一个容易被忽略的是etcd的--quota-backend-bytes。默认是2GB,意思是etcd数据库超过这个值就会告警并拒绝写入。如果你集群里有大量历史事件或ConfigMap,很快就能涨上去。我在一个运行了半年的集群上遇到过,etcd空间用完,所有写操作都失败,排查了半天才发现是事件记录太多。解决办法是定期清理旧事件,或者调整etcd的压缩参数,但最根本的还是要控制集群里的资源数量,不要无节制地创建各种ConfigMap和Secret。
2.2 kubelet与容器运行时:节点侧的稳定器
很多人把精力放在控制平面的优化上,忽略了kubelet这个“每个节点的管家”。实际上,生产环境最常见的节点级故障,十有八九和kubelet配置有关。
kubelet维护Pod的声明周期,调度器只是决定Pod应该放在哪个节点,真正干活的还是kubelet。它的配置中--eviction-hard、--system-reserved、--kube-reserved这三个数值决定了节点在什么条件下会开始驱逐Pod。
默认的--eviction-hard是memory.available<100Mi,nodefs.available<10%,imagefs.available<15%,如果你直接用在生产环境,内存告警线就太低了,等触发驱逐时,节点已经被打到OOM边缘。我通常会给每个节点至少预留15到20%的系统资源,把驱逐线抬高一点:
apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration evictionHard: memory.available: "1Gi" nodefs.available: "15%" imagefs.available: "20%"注意这里有个细节:system-reserved里设置的值要小于节点总资源,它越界了你就会看到大量Pod被驱逐,但你又找不到程序在跑什么。我之前在8核16G的节点上给系统预留了12G,结果一个普通应用Pod刚跑起来就被Evicted,日志里全是The node was low on resource: memory。后来才意识到预留值给大了,业务无法申请到足够资源。
容器运行时的配置同样重要。现在大多数环境都用containerd,务必确认/etc/containerd/config.toml里的SystemdCgroup和kubelet一致。不一致的典型症状是:Pod能创建但一直无法进入Running,kubelet日志反复报failed to create containerd task: failed to start shim。同时容器日志的轮转也要在containerd层面配好max_size和max_file,否则日志文件会无限增长,把节点磁盘打满,进而引发节点级别的驱逐。
3. 网络与外网访问:把服务暴露做得更稳
3.1 ExternalIPs:最容易用错的一个字段
k8s里暴露服务的方式很多,Service Type有ClusterIP、NodePort、LoadBalancer,还有一个不太起眼但很实用的字段叫externalIPs。很多人在搜索k8s externalIPs配置时都会困惑:这不就是把节点的IP填进去吗,有什么好说的?
其实externalIPs真正的用途是让你在Service上绑定一个外网固定IP,流量直接打进你指定的地址,完全不走NodePort的随机端口映射。它适合的场景是:你有几个公网IP,希望直接映射到某个Service上,而且不想引入负载均衡器或Ingress Controller。
配置方式很简单:
apiVersion: v1 kind: Service metadata: name: my-web-service spec: type: ClusterIP clusterIP: 10.96.0.10 externalIPs: - 203.0.113.10 ports: - port: 80 targetPort: 8080这里有一个必须注意的坑:externalIPs不会做高可用。你把它绑到一台节点的IP上,如果这台机器挂了,流量就断了。我在一个内部系统上踩过这个坑,原以为Service天然有负载均衡能力,结果节点重启后整个系统失联。后来改成用Keepalived提供一个虚拟IP,再把虚拟IP填到externalIPs里,才解决了单点问题。
还有一个容易踩的坑:如果你同时设置externalIPs和NodePort,去访问那个外部IP时,可能直接走的是NodePort的转发规则,而不是你想直接进入Pod的逻辑。这个行为在不同CNI插件下表现还不一样,建议不要在同一个Service上混用这两种暴露方式。
3.2 CNI插件选型对比与调参要点
网络插件是k8s里最容易被当成“黑盒”的部分。很多集群一装好就默认用了Flannel,跑起来倒是能连通,但一旦涉及NetworkPolicy、性能压测、跨节点大流量,就会暴露问题。
先给一个我的选型经验:Flannel适合小集群、测试环境,它的VXLAN模式部署简单、故障率低,但它不支持NetworkPolicy,性能在跨节点场景下损耗比较明显;Calico是生产环境最稳妥的选择,支持NetworkPolicy,路由模式和BGP模式下的性能损耗很小,但配置项多,调试起来更费劲;Cilium是后起之秀,基于eBPF,性能和可观测性都强,但要求的内核版本比较新,对运维人员的要求也更高。
选好插件后,第一个要调的是MTU。默认情况下Flannel和Calico都会用MTU=1500,如果你的底层网络是VXLAN或Overlay,实际传输的数据帧会被封装一次,1500反而会触发分片。我在一个物理网络MTU是9000的机房里,把Flannel的MTU调成1480,Pod之间的TCP吞吐直接提升了30%。这个调参动作不大,但收益极其明显。
另一个重要配置是IPIP模式改成BGP模式。Calico默认用BGP直接在三层路由,不需要额外封装,性能最好。但BGP模式要求你的物理路由器支持并配置BGP邻居,否则节点之间的流量还是得走Overlay。如果你不想动网络设备,就保持IPIP模式,但把CALICO_IPV4POOL_IPIP调成Never,让Pod IP直接路由到宿主机,在某些网络环境里也会有不小的性能提升。
4. 生产级性能与稳定性优化实践
4.1 生产环境常见故障与排查思路
生产环境里的k8s故障,很多不是配置写错,而是没考虑到资源、时间和流量的变化。我整理了几个高频场景,都是按真实出镜率排的。
Pod一直Pending是最常见的。八成原因是资源不够或节点亲和性没有满足。排查时不要只盯资源,先kubectl describe pod看Events,里面会直接给出调度失败的原因。如果是Insufficient memory,看节点剩余资源;如果是nodeSelector不匹配,看标签有没有打对。
Pod反复Evicted排第二。这个在节点内存或磁盘紧张时特别明显,kubelet会优先杀掉占用资源最大的Pod。排查思路是看节点状态kubectl describe node里的Pressure项:MemoryPressure还是DiskPressure,之后检查是不是业务容器没设置Requests,导致调度器认为节点资源充足,但实际运行时节点根本扛不住。所以生产环境一定要给工作负载设置Requests和Limits,否则你的调度决策就像闭着眼睛做的。
还有一个容易让人误判的故障是域名解析间歇性失败。Pod能起来,但访问其他服务的域名时经常超时。这种问题往往出在CoreDNS或NodeLocalDNSCache上。先查CoreDNS的Pod有没有重启,再查节点上的/etc/resolv.conf里的nameserver指向是否正常。如果CoreDNS没问题,就要怀疑是conntrack表满了,这个在大流量、短连接场景下最常见,需要把net.netfilter.nf_conntrack_max调大一点。
4.2 GPU等异构资源调度配置
如果你的业务涉及模型推理或机器学习训练,k8s调用GPU是绕不开的话题。今年大模型相关的项目特别多,标题里出现的“k8s调用GPU”搜索热度一直很高,因为这块配置的坑是真的多。
先理清一个概念:k8s本身不认识GPU硬件,它需要Device Plugin把GPU资源作为可调度资源上报给kubelet。NVIDIA官方提供了nvidia-device-plugin,装好后,节点上就会出现nvidia.com/gpu这种可分配资源,你在Pod里就能声明:
resources: limits: nvidia.com/gpu: 1这里有几个前置条件:节点上要安装NVIDIA驱动,容器运行时要设置为nvidia容器运行时,或在containerd里配置好nvidia-container-runtime。只装device plugin,没配运行时,Pod调度上去后会报Failed to create pod sandbox: error getting image config之类的问题。
我踩过最深的坑是驱动版本和CUDA版本不匹配。如果你在Pod里跑的镜像需要CUDA 12.0,而宿主机驱动只支持CUDA 11.x,容器启动时会直接报找不到libcuda.so。这不是k8s的问题,但排查起来特别迷惑,因为Pod的状态是CrashLoopBackOff,日志里全是NVIDIA相关的报错。建议在给GPU节点打标签或调度约束之前,先在节点上用nvidia-smi确认驱动支持的最高CUDA版本。
还有一个容易被忽略的问题:GPU节点必须设置nodeSelector或nodeAffinity,否则普通任务也可能被调度到GPU节点上占资源。同时建议给GPU节点设置Taints,只允许带对应Tolerations的工作负载上去跑,避免“一颗原子弹带一堆步兵”的资源分配尴尬。
4.3 监控、日志与告警的基础配置
很多人觉得监控是“装了就完事”,但实际上,监控体系本身的配置和优化,对k8s集群稳定性的帮助,比大多数业务优化都大。
基础标配是metrics-server加Prometheus。metrics-server负责给kubectl top提供数据,让HPA能正常工作,它只是一个数据采集器,本身不做监控展示。Prometheus负责抓取指标、存储和告警。如果你只是要“能看到Pod CPU和内存”,装metrics-server就够;但你要做容量规划、长期趋势分析,必须上Prometheus。千万不要把metrics-server当成监控系统用,它的数据保留时间极短,对排查历史问题毫无帮助。
日志采集方面,我推荐Fluent Bit加Elasticsearch或Loki的方案。Fluent Bit比Fluentd更省资源,而且它不抢Pod的CPU资源。这里要优化一个配置:容器日志的路径解析和Pod标签注入。如果你不给日志打上namespace和pod_name的标签,排障时想“按某个Pod查日志”就会非常痛苦,只能靠时间硬筛。
告警规则别贪多。很多人一开始就配置几十条告警,比如Pod重启超过3次、API Server延迟超过1秒,结果全是噪音,真正出问题的时候反而被淹没。我建议告警规则控制在10条以内,优先覆盖:节点NotReady、Pod长时间Pending、etcd写入延迟过高、磁盘空间不够。把这几个核心指标看住,集群稳定性就有基本保障了。
5. 常见问题速查与避坑心得
5.1 高频问题速查表
整理一份我在实际维护中用得最多的问题处理速查表,直接照着排查能省不少时间。
| 现象 | 可能原因 | 快速排查/解决 |
|---|---|---|
| kubeadm init报apiserver not healthy | 镜像拉取失败、残留数据、cgroup驱动不一致 | crictl ps -a看容器状态;先kubeadm reset再重试;确认运行时SystemdCgroup=true |
| Pod一直Pending | 资源不足、节点亲和性不满足、污点未容忍 | kubectl describe pod看Events;kubectl get nodes看剩余资源 |
| Pod不断Evicted | 节点MemoryPressure/DiskPressure | 检查节点Pressure状态;给业务设置Requests/Limits |
| DNS解析间歇性失败 | CoreDNS异常、conntrack表满 | 查coredns pod日志;调整nf_conntrack_max参数 |
| 新Service外部访问不通 | externalIPs绑了单点IP、网络安全组限制 | 改用虚拟IP;检查节点iptables规则 |
| Pod调度到GPU节点但启动失败 | container runtime没配nvidia运行时、驱动版本不匹配 | nvidia-smi验证驱动;containerd配置nvidia runtime;查看Pod事件 |
| 节点磁盘被日志打满 | 容器日志轮转没配置 | 在containerd配置log_max_size、log_max_file;清理旧日志 |
5.2 我的几条实操心得
最后分享几个个人觉得最值得记住的经验。
配置变更要“小步快跑”。不要在一次维护窗口里同时升级CNI、修改kubelet参数、更新内核,任何一步出问题,你都很难定位是哪个变更导致的。我一般会先把变更拆成独立的小批次,每完成一个就观察15分钟,确认没有异常再继续下一步。这比事后花三小时回滚稳妥得多。
版本兼容矩阵真的要看。k8s版本、containerd版本、CNI插件版本、device plugin版本,这四者的兼容性关系,在升级前必须查清楚。我遇到过最惨的一次是只升级了kubelet,从1.24跳到1.26,结果cgroup驱动、CSI驱动、NodePort行为全都变了,一夜之间集群资源统计异常,业务方全都炸毛。先看官方支持矩阵,再做升级,这个习惯能救你一命。
给节点预留资源时,宁可多留不要抠。很多人觉得每个节点预留10%就够了,但实际上系统本身有日志、镜像缓存、内核缓冲区,再加上节点上的DaemonSet,预留低于15%很容易在业务高峰期触发驱逐。我现在的做法是控制平面节点预留至少2核2G,工作节点按总资源的15%到20%预留,之后几乎没有再遇到过节点级的资源瓶颈。