☰
虚拟化环境手动部署Kubernetes v1.23.1集群实战指南
2026/10/22 14:43:30 网站建设 项目流程

1. 为什么在虚拟化环境里手动搭一套 Kubernetes 集群仍然值得做

如果你已经用惯了云厂商托管的容器服务,可能会觉得手动部署 Kubernetes 是一件费力不讨好的事。但我在实际工作中发现,真正能把集群运维做扎实的人,几乎都经历过从裸机或虚拟机上一砖一瓦搭建集群的过程。原因很简单:托管服务把控制平面藏起来了,你只能看到 API Server 暴露出来的那一层,一旦遇到网络插件异常、证书过期、etcd 性能抖动这类问题,没有亲手搭过集群的人往往连排查的入口都找不到。

这次要聊的就是在本地虚拟化环境里,用 kubeadm 部署一套 Kubernetes v1.23.1 集群的完整过程。选这个版本不是随便挑的,v1.23 是 Kubernetes 生命周期里一个比较特殊的节点,它正式移除了对 Docker 作为容器运行时的直接支持,默认走 containerd,同时 v1.23.x 系列在 kubeadm 的配置 API 上也做了不少调整。换句话说,网上大量基于 v1.20 甚至更早版本的教程,直接照抄到 v1.23 上会踩一堆坑,这也是我写这篇东西的核心动机。

这套方案适合谁?如果你是想系统学习 Kubernetes 架构的运维或后端开发,或者需要在本地复现一套接近生产拓扑的测试环境,再或者你所在团队要自建机房里的容器平台,那这套流程基本可以直接参考。整套集群我用的是一台控制平面节点加两台工作节点,操作系统选的是 Ubuntu 20.04,虚拟化平台用的是常见的桌面级虚拟机软件。资源分配上,控制平面给 2 核 4G,工作节点各 2 核 2G,这个配置跑一个学习或测试集群足够了,但如果要压测或者跑重负载,内存至少翻倍。

需要提前说明的是,v1.23.1 这个版本对容器运行时的要求比较明确,containerd 是首选,而且 kubelet 和 containerd 之间的接口走的是 CRI,不再依赖 dockershim。这个变化直接影响了后面安装步骤里的很多细节,比如你不能再通过docker ps去看容器状态,得用crictl。这些差异我会在对应环节里逐个点出来。

2. 部署前的整体设计与关键选型拆解

2.1 集群拓扑与资源规划思路

在动手之前,先把拓扑定下来。我采用的是一主两从的经典结构,控制平面节点承担 API Server、Controller Manager、Scheduler 和 etcd 的全部职责,两个工作节点只跑业务负载。这种结构在测试环境里最省资源,同时又能完整演示调度、服务发现、滚动更新这些核心能力。

资源分配上有个经验值可以参考:控制平面的内存不能低于 2G,否则 etcd 在写入频繁时容易 OOM;工作节点的内存取决于你要跑什么,如果只是跑几个 Nginx 和测试用 Pod,2G 够用,但一旦涉及 Java 应用或者带 sidecar 的服务网格,4G 起步。CPU 方面,每个节点至少 2 核,因为 kubelet 和容器运行时本身就要占掉一部分。

网络规划是另一个容易被忽视的点。我用的 Pod 网段是10.244.0.0/16,Service 网段是10.96.0.0/12,这两个网段不能和宿主机所在网段重叠,否则会出现路由冲突。虚拟机的网络模式我选的是 NAT,三台机器在同一个虚拟网段内互通,同时能访问外网拉取镜像。如果你用的是桥接模式,记得给每台机器分配独立的 IP,并且确保它们之间没有防火墙拦截。

提示:虚拟机克隆出来的节点,网卡 MAC 地址和 machine-id 会重复,必须手动改掉,否则 kubelet 注册节点时会出问题。这个坑我在第一次搭的时候踩得很深,后面会详细说怎么处理。

2.2 为什么选 containerd 而不是 Docker

v1.23 这个版本最显著的变化就是弃用了 dockershim。在 v1.23 之前,kubelet 内置了一个 dockershim 组件,用来把 Docker 的 API 翻译成 CRI 接口。从 v1.23 开始,这个组件被正式移除,意味着 kubelet 不能再直接跟 Docker 对话了。

