
1. 为什么在虚拟化环境里手动搭一套K8s仍然值得做很多人第一次接触容器编排都是从托管集群开始的。点几下鼠标一个可用的控制面就出来了省心是真省心。但用久了会发现一个尴尬的问题一旦节点状态异常、证书过期、网络插件抽风排查时脑子里是空的——因为整个搭建过程被平台封装成了黑盒你根本不知道每个组件是怎么起来的、配置文件长什么样、组件之间靠什么通信。所以我的建议是哪怕日常工作用的是托管服务也一定要找机会在本地虚拟化环境里用 kubeadm 从零手搭一套集群。这个过程会逼着你直面几个绕不开的东西容器运行时怎么和 kubelet 对接、控制面组件以什么形式运行、Pod 网段和服务网段为什么要提前规划、证书和 token 是怎么流转的。这些认知在排障时就是你的底气。这篇内容面向的是有一定 Linux 基础、想真正搞懂 K8s 集群搭建细节的读者。我会以 v1.23.1 这个版本为例在本地虚拟化软件里搭一套“一主两从”的集群。选这个版本不是随便挑的v1.23 是一个分水岭版本——它正式移除了对 Docker 作为容器运行时的直接支持默认走的是符合 CRI 标准的 containerd。很多老教程还在用 Docker照着做会卡在 kubelet 起不来这一步。所以这个版本特别适合拿来理解“容器运行时接口”这件事。整套流程我会拆成环境规划、系统初始化、运行时与 kubeadm 安装、控制面初始化、工作节点加入、网络插件部署、验证与常见故障这几块来讲。每一步我都会说清楚“为什么这么做”而不只是“照着敲”。你跟着走一遍大概率能一次跑通就算中间报错我也把几个高频坑的排查链路写出来了方便你对照定位。2. 虚拟化环境下的节点规划与资源分配2.1 集群拓扑与角色划分先定拓扑。我用的是一主两从也就是一个控制面节点加两个工作节点。这个规模足够跑通所有核心功能又不会把本地机器的资源吃光。控制面节点承担 etcd、API Server、调度器和控制器管理器的职责工作节点只跑业务负载。如果你本地内存比较紧张也可以先搭一主一从后面再加节点kubeadm 支持动态扩容不影响已有集群。节点命名我习惯用有意义的前缀比如k8s-master、k8s-node1、k8s-node2这样在kubectl get nodes里一眼就能分清角色。主机名在 K8s 里不是随便起的kubelet 注册节点时会用主机名作为节点名如果主机名重复或者不规范后面加节点会出各种奇怪问题。所以每台机器装完系统第一件事就是改主机名并且写进/etc/hosts保证三台机器能通过主机名互相解析。角色主机名建议配置说明控制面k8s-master2 核 4G磁盘 40G跑控制面组件与 etcd工作节点k8s-node12 核 2G磁盘 40G跑业务 Pod工作节点k8s-node22 核 2G磁盘 40G跑业务 Pod这里有个容易被忽略的点控制面节点的内存不能给太小。etcd 对磁盘 IO 和内存都比较敏感2G 内存跑控制面会非常吃力动不动就 OOM。我建议控制面至少 4G工作节点 2G 起步。磁盘方面40G 是底线因为镜像、容器层、日志都会占空间尤其是你后面要拉网络插件的镜像几个镜像下来就是几百兆。2.2 网络模式的选择与固定 IP虚拟化软件一般提供三种网络模式桥接、NAT、仅主机。这三种对集群搭建的影响很大选错了后面节点之间通不了信加节点会一直卡在超时。桥接模式会让虚拟机直接接入物理局域网每台机器拿到和宿主机同网段的 IP节点之间互通最省事也最接近真实生产环境。缺点是 IP 可能由路由器动态分配重启后变化。NAT 模式下虚拟机在一个独立子网里能访问外网但外部访问虚拟机需要端口映射节点之间通信没问题适合宿主机网络环境受限的场景。仅主机模式只能宿主机和虚拟机互通虚拟机上不了外网拉镜像会失败除非你自建镜像仓库。我的建议是优先用桥接然后给每台机器配置静态 IP。静态 IP 这件事在 K8s 里几乎是必须的因为 kubeadm 初始化时会把 API Server 的地址写进证书和 kubeconfig如果控制面 IP 变了整个集群的证书就全对不上了只能重装。配置静态 IP 的方式取决于你用的系统以常见的网络管理工具为例改完配置文件后记得重启网络服务并验证。# 查看当前网卡与 IP确认网卡名称 ip addr # 编辑网络配置以 netplan 为例路径因发行版而异 sudo vim /etc/netplan/01-netcfg.yaml配置里要写清楚地址、网关、DNS。DNS 很关键因为拉取镜像要靠它解析域名。我一般填两个公共 DNS 加一个本地网关保证解析稳定。改完之后用ping和nslookup验证一下外网连通性和域名解析别等到装到一半才发现上不了网。2.3 时间同步为什么不能省这个问题我必须单独拎出来讲。K8s 集群里大量依赖时间戳做判断证书有效期、etcd 的租约、事件排序、token 过期。如果三台机器时间差得比较多会出现一些非常隐蔽的故障比如节点明明活着却被判定为 NotReady或者加入集群时 token 校验失败。所以每台机器都要开时间同步。大多数发行版自带时间同步服务确认它处于运行状态即可。如果没开手动装一个并设置开机自启。验证方法是三台机器分别执行date看时间是否一致误差控制在几秒内就没问题。这个步骤花不了两分钟但能帮你省掉后面几小时的迷惑排查。3. 系统层面的初始化那些不做就报错的细节3.1 关闭交换分区与内核参数调整kubelet 默认要求关闭交换分区。原因在于 K8s 的调度是基于节点可分配资源来算的如果开了 swap内存压力大时系统会把内存页换出去导致 Pod 的实际可用内存和调度器算出来的不一致性能表现变得不可预测。所以第一步就是关掉 swap并且写进/etc/fstab永久生效否则重启后又回来了。# 临时关闭 sudo swapoff -a # 永久关闭注释掉 fstab 里的 swap 行 sudo sed -i /swap/s/^/#/ /etc/fstab # 验证输出应为空 free -h | grep -i swap接下来是内核参数。容器网络依赖网桥转发需要开启br_netfilter模块并让 iptables 能处理桥接流量。这几个参数不设置后面网络插件装上了 Pod 之间也通不了。把它们写进配置文件然后加载生效。cat EOF | sudo tee /etc/modules-load.d/k8s.conf br_netfilter overlay EOF 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 EOF sudo modprobe br_netfilter sudo modprobe overlay sudo sysctl --systemoverlay模块是给 containerd 用的它默认的存储驱动就是 overlayfs提前加载避免运行时找不到驱动。ip_forward打开是为了让节点具备转发能力Pod 跨节点通信要靠它。3.2 防火墙与 SELinux 的处理策略防火墙这块要分情况。如果你是在完全隔离的实验环境最省事的做法是直接关掉防火墙和 SELinux避免各种端口被拦。但如果是稍微正式一点的环境我建议保留防火墙按需放行端口这样更接近生产实践。K8s 控制面需要放行的端口不少API Server 的 6443、etcd 的 2379 和 2380、调度器和控制器管理器的 10259 和 10257。工作节点上 kubelet 用 10250NodePort 服务默认用 30000 到 32767 这个范围。网络插件还会用到额外的端口比如某些插件需要放行 VXLAN 的 8472。这些端口如果被拦症状通常是节点加不进来或者 Pod 跨节点不通。SELinux 在 enforcing 模式下会阻止容器访问某些路径导致容器启动失败。实验环境直接设成 permissive 或者 disabled 最省心。如果坚持用 enforcing需要额外配置策略复杂度会上升不少新手不建议一上来就挑战。提示关防火墙和 SELinux 只是实验环境的权宜之计正式环境一定要按最小权限原则放行端口别图省事直接全关。3.3 主机名解析与 hosts 配置前面提过主机名的重要性这里说具体操作。每台机器执行hostnamectl set-hostname设置主机名然后在/etc/hosts里把三台机器的 IP 和主机名对应关系写全。这样做的目的是让节点之间通过主机名就能互相找到kubelet 注册和 kubeadm 加节点时都会用到。# 在每台机器上执行主机名按角色改 sudo hostnamectl set-hostname k8s-master # 三台机器的 /etc/hosts 都追加以下内容 cat EOF | sudo tee -a /etc/hosts 192.168.1.10 k8s-master 192.168.1.11 k8s-node1 192.168.1.12 k8s-node2 EOF写完之后用ping k8s-node1验证一下解析是否生效。这一步看着简单但经常有人只改了主机名没写 hosts结果加节点时报“no such host”排查半天。4. 容器运行时与 kubeadm 的安装取舍4.1 为什么 v1.23 要选 containerd 而不是 Docker这是本篇最核心的一个技术点值得展开讲。在 v1.23 之前kubelet 内置了一个叫 dockershim 的适配层用来把 Docker 的接口翻译成 K8s 需要的 CRI 接口。Docker 本身并不是为 K8s 设计的它有自己的 CLI、自己的镜像管理、自己的网络模型K8s 只是借用它。这种“套一层”的方式维护成本高而且 Docker 的很多功能 K8s 根本用不上。v1.23 开始dockershim 被正式移除。这意味着 kubelet 不再直接跟 Docker 对话而是通过标准的 CRI 接口跟容器运行时通信。containerd 本身就是从 Docker 里拆分出来的核心运行时它原生支持 CRI性能更好调用链更短。所以在这个版本上正确的选择是 containerd。这里要澄清一个常见误解用 containerd 不代表你不能用 Docker 构建的镜像。镜像格式是 OCI 标准Docker 构建的镜像 containerd 照样能跑。区别只在于运行时不在镜像本身。你平时用docker build构建镜像完全没问题只是节点上不再需要装 Docker 引擎了。4.2 containerd 的安装与关键配置containerd 的安装方式有好几种用系统包管理器装最省事。装完之后要做一件关键的事生成默认配置并修改。默认配置里SystemdCgroup是 false而 kubelet 默认用 systemd 作为 cgroup 驱动两边不一致会导致节点起来后 Pod 反复重启报错信息里能看到 cgroup 相关的字样。# 生成默认配置 sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml # 修改关键项把 SystemdCgroup 改成 true sudo sed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml # 重启并设置开机自启 sudo systemctl restart containerd sudo systemctl enable containerd改完配置后确认 containerd 状态是 active。另外containerd 默认的 pause 镜像地址在国内网络环境下可能拉不动如果你遇到拉取 pause 镜像超时可以在配置里把 sandbox 镜像地址换成可访问的镜像源。这个细节很多人第一次搭会踩表现为 kubelet 日志里一直报 sandbox 创建失败。4.3 kubeadm、kubelet、kubectl 三件套的安装这三个组件的分工要搞清楚kubeadm 负责集群的初始化和节点加入是个“一次性工具”kubelet 是每个节点上的常驻进程负责真正管理容器kubectl 是命令行客户端用来跟集群交互。三者版本要尽量对齐避免版本偏差带来的兼容问题。安装时要注意版本锁定。包管理器默认可能装最新版而我们要的是 v1.23.1所以安装命令里要指定版本号。装完之后用kubeadm version和kubelet --version确认版本一致。# 添加软件源后指定版本安装 sudo apt-get install -y kubelet1.23.1-00 kubeadm1.23.1-00 kubectl1.23.1-00 # 锁定版本防止被自动升级 sudo apt-mark hold kubelet kubeadm kubectl # 设置 kubelet 开机自启此时还没初始化起不来是正常的 sudo systemctl enable kubelet这里有个现象要提前说明kubelet 装完并 enable 之后systemctl status kubelet会显示不断重启。这是正常的因为还没执行 kubeadm initkubelet 找不到配置文件。等初始化完成后它就会稳定下来。新手看到这个容易慌以为装错了其实不用管。5. 控制面初始化一条命令背后的门道5.1 初始化命令的参数怎么定控制面初始化是整个搭建过程的高潮一条kubeadm init命令背后涉及镜像拉取、证书生成、静态 Pod 创建、etcd 启动、kubeconfig 生成等一系列动作。命令里的参数不是随便填的每个都影响后续行为。sudo kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --image-repositoryregistry.aliyuncs.com/google_containers \ --kubernetes-versionv1.23.1 \ --service-cidr10.96.0.0/12 \ --pod-network-cidr10.244.0.0/16--apiserver-advertise-address填控制面的 IPAPI Server 会监听并对外宣告这个地址工作节点就是通过它来通信的。--image-repository指定镜像仓库因为默认仓库在国内拉取很慢甚至失败换成可访问的镜像源能大幅提升成功率。--kubernetes-version明确版本避免拉错。--service-cidr是 Service 的虚拟 IP 段--pod-network-cidr是 Pod 的网段这两个段不能和宿主机所在网段重叠否则路由会冲突。网段规划这里给个经验值Service 段用10.96.0.0/12Pod 段用10.244.0.0/16这是官方示例里常用的组合兼容性好。如果你的物理网络用的是 10 开头的网段那就要换成别的比如 172.16 开头避免撞车。5.2 初始化过程中的正常输出与异常信号执行 init 后屏幕上会滚动大量日志。正常的流程是先做一系列 preflight 检查然后拉取镜像接着生成证书、写配置文件、启动静态 Pod。看到最后打印出“Your Kubernetes control-plane has initialized successfully”就说明成功了。如果卡住重点看卡在哪一步。卡在拉镜像多半是网络问题检查镜像仓库地址和 DNS。卡在 preflight通常是系统初始化没做全比如 swap 没关、端口被占。报错信息一般比较明确照着提示改就行。初始化成功后命令会输出一段配置 kubectl 的指令和一段加入集群的 token 命令。这两段一定要保存下来尤其是加入命令工作节点就靠它。token 默认 24 小时过期过期了可以用kubeadm token create --print-join-command重新生成。5.3 配置 kubectl 与验证控制面状态按提示把 admin 的 kubeconfig 拷到当前用户目录然后就能用 kubectl 了。第一次执行kubectl get nodes会看到控制面节点处于 NotReady 状态。别急这是正常的因为网络插件还没装节点缺少 CNI 配置就无法进入 Ready。mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config # 查看节点此时应为 NotReady kubectl get nodes # 查看控制面 Pod 是否都 Running kubectl get pods -n kube-systemkube-system命名空间里应该能看到 etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy 这些 Pod。它们都是以静态 Pod 形式运行的由 kubelet 直接管理不经过调度器。如果某个组件没起来用kubectl describe pod和kubectl logs看具体原因常见的是镜像没拉到或者证书有问题。6. 工作节点加入与网络插件落地6.1 工作节点加入集群的完整过程工作节点加入前要确保它已经完成了前面所有的系统初始化关 swap、调内核参数、装 containerd、装 kubelet 三件套。这些步骤一个都不能少否则加节点时会报各种错。加入命令就是初始化时输出的那条形如kubeadm join 192.168.1.10:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx。在每台工作节点上执行看到“This node has joined the cluster”就成功了。然后回到控制面执行kubectl get nodes应该能看到新节点状态同样是 NotReady等网络插件装好就会变 Ready。如果加入时报连接超时检查控制面 IP 是否可达、6443 端口是否放行。如果报 token 无效说明 token 过期了重新生成一个。如果报证书 hash 不匹配说明命令里的 hash 和控制面实际的不一致用kubeadm token create --print-join-command重新获取完整命令最稳妥。6.2 网络插件的选择与部署CNI 插件是集群的“神经系统”没有它 Pod 之间无法通信节点也进不了 Ready。常见的选择有 Calico、Flannel、Cilium 等。Flannel 配置简单、资源占用低适合实验环境快速跑通Calico 功能更全支持网络策略适合想进一步学习网络策略的场景。我这里以 Flannel 为例因为它和前面规划的10.244.0.0/16网段是默认匹配的部署最省事。如果选 Calico要注意它的默认网段可能和规划的不一样需要改配置。# 部署 Flannel kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml # 观察 Pod 启动情况 kubectl get pods -n kube-flannel kubectl get pods -n kube-system部署后稍等一两分钟再执行kubectl get nodes三个节点应该都变成 Ready 了。如果某个节点还是 NotReady用kubectl describe node 节点名看 Conditions 里的具体原因常见的是 CNI 配置没下发到该节点或者镜像拉取失败。6.3 验证集群是否真正可用节点 Ready 只是第一步真正要验证的是 Pod 能不能跑、跨节点能不能通、Service 能不能访问。我一般用几个递进的测试来确认。先跑一个单 Pod确认调度和运行正常。再跑一个 Deployment 带多个副本确认能跨节点分布。然后创建一个 Service确认集群内能通过 Service IP 访问。最后用kubectl exec进到 Pod 里 ping 另一个节点上的 Pod IP确认跨节点网络通。# 跑一个测试 Deployment kubectl create deployment nginx-test --imagenginx --replicas3 # 查看 Pod 分布应该分散在不同节点 kubectl get pods -o wide # 暴露 Service kubectl expose deployment nginx-test --port80 --typeNodePort # 查看 Service kubectl get svc如果 Pod 一直 Pending多半是资源不够或者有污点没容忍。如果 Pod 起来了但跨节点 ping 不通检查 CNI 插件是否在所有节点都正常运行以及防火墙是否放行了插件需要的端口。这几步走通集群就算真正可用了。7. 搭建过程中最容易卡住的几个坑7.1 镜像拉取失败的各种表现镜像拉取失败是新手遇到最多的坑表现五花八门。有的是 kubeadm init 阶段卡在拉控制面镜像有的是节点加入后 pause 镜像拉不下来有的是网络插件镜像拉不动。根因基本都是网络访问问题。应对思路分两层。第一层是换镜像源kubeadm init 时用--image-repository指定可访问的仓库containerd 配置里把 sandbox 镜像地址也换掉。第二层是提前把镜像拉好用kubeadm config images pull先把控制面镜像拉到本地确认没问题再 init。如果某个镜像实在拉不动可以在能访问的机器上拉下来导出成 tar 包再导入到目标节点。注意换镜像源时要确认镜像的完整路径和 tag 对得上不同源的镜像命名可能略有差异拉错版本会导致组件启动失败。7.2 节点 NotReady 的排查链路节点 NotReady 是个笼统的现象背后原因很多。我的排查顺序是这样的先kubectl describe node看 Conditions找到具体是哪个条件不满足。如果是NetworkUnavailable为 true那就是 CNI 没装好。如果是KubeletNotReady再去看 kubelet 的日志。kubelet 日志用journalctl -u kubelet -f看重点找 error 级别的信息。常见的有 cgroup 驱动不匹配、证书过期、容器运行时连不上。cgroup 不匹配的报错里会明确提到 systemd 和 cgroupfs这时候回去检查 containerd 配置里的SystemdCgroup是不是 true。容器运行时连不上检查 containerd 是否在运行、socket 路径是否正确。7.3 证书与 token 过期后的补救kubeadm 生成的证书默认有效期一年token 默认 24 小时。实验环境里集群放几天再回来很可能就遇到证书过期导致集群不可用。证书过期的典型症状是 kubectl 连不上 API Server报 x509 证书错误。补救办法是用kubeadm certs check-expiration查看证书状态用kubeadm certs renew all续期然后重启控制面组件。token 过期就简单了重新生成一个加入命令即可。这些操作我都建议在集群还健康的时候就演练一遍真出问题时才不会手忙脚乱。8. 搭完之后还能做些什么集群跑起来只是起点。接下来可以练的东西很多给节点打标签做调度约束、部署有状态应用挂持久卷、配置 Ingress 暴露服务、用 NetworkPolicy 做网络隔离、把 etcd 数据备份恢复走一遍。每一个方向都能加深你对 K8s 的理解。我个人在本地环境里最常做的一件事是故意搞破坏然后练习恢复。比如手动删掉一个控制面静态 Pod 的 manifest看 kubelet 会不会自动重建比如把某个节点的 containerd 停掉观察 Pod 状态怎么变化比如把 etcd 的数据目录备份后清空练习从备份恢复。这种“破坏—恢复”的循环比顺顺利利搭十遍学到的都多。最后分享一个小习惯每次搭完集群把关键的配置文件和命令整理成一个脚本或者笔记。下次再搭直接照着走省去回忆的时间。环境会变版本会变但底层的原理和排查思路是通用的这才是真正值得沉淀的东西。