这两年容器编排基本成了后端团队的标配,Kubernetes 更是绕不开的那个关键词。我在本地环境和云服务器上都折腾过好几轮集群,从kubeadm init报错一头雾水,到后来能熟练处理各种证书过期、网络插件冲突的坑,踩过的雷不算少。这篇就以“k8s单master集群搭建”为主线,把从零到一的过程完整拆开讲,包含环境规划、组件选择、初始化参数、加入工作节点以及常见故障排查。目标很直接:照着走能搭出一套稳定的单控制面集群,同时让你明白每一步为什么要这么做,遇到问题知道从哪里下手。
先说清楚适用对象:想自己动手学习 Kubernetes 原理的开发者,或者刚接手内部测试环境、想快速起一套集群的人。单 master 架构虽然不具备高可用能力,但对学习、开发、测试、演示来说完全够用,资源占用也比多控制面集群小得多。
1. 整体架构与版本选型:为什么从单 Master 开始
1.1 控制面组件拆解
Kubernetes 集群从角色上分两类节点:控制平面节点和工作节点。单 Master 集群就是只有一台机器承担全部控制面组件,剩下的机器作为工作节点运行业务负载。
控制面核心组件包括:
kube-apiserver:所有组件通信的中枢,外部请求(kubectl、API 调用)也走它,相当于整个集群的“入口网关”。kube-controller-manager:负责维持集群期望状态,比如副本数不够就补 Pod,节点掉线就标记状态。kube-scheduler:决定新建的 Pod 落在哪个节点上,会根据资源余量、亲和性、污点等条件做调度决策。etcd:集群所有数据的存储后端,状态、配置、资源对象都存在这里面,是整个集群的“数据库”。
单 Master 部署的核心特征是“控制面和数据面都在一台机器上”,但工作节点的 kubelet、kube-proxy、容器运行时都是独立运行的。这个结构清晰,适合理解 Pod 调度和组件通信链路。先把单控制面的链路吃透,后续再过渡到 HA 集群或托管集群时,很多概念就能直接复用。
1.2 版本与运行时选型依据
搭建前最重要的一件事是统一版本。Kubernetes 迭代很快,小版本升级带来的 API 变化、特性开关差异都可能成为集群不稳定的诱因,所以控制面、工作节点、kubeadm、kubectl 这些组件的版本最好保持一致。
我常用的组合是:
| 组件 | 版本 | 说明 |
|---|---|---|
| Kubernetes | 1.28.x | 稳定、支持周期内,资料也丰富 |
| 容器运行时 | containerd 1.7.x | 稳定、资源占用适中,当前主流选择 |
| 操作系统 | Ubuntu 22.04 / CentOS 7.9 / Rocky 9 | 内核版本高一点更稳 |
| kubeadm / kubelet / kubectl | 与集群版本一致 | 避免版本不匹配的坑 |
| CNI 插件 | Calico 3.27 / Flannel | 按网络需求选择 |
容器运行时我没有选 Docker 的原因是:Kubernetes 从 1.24 版本开始移除了对 Docker 的直接支持,虽然可以通过 cri-dockerd 桥接,但 containerd 本身就是为 CRI 设计的,链路上少一层转换,性能更好,排障也更直接。这里说的“移除支持”不是不能用了,而是 kubelet 不再直接调用 Docker API,需要额外适配器,对学习和新环境来说没有意义,直接用 containerd 才是正路。
1.3 网络模型与网段规划
Kubernetes 对内网段规划有两块必须提前想清楚:Service 网段和 Pod 网段。这两个网段不能与宿主机现有网段冲突,否则会出现路由异常、Pod 无法访问等诡异问题。
我在实验环境里常用的规划如下:
- 节点网段:
192.168.1.0/24(宿主机局域网) - Pod 网段:
10.244.0.0/16(分配给 Flannel 网段) - Service 网段:
10.96.0.0/12(kubeadm 默认就是它)
如果局域网规划比较拥挤,可以调整 Service 网段,但要注意 Pod 网段必须和 CNI 插件的默认值对齐。以 Flannel 为例,它的默认网段就是10.244.0.0/16,你不加参数它会按这个来,所以初始化时--pod-network-cidr=10.244.0.0/16和 Flannel 默认值的一致性是最省心的。
注意:规划好之后,整个集群生命周期内尽量不要再改网段。改网段的代价基本等于重建集群,特别是已有业务数据时非常痛苦。
2. 环境准备与组件安装:两个容易埋雷的环节
2.1 系统基础配置
不管用哪套 Linux 发行版,有三项基础配置是绕不开的:主机名解析、时间同步、内核模块。看起来简单,没做好后面会埋不少坑。
我一般按这个顺序处理:
设置主机名。控制面节点用
k8s-master,工作节点用k8s-node1、k8s-node2这类清晰命名。同时改/etc/hosts,把集群内所有节点的 IP 和主机名对应关系写进去,保证节点之间可以互相解析。kubeadm 生成证书时依赖主机名和 IP 的对应关系,解析不通会导致初始化失败或者节点加入异常。安装时间同步。推荐
chrony或systemd-timesyncd,任选其一。集群内证书校验依赖时间一致,时间偏差超过五分钟,组件间的 TLS 握手就会出现证书有效性错误,kubeadm init时经常看到x509: certificate has expired or is not yet valid这类报错。很多新手会误以为是证书本身出了问题,实际上就是时钟没对准。关闭 swap。这个其实是 kubelet 的默认要求,虽然新版 kubelet 支持开启 swap 运行,默认还是需要关掉。刷不刷
swapoff -a可能直接影响kubeadm init是否能通过预检。加载内核模块,关键是
br_netfilter,并调整net.bridge.bridge-nf-call-iptables等内核参数,确保 iptables 能正确处理桥接流量。
用了这么多年 Linux 的经验是:尽量把这类初始化做成脚本,存到本地配置仓库里,遇到新环境直接跑一遍,避免反复手工操作漏掉步骤。
2.2 containerd 安装与配置要点
容器运行时我选的是 containerd,安装方式取决于你的发行版。Ubuntu 可以用官方源,CentOS 系用 yum 源,核心是装完之后要生成一份调整好的配置文件。
# 生成默认配置,然后修改 containerd config default | sudo tee /etc/containerd/config.toml > /dev/null生成配置后有两个必须改的点:
第一个是镜像加速器。国内拉取registry.k8s.io的镜像经常会超时,所以需要把 sandbox 镜像地址或者整个 registry 配置成可用镜像源。具体配置参考你所在网络环境能访问的源,不同地区选择不同,不要照搬网上的域名。
第二个是SystemdCgroup参数。containerd 默认使用 cgroupfs 作为 cgroup 驱动,而 kubelet 默认期望 systemd 驱动,二者不一致会导致节点状态一直NotReady。所以要把配置里的SystemdCgroup设为true。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true注意,如果你改动了 containerd 的配置,务必systemctl restart containerd。改完配置忘了重启,是所有新手最容易踩的坑,restart 之后ctr version验证一下再继续。
2.3 kubeadm / kubelet / kubectl 安装
这组组件只能用 k8s 官方源安装,不能用系统自带的旧版本。发行版用 yum 或 apt 添加官方源之后,安装指定位版本。
# Ubuntu apt-get update apt-get install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00 # CentOS/Rocky yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2安装完不要急着启动 kubelet。此时集群还没初始化,kubelet 会不断尝试连接 apiserver 但连不上,状态会一直处于异常或反复重启,这是正常现象。先设置systemctl enable kubelet,但不要执行systemctl start kubelet。等kubeadm init完成后,kubelet 自然会由 kubeadm 拉起来。
版本一致性是集群的“潜规则”。控制面版本如果比节点版本新,节点可能被拒绝注册;反过来旧版本节点虽然能注册,但很多 API 对象处理起来会有兼容问题。最稳妥的做法是集群内所有 k8s 组件版本完全一致。
3. 控制面初始化:kubeadm init 全流程
3.1 预检与初始化命令
核心命令是kubeadm init,但参数需要在动手前确认清楚。以下是我在单 Master 集群中常用的完整初始化命令:
kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --control-plane-endpoint=192.168.1.10 \ --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers \ --kubernetes-version=v1.28.2 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=10.96.0.0/12参数含义逐个说清楚:
--apiserver-advertise-address:apiserver 对外公布的地址,控制面节点自身的 IP。如果机器有多个网卡,必须显式指定,否则 kubeadm 可能选错网卡。--control-plane-endpoint:高可用场景下是负载均衡器地址,单节点时填同一个 IP 就行。--image-repository:镜像仓库地址。默认是registry.k8s.io,国内网络经常拉不动,换一个可用仓库可以避免卡在kubeadm init拉镜像阶段。--kubernetes-version:明确指定版本,避免默认版本与 kubelet 不一致。--pod-network-cidr:Pod 网段,与 CNI 插件默认网段对应。--service-cidr:Service 网段,不指定默认就是10.96.0.0/12。
执行前可以先跑kubeadm config images pull,用同一组参数把镜像先拉下来,避免在 init 过程中因拉镜像超时中断。镜像都本地就绪后,再跑 init 会快很多。
3.2 初始化过程中的常见阻断点
kubeadm init失败最常见的原因集中在预检阶段(preflight)。我遇到过并实际解决过的错误主要有:
- 端口被占用。
6443、10250、2379、2380这些端口被其他进程占用,kubeadm直接拒绝继续。先ss -lntp查端口占用,能换个端口就换,不能换就停掉冲突服务。 - swap 未关闭。提示
running with swap on is not supported,执行swapoff -a,并注释掉/etc/fstab里的 swap 行,防止重启后 swap 又自动挂回来。 - 容器运行时 sock 找不到。kubeadm 默认会扫描
/var/run/containerd/containerd.sock,你没启动 containerd 或者配置路径不对,预检直接失败。确保 containerd 服务状态正常。 - 版本不匹配。比如 kubeadm 是 1.28 但指定
--kubernetes-version=v1.27.3,预检会报版本不一致。这时候要么统一 kubeadm 版本,要么让版本参数跟组件一致。
3.3 初始化成功后的输出信息
当kubeadm init最后输出:
Your Kubernetes control-plane has initialized successfully!这时候意味着控制面组件已经全部起来了。这段输出信息很大,保存好三个关键部分:
第一是给管理员用的 kubeconfig 配置命令。默认情况下只有 root 能直接访问集群,普通用户需要把配置文件拷到自己的目录:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config第二是工作节点加入集群的指令,形如:
kubeadm join 192.168.1.10:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx这条命令有效期为 24 小时,如果过期了可以重新生成 token,后面详细讲。
第三是 kubeconfig 文件的位置。管理集群所有操作的入口就是/etc/kubernetes/admin.conf,这个文件要备份好,一旦丢失,意味着你失去了对集群的管理权限。
初始化成功之后,立刻验证控制面状态:
kubectl get nodes此时 master 节点状态大概率是NotReady,别慌,原因有两层:一是 CNI 网络插件还没装,节点上的 Pod 网络就不通;二是 kubelet 要等网络就绪后才会把节点状态标记为 Ready。继续下一步即可。
4. 容器网络插件安装:Flannel 还是 Calico
4.1 网络插件选型对比
CNI 插件是 Kubernetes 集群里跳过就直接瘫痪的一环。安装之前先确定选型,不同插件对初始化参数的依赖完全不同。我实际用过两者,简单对比:
| 维度 | Flannel | Calico |
|---|---|---|
| 网络模式 | VXLAN / host-gw | BGP / VXLAN / IPIP |
| 网络策略 | 不支持 | 支持完整 NetworkPolicy |
| 性能 | 中规中矩 | BGP 模式下性能更好 |
| 部署复杂度 | 简单 | 稍复杂,依赖 etcd 存储 |
| 适用场景 | 学习、测试、小型内部环境 | 生产环境、需要网络策略的场景 |
单 Master 学习环境优先推荐 Flannel,原因很实际:部署最简单,默认网段和kubeadm init参数保持一致,装完就能用。如果后续要学 NetworkPolicy 或者模拟生产环境网络隔离,可以换 Calico。换插件之前要先删掉已有的 Flannel DaemonSet,否则新旧插件抢 iptables 规则,网络会变得一团糟。
4.2 Flannel 安装流程
Flannel 安装本质就是创建一个 DaemonSet,让每个节点上自动运行 flanneld 进程。下载官方 manifest 文件然后 apply 即可:
wget https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml kubectl apply -f kube-flannel.yml这里有个必须注意的点:下载下来后要确认 manifest 里的Network字段是否为10.244.0.0/16,如果你的--pod-network-cidr参数改了,必须同步改 manifest 里的网络段,否则 flanneld 和 kube-controller-manager 分配的 Pod CIDR 对不上,跨节点 Pod 通信会直接断掉。
我实际遇到过一种情况:manifest 下载没问题,apply 也成功,但 Pod 都是CrashLoopBackOff。查看 flanneld 日志发现是Network 10.244.0.0/16 does not match the existing interface subnet。原因就是节点上之前手动创建了 docker 网桥或者其他虚拟网卡,把网段占用了。这种环境干净的机器反而少踩这种坑。
安装完成后验证:
kubectl get pods -n kube-flannel等到所有 flannel Pod 都Running,再查看节点状态:
kubectl get nodes正常情况下 master 节点会从NotReady变成Ready。如果还卡在NotReady,大概率是 kubelet 的 cgroup 驱动跟 containerd 不一致,回到上一节检查SystemdCgroup配置,再重启 containerd 和 kubelet。
4.3 网络插件安装后的验证
节点 Ready 只是第一步,还需要确认控制面核心服务状态:
kubectl get pods -n kube-systemkube-apiserver、kube-controller-manager、kube-scheduler、etcd这几个 Pod 都应该是Running。同时确认coredns是否 Running,DNS 组件不工作的话,集群内服务发现全废。
做个最简单的连通性验证:
kubectl run nginx-test --image=nginx --port=80 kubectl expose pod nginx-test --port=80 --name=nginx-test-svc kubectl run --rm -it test-pod --image=busybox -- /bin/sh # 在容器内执行 wget -O- http://nginx-test-svc:80能访问就说明 Pod 间通信、Service 转发、DNS 解析都正常,集群的核心链路已经通了。
5. 工作节点加入与多节点验证
5.1 节点加入前置条件
工作节点加入前,环境准备和控制面节点完全一致:需要安装 containerd 并配置 cgroup、安装 kubelet 和 kubeadm、关闭 swap、加载内核模块。不要偷懒跳过这些步骤,我见过只装 kubelet 就执行 join 的,结果 kubelet 不断重启,节点一直处于NotReady。
准备好以后,在工作节点执行控制面初始化时输出的 join 命令:
kubeadm join 192.168.1.10:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx正常情况下会输出:
This node has joined the cluster: * Certificate signing request was sent to apiserver and a response was received. * The kubelet was informed of the new secure connection.回到控制面节点运行:
kubectl get nodes节点STATUS只要变成Ready,就是加入成功了。
5.2 Token 过期与重新生成
之前说过 join token 有效期是 24 小时。如果初始化后隔了几天才想起加节点,或者 token 被误删了,不要重新初始化控制面,直接重新生成:
kubeadm token create --print-join-command它会生成新的 token 和对应的--discovery-token-ca-cert-hash,拿这条新命令去节点上 join 即可。
还有一种情况需要留意:如果控制面节点的证书也换了,比如重新生成过 CA 证书,那 hash 也会变。稳妥的做法是先执行:
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'重新计算 CA 的 hash 值。
5.3 单 Master 集群的调度限制
单 Master 集群默认情况下 master 节点不让跑业务 Pod,因为初始化时会给 master 节点打上污点node-role.kubernetes.io/control-plane:NoSchedule。如果集群一共就两台机器,master 不让调度负载,资源就浪费了一半。这时候有两种选择:
第一种是用上 tolerations 的方式,专门为需要跑在 master 上的 Pod 加容忍。比如我想在 master 上跑监控组件:
tolerations: - key: node-role.kubernetes.io/control-plane effect: NoSchedule第二种是直接移除 master 的污点,让所有业务 Pod 都能调度到控制面节点:
kubectl taint nodes k8s-master node-role.kubernetes.io/control-plane-这个操作在生产环境不推荐,但在单 Master 学习和测试环境中很实用。我个人一般保留污点,然后给特定 DaemonSet 或关键组件显式加容忍,这样更贴近真实生产习惯。
6. 常见故障与排查实录
6.1 节点处于 NotReady 状态
节点 NotReady 是集群排障遇到最多的状态,原因集中在几个方面。
第一是 kubelet 没起来。在生产里我总结出一条排查命令组合:
systemctl status kubelet journalctl -u kubelet -f日志里如果反复出现failed to connect to http://localhost:10248/healthz,说明 kubelet 本身没有成功注册,先看 containerd 是否健康、cgroup 驱动是否匹配。
第二是 CNI 插件没就绪。节点网络配置是 CNI 插件写出来的,插件没装好,kubelet 就会一直上报networkPluginNotReady。这个时候进节点查:
ls /etc/cni/net.d/如果这个目录是空的,说明 CNI 插件没在 kubelet 发起调用前生成配置,重新检查 Flannel 或 Calico 的 DaemonSet。
第三是容器运行时本身异常。ctr version连不上 containerd 就没法用了。
6.2 kubeadm init 失败后如何清理重来
初始化失败最忌讳带着残留状态重跑,不清理干净大概率还是失败,而且报错也更难读。我的清理清单如下:
kubeadm reset -f rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X ipvsadm --clear # 如果用了 ipvs 模式kubeadm reset会自动清理 kubelet 相关配置,但/etc/kubernetes和/var/lib/etcd这些目录不一定删得干净,必须手动清理。iptables 规则如果不重置,集群重建后新旧规则叠加,网络行为会非常诡异,我确实被这种问题坑过很久。
清理完再重新走 init 流程,基本都能顺利通过。
6.3 证书过期与续期
Kubernetes 证书默认有效期是一年,实验环境跑得久一点就会遇到所有组件突然全线失效。遇到x509: certificate has expired or is not yet valid,不用慌,先看证书期限:
kubeadm certs check-expiration单 Master 场景最直接的方式是:
kubeadm certs renew all systemctl restart kubelet这里有个细节:renew all只会续期证书并把新证书写到对应位置,但 apiserver 等组件都是静态 Pod,需要重启 kubelet 让它们以新的证书重新拉起来。操作顺序不能反。
CPU、内存占用偏高的排查思路在单 Master 集群里格外重要。因为所有核心组件挤在一台机器上,etcd、apiserver、kubelet 同时跑,如果机器配置只有 2C4G,压力大会出现 apiserver 响应慢、etcd 同步滞后等连锁反应。建议单 Master 实验集群至少给到 4C8G,不要让业务负载太猛。真要跑较大的压力测试,还是老老实实上多节点生产配置。
7. 最终可用性验证与加固建议
7.1 端到端验证六步法
集群搭好不等于“能用”,我一般按固定流程做最终验收,确保核心链路无死角。
第一步,节点状态检查:
kubectl get nodes -o wide所有节点STATUS必须为Ready。
第二步,核心组件检查:
kubectl get pods -n kube-system所有系统组件READY列为1/1。
第三步,DNS 功能验证。前面用 busybox 测试过,这里再确认一次coredns的 Pod IP 能响应解析。
第四步,创建测试 Deployment:
kubectl create deployment echo-pod --image=busybox -- sleep 3600第五步,验证 Service 暴露和代理转发。把 Deployment 暴露成 NodePort,从外部节点访问NodeIP:NodePort,能通才说明 kube-proxy 工作正常。
第六步,验证节点故障感知。将某节点上的 Pod 删掉,观察它是否在其他节点被重新调度。虽然单 Master 集群没有集群级高可用,但工作节点上的业务 Pod 是有自愈能力的。
7.2 安全与运维建议
集群能用只算第一步,后续运维才真正决定这集群能稳定跑多久。
证书管理要按月检查,推荐写个定时任务,每两周输出一次kubeadm certs check-expiration的结果,通知到期时间。
etcd 备份这件事单 Master 也要做。执行:
etcdctl --endpoints=127.0.0.1:2379 snapshot save /backup/etcd-snapshot.dbetcd 数据是集群唯一真源,丢了它,kubectl 里所有配置、状态全部清空。没有备份的集群和有备份的集群,故障恢复时间差一轮数量级。
最后说一句:单 Master 集群的边界要心里有数。控制面节点挂了,整个集群的管理面就瘫了,已有的业务 Pod 虽然还在跑,但无法调度、无法伸缩、无法更新。所以这类集群不要承载核心生产业务,更适合作为预发验证、功能测试、技术培训的场景。等真正跑熟了这一套部署流程,再往多控制面或托管集群迁移,基本上就是在现有知识上做增量,不会有太多学习成本。
我个人踩过最多坑的还是网络插件和 cgroup 驱动这两个点,每次换环境第一件事就是确认SystemdCgroup和 Pod 网段的一致性。把这一拍想明白了,后面就是机械操作,基本不会翻车。