那为什么是 containerd 而不是 CRI-O?从我的实际使用来看,containerd 的生态成熟度更高,配置相对简单,而且它本身就是 Docker 的底层运行时,镜像格式完全兼容。CRI-O 更轻量,但配置起来对新手不太友好,文档也相对少一些。对于学习和测试场景,containerd 是更稳妥的选择。

这里有个关键点要理解:containerd 和 Docker 用的是同一套镜像,但命令行工具不一样。以前你用docker ps看容器,现在要用crictl ps;以前用docker images,现在用crictl images。crictl是 CRI 的通用客户端,它直接跟容器运行时对话,不经过 Docker 那一层。刚开始会不习惯,但用久了你会发现它其实更直接。

2.3 kubeadm 的定位与它能做什么、不能做什么

kubeadm 是官方提供的集群引导工具,它的职责很明确:帮你把控制平面的核心组件跑起来,把节点加入集群,把证书生成好。但它不负责网络插件、不负责存储、不负责监控,这些都要你自己装。很多人误以为 kubeadm 能一键搞定所有事,结果装完发现 Pod 之间不通,就是因为没装 CNI 插件。

kubeadm 的工作流程大致分两步:kubeadm init在控制平面节点上初始化集群,生成证书、kubeconfig 文件,启动静态 Pod 形式的控制平面组件;kubeadm join在工作节点上执行,通过 bootstrap token 向控制平面注册自己。整个过程是幂等的,如果中途失败,可以用kubeadm reset清理后重来。

v1.23 的 kubeadm 在配置 API 上用的是kubeadm.k8s.io/v1beta3,这个版本和之前的 v1beta2 有一些字段差异,比如ClusterConfiguration里的controlPlaneEndpoint和networking字段的写法有微调。如果你从旧教程复制配置文件,一定要检查 API 版本号,否则 kubeadm 会直接报错拒绝解析。

3. 核心细节解析与实操前的系统准备

3.1 操作系统层面的基础配置

三台虚拟机装好 Ubuntu 20.04 之后,第一件事是更新系统并安装基础工具。我习惯先执行apt update && apt upgrade -y,然后装上curl、apt-transport-https、ca-certificates、gnupg、lsb-release这几个包。这些工具在后面添加软件源、下载二进制文件时都会用到。

接下来是关闭交换分区。Kubernetes 的设计假设是节点有足够的内存,它不希望 kubelet 把内存页换到磁盘上,因为那会严重影响调度和性能。临时关闭用swapoff -a,永久关闭要编辑/etc/fstab,把 swap 那一行注释掉。改完之后用free -h确认 swap 那一行全是 0。

内核参数也需要调整。主要是开启网桥的 iptables 转发,以及确保 IPv4 转发是打开的。具体操作是创建/etc/modules-load.d/k8s.conf文件,写入br_netfilter和overlay两个模块名,然后用modprobe加载。接着在/etc/sysctl.d/k8s.conf里写入net.bridge.bridge-nf-call-iptables=1、net.bridge.bridge-nf-call-ip6tables=1、net.ipv4.ip_forward=1,最后执行sysctl --system让配置生效。

注意:这几步看起来琐碎,但少做任何一步都可能导致后面 Pod 网络不通或者节点无法注册。我建议把这几条命令写成一个脚本,三台机器都跑一遍,省得漏掉。

3.2 主机名与 hosts 解析的规范设置

主机名要改成有意义的名称,我一般用k8s-master、k8s-node1、k8s-node2这种命名。用hostnamectl set-hostname改完之后,记得在每台机器的/etc/hosts里加上三台机器的 IP 和主机名映射。这一步很关键,因为 kubeadm 在初始化时会用主机名去注册节点,如果解析不了,join 的时候会卡住。

另外,虚拟机克隆导致的 machine-id 重复问题必须处理。每台机器的/etc/machine-id和/var/lib/dbus/machine-id要保证唯一。最简单的办法是删掉这两个文件,然后执行systemd-machine-id-setup重新生成。网卡 MAC 地址如果也重复了,需要在虚拟机设置里重新生成,或者在系统里手动改配置文件。

3.3 容器运行时 containerd 的安装与配置

containerd 的安装我推荐用官方提供的二进制包,而不是 apt 源里的版本,因为 apt 源里的版本往往偏旧,可能不满足 v1.23 的要求。下载地址可以从 containerd 的 GitHub Release 页面找,选containerd-1.4.x或1.5.x的 linux-amd64 压缩包,解压后把bin目录下的文件复制到/usr/local/bin。

