K8s 部署这件事,说难其实也不难,说简单也真不简单。我见过太多人卡在同一个地方:minikube 玩得飞起,一到生产环境就手足无措;也有人照着博客一步步敲命令,集群是起来了,但网络插件一配就崩,Pod 调度不上去,整个周末都在跟 CrashLoopBackOff 搏斗。这篇文章我打算从实际部署的角度出发,把从环境规划、组件选型、安装步骤到验证检查的完整链路梳理一遍,重点讲那些文档里不会写、只有真正动过手才知道的细节。如果你正准备在裸机、虚拟机或者云主机上部署一套可用的 K8s 集群,这篇文章应该能帮你少走不少弯路。
1. 部署 K8s 之前,先想清楚这三件事
很多人一上来就急着装 kubeadm,装完才发现架构选错了、容器运行时没配好、网络插件跟 CNI 冲突,最后推倒重来。这种事情我见过太多次了。所以在动手之前,用半小时把下面三个问题想明白,比盲目执行十条命令有价值得多。
1.1 这套集群是拿来干什么的
部署目标决定了集群的规模和架构。如果只是本地开发、跑几个学习用的 Pod,单节点的 minikube 或者 kind 完全够用,没必要上 kubeadm 折腾三台机器。如果是公司内部测试环境,需要模拟生产的部署方式,那一个三节点的 kubeadm 集群是起步配置。如果是生产环境承载真实业务流量,那要考虑的东西就完全不同了:高可用、持久化存储、日志监控、备份恢复、安全合规,每一项都是一大块工程。
我建议你在部署之前,先明确回答下面几个问题:
- 这套集群的预期规模是多少?50 个 Pod 以内还是上千个 Pod?
- 是否需要高可用?控制平面挂掉能不能接受?
- 应用是有状态还是无状态为主?对持久化存储有什么要求?
- 现有基础设施是裸机、虚拟机还是公有云?网络环境是否允许节点间互通?
- 团队对 K8s 的熟悉程度如何?有没有人能承担后续运维的职责?
这些问题看起来基础,但很多人部署完集群之后才发现满足不了业务需求,结果推倒重来。我一个朋友的公司,第一套集群是用三台 4C8G 的虚拟机搭的,跑得也挺好。后来业务涨了,内存经常打满,Pod 频繁 OOMKilled,扩容的时候才发现当初没有预留足够的管理节点资源,control plane 和 worker 混部,一扩容就雪崩。最后只能花了一个周末重构集群。这个教训非常典型。
1.2 裸机、虚拟机还是云托管,各有各的坑
先说裸机部署。裸机的好处是性能无损耗、资源利用率高,坏处是所有东西都要自己管。操作系统装好之后,内核参数调优、容器运行时安装、网络插件选型、负载均衡方案,每一步都要自己动手。如果你的机器配置不高,或者机房网络比较特殊,还需要考虑 DNS、NTP、YUM 源这些基础服务的可用性。国内裸机部署最大的坑就是镜像拉取超时,这个后面我会专门讲。
虚拟机部署是目前最常见的测试环境方案。VMware 或者 KVM 开三台虚拟机,配置个 2C4G 就能跑起来。但在虚拟机里跑 K8s 有两个天然问题:一是时间同步,虚拟机容易漂移,必须配好 NTP,否则 etcd 会因为时间不一致各种报错;二是磁盘 IO 性能,如果宿主机是机械硬盘,etcd 的性能会很难看。
云托管方案,包括云厂商提供的 K8s 服务,虽然"开箱即用",但一样有需要注意的点。托管集群的管理节点不用操心,但 worker 节点的网络方案、存储插件、节点亲和性调度,这些问题仍然需要自己处理。而且托管服务往往有一些使用限制和配额,盲目使用的话,后期迁移成本会很高。
1.3 版本选型:别用最新的,要用最稳的
K8s 的发布节奏是每年三个大版本,每个版本支持大概 14 个月。选择一个合适的版本,需要在功能、稳定性和社区支持之间做平衡。我的个人建议是:生产环境选择 N-2 或者 N-1 的版本,也就是距离当前最新版本落后一到两个 minor version,等新版本的已知问题修得差不多了再考虑升级。
在选择 K8s 版本的同时,还需要同时确认配套的容器运行时版本、CoreDNS 版本、etcd 版本以及 CNI 插件的兼容性。这些组件之间的版本矩阵如果不匹配,轻则功能异常,重则整个集群无法启动。好在 Kubernetes 官方提供了详细的版本兼容矩阵,部署前花几分钟核对一下很有必要。
另外,尽量选择 CNCF 活跃维护的版本线,避免使用已经 EOL(End of Life)的版本。比如 1.24 之前 Docker 运行时还能直接用 CRI 桥接,但从 1.24 开始 Dockershim 被移除,如果你还停留在特别老的版本,迁移到 containerd 的工程量会非常大。选择一个维护状态良好的版本线,等于给你的集群留了一条后路。
2. 环境准备与组件选型:提前排掉 80% 的坑
这一节我基于常见实践做一次完整的推荐搭配。无论是哪套方案,环境准备的质量直接决定后续部署的顺利程度。
2.1 节点规划与推荐配置
下面这份配置表是基于我多次部署经验的总结,不同负载下你可以适当调整:
| 节点类型 | CPU | 内存 | 磁盘 | 推荐数量 | 建议系统 |
|---|---|---|---|---|---|
| Control Plane 最小 | 2 核 | 4 GB | 50 GB SSD | 1-3 台 | Ubuntu 22.04 / CentOS 7.9+ |
| Control Plane 生产建议 | 4 核 | 8 GB | 100 GB SSD | 3 台 | Ubuntu 22.04 LTS |
| Worker 节点(测试) | 2 核 | 4 GB | 50 GB | 2-3 台 | Ubuntu 22.04 |
| Worker 节点(生产) | 8 核+ | 16 GB+ | 200 GB+ SSD | 3+ 台 | Ubuntu 22.04 LTS |
Control Plane 至少需要一台,但强烈建议 3 台做高可用。etcd 的写入性能对磁盘延迟极其敏感,机械硬盘做 etcd 的数据目录会导致集群响应时间呈数量级恶化,所以 etcd 的磁盘一定选纯 SSD。我的经验是 SSD 和 HDD 在 etcd 写入延迟上的差距能有 10 倍以上,高负载时甚至更多。
Worker 节点的配置要根据实际负载来定。如果主要是跑 Java 类的应用,内存要给足;如果跑的是 AI 推理这类 GPU 负载,还需要另外考虑 GPU 驱动的适配。如果拿不准,建议先从低配开始,后续横向扩容机器即可。
2.2 操作系统与内核参数调优
Ubuntu 22.04 和 CentOS 7.9 是我用得最多的两个系统。如果是新环境,我更推荐 Ubuntu 22.04 LTS。内核版本相对较新,对 cgroup v2、iptables 和 overlayfs 的支持更完善,坑少。CentOS 7.9 目前也已经进入 EOL 状态了,不太推荐新建集群使用。
不管选哪个系统,下面这几个内核参数是部署前应该提前配置的:
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 --system这里逐项解释一下:
net.bridge.bridge-nf-call-iptables = 1和net.bridge.bridge-nf-call-ip6tables = 1,这两个参数确保经过 Linux 网桥的流量会经过 iptables 过滤。K8s 的 Service 和 NetworkPolicy 极度依赖 iptables 规则,如果这个参数没开,ClusterIP 模式的 Service 会直接不通。这是新手部署最爱踩的一个坑。net.ipv4.ip_forward = 1开启 IPv4 转发,这是 Pod 之间和 Pod 访问外部网络的基础。vm.swappiness = 0建议在物理机上设置,减少 swap 的使用。其实 K8s 节点本来就不建议开 swap,kubelet 默认会检测到 swap 并拒绝启动,除非显式设置--fail-swap-on=false。
另外建议关闭 firewalld 或 ufw 这类防火墙服务。K8s 组件之间的通信端口非常多,自行管理防火墙规则很容易遗漏,后续排查起来非常痛苦。如果公司安全要求必须开防火墙,那也要严格按照 K8s 官方文档把端口规则配置好,并且反复核对。
2.3 容器运行时:containerd 几乎是唯一解
在 Docker 还兼容 K8s 的年代,很多人习惯每台节点装一个 Docker。但 K8s 从 1.24 版本开始移除了 Dockershim,官方对 Docker 的支持仅限于通过 CRI 兼容层。现在主流的选择基本只有两个:containerd 和 CRI-O。我的建议是直接使用 containerd。
为什么选 containerd?原因有几点:它是 Docker 公司捐赠给 CNCF 的组件,跟 Docker 共享核心的容器运行时技术;它默认就被 kubeadm 集成,安装之后改一行配置就能用;它比 CRI-O 的社区使用人数多得多,出了问题搜索解决方案更方便。
containerd 安装之后,需要把 CRI 插件启用并把SystemdCgroup设为true。这一步很多人忽略,导致 Pod 运行时出现 cgroup 相关报错,CPU、内存限额完全失效。配置文件一般在/etc/containerd/config.toml,手工修改比较繁琐,推荐用下面这种方式初始化:
containerd config default | sudo tee /etc/containerd/config.toml sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml sudo sed -i 's|registry.k8s.io/pause:3.6|registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.9|g' /etc/containerd/config.toml sudo systemctl restart containerd后面这个 sed 替换的是 pause 镜像地址,这一步在国内环境几乎是必须的,否则kubeadm init拉取 pause 镜像时会被卡住超时。如果你用的是云服务器且配置了可靠的镜像加速,也可以替换成你自己的加速地址,不一定非得是阿里云。
还有一点要提醒:containerd 默认没有docker命令,习惯用docker ps的人一开始会不习惯。你可以装一个crictl作为替代,它提供的crictl ps、crictl logs命令能直接查看容器状态和日志,定位问题非常有用。
2.4 kubeadm、kubelet、kubectl 的安装版本必须一致
kubeadm、kubelet、kubectl 三个组件的版本必须手动指定为完全一致的版本号。用apt install kubeadm kubelet kubectl这种写法默认会装最新版,但生产环境需要锁定版本,否则三个组件之间如果出现小版本差异,会出现一些难以预料的兼容问题。
安装的时候先配置 Kubernetes 的 apt 源或者 yum 源,然后指定版本安装。CentOS/RHEL 系的 yum 源和 Ubuntu 的 apt 源不一样,网上教程很多,照着配即可。需要注意的是,不同 K8s 版本对应的源地址略有不同,最好以官方文档的源为准。
安装完成后,建议用apt-mark hold kubeadm kubelet kubectl锁定这三个组件的版本,避免后续误apt upgrade导致三个组件版本不一致,那是非常经典的翻车事故。
2.5 网络插件选型:Calico 还是 Flannel
CNI 插件是集群里另一个重要组件,负责为每个 Pod 分配 IP 并打通节点之间的网络。主流的开源方案就两个:Flannel 和 Calico。
Flannel 的特点是简单、轻量,使用 VXLAN 或者 host-gw 模式做 Overlay 网络,性能还算可以,配置非常简单,适合小型测试集群。但如果你的集群需要 NetworkPolicy 网络策略、更细粒度的网络控制或者更高的跨节点吞吐量,Flannel 就不够用了。
Calico 基于 BGP 路由协议,网络转发路径更短,性能更好,同时原生支持 NetworkPolicy。在裸机部署场景,Calico 的host-gw或BGP模式基本能做到物理网络级别的性能。代价是配置复杂度高一些,对内核模块、内核参数有一些要求。
我自己的选择标准是这样的:生产环境一律 Calico,测试环境如果没有特殊需求也可以用 Flannel。原因很简单,如果在测试环境用了 Flannel,生产环境规划 Calico,那两套环境的网络行为就不一致,测试结果参考价值会打折扣。
Calico 部署时,如果是裸机环境且节点之间路由可以互通,可以用下面这个命令直接安装:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/master/manifests/calico.yaml但如果网络环境不允许 BGP 协议互通,或者不想让 Calico 直接改节点路由表,可以使用 VXLAN 封装的 IPIP 模式或者直接修改 Calico 的环境变量,设置为 VXLAN 模式。这个细节部署时很容易漏,先想清楚你的网络环境,再决定用哪种模式。
3. 从零开始:用 kubeadm 部署一套三节点集群
现在进入最核心的内容。这里的步骤可以在任意节点上执行,但kubeadm init需要在第一台控制平面节点上执行,kubeadm join在其余节点上执行。下文以 Ubuntu 22.04 + containerd 为例。
3.1 hosts 解析与免密登录
为了让集群中所有节点之间可以互相通信,先在每台节点的/etc/hosts里加上所有节点的解析。尤其要避免节点名带下划线或者特殊字符,K8s 对节点名称的约束比较严格。
cat <<EOF | sudo tee -a /etc/hosts 192.168.1.11 k8s-master01 192.168.1.12 k8s-node01 192.168.1.13 k8s-node02 EOF接着配置 SSH 免密登录,方便主节点操作所有节点。这个步骤不是强制的,但能提高后续运维便利性。
ssh-keygen -t rsa -b 4096 ssh-copy-id root@k8s-node01 ssh-copy-id root@k8s-node023.2 所有节点统一初始化
在每台节点上依次执行下面的内容。这里我把前面提到的配置一并汇总:
# 1. 设置主机名(每台节点不同) sudo hostnamectl set-hostname k8s-master01 # 或者 k8s-node01 / k8s-node02 # 2. 加载内核模块 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # 3. 配置内核参数 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 --system # 4. 关闭 swap(K8s 1.24+ 默认要求) sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstab再说下为什么不建议开着 swap 跑集群。K8s 的资源模型基于 requests 和 limits 做调度决策,如果节点上开着 swap,kubelet 对内存的判断就会失真,Pod 的内存配额也就失去了实际约束力。不过如果你确实需要 swap 来应对突发内存峰值,从 1.22 版本开始 K8s 支持 NodeSwap 特性,但那是后话,生产环境我仍然建议关闭。
3.3 安装 containerd、kubeadm、kubelet、kubectl
以下是 Ubuntu 22.04 的安装方式,CentOS 类似,只是包管理器不同:
# 安装 containerd sudo apt-get update && sudo apt-get install -y containerd containerd config default | sudo tee /etc/containerd/config.toml sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml sudo sed -i 's|registry.k8s.io/pause:3.6|registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.9|g' /etc/containerd/config.toml sudo systemctl restart containerd sudo systemctl enable containerd # 安装 kubeadm、kubelet、kubectl(这里以 1.28.2 为例) sudo apt-get update sudo apt-get install -y apt-transport-https curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/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.28/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet=1.28.2-* kubeadm=1.28.2-* kubectl=1.28.2-* sudo apt-mark hold kubelet kubeadm kubectl这里三个组件的版本尽量跟官方仓库的可用版本保持一致。如果你使用国内源,把pkgs.k8s.io替换成对应的镜像源地址即可。
3.4 初始化控制平面节点
所有前置工作做完之后,在控制平面节点上执行kubeadm init。这里需要根据你的网络环境选择合适的参数:
sudo kubeadm init \ --apiserver-advertise-address=192.168.1.11 \ --control-plane-endpoint=192.168.1.11 \ --kubernetes-version=v1.28.2 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=10.96.0.0/12 \ --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers几个参数的含义我简单解释一下:
--apiserver-advertise-address:API Server 对外广播的 IP 地址,一般就是本机 IP。--control-plane-endpoint:高可用场景下的负载均衡 VIP 或域名。单控制平面时可以跟 advertise-address 一致。--pod-network-cidr:Pod 网段。这个网段必须和后面安装的网络插件保持一致。Calico 默认用192.168.0.0/16,Flannel 默认用10.244.0.0/16,选哪个取决于你用哪个 CNI。--image-repository:镜像仓库地址。用默认的registry.k8s.io在国内几乎必失败,替换成国内镜像仓库能省掉无数麻烦。
初始化成功后,会输出一段kubeadm join的命令,这个是加入集群用的,一定要保存好。如果没保存,也可以之后在控制平面上执行kubeadm token create --print-join-command重新生成。
初始化完成后,当前用户的 kubectl 配置还没有生效。按提示执行下面的配置:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config3.5 安装网络插件(以 Calico 为例)
CNI 插件必须在集群初始化成功后立刻安装,否则节点状态会一直停留在NotReady。Calico 的安装方式如下:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/tigera-operator.yaml kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/custom-resources.yaml安装之前必须确认custom-resources.yaml里的cidr字段。默认是192.168.0.0/16。如果你在kubeadm init时的--pod-network-cidr写的不是这个网段,需要先下载 custom-resources.yaml,把cidr改成和--pod-network-cidr一致,然后再kubectl create。否则 Pod 网段和 Calico 的期望网段不一致,网络直接不通。
安装完成后,可以观察一下 Pod 状态:
kubectl get pods -n calico-system -w等所有 calico 相关 Pod 变成 Running 之后,再执行kubectl get nodes确认节点状态是否从NotReady变为Ready。
3.6 加入 Worker 节点
在剩下的 worker 节点上执行之前保存的kubeadm join命令。如果 token 过期了,在控制平面节点重新生成即可:
kubeadm token create --print-join-command把输出复制到 worker 节点执行,大约几分钟后,在控制平面节点上执行:
kubectl get nodes如果所有节点都处于Ready状态,恭喜你,一套基础集群已经搭建完成了。如果某个节点仍然NotReady,八成是网络插件的问题,建议按后边的排查思路找原因。
4. 集群验证与常用排查手段
部署完成不等于一切正常。一个集群能不能用于承载应用,需要做一系列验证。这也是我通常建议"部署完别急着上线,先跑一轮体检"的原因。
4.1 验证 CoreDNS、Service 和 Pod 网络
部署完成后,第一件事是验证集群内部 DNS 解析和 Service 转发是否正常。
# 查看所有命名空间的 Pod 状态 kubectl get pods -A # 查看节点状态和污点 kubectl get nodes -o wide kubectl describe node k8s-node01 | grep -A5 Taints # 启动一个测试 Pod,验证 DNS 解析 kubectl run test-pod --image=busybox -- sleep 3600 kubectl exec -it test-pod -- nslookup kubernetes.default.svc.cluster.local如果nslookup能返回正常的 IP,说明 CoreDNS 和 kube-dns Service 工作正常。接着验证跨节点的 Pod 互访:在 node01 上跑一个 Pod,在 node02 上用另一个 Pod ping 它的 IP。如果能通,网络插件基本没问题。
还有一个非常关键的验证点:Service 的 ClusterIP 访问。创建一个简单的 Deployment 和 Service:
kubectl create deployment nginx --image=nginx kubectl expose deployment nginx --port=80 --type=ClusterIP kubectl get svc curl http://<ClusterIP>如果能返回 nginx 的默认页面,说明 Service 的 iptables/IPVS 转发链路是通的。很多集群问题是在这一步才暴露出来,尤其是网络插件模式与内核模块不匹配的情况。
4.2 节点 NotReady 的排查链路
节点 NotReady 是部署期最常见的故障,没有之一。遇到它不要慌,按下面的排查链路一步步来:
- 查看节点状态和条件:
kubectl describe node <node-name>,看 Conditions 下面的Ready状态是什么,一般会有关键错误信息。 - 查看 kubelet 状态:登录到问题节点上,执行
systemctl status kubelet和journalctl -u kubelet -f。kubelet 的日志是定位问题的金钥匙。 - 查看容器运行时状态:
crictl ps和crictl pods,检查 containerd 是否能正常管理容器。 - 检查 CNI 是否部署完成:
kubectl get pods -n kube-system | grep calico或者对应网络插件的命名空间,确认 Pod 是否 Running。 - 检查网络连通性:节点之间的 6443 端口(API Server 端口)是否互通,Pod 网段的路由是否正确。
我见过最多的情况是:kubelet 正常,containerd 正常,但 calico-node 的 Pod 一直 CrashLoopBackOff。最常见的原因是内核模块缺失,比如 Calico 需要nf_conntrack或ipset模块,如果宿主机没开启,就会一直初始化失败。这种问题处理方式很直接:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack如果你用的是 Ubuntu 22.04,IPVS 模块默认不会加载,建议提前在/etc/modules-load.d/k8s.conf里加上这些模块。
4.3 镜像拉取失败的解决思路
前面提到国内环境镜像拉取是最常见的问题之一。如果你的集群部署顺利,那可能是你已经用了我建议的国内镜像源。但即便配了国内源,也可能遇到一些特殊的镜像仓库地址。
比如,部署应用时拉取的是 Docker Hub 的镜像,这时候可以给 containerd 配置镜像加速:
sudo tee /etc/containerd/config.toml <<EOF version = 2 [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://docker.m.daocloud.io"] EOF sudo systemctl restart containerd不同的镜像加速服务在不同地区的表现不一样,建议多试几个。不要一次配好就不管了,加速服务挂了的情况也时有发生。如果 Pod 卡在ImagePullBackOff,先执行:
kubectl describe pod <pod-name> | grep -A5 Events事件里会明确写着拉取失败的原因,是超时、认证失败还是镜像不存在。对症下药比瞎猜高效得多。
4.4 kubeadm 重置集群的正确姿势
调试过程中搞坏了集群,需要一条干净的重置路径。kubeadm reset是官方提供的重置命令,会在执行后自动清理已安装的 K8s 相关服务,但不会清理 CNI 插件的配置和数据。
重置的正确顺序是:先kubectl drain节点,然后删除节点,再执行kubeadm reset,最后清理网络插件的残留。不然 kube-proxy 和 calico 的残留规则可能会影响新集群。
kubectl drain <node-name> --delete-emptydir-data --force --ignore-daemonsets sudo kubeadm reset -f sudo rm -rf /etc/cni/net.d /var/lib/calico sudo rm -rf /var/lib/kubelet /etc/kubernetes sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F重置完的节点基本就是一台干净的机器,可以重新初始化或者重新加入新集群。记住顺序不能反,先把工作负载驱逐迁移走,再清理容器和配置,否则可能影响其他节点上的应用。
5. 生产环境部署前的进阶规划
到这里,一套三节点集群已经能跑了,日常开发测试完全够用。但如果你打算让这套集群上生产,下面几个点还需要认真规划。
5.1 高可用控制平面:3 个 Master 是最基本的
单控制平面节点的集群,只要控制节点宕机或者重启,整个集群的调度能力就没了。生产环境的做法是部署至少 3 个控制平面节点,前面加一个负载均衡器(比如 HAProxy + Keepalived 或云厂商的负载均衡),把 API Server 的 6443 端口流量负载分发到所有控制平面节点上。
用 kubeadm 搭建高可用控制平面的过程比单节点稍微复杂一点:先初始化第一个控制平面节点,然后依次把其余控制平面节点加入集群,加入时同样执行kubeadm join,只是要加上--control-plane参数。负载均衡的地址要提前规划好,比如192.168.1.10:6443作为--control-plane-endpoint。
etcd 默认会以静态 Pod 的方式运行在控制平面节点上,3 个控制平面节点组成一个高可用的 etcd 集群。如果你已经有外部部署的 etcd 集群,也可以让 K8s 使用外置 etcd,但维护成本会更高,一般中小团队不建议这么干。
5.2 存储方案:本地存储还是集中存储
有状态应用必然要用到持久化存储。K8s 的存储体系比较繁琐,从 PV、PVC、StorageClass 到 CSI 插件,一层一层堆叠,理解成本不低。如果只是测试环境,可以用 hostPath 或 local 类型的 PV,但这两种方式在生产环境都有很大的局限性:数据绑死在节点上,Pod 一旦被调度到其他节点,数据就丢了。
生产环境建议直接上 CSI 存储方案。裸机环境可以用 Rook-Ceph,云上环境可以用云厂商提供的云盘 CSI。Rook-Ceph 功能强大,但运维成本不低,而且对硬件资源有一定要求。如果你的集群只是跑几十个 Pod,又不想投入太多人力运维存储,先想想是否有必要引入 Ceph,过度设计同样是个问题。
5.3 监控与日志:别等问题发生了再想办法
集群部署完之后,第一件事应该就是把监控和日志体系搭起来。Prometheus + Grafana 是目前监控事实上的标准方案。Kube-prometheus-stack 这个 Helm chart 可以一次性把 Prometheus、Grafana、Alertmanager 和各类 exporter 都装好,非常方便。默认开机自带的 CPU、内存、磁盘、网络监控规则已经很完善,建议部署之后立刻用起来。
日志这块,如果集群规模不大,Loki + Promtail 是轻量级方案;如果要求比较高,ELK 或者 EFK 更成熟。日志系统的核心指标是检索速度和存储成本,选型之前先预估一下每天的日志量,再决定用什么方案,避免装完之后日志量上来直接把磁盘撑爆。
5.4 弹性伸缩与资源配额
生产集群一定要规划好资源配额。通过 ResourceQuota 和 LimitRange 可以限制每个命名空间的资源上限,避免某个团队创建大量 Pod 把集群资源耗尽,直接影响其他业务的稳定性。HPA(Horizontal Pod Autoscaler)可以基于 CPU、内存或者自定义指标自动扩缩容,对于流量波动明显的业务很有用。但如果集群本身资源有限,HPA 扩到一定程度就会触发节点资源不足,这时候需要配合 Cluster Autoscaler 或者云厂商的节点组自动扩容。裸机环境下,节点自动伸缩比较难做,一般就是在资源快耗尽时人工加机器。
5.5 安全加固:看似繁琐但必须做
安全这个话题很大,我只挑几个最切身、最容易出问题的地方讲。
第一个是 RBAC 权限控制。默认安装的集群只有一个kubernetes-admin用户,权限超级大。建议为不同角色创建独立的 ServiceAccount 和 RBAC 绑定,比如开发人员只给某个命名空间的只读权限,运维人员才有集群级别的管理权限。这样即使某个人误操作,影响范围也被限制在特定命名空间。
第二个是 Secret 的加密存储。开启 etcd 加密是必须的,K8s 从 1.28 版本开始对EncryptionConfiguration的强制要求已经放到了 Stage 1,也就是还没有强制响应,但建议直接开启。通过EncryptionConfiguration配置 AES 加密,可以在 etcd 层面保护 Secret 数据。不然的话,任何能直接读取 etcd 数据的人都能拿到集群里的所有密码和 Token。
第三个是镜像和供应链安全。配置私有镜像仓库并且启用镜像扫描,避免把含有漏洞或恶意代码的镜像直接部署到集群里。生产环境的镜像应该只从私有仓库拉取,不要直接用 Docker Hub 的公共镜像。
6. 网络插件之外的扩展网络能力
基础集群部署好之后,如果你需要更复杂的网络能力,比如多网卡、VLAN 隔离或者多集群互联,可以关注 Multus 和 MetaPlugs 这类 CNI 插件组合。这里特别提一下热词中频繁出现的k8s multus 网络 vlan 配置,说明很多人已经开始接触到多网络平面了。
Multus 本身不是一个真正意义上的网络插件,它是一个"多网络插件管理器":它允许你在一个 Pod 上挂多个网络接口,每个接口由不同的 CNI 插件提供。比如,一个 Pod 既可以通过主 CNI(Calico)加入集群网络,又可以通过一个 VLAN 类型的 CNI(比如 macvlan 或者 vlan 插件)直连物理局域网。
配置 Multus 之后,每个额外的网络都以 NetworkAttachmentDefinition 对象来声明,比如创建一个 VLAN 100 的接入网络:
apiVersion: k8s.cni.cncf.io/v1 kind: NetworkAttachmentDefinition metadata: name: vlan-100 spec: config: | { "cniVersion": "0.3.1", "type": "vlan", "master": "eth1", "vlanId": 100, "ipam": { "type": "host-local", "subnet": "192.168.100.0/24" } }在 Pod 的注解里引用这个网络:
annotations: k8s.v1.cni.cncf.io/networks: vlan-100这样 Pod 启动后就会有两个网络接口:eth0 走集群网络,net1 走 VLAN 100 的物理网络。这个方案在裸机数据中心环境里非常实用,可以无缝对接已有的物理网络规划。
但要注意,VLAN 的 CNI 插件通常需要节点上的物理网卡支持 VLAN 标签模式,如果网卡不支持或者驱动有问题,Pod 的网络接口会起不来。而且这类直通网络的 IP 管理往往比较粗糙,如果对 IP 分配策略要求高,建议用whereabouts这一类的 IPAM 插件来补充管理。
7. 在 K8s 上部署常见业务:LNMP 与 AI 模型推理
有了集群基础,接下来就是在上面跑真实业务了。下面两个场景是从实际项目需求中提炼出来的典型:LNMP 架构和 AI 大模型推理服务,正好也贴近热词中高频出现的内容。
7.1 LNMP 架构在 K8s 中的落地要点
LNMP 是 Linux + Nginx + MySQL + PHP 的经典组合,很多跑 PHP 业务的团队都有相关的迁升需求。在 K8s 上部署 LNMP 跟传统单机部署很不一样,核心变化是拆分和编排。
首先是 MySQL。作为有状态服务,MySQL 在 K8s 里通常用 StatefulSet 定义,并且要给它一块持久化存储,然后通过一个 headless Service 暴露给集群内部访问。如果你要部署 MySQL 主从,那就用 StatefulSet 加上一个初始化容器来自动配置主从复制关系,或者直接用云厂商的托管数据库。
然后是 PHP-FPM 和 Nginx。PHP-FPM 以 Deployment 方式运行,Nginx 作为反代层放在 PHP 前面,两个服务之间通过 Service 通信。这里最容易踩的一个坑是:传统 LNMP 架构里 Nginx 直接配置fastcgi_pass 127.0.0.1:9000,迁到 K8s 之后,PHP-FPM 不在同一台机器上了,需要把 fastcgi_pass 改成 PHP-FPM 的 Service 地址,比如php-fpm:9000。另外,Nginx 配置文件需要挂载进去,可以通过 ConfigMap 管理,如果修改了配置要记得重新加载或滚动更新。
还有一个细节是会话保持。传统单体 PHP 应用一般把 Session 存在本机文件系统或者 MySQL 里,但在 K8s 里,PHP Pod 可能会被调度到任何节点,重建之后本机文件系统丢失,Session 自然也就丢了。所以线上环境务必将 Session 存到 Redis 或者 MySQL 这类外部存储里,不能用本地文件。
7.2 大模型推理服务的容器化部署思路
热词里大量出现的 Ollama、vLLM、DeepSeek 本地部署、Dify 大模型工作流平台,本质上都是在 K8s 上跑 GPU 负载。这跟普通无状态应用有非常大的不同,需要额外处理 GPU 资源适配。
第一件事是确认节点上有可用的 GPU 驱动和运行时。NVIDIA 的 GPU 操作符(GPU Operator)可以自动配置节点上的驱动、Container Toolkit 和 DCGM 监控,装好之后 K8s 节点就能上报 GPU 资源数量。之后在部署推理服务时,在 Pod 声明里写:
resources: limits: nvidia.com/gpu: 1调度器自然会把这个 Pod 调度到具有 GPU 的节点上。如果没有提前安装 GPU 相关组件,不管你怎么声明nvidia.com/gpu,集群都不会识别这个资源,Pod 只会一直 Pending。
第二件事是模型文件的存储。大模型的权重文件动辄几十 GB,如果每次扩容都重新下载一次模型,一来速度慢,二来浪费带宽。更好的方式是把模型放在共享存储里,比如 PVC 或者对象存储挂载,Pod 启动时只加载需要的部分。vLLM 这类推理框架也支持从 Hugging Face 或 ModelScope 直接加载,但生产环境的稳定做法还是先把模型缓存到本地或共享存储。
第三件事是推理服务的扩缩容。LLM 推理是典型的长尾负载,高峰期和低谷期并发差距很大。用 HPA 做弹性伸缩是可行的,但要注意的是推理服务一般需要加载模型后才算 Ready,启动时间往往很长。这时候需要为启动过程设置合理的startupProbe,否则探针可能在模型加载完成之前就判定容器不健康,把正在启动的 Pod 杀掉了。
最后提醒一下,在 K8s 上部署大模型推理,尽量不要直接跑裸的 Ollama 或 vLLM 原始容器,最好用 Helm Chart 或者自建的 Operator 来管理,同时接入 Grafana 看板和日志排查。系统性地做,比零散地起容器更靠谱。
8. 排障实录:从集群部署到应用上线的完整排查日志
按照经验分享的原则,我用一个虚构但极典型的案例,把从集群部署完成到应用真正跑起来之间可能遇到的问题串起来讲一遍。这个过程能帮你理解上面那些配置选项到底是为了什么而存在。
假设我按照第 3 节的操作搭好了一套三节点集群,节点全部 Ready,Calico 运行正常。然后我创建了一个 Deployment:
kubectl create deployment demo --image=nginx:1.25 kubectl scale deployment demo --replicas=3 kubectl get pods结果出现了一个 Pod 状态变成Pending。第一反应自然是:
kubectl describe pod demo-xxxx-yyyy | tail -30Events 里提示0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had untolerated taint。
这里其实非常典型。Insufficient cpu说明某个节点的 CPU 资源已经不足以满足这个 Pod 的 requests 了。untolerated taint则是因为控制平面节点默认带着node-role.kubernetes.io/control-plane:NoSchedule的污点,业务 Pod 默认不会调度到控制平面上。
解决思路有两条:要么扩充 worker 节点的资源,要么给控制平面节点去掉污点。对于测试环境,可以去掉控制平面节点的污点来增加 Pod 容量:
kubectl taint nodes k8s-master01 node-role.kubernetes.io/control-plane:NoSchedule-但生产环境不建议这么做。控制平面节点最好保持纯粹,只为系统组件服务。正确的做法是给集群添加更多 worker 节点,或者把 Pod 的资源 requests 调小。继续往下,如果 Pod 已经从 Pending 变成了 ContainerCreating,但一直起不来,参照事件调日志排查:
kubectl describe pod <pod-name> | grep -A20 Events journalctl -u kubelet -f crictl ps -a这里面最常遇到的错误是failed to pull image、failed to create containerd task、或者是sandbox image拉不下来。逐一对应排查即可。我再次强调一下,containerd 的日志在/var/log/containerd/containerd.log,出问题先去看日志,比自己瞎猜高效得多。
再往下,如果 Pod 已经是 Running 了,但你发现应用无法访问,比如访问 NodePort 的 IP 加端口不通,那就要查 Service 和 kube-proxy 了。
kubectl get svc kubectl get endpoints demo kubectl logs -n kube-system kube-proxy-xxxx如果 endpoints 有值,说明后端 Pod 是正常的;如果 kube-proxy 日志里报 iptables 规则同步失败,大概率是节点上的 iptables 和内核版本不匹配。这个坑在 Ubuntu 22.04 上很少见,但 CentOS 7 老内核上经常遇到,解决方式往往是升级内核或者切换 kube-proxy 模式为 IPVS 并加载对应内核模块。
排障的通用路径永远是:观察现象 -> 收集信息 -> 缩小范围 -> 定位根因 -> 修复验证,不要跳过中间的任何一步。很多时候问题并不是出在你第一眼怀疑的地方,比如 Pod 起不来,不一定是镜像问题,可能是存储卷挂载、探针配置或者资源限制导致的。日志里其实写得很明白,只是不少新手没有习惯先看 logs 再下结论。
9. 我的一点部署体会
这些年大大小小的 K8s 集群部署了接近两位数,踩过的坑装了满满一箩筐。如果让我把最重要的经验提炼成几句话,大概是这样的:
第一,环境准备阶段花的每一分钟都是值得的。内核参数没配好、镜像源没换、containerd 的 cgroup 驱动没改,这些问题到了后面都会以各种奇怪的方式暴露出来,排查成本比配置成本高得多。
第二,版本锁定是一条铁律。kubeadm、kubelet、kubectl、containerd、CNI 插件,每一个的版本都要明确锁定,绝不能靠系统默认的最新版。升级的时候小版本之间要逐档走,跨大版本升级之前先翻官方的升级文档,别自己创造迁移方案。我见过一次从 1.26 直接跳到 1.29 的升级,最后 API 兼容层崩了,业务 Pod 全挂,只能回滚到备份恢复,场面一度很混乱。
第三,集群部署完成只是一个起点。监控、日志、备份、安全的优先级应该排在业务上线之前。这个道理很多人都认同,但真正能做到的团队不多。不要把生产环境当成测试环境来运营,等出了问题才想起来补课,那时候的成本往往已经是正常成本的十倍都不止了。
第四,中文环境和英文环境下遇到的坑不太一样。国内部署最大的问题是镜像拉取和文档过期,建议遇到问题先看官方文档的 release notes 和变更记录,再参考社区帖子。前后顺序搞反了的话,很容易被过时的方案带偏。
最后也是一点个人习惯:不管部署哪个系统组件,我都会把相关命令和配置文件复制一份放到自己的笔记里,标注上版本和日期,下次部署直接照着整理好的版本执行,事半功倍。随着时间的推移,这套自己的部署纪要会变得越来越有价值。
希望这篇围绕 K8s 部署的明细能对你有所帮助。过程中如果有哪个环节你觉得自己踩过更深或者更有价值的坑,欢迎在交流时多聊聊实际案例,很多经验确实是用时间堆出来的。