K8s部署这事儿,网上教程一抓一大把,但绝大多数都是拿单master集群糊弄事,真正到了生产环境,高可用、网络、存储、监控这些坑一个都躲不掉。我这两年经手过的集群没有十个也有八个,从裸机到云主机都踩过,这篇就把从零搭建一套生产可用K8s集群的完整过程捋一遍,重点是三master高可用架构怎么设计、kubeadm怎么用才不出幺蛾子,以及部署完Prometheus、GPU调度这些扩展能力时那些文档里不写的细节。适合刚考完CKA想落地实操的人,也适合已经在跑集群但总被各种诡异问题折腾的运维同学。
1. 部署K8s前的四个关键决策
1.1 为什么选kubeadm而不是二进制或发行版自带工具
K8s集群的搭建方式大致有三条路:纯二进制手动部署、kubeadm工具部署、Rancher/Kubekey这类发行版自带部署工具。我见过不少人一上来就挑战二进制部署,觉得这样才能“掌握原理”,实际上二进制方式光是把etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy这些组件的证书、参数、systemd unit文件理顺,就够折腾一整天的,而且很容易出一些低级的配置错误,排查起来特别费劲。
我个人的建议是无脑选kubeadm。原因很实在:kubeadm是官方维护的部署工具,大部分版本升级、证书续期这些操作官方都替你考虑到了,还能通过配置文件(kubeadm-config.yaml)把集群参数固化下来,方便后续审计和复现。相比二进制部署,kubeadm把最复杂的TLS引导、RBAC授权、控制面组件静态Pod生成这些环节自动化了,但它又不是黑盒,你完全可以通过kubeadm config print init-defaults查看默认配置,通过kubectl -n kube-system get pod -o yaml查看组件的真实启动参数,这对于理解K8s内部机制完全够用。
至于Rancher、Kubekey这类图形化部署工具,适合对K8s本身不熟、主要是想快速跑业务的人。但用这类工具的问题在于:一旦集群出问题,排查问题时你对底层组件是无感的,反而更难定位。而且Kubekey这种工具虽然能帮你快速搭出高可用集群,但版本定制、网络插件、运行时方案这些选项它都替你做了默认选择,你如果不清楚这些默认值的含义,后面调整起来会非常被动。
1.2 高可用架构怎么设计:3个master的来龙去脉
生产环境至少要3个master节点,这个不是玄学,而是etcd的选举机制决定的。etcd作为K8s的唯一数据源,必须通过Raft协议保证数据一致性,而Raft协议要求集群中超过半数的节点存活才能选出leader、才能对外提供服务。2个master节点挂掉1个就只剩50%,不满足“超过半数”的要求,集群直接不可用。3个master允许挂掉1个,5个master允许挂掉2个,以此类推,所以奇数个是性价比最高的选择。
很多人会忽略一个问题:三台master的高可用,不仅仅指etcd的高可用,还包括kube-apiserver的高可用。etcd是集群存储层,kube-apiserver是K8s的入口,worker节点的kubelet、kube-proxy以及所有kubectl命令都要访问apiserver。如果只做了etcd高可用,apiserver挂了,整个集群依然处于“能存数据但无法读写”的状态,等于白搭。
所以三台master架构中,每台master上都会运行一套完整的控制面组件(apiserver、controller-manager、scheduler),其中controller-manager和scheduler通过--leader-elect参数实现选主,同一时刻只有leader真正工作,这个机制K8s已经内置,无需额外处理。唯独apiserver是无状态的,它不选主,需要前端有一个负载均衡器来分发流量。这就是为什么在搭建高可用集群时,总要在master前面加一层Haproxy+Keepalived或者云厂商的SLB。负载均衡器把来自kubelet、kubectl、kube-proxy的请求分发给三台master的apiserver,某一台master挂了,负载均衡自动摘除它,集群控制面就依然是可用的。
1.3 容器运行时:docker被弃用后containerd成为主流
K8s从1.24版本开始彻底移除了对Docker作为运行时(dockershim)的支持,这个消息当时劝退了不少人,但实际用起来没那么可怕。Docker本身是一套完整的容器工具链,包含build、ship、run等多个模块,而K8s真正需要的只是其中“运行容器”的那一部分——也就是containerd。Docker底层本来就是调用containerd来跑容器的,所以K8s直接对接containerd,等于把中间商去掉了,性能还更好。
部署containerd时有个容易踩坑的地方:默认配置文件里SystemdCgroup是false,而K8s要求容器运行时的cgroup驱动必须和kubelet保持一致,都使用systemd。如果这里不改成true,集群初始化后节点会一直报kubelet cgroup driver相关的错误,pod创建后状态异常。另外国内网络环境下,containerd拉取镜像需要配置registry mirror,这个后面实操部分会细说。
选择containerd还有一个隐藏的好处:排查问题的时候更干净。以前用docker作为运行时,docker ps、docker logs、docker exec这些命令都能查到容器状态,现在统一用crictl这一套命令行工具(crictl ps、crictl logs、crictl exec),它本身就是K8s为容器运行时设计的标准CLI,和kubelet、kube-proxy等组件协作更直接,不需要在docker和crictl之间来回切换。
1.4 版本与组件选型:稳定优先
K8s的版本迭代非常快,每年会有3-4个小版本发布,但生产环境千万别追新。我见过有人用最新的K8s版本搭配旧版Calico,结果网络插件死活起不来,折腾了半天发现是版本兼容性问题。选版本的核心原则是:在官方支持周期内选最新稳定版,同时确认所有配套组件(containerd、Calico、CoreDNS等)的版本兼容矩阵。
这里有一个官方工具可以帮你确认版本兼容性:kubectl version --short查看当前版本,K8s官方文档页面有详细的“已发布版本”和“版本偏差支持策略”。例如,kubeadm支持安装的K8s版本比kubelet最多提前一个次要版本。实操中我建议选一个已经发布了3-6个月的小版本,比如说K8s 1.28.x或1.29.x,这个阶段的bug修得差不多了,各种配套组件的新版本也都适配了,网上的踩坑经验也比较丰富。
配套组件方面,容器运行时containerd选1.7.x,因为1.7是稳定维护分支;网络插件Calico选v3.27+,这个版本对K8s 1.28+支持良好;Ingress Controller选ingress-nginx,版本4.8+;监控套件用kube-prometheus-stack的25.x版本。把这些组件的版本提前锁定,部署的时候才不会手忙脚乱。
2. 环境准备与基础配置
2.1 服务器规划与网络规划
假设你手里有三台master和三台worker(小规模生产环境这个配置够用),规划时要考虑操作系统、硬件配置、网络三段式设计。
操作系统方面,我优先推荐Ubuntu 22.04 LTS或Rocky Linux 9,这两个系统对容器生态的支持都很好,内核版本也够新,不会出现因为内核太老而无法运行overlayfs或iptables nftables的问题。Ubuntu的好处是文档多、踩坑经验多,Rocky的好处是更贴近RHEL系运维习惯。两者选哪个不重要,重要的是所有节点的操作系统版本必须一致,内核参数配置必须一致,否则后面排查问题的时候,环境差异会严重干扰判断。
内存和CPU规划有一个底线:master节点至少4核8G,worker节点至少8核16G,如果机器太差,apiserver、etcd调度大量pod时CPU和内存都会吃紧。磁盘方面,所有节点建议SSD,etcd所在的master节点尤其不能省,因为etcd的fsync性能直接决定集群的响应速度,机械盘跑etcd简直是灾难。
网络规划是很容易被忽略的点。K8s集群内部需要三个网段:节点网络(物理机IP)、Pod网段(Pod IP分配范围)、Service网段(Service VIP分配范围)。这三个网段必须提前规划好,并且不能互相重叠。比如节点网络是192.168.10.0/24,Pod网段可以定10.244.0.0/16,Service网段可以定10.96.0.0/12。这个规划逻辑是:Pod网段和Service网段是内部虚拟网络,不影响外部网络,但如果不提前规划好,两个网段重叠后,K8s内部路由会乱套,排查起来极其痛苦。
2.2 基础环境配置:主机名、hosts、内核参数、防火墙
这部分内容虽然简单,但每一步都很关键,而且顺序不能乱。
先设置主机名的规范。我的建议是:k8s-master01、k8s-master02、k8s-master03、k8s-node01、k8s-node02、k8s-node03这种命名格式。主机名不要用默认的localhost或者带特殊字符的名字,因为K8s的节点名称就是主机名,kubectl查看节点时显示的就是这个名字,主机名混乱会严重影响多节点管理。
然后修改所有节点的/etc/hosts文件,把6台机器的IP和主机名对应关系写进去。这一步是为了让节点之间可以互相通过主机名解析通信,避免后续kubeadm init时因为节点名无法解析而报错。
内核参数是K8s部署的重中之重,特别是以下两个:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 vm.swappiness = 0 EOF sudo sysctl --systemnet.bridge.bridge-nf-call-iptables = 1确保iptables规则能作用于通过Linux网桥的流量,这个参数不设的话,K8s Service的ClusterIP模式会失效,因为kube-proxy的流量转发规则只在主机网络层生效,对网桥转发流量不生效。net.ipv4.ip_forward = 1开启IP转发,这是CNI插件和Service流量转发的前提。vm.swappiness = 0尽量关闭swap的使用,因为K8s对swap的支持比较特殊,1.22之前直接要求关掉,1.22之后虽然支持swap但默认行为还是不建议。
防火墙和swap的处理,文档上说的是“关闭”,但实际执行时我建议分情况处理。如果是测试环境、内网环境,直接swapoff -a并且注释掉fstab中的swap条目,防火墙可以关闭。如果是生产环境,防火墙关不关取决于你公司的安全策略,但至少要把6443(apiserver)、2379-2380(etcd)、10250(kubelet)等端口放通。swap则必须关闭,因为K8s默认不允许节点使用swap,不关的话kubelet会直接报错拒绝启动。
2.3 安装containerd并完成关键配置
安装containerd有两种方式:apt/dnf源直接安装,或者从GitHub Release下载二进制包。用apt/dnf安装最方便,但要注意源里containerd版本可能比较老,建议先确认版本再装。
Ubuntu下安装:
sudo apt-get update sudo apt-get install -y containerd装完以后,生成默认配置并修改关键参数:
sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后编辑/etc/containerd/config.toml,重点改两处:
第一处是SystemdCgroup。在[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]段落中,把SystemdCgroup = false改成SystemdCgroup = true。这个参数让containerd使用systemd cgroup驱动,与kubelet保持一致,否则kubelet启动后节点会一直处于NotReady状态。
第二处是镜像加速。在[plugins."io.containerd.grpc.v1.cri".registry.mirrors]段落中加入:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://docker.m.daocloud.io"]如果你所在的网络环境访问docker.io本身没问题,这步可以跳过。但如果是在国内环境或者公司内网环境,不配置镜像加速的话,后面kubeadm init拉取kube-apiserver、etcd这些镜像时会直接超时失败。
改完配置后重启containerd:
sudo systemctl restart containerd sudo systemctl enable containerd用systemctl status containerd确认服务状态为running。这里有个小技巧:crictl info命令可以快速查看containerd的配置是否生效,如果能看到systemdCgroup: true,说明配置改对了。
2.4 安装kubeadm、kubelet、kubectl
三个组件的版本必须一致。安装前先确认你的K8s目标版本,比如1.29.1,然后用以下命令安装指定版本:
Ubuntu下:
sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/' | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet=1.29.1-1.1 kubeadm=1.29.1-1.1 kubectl=1.29.1-1.1注意这里必须加版本号,直接装最新版容易和你想用的版本不一致。还要执行sudo apt-mark hold kubelet kubeadm kubectl把版本锁住,防止意外升级导致kubelet和apiserver版本不匹配。
安装完成后,暂时不要启动kubelet。因为kubelet在加入集群之前启动会一直报错找不到apiserver,这是正常的。所有配置工作就绪后,由kubeadm在init或join时自动启动它。
3. 高可用集群搭建实操
3.1 部署Haproxy+Keepalived负载均衡层
在三台master节点上部署Haproxy和Keepalived,实现apiserver的负载均衡和VIP漂移。VIP(虚拟IP)是部署高可用集群的关键,比如规划VIP为192.168.10.100,这个IP会由Keepalived自动绑定到当前的健康master节点上。
先在所有master上安装:
sudo apt-get install -y haproxy keepalivedHaproxy的配置/etc/haproxy/haproxy.cfg如下:
global log /dev/log local0 maxconn 4096 defaults log global mode tcp option tcplog option dontlognull retries 3 timeout connect 5s timeout client 50s timeout server 50s frontend k8s-apiserver bind *:16443 mode tcp default_backend k8s-masters backend k8s-masters mode tcp balance roundrobin server k8s-master01 192.168.10.11:6443 check server k8s-master02 192.168.10.12:6443 check server k8s-master03 192.168.10.13:6443 check这里有个细节:Haproxy监听在16443端口而不是6443,是因为apiserver本身占用了6443,Haproxy用16443接收外部流量再转发给各master的6443。负载均衡层与上层流量之间是解耦的。
Keepalived的配置/etc/keepalived/keepalived.conf:
global_defs { router_id LVS_K8S } vrrp_instance VI_1 { state MASTER interface ens33 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 123456 } virtual_ipaddress { 192.168.10.100 } }注意:三台master的state分别设置为MASTER、BACKUP、BACKUP,priority分别设置为100、90、80。同一时间只有最高priority的节点持有VIP,Master挂了之后BACKUP自动接管。
重启服务后,用ip addr show确认VIP是否绑定在当前节点上。
3.2 初始化第一个master节点
初始化前需要准备一个kubeadm配置文件,因为默认配置下Pod网段是10.96.0.0/12(Service网段),而我们需要自定义Pod网段为10.244.0.0/16。这里我强烈推荐用配置文件而不是纯命令行参数,因为配置文件可以复查、可以版本管理。
创建kubeadm-config.yaml:
apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.29.1 controlPlaneEndpoint: "192.168.10.100:16443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443这里controlPlaneEndpoint必须设置为VIP加Haproxy端口192.168.10.100:16443,这是三master高可用集群最核心的配置。所有节点的kubelet、kube-proxy都会通过这个地址访问apiserver,即使当前master节点宕机,VIP漂移到其他master后,流量依然能正常转发。
执行初始化:
sudo kubeadm init --config=kubeadm-config.yaml --upload-certs--upload-certs参数会把控制面证书上传到etcd中,这样后面的master节点join时才能自动获取证书。初始化过程中如果报错,先不要慌,kubeadm reset之后检查前面的配置项,特别是SystemdCgroup和镜像加速,90%的问题都出在这两个上面。
初始化成功后会输出一段join命令,包括master节点join和worker节点join的命令,一定要保存下来。
然后配置kubectl:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config用kubectl get nodes验证第一个master节点状态,此时应该是NotReady,因为还没有安装CNI网络插件,这是正常的。
3.3 加入其余master节点与worker节点
第二个和第三个master节点加入的方式与第一个不同,不能直接执行kubeadm init,要用join命令。
master02节点执行:
sudo kubeadm join 192.168.10.100:16443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash> \ --control-plane --certificate-key <certificate-key>这里的关键点是--control-plane参数,表示以控制面节点身份加入。加入成功后,master02会自动生成apiserver、etcd等静态Pod。--certificate-key就是kubeadm init --upload-certs时输出的那个key,用来解密上传到etcd的证书。
worker节点加入时不需要--control-plane和--certificate-key:
sudo kubeadm join 192.168.10.100:16443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>如果token过期了,可以在master节点上重新生成:
sudo kubeadm token create --print-join-command所有节点加入后,在master01上执行kubectl get nodes,应该能看到3个master和3个worker节点,状态都是NotReady,还没变成Ready是因为网络插件还没安装。
3.4 验证集群状态与高可用故障演练
集群搭建好后,做一次完整的高可用验证非常有必要,不要带着隐患上线。
先验证基础状态:
kubectl get nodes -o wide kubectl get pods -n kube-system -o wide kubectl get cskubectl get cs查看控制面组件状态,如果输出的是unhealthy不要慌,这是1.24版本后kube-controller-manager和kube-scheduler的健康检查端口没有暴露导致的误报,不影响实际功能。
然后是故障演练。模拟master01宕机,在master01上执行sudo shutdown -h now,然后观察:
- VIP是否漂移到master02(在master02上执行
ip addr show检查) kubectl get nodes是否还能正常返回- 现有pod是否还在正常运行
正常情况下,master01宕机后,VIP会漂移,apiserver请求自动分发到master02和master03,集群对外服务不受影响。同时etcd集群中master02、master03依然满足多数派条件,读写正常,集群状态为healthy。这就是三master高可用的价值所在。
故障恢复后,master01重新开机,节点会自动重新加入集群,无需手动干预。但这个过程中要注意时间同步,所有节点都要配置NTP服务,如果节点间时间偏差超过阈值,etcd会报etcdserver: request timed out之类的错误,到时候排查起来又得浪费半天。
4. 网络插件与基础运维命令
4.1 CNI网络插件选定与安装(Calico)
CNI插件是K8s集群的“神经系统”,负责给每个pod分配IP并打通跨节点通信。市面上主流的CNI插件有Calico、Flannel、Cilium等,我选型时的标准很简单:性能优先选Cilium,兼容性优先选Calico。
Calico是基于BGP协议的三层网络方案,性能好、支持NetworkPolicy,而且部署简单、排错直观,是生产环境最常见的CNI。安装方式:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml安装前要确认calico.yaml中CALICO_IPV4POOL_CIDR变量值是否和你规划的Pod网段一致(默认是192.168.0.0/16,需要改成10.244.0.0/16)。如果不改,Pod IP的分配网段和你规划的网段对不上,网络很容易乱。
等Calico的pod全部Running后,集群节点状态会从NotReady变成Ready。验证网络是否正常:
kubectl run test-pod --image=busybox -- sleep 3600 kubectl exec test-pod -- ping <另一个节点的pod IP>能ping通则说明跨节点网络正常。不能ping通的话,检查各节点的/proc/sys/net/ipv4/ip_forward是否为1,以及防火墙是否放行了BGP端口(默认179端口)。
4.2 K8s常用命令速查
部署完集群,日常运维命令大概这几类,整理成速查表:
| 场景 | 命令 |
|---|---|
| 查看节点及资源 | kubectl get nodes -o wide |
| 查看pod及所在节点 | kubectl get pods -A -o wide |
| 查看pod详情 | kubectl describe pod <pod-name> -n <namespace> |
| 查看pod日志 | kubectl logs -f <pod-name> -n <namespace> |
| 进入pod执行命令 | kubectl exec -it <pod-name> -- /bin/sh |
| 查看service/endpoints | kubectl get svc,ep -A |
| 查看deployment伸缩 | kubectl scale deployment <name> --replicas=5 -n <ns> |
| 节点维护(排空) | kubectl drain <node> --ignore-daemonsets --delete-emptydir-data |
| 节点恢复调度 | kubectl uncordon <node> |
| 查看事件(排错核心) | kubectl get events --sort-by=.metadata.creationTimestamp |
这里单独强调两个高频命令。kubectl get events是排错时的第一选择,deployment创建后pod状态一直Pending或CrashLoopBackOff,多半能在这里看到拉镜像失败、磁盘压力、配额不足等明确信息。kubectl drain是做节点维护时的标准操作,它会先驱逐节点上的pod到其他节点,然后标记节点不可调度,这样重启服务器不影响业务。但要注意--ignore-daemonsets参数必须加,否则DaemonSet的pod被驱逐后会立刻重建,导致drain卡住。
4.3 Service对外暴露:NodePort、LoadBalancer、ExternalIPs的实战取舍
K8s中pod本身是不能被外部直接访问的,需要Service来做一层抽象。Service的对外暴露方式有三种,很多人都搞不清怎么选,我这里直接给结论。
NodePort是最简单的暴露方式,Service会分配一个30000-32767之间的端口,每个节点都会监听这个端口并转发到对应的pod。适合临时调试、内网访问,不适合生产环境对外提供服务,因为会占用节点端口,而且客户端访问时直接打到了物理节点上,缺少负载均衡和高可用。
LoadBalancer一般配合云厂商SLB或裸金属上的MetalLB使用。Service定义type: LoadBalancer后,云平台会自动创建一个负载均衡器,外部流量通过LB进入集群。这是云上K8s服务的标准方案,但在裸金属集群中需要额外部署MetalLB来提供类似的能力。
ExternalIPs是一个容易被忽略的选项。给Service指定externalIPs字段后,外部流量可以通过这个IP直接访问Service,而无需经过NodePort或LoadBalancer。这个方案适合集群中已经预留了公网IP的情况,配置直观,但缺少健康检查和负载均衡,只适合IP固定且数量少的场景。
我生产环境常用的组合是:前端外部流量走LoadBalancer(或者Ingress + LoadBalancer),集群内部服务之间直接使用Service的ClusterIP访问,调试时临时用kubectl port-forward来验证单个pod。这三个方式配合起来,基本覆盖各种访问需求。
4.4 命名空间与资源限额管理
多业务共用一个集群时,命名空间(Namespace)和资源配额(ResourceQuota)一定要提前规划,否则后面管理会失控。
命名空间是逻辑隔离单位,每个团队或业务一个命名空间,之间通过RBAC控制访问权限。创建命名空间:
kubectl create namespace dev kubectl create namespace prod资源配额的目的是防止某个团队把集群资源占满。给命名空间配置一个默认资源额度:
apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: "10" requests.memory: 20Gi limits.cpu: "20" limits.memory: 40Gi pods: "50"配置完成后,dev命名空间下所有pod的资源请求总和不能超过上述额度。如果新建的pod没有声明resources字段,会被LimitRange拦截。这个配置能有效避免测试环境把生产环境资源挤爆的情况。
5. 扩展能力:GPU调度与监控体系
5.1 让K8s调用GPU:device plugin工作原理
如果你的工作负载涉及AI训练或推理,需要在K8s上使用GPU资源。K8s本身不认识GPU,需要NVIDIA device plugin让kubelet感知到节点上的GPU资源。
核心流程是:在GPU节点上部署一个DaemonSet(nvidia-device-plugin),这个插件通过nvidia-container-toolkit申请GPU设备,并将节点上的GPU数量上报给kubelet。之后,kubelet会将这些GPU以nvidia.com/gpu资源的形式暴露给调度器。调度器在调度pod时,如果pod声明了resources.limits["nvidia.com/gpu"]: 1,就会把pod调度到有GPU的节点上,并保证独占一个GPU。
实测部署步骤如下:
- 在GPU节点上安装NVIDIA驱动和nvidia-container-toolkit:
sudo apt-get install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=containerd sudo systemctl restart containerd- 部署device plugin:
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/main/nvidia-device-plugin.yml- 验证GPU资源可见:
kubectl describe node <gpu-node> | grep nvidia.com/gpu输出能看到nvidia.com/gpu: 4之类的信息,说明这个节点有4个GPU可分配。
需要提醒的是:GPU节点打上污点(taint)是常见实践,防止普通pod占用GPU节点。但如果所有GPU节点都打了NoSchedule污点,而GPU工作负载没有设置对应的容忍(toleration),pod会一直处于Pending状态。所以要么GPU工作负载声明容忍,要么单独用节点亲和性控制调度。
5.2 部署Prometheus监控K8s集群
K8s集群跑起来后,监控就是刚需。官方推荐的最省事方式是直接部署kube-prometheus-stack,它是一整套预配置好的监控方案,内置了Prometheus、Alertmanager、Grafana,以及一堆现成的告警规则和Dashboard。
用Helm部署:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack \ --namespace monitoring --create-namespace \ --version 55.0.0部署完成后,Grafana默认账号密码是admin/prom-operator,Dashboard模板已经内置了K8s集群资源、节点指标、Pod指标、Etcd等常用视图。验证监控目标是否正常:
kubectl get svc -n monitoring kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090打开http://localhost:9090/targets,检查各targets的up状态。常见的坑是某些Exporter因为RBAC权限不足无法拉取指标,或者Node Exporter与集群节点网络不通,targets显示down。这些问题的排查路径基本都是:先用kubectl get pods -n monitoring确认Exporter Pod是Running,再查看它的日志,找到权限或网络相关的具体报错。
Prometheus监控数据落盘有两点建议:一是单独挂一块PV给Prometheus,避免数据落在系统盘被写满;二是根据集群规模调整storage.tsdb.retention.time,默认保留15天就够了,不要贪多,磁盘吃紧会导致Prometheus写入失败,数据反而不完整。
5.3 Operator模式:K8s自动化的精髓
K8s项目看多了你会发现,"K8s + Operator"几乎成了复杂应用部署的标配。Operator不是什么神秘技术,简单说就是把运维某个应用的经验和自动化逻辑写进代码,用K8s控制器模式来管理这个应用的生命周期。
以最经典的Prometheus Operator为例。我们上面用Helm部署的kube-prometheus-stack,底层就是Prometheus Operator在起作用。你不再需要手动管理Prometheus配置,而是通过定义ServiceMonitor资源来描述要监控的目标,Operator会监听这些资源的变化,自动生成Prometheus的抓取配置。新增一个服务的监控,只需要创建一个ServiceMonitor对象,抓取路径、间隔、端口全都在里面声明,Operator替你落盘配置并触发Prometheus热加载。
Ez在MySQL、Redis这些中间件上也有对应Operator(如Percona XtraDB Cluster Operator),它们能让数据库在主从切换、备份恢复这些操作上实现高度自动化。虽然学习Operator本身有成本,但一旦掌握了"声明期望状态 + 控制器调谐"这个模型,你会发现K8s的大部分运维工作都可以抽象成这套逻辑,这也是为什么面试题里经常出现Operator相关的案例——因为它真正反映了K8s的设计哲学。
6. 常见问题与避坑实录
6.1 初始化失败的排查思路
kubeadm init报错的场景我见过太多次了,新手最容易在初始化失败后反复reset再试,浪费大量时间。其实把排查思路理顺,大多数问题能在几分钟内定位。
第一类问题是镜像拉取失败。确认方法是执行kubeadm init --config=kubeadm-config.yaml -v 5增加日志级别,或者在报错信息中看到failed to pull image或http: server gave HTTP response to HTTPS client。解决办法优先配置containerd镜像加速,然后再试。不要用docker拉镜像再通过ctr导入,这个办法虽然有些人推荐,但导入的镜像tag容易对不上kubeadm的期望tag,麻烦得很。
第二类问题是kubelet启动失败。确认方法是看kubelet服务状态和日志:
journalctl -u kubelet -fkubelet启动失败的常见原因是SystemdCgroup配置不一致,报错信息里会出现failed to run Kubelet: failed to validate kubelet flags或cgroup driver相关的提示。这个问题的根因前面说过:containerd和kubelet必须使用同一套cgroup驱动。解决方法是修改containerd配置后重启containerd,如果kubelet尝试启动过且产生了旧配置,执行kubeadm reset后重新init。
第三类问题是apiserver证书配置错误。报错信息会出现certificate signed by unknown authority或failed to parse...apiserver.crt。遇到这类问题,检查kubeadm-config.yaml中的controlPlaneEndpoint和advertiseAddress是否填对,特别是IP地址和hosts文件中的映射是否一致。
6.2 镜像拉不下来的处理办法
K8s集群跑起来后,业务pod镜像拉不下来是高频问题。这个问题的难点在于:不同的镜像源、不同的网络环境,表现不一样,处理方式也不一样。
对于docker.io官方镜像,前面配置镜像加速就能解决。对于quay.io这类国外源,镜像加速不覆盖,可以在配置中继续添加多个mirror,或者通过本地私有镜像仓库中转。对于私有镜像仓库,K8s需要提前在节点上配置好仓库访问凭证:
kubectl create secret docker-registry registry-key \ --docker-server=<registry-url> \ --docker-username=<username> \ --docker-password=<password>然后在Deployment中引用imagePullSecrets:
spec: imagePullSecrets: - name: registry-key还有一个隐藏坑:有时候镜像明明存在,但拉取还是失败,报manifest unknown。这种情况往往是因为镜像tag不规范,比如tag带了大写字母或者特殊字符。K8s对镜像tag的命名规范限制比docker更严格,统一小写加数字的tag能避免这个问题。
6.3 节点NotReady问题
节点状态为NotReady是非常常见的集群异常,引起的原因也五花八门,我按出现频率整理成表:
| 现象 | 原因 | 排查方法 |
|---|---|---|
| kubelet不停重启 | cgroup driver不一致 | 检查containerd的SystemdCgroup |
| 节点显示NotReady但pod正常 | 网络插件(Calico)故障 | 检查calico-node pod状态和日志 |
| 系统资源不足 | 磁盘满、内存耗尽 | df -h、free -h检查,清理日志 |
| 证书过期 | 节点加入时间过长 | kubeadm certs check-expiration查看证书状态 |
磁盘满这个坑尤其隐蔽。K8s节点长时间运行后,/var/log下的容器日志、/var/lib/docker或/var/lib/containerd的镜像层文件都可能占满磁盘。特别是默认的logrotate配置在没有配置的情况下是不生效的,会导致日志无限增长。解决方法是:在集群中部署一个日志清理的DaemonSet(比如logrotate的容器化版本),或者给每个节点配置journald的日志限额,设置SystemMaxUse参数,防止日志无限积累。
6.4 证书过期与集群升级
证书管理是K8s集群运维的一个隐藏大坑。K8s组件间通信的大量证书默认有效期是1年(kubeadm生成的),到期后集群会突然不可用,而且报错信息往往很隐晦。
检查证书有效期:
kubeadm certs check-expiration输出中会列出每个证书的到期时间。证书到期前,可以手动续期:
kubeadm certs renew all执行后需要重启kubelet和kube-apiserver等组件让新证书生效,一般是重启所有控制面组件的pod(kubectl -n kube-system delete pod -l component=kube-apiserver这种方式)。
集群升级的路径是逐版本升级,不能跨大版本跳。比如1.28升1.29,要先升级kubeadm(kubeadm upgrade apply v1.29.1),再升级kubelet(逐节点kubeadm upgrade node,最后更新kubectl)。这个过程看起来繁琐,但它保证apiserver和kubelet的版本差不至于失控。
这里最大的经验教训是:证书续期和版本升级这种操作,一定要先在测试集群上演练一遍,然后再在生产环境执行。我见过一个生产集群因为证书过期导致全部节点的kubelet无法连接apiserver,只好半夜起来批量操作,那个教训至今印象深刻。
6.5 几个容易忽略的集群全局配置
除了上面这些具体问题,还有几个全局配置项,配置时容易忽略,但影响面极大。
第一个是时间同步。K8s集群所有节点必须保证时间一致,偏差过大会导致:
- etcd集群节点间心跳超时,频繁选主
- 证书校验失败(证书的validity period判断依赖系统时间)
- 日志、事件时间戳混乱,排查问题时无法对齐
所有节点必须配置systemd-timesyncd或chrony进行NTP同步,这个成本极低但收效极高。
第二个是DNS配置。K8s集群内的CoreDNS是服务发现的基础,但很多集群的CoreDNS偶尔会出现解析失败的问题。不只是kubectl exec进入pod中的DNS解析,更关键的是业务pod之间通过Service名称互相访问时,如果CoreDNS故障,整个微服务架构直接瘫痪。建议把CoreDNS设置为多副本(默认是2副本),并且给CoreDNS的pod设置合适的反亲和性,避免所有副本调度到同一台节点上。
第三个是etcd的定期备份。etcd存储了整个集群的所有状态,一旦数据损坏或误删,恢复成本极高。建议配置一个cron任务定期执行etcdctl snapshot save,备份数据保存到独立位置(比如对象存储或另一台机器)。不要等到集群出问题了才想起来备份——没有备份的恢复操作,通常会发展成“重建集群加手动恢复业务”,这个痛苦我希望你永远不用经历。
写到这里,K8s部署从架构设计、环境准备、高可用集群搭建、网络配置、监控扩展到常见故障排查,一条线走完了。我个人的体会是:K8s部署本身不复杂,复杂的是搞清楚每一步背后的原理,以及提前想到那些早晚会遇到的问题。证书、时间同步、磁盘清理、etcd备份,这四件事在集群刚建好时就做好,后续能省掉大量半夜救火的麻烦。框架搭好之后,再往里面加监控、加GPU调度、加Operator,都会顺畅很多。每个坑都亲自踩过一遍之后,下次再搭集群,你也会有自己的那份checklist。