安装完之后要生成默认配置。执行containerd config default > /etc/containerd/config.toml,然后编辑这个文件。有两个地方必须改:一是SystemdCgroup要设成true,因为 kubelet 默认用 systemd 作为 cgroup 驱动,如果 containerd 用 cgroupfs,两者不一致会导致 Pod 启动失败;二是sandbox_image要改成能访问到的镜像地址,默认的k8s.gcr.io/pause在国内网络环境下可能拉不下来。

改完配置后,用 systemd 管理 containerd 服务。创建/etc/systemd/system/containerd.service文件,内容参考官方模板,然后systemctl daemon-reload、systemctl enable containerd、systemctl start containerd。启动后用systemctl status containerd确认状态是 active,再用ctr version看看客户端和服务端版本是否一致。

3.4 kubelet、kubeadm、kubectl 的安装

这三个组件的安装方式我习惯用 apt 源。先添加 Kubernetes 的 apt 仓库,导入 GPG 密钥,然后apt update,接着指定版本安装。v1.23.1 对应的包版本号是1.23.1-00,安装命令是apt install -y kubelet=1.23.1-00 kubeadm=1.23.1-00 kubectl=1.23.1-00。

装完之后要做的第一件事是apt-mark hold,把这三个包锁定,防止系统自动升级。Kubernetes 的版本升级需要遵循特定的顺序,不能随便让 apt 帮你升。锁定命令是apt-mark hold kubelet kubeadm kubectl。

kubelet 服务这时候会自动启动,但因为它找不到配置文件,会不断重启。这是正常现象,等 kubeadm init 执行完之后,kubelet 会拿到配置并正常运行。你可以先用systemctl status kubelet看一眼,确认它确实在反复重启,而不是报了什么奇怪的错误。

4. 控制平面初始化与工作节点加入的完整实操

4.1 kubeadm init 的配置文件与参数计算

直接跑kubeadm init加一堆命令行参数也能成功,但我更推荐用配置文件,因为可读性好,也方便版本管理和复用。配置文件我命名为kubeadm-config.yaml,内容大致如下:

apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.23.1 controlPlaneEndpoint: "k8s-master:6443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd

这里有几个参数需要解释。controlPlaneEndpoint我写的是主机名加端口,这样即使以后控制平面 IP 变了,只要 hosts 解析对,集群还能用。podSubnet要和后面装的 CNI 插件网段一致,我用的是 Flannel 的默认网段10.244.0.0/16。cgroupDriver设成 systemd,和 containerd 的配置保持一致。

执行初始化之前,可以先跑kubeadm config images pull把需要的镜像拉下来。v1.23.1 需要的镜像包括kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd、coredns。如果网络环境拉取官方镜像有困难,可以提前用kubeadm config images list列出所有镜像,然后逐个替换成可访问的镜像源。

初始化命令是kubeadm init --config kubeadm-config.yaml --upload-certs。--upload-certs是为了后面加控制平面节点时能自动分发证书,虽然我们这次只用一个控制平面,但加上也无妨。执行过程大概需要一两分钟,成功后会输出一段 join 命令,一定要复制保存下来,工作节点加入时要用。

4.2 初始化后的收尾配置

init 成功之后,按照提示配置 kubectl 的 kubeconfig。普通用户执行mkdir -p $HOME/.kube、cp /etc/kubernetes/admin.conf $HOME/.kube/config、chown $(id -u):$(id -g) $HOME/.kube/config。如果是 root 用户,直接export KUBECONFIG=/etc/kubernetes/admin.conf也行。

这时候用kubectl get nodes应该能看到控制平面节点,状态是NotReady。这是正常的,因为还没装 CNI 插件。用kubectl get pods -n kube-system可以看到控制平面的静态 Pod 都在运行,但 CoreDNS 可能处于 Pending 状态,也是因为网络没就绪。

4.3 安装 Flannel 网络插件

CNI 插件我选的是 Flannel,原因是配置简单,对测试环境足够用。安装方式是直接 apply 官方的 YAML 文件。但要注意,Flannel 的默认网段是10.244.0.0/16,必须和 kubeadm 配置里的podSubnet一致,否则 Pod 之间无法通信。

apply 之后,用kubectl get pods -n kube-flannel观察 Pod 状态。正常情况下每个节点上会起一个 Flannel Pod,状态从Init变成Running。等所有 Flannel Pod 都 Running 之后,再kubectl get nodes,控制平面节点应该变成Ready了。

提示:如果 Flannel Pod 一直起不来,先检查节点的/etc/hosts解析是否正常,再检查内核模块br_netfilter是否加载。我遇到过因为br_netfilter没加载导致 Flannel 无法创建网桥的情况,排查了半天才发现是内核参数没生效。

4.4 工作节点加入集群

工作节点的加入命令就是 init 输出里那段kubeadm join。在 node1 和 node2 上分别执行,注意要加--v=5方便看日志。执行过程中会先做预检,然后从控制平面拉取配置,最后启动 kubelet 并注册节点。

join 成功之后,回到控制平面执行kubectl get nodes,应该能看到三个节点都是Ready状态。再用kubectl get pods -n kube-system -o wide确认所有系统 Pod 都分布在三个节点上正常运行。

如果 join 失败,最常见的两个原因是 token 过期和网络不通。token 默认 24 小时过期,过期了就在控制平面执行kubeadm token create --print-join-command重新生成。网络不通的话,检查工作节点能不能 ping 通控制平面的 6443 端口,以及防火墙有没有放行。

5. 常见问题与排查技巧实录

5.1 节点 NotReady 的排查路径

节点 NotReady 是最常见的问题,排查顺序我一般是这样:先kubectl describe node <节点名>看 Conditions 部分,找到具体是哪个条件不满足。如果是NetworkUnavailable为 true,说明 CNI 没装好;如果是KubeletNotReady,就要去节点上看 kubelet 的日志。

kubelet 日志用journalctl -u kubelet -f看,重点找failed和error关键字。我遇到过因为 containerd 的SystemdCgroup没开导致 kubelet 报failed to create pod sandbox的情况,日志里会明确写 cgroup 驱动不匹配。还有一种情况是节点时间不同步,证书校验失败,这时候要装ntp或chrony做时间同步。

5.2 镜像拉取失败的应对方法

v1.23.1 的很多镜像在默认源里可能拉不下来,尤其是k8s.gcr.io开头的。解决办法有两个:一是提前在能访问的机器上拉好镜像,然后docker save成 tar 包,再ctr images import到目标节点;二是修改 kubelet 的配置,把镜像地址替换成可访问的镜像源。

用ctr导入镜像的时候要注意命名空间。kubelet 用的是k8s.io命名空间,所以导入命令要加-n k8s.io参数,比如ctr -n k8s.io images import pause.tar。如果不加这个参数,镜像导入到了 default 命名空间,kubelet 是看不到的。

5.3 证书过期与集群恢复

kubeadm 生成的证书默认有效期是一年。如果集群放了一年没动,再启动时 kubelet 会报证书过期。这时候可以用kubeadm certs check-expiration查看所有证书的到期时间,用kubeadm certs renew all续期。续期之后要重启控制平面的静态 Pod,方法是把/etc/kubernetes/manifests下的 YAML 文件临时移走再移回来,kubelet 会自动重建这些 Pod。

注意:证书续期后,/etc/kubernetes/admin.conf里的客户端证书也需要更新,否则 kubectl 会连不上。最省事的办法是续期后重新执行一遍 kubeconfig 的复制步骤。

5.4 常见问题速查表

问题现象可能原因排查命令解决方法
节点 NotReadyCNI 未安装kubectl describe node安装 Flannel 或其他 CNI
Pod 一直 Pending资源不足或污点kubectl describe pod调整资源或去掉污点
镜像拉取失败网络不通或镜像源不可用crictl pull <镜像>替换镜像源或手动导入
kubelet 反复重启配置错误或证书问题journalctl -u kubelet检查配置和证书有效期
join 失败token 过期或端口不通kubeadm token list重新生成 token 或检查防火墙
Pod 之间不通网段冲突或内核参数ip route、sysctl -a检查 podSubnet 和转发参数

5.5 几个我踩过的坑和对应的经验

第一个坑是虚拟机克隆后 machine-id 重复。表现是第二个节点 join 的时候,控制平面把它识别成了第一个节点,导致节点信息混乱。解决办法就是前面说的,删掉 machine-id 文件重新生成。

第二个坑是 containerd 的sandbox_image没改。默认配置里写的是k8s.gcr.io/pause:3.6,拉不下来,导致所有 Pod 都卡在SandboxNotReady。改成本地能访问的 pause 镜像地址就好了。

第三个坑是 Flannel 的网段和 kubeadm 配置不一致。我一开始 kubeadm 里写的podSubnet是10.244.0.0/16,但 Flannel 的 YAML 里默认也是这个,按理说没问题。但后来我改过 kubeadm 的网段忘了改 Flannel,结果 Pod 之间完全不通。所以这两个地方的网段必须严格一致。

第四个坑是时间不同步。虚拟机挂起再恢复后,时间可能和宿主机差很多,导致证书校验失败。装个chrony并配置成开机自启,基本能避免这个问题。

6. 集群验证与日常运维的实用建议

6.1 用测试 Pod 验证集群功能

集群搭好之后,别急着往上跑业务,先用一个简单的测试 Pod 验证基本功能。我一般会跑一个 Nginx Deployment,副本数设成 3,然后用 NodePort 类型的 Service 暴露出来。如果三个副本能均匀分布在三个节点上,并且通过任意节点的 IP 加 NodePort 都能访问到 Nginx 页面,说明调度、网络、服务发现都是正常的。

验证命令大概是kubectl create deployment nginx --image=nginx --replicas=3,然后kubectl expose deployment nginx --port=80 --type=NodePort。用kubectl get pods -o wide看 Pod 分布,用kubectl get svc看分配的 NodePort,最后用curl测试访问。

6.2 日常巡检的几个关键指标

集群跑起来之后,日常巡检我重点关注几个东西:节点状态、系统 Pod 状态、etcd 健康度、证书有效期。节点状态用kubectl get nodes一眼就能看;系统 Pod 用kubectl get pods -n kube-system检查有没有频繁重启的;etcd 健康度可以用kubectl -n kube-system exec etcd-<节点名> -- etcdctl endpoint health来看;证书有效期用kubeadm certs check-expiration定期查。

资源使用率也要关注,尤其是控制平面的内存。如果 etcd 的内存持续增长,可能是写入太频繁或者有大量历史版本没压缩。etcd 默认会保留最近 5 分钟的修订版本,如果写入量大,内存会涨得比较快。可以通过调整--auto-compaction-retention参数来控制压缩频率。

6.3 集群的备份与恢复思路

etcd 的数据是集群的核心,备份 etcd 就等于备份了整个集群的状态。备份命令是etcdctl snapshot save,恢复用etcdctl snapshot restore。备份的时候要注意指定正确的证书路径,否则连不上 etcd。恢复的时候要先停掉控制平面的静态 Pod,恢复完数据再重新启动。

除了 etcd,/etc/kubernetes目录下的证书和配置文件也要备份。这些文件丢了,集群就彻底起不来了。我习惯把这两个东西打包放到一个安全的地方,每次集群有重大变更后都更新一次备份。

6.4 版本升级的注意事项

v1.23.1 升级到更高版本时,不能跳版本,必须一个小版本一个小版本地升。比如从 1.23 升到 1.24,要先升 kubeadm,再升控制平面,再升 kubelet 和 kubectl,最后升工作节点。每一步都要用kubeadm upgrade plan确认可行性,不能直接apt upgrade。

升级前一定要备份 etcd 和证书,升级过程中如果卡住,可以用kubeadm upgrade的回滚功能恢复。另外,升级前要确认所有节点都是 Ready 状态,没有 Pending 的 Pod,否则升级过程中可能出问题。

6.5 资源清理与集群重置

如果集群玩坏了想重来,用kubeadm reset可以清理节点上的 Kubernetes 组件。但 reset 不会清理 CNI 的网络配置和 iptables 规则,需要手动清理。我一般会执行kubeadm reset -f,然后rm -rf /etc/cni/net.d、rm -rf $HOME/.kube,再用iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X清空 iptables 规则。最后重启一下机器,基本就干净了。

提示:reset 之前确认一下这个节点上没有重要的数据,因为 reset 会删掉所有容器和卷。如果是生产环境,千万别随便 reset。

这套集群我前后搭了不下十次,每次都会遇到一些新的小问题,但核心流程是稳定的。v1.23.1 虽然已经不是最新版本,但它的很多设计思路和后续版本是一致的,把这套流程走通,再去看更新的版本或者托管服务,理解起来会轻松很多。我个人在实际操作中的体会是,手动搭集群最大的价值不在于搭本身,而在于搭的过程中被迫去理解每一个组件的作用和它们之间的依赖关系,这种理解是看多少文档都换不来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询