1. 为什么“kubeadm 1.23.0 高可用集群 + Docker”仍然值得照着做
很多人看到 kubeadm、Kubernetes、1.23.0 这几个词,第一反应是“版本有点老了吧”。但从技术演进来看,1.23.0 正好踩在一个关键节点上:Kubernetes 从 1.24 起移除了内置的 Dockershim,也就是说 1.23.x 是最后一个可以“Docker 直接作为容器运行时、kubelet 原生对接”的稳定序列。这并不代表 1.23.0 是淘汰版本,恰恰相反,现在生产环境里仍有大量存量集群跑在 1.23 上做长期维护,很多新项目为了兼容旧镜像和内部镜像仓库,也愿意把版本锁定在这一代。
这篇内容围绕的是,如何用 kubeadm 搭出一套控制平面可用的高可用集群,底层容器运行时用 Docker,而不是 containerd 或 cri-o。我会按真实的操作顺序来写:从架构规划、基础环境、负载均衡、Docker 与 kubelet 初始化,到后面 Master 副本加入、Worker 加入、网络插件落地和故障验证,全部是踩过坑之后确认可行的步骤。适合三类人看:一是刚接手 1.23 存量集群的运维,二是想在生产环境里复刻一套多控制面集群的工程师,三是正在做 Kubernetes 高可用认证或面试准备、需要一个完整实验过程的人。
为什么要把版本定在 1.23.0 而不是 1.23.17 这种修复版本?从务实角度看,kubeadm 在 1.23.0 上的集群初始化流程已经非常成熟,更高的小版本主要是安全补丁和稳定性修复。如果只是学习或验证高可用机制,1.23.0 足够;如果是生产环境,我更建议你初始化时仍用 v1.23.0,随后再通过 kubeadm upgrade 推到 1.23.17。原因是 1.23.0 的安装文档、配置样例最多,遇到问题最容易搜索到同类报错。
1.1 高可用集群到底在解决什么问题
集群高可用不是说“只要 Master 有 3 台就行了”,它要解决两类问题。
第一类是 API Server 入口可用。kubectl、kubelet、scheduler、controller-manager 都要访问 kube-apiserver,只要这个入口挂了,整个控制平面基本就失去响应。所以我们需要一个虚拟 IP 和负载均衡层,把请求分散到多台 Master 的 API Server 上。某个 API Server 故障时,负载均衡器自动摘除,请求不会中断。
第二类是控制平面组件数据一致。kubeadm 默认创建的是堆叠 etcd,也就是每一台 Master 上既跑管理组件,也跑一个 etcd 节点。三个 etcd 节点组成一个小集群,写入需要多数派同意。所以生产环境至少要有 3 台 Master,挂掉 1 台,剩余 2 台仍然满足多数派要求,集群可以继续工作。如果只有 2 台 Master,挂 1 台后只剩 1 个 etcd,连读都困难,更谈不上自愈。
1.2 版本选择时最容易忽略的 Docker 兼容性窗口
Docker 版本本身也在迭代。Kubernetes 1.23.0 官方测试过的 Docker 版本是 20.10.x,所以初始化过程中不要图新鲜装 Docker CE 24 或 25 那种很新的版本。虽然很多时候也能跑通,但 dockershim 与新版 Docker 的集成情况并不在官方兼容矩阵内,遇到“容器创建超时”这类诡异问题很难定位。建议统一装 Docker CE 20.10.x 的最后一个版本,也就是 20.10.24,稳妥且验证充分。
额外说一句,如果未来要升级到 1.24 以上,不是简单把 kubeadm 版本升上去就行,容器运行时侧必须先做迁移,要么使用 cri-dockerd 做兼容桥接,要么直接换 containerd。现在用 1.23.0 + Docker 搭集群,等于是给自己留了一个相对明确的迁移窗口,而不是在边界状态里硬抗。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高可用架构与 IP 规划:先把三层入口想清楚
动手敲命令之前,我习惯先把整个拓扑画出来。没有拓扑就开始初始化,后面 join 的时候多半会卡在证书、Endpoint、VIP 不通这些问题上。
我这里规划的拓扑是经典的堆叠 etcd 高可用结构:
- 3 台 Master:运行 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、HAProxy、Keepalived
- 2 台 Worker:运行业务 Pod、kubelet、kube-proxy
- 1 个虚拟 IP:作为 kube-apiserver 的统一访问入口,也作为所有 Master 节点 kubelet 连接 API Server 的地址
- Docker 作为容器运行时,socket 使用默认的 /var/run/dockershim.sock
这种布局下,HAProxy 和 Keepalived 直接跑在 Master 节点上,不额外占用两台 LB 机器。虚拟 IP 在 3 台 Master 之间漂移,哪台是 Keepalived 的主节点,虚拟 IP 就在哪台上。请求到达虚拟 IP 后,由本机的 HAProxy 转发到任意一台存活的 API Server。
2.1 堆叠 etcd 与外部 etcd:kubeadm 场景下的选择
理解 etcd 位置很关键。kubeadm 支持两种高可用形态:堆叠 etcd 和外部 etcd。
堆叠 etcd 的好处是部署简单,kubeadm init 一把起,不用单独管理 etcd 集群。坏处是控制平面与存储强耦合,Master 整机宕机时,该节点上既没有 apiserver 也没有 etcd;但只要超过一半的 Master 正常,集群就能继续工作。外部 etcd 则是把 etcd 独立部署在 3 台机器上,与 Kubernetes 控制面相分离,维护成本明显高,通常用于超大规模或对存储稳定性有极高要求的场景。
新手上手或者中小规模生产,我建议直接用 kubeadm 默认的堆叠 etcd。除非你已经有独立的 etcd 运维标准,否则不要一上来就搞外部 etcd,那不是高可用理解上的第一优先级,复杂度反而会掩盖很多基础问题。
2.2 一个可以直接复制的主机规划表
我用 192.168.26.0/24 这个网段做演示,实际部署时根据你的环境替换。所有节点要求能互相 ping 通,并且要开对应端口,或者干脆在实验环境关掉 firewalld。
| 主机名 | IP 地址 | 角色 | 配置建议 |
|---|---|---|---|
| k8s-master-1 | 192.168.26.31 | Master / etcd / HAProxy / Keepalived | 4C8G |
| k8s-master-2 | 192.168.26.32 | Master / etcd / HAProxy / Keepalived | 4C8G |
| k8s-master-3 | 192.168.26.33 | Master / etcd / HAProxy / Keepalived | 4C8G |
| k8s-worker-1 | 192.168.26.34 | Worker | 按业务评估 |
| k8s-worker-2 | 192.168.26.35 | Worker | 按业务评估 |
| 虚拟 IP | 192.168.26.30 | kube-apiserver 访问入口 | 不落在具体网卡配置里 |
这里要注意一点,虚拟 IP 不要与任何真实节点 IP 冲突,也不要设置在 DHCP 自动分配池里,否则容易在节点重启后出现地址被占用的情况。我在实验环境里习惯单独预留一个 30 号地址给 VIP,就是不想跟其他服务抢。
2.3 API Server 入口为什么要放在 VIP,而不是客户端直连
客户端直接连接 192.168.26.31:6443 也可以工作,但那不叫高可用。万一这台 Master 宕机,你所有 kubectl 命令和集群内组件的连接都会断,需要手动改配置。使用 VIP 之后,请求始终打到 192.168.26.30:6443,Keepalived 负责让 VIP 在存活节点间漂移,HAProxy 负责把流量转发到某一台真实的 API Server。客户端视角里,地址永远不变,后端节点却可以随时故障切换。
kubeadm 提供的 controlPlaneEndpoint 参数就是干这个用的。初始化时如果指定了这个地址,所有证书、kubeconfig、组件连接都会使用这个稳定入口,而不是绑定某一台 Master 的 IP,这是整个高可用集群能不能成立的基础。
3. 基础环境三件套:主机名、内核模块与版本锁定
不管是 Master 还是 Worker,节点基础环境必须一致。我在多次部署中发现,很多问题不是 kubeadm 命令写错,而是某台机器没关 swap、没做主机名映射、内核模块没加载,导致 kubelet 或网络组件行为异常。这部分建议 5 台机器全部执行一遍,最好写成脚本。
3.1 主机名、hosts 与时间同步
先设置主机名和 hosts。Kubernetes 组件之间通过主机名互相识别,没有 DNS 的情况下,/etc/hosts 必须做得干净。
bash复制# 每台机器分别执行
hostnamectl set-hostname k8s-master-1 # 其他节点分别改成自己的名字
然后在所有节点维护一份统一的 hosts 内容。如果你们内部有 DNS,可以不加,但实验环境加一下会更省事。
bash复制cat >> /etc/hosts <<EOF
192.168.26.31 k8s-master-1
192.168.26.32 k8s-master-2
192.168.26.33 k8s-master-3
192.168.26.34 k8s-worker-1
192.168.26.35 k8s-worker-2
192.168.26.30 k8s-lb
EOF
时间同步是很多人忽略但影响巨大的项。etcd 对时钟偏移极其敏感,Master 之间时间差太大会直接导致 Raft 心跳异常,甚至出现证书验签失败。建议用 chrony 做同步:
bash复制yum install -y chrony
systemctl enable --now chronyd
chronyc sources
3.2 关闭 swap、开放内核转发、加载 IPVS 模块
kubelet 默认要求 swap 为 0,如果有 swap 而不关闭,初始化时 kubelet 会一直起不来。关闭 swap 要同时处理 /etc/fstab,否则重启后 swap 又回来了。
bash复制swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab
再写入 Kubernetes 要求的内核参数。这些参数主要解决两个问题:一个是 bridge 网络流量要经过 iptables,另一个是开启 IP 转发,让 Pod 与 Service 之间能够路由。
bash复制cat > /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
modprobe br_netfilter
sysctl --system
如果想让 kube-proxy 使用 IPVS 模式,顺手把常用 IPVS 模块也加载掉:
bash复制cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash
modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack_ipv4
EOF
chmod +x /etc/sysconfig/modules/ipvs.modules
bash /etc/sysconfig/modules/ipvs.modules
实验环境如果不想纠结网络栈,用默认 iptables 模式也能跑。生产环境我更建议开 IPVS,因为 Service 数量多的时候,iptables 规则链会变得非常长,转发效率下降明显。
3.3 用仓库源锁定 kubeadm、kubelet、kubectl 版本
Kubernetes 的 yum 仓库基础地址可以选用云厂商提供的镜像源。下面以阿里云镜像为例,如果你在海外网络环境直接访问官方源也可以,命令结构一致。
bash复制cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=0
EOF
yum clean all
yum makecache
这里强调一点:kubelet、kubeadm、kubectl 三个 rpm 包必须全部锁定到 1.23.0,不能一个命令装 latest。三者的版本差会直接导致 init 或 join 时组件间通讯协议不匹配。
bash复制yum install -y kubelet-1.23.0 kubeadm-1.23.0 kubectl-1.23.0
装完立刻验证:
bash复制kubeadm version
kubelet --version
kubectl version --client
三条命令输出版本号后,再执行 systemctl enable kubelet,注意这里只需要 enable,不要马上启动。kubelet 在没有配置和证书之前是启动不起来的,会出现 crash 循环,这是正常现象,不用慌。
4. HAProxy + Keepalived:让 API Server 先有一个稳定入口
这层组件我习惯在初始化 Kubernetes 之前先配好,因为 kubeadm init 阶段 kubelet 要通过 controlPlaneEndpoint 连接 API Server,如果 VIP 后端的负载均衡还没准备好,初始化会卡在等待 API Server 健康检查那一步。
4.1 HAProxy 为什么只做四层转发
kube-apiserver 对外通信既有 HTTP 请求,也有大量基于 TLS 的长连接,比如 kubelet 上报状态、kubectl exec 这类流式请求。HAProxy 工作在四层 TCP 模式,不会去解析上层 HTTP 内容,直接把 TCP 流量原样转发到 6443 端口,这是最稳妥的方式。对于 Kubernetes API Server,完全不需要七层负载均衡器去理解请求路径,反而会增加代理复杂度和故障点。
3 台 Master 上都要安装 HAProxy,因为虚拟 IP 漂移到哪台机器,那台机器就必须能承担转发任务。
bash复制yum install -y haproxy keepalived
4.2 三台机器一致的 HAProxy 配置
配置文件统一放在 /etc/haproxy/haproxy.cfg。最简配置如下:
bash复制cat > /etc/haproxy/haproxy.cfg <<EOF
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
defaults
log global
mode tcp
option tcplog
option dontlognull
retries 3
timeout connect 10s
timeout client 300s
timeout server 300s
listen kube-apiserver
bind 0.0.0.0:6443
mode tcp
balance roundrobin
server k8s-master-1 192.168.26.31:6443 check fall 3 rise 2
server k8s-master-2 192.168.26.32:6443 check fall 3 rise 2
server k8s-master-3 192.168.26.33:6443 check fall 3 rise 2
EOF
bind 0.0.0.0:6443 表示本机所有 IP 上的 6443 端口都监听。这样 VIP 在 192.168.26.30 上时,流量也能被正常接收。如果不 bind 0.0.0.0 而是 bind 192.168.26.30:6443,在 Keepalived 切换 VIP 的一瞬间,旧节点上的 HAProxy 会丢失监听,新节点还没接管 VIP,中间就会出现几十毫秒的抖动,实际没有必要。
这里 timeout 值可以按需调大。kubectl exec、日志查看这类长连接如果经常断,优先怀疑 timeout client/server 设置太小。
三台机器的 HAProxy 配置完全一样,然后分别启动:
bash复制systemctl enable --now haproxy
ss -lntp | grep 6443
4.3 Keepalived 的健康检查脚本,比默认检测靠谱
Keepalived 的作用是维护虚拟 IP。只装 Keepalived 而不做健康检查,会出现一种尴尬情况:VIP 所在的机器 HAProxy 已经死了,但 Keepalived 进程还活着,VIP 不切换,集群全部请求失败。
所以我加了一个探测脚本,定时请求本机 HAProxy 背后的 API Server 健康检查接口:
bash复制cat > /etc/keepalived/check_apiserver.sh <<'EOF'
#!/bin/sh
curl -skf https://127.0.0.1:6443/healthz >/dev/null 2>&1
EOF
chmod +x /etc/keepalived/check_apiserver.sh
Keepalived 配置按主备优先级区分。我这里三台 Master 采用 BACKUP 模式加不同 priority,优先级最高的节点一开始会成为 VIP 持有者。全部用 BACKUP 而不是一台 MASTER,好处是配置更统一,切换逻辑也更简单。
bash复制cat > /etc/keepalived/keepalived.conf <<EOF
vrrp_script check_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 2
fall 3
rise 2
weight -50
}
vrrp_instance k8s-api {
state BACKUP
interface eth0
virtual_router_id 66
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass k8s_api_123
}
virtual_ipaddress {
192.168.26.30/24 dev eth0
}
track_script {
check_apiserver
}
}
EOF
注意 interface 要改成你自己的实际网卡名。云服务器有时默认网卡不是 eth0,可能是 ens5 或 enp1s0,用 ip addr 查一下再填。
还要注意,3 台机器的 priority 不能相同。比如 k8s-master-1 配 110,k8s-master-2 配 105,k8s-master-3 配 100。如果相同,VRRP 会认为配置异常,可能出现双 VIP 或频繁切换。
修改完配置后启动并观察:
bash复制systemctl enable --now keepalived
ip addr show eth0 | grep 192.168.26.30
如果 VIP 出现在 k8s-master-1 上,说明 Keepalived 工作正常。这时候从其他机器 curl 一下 VIP 上的健康检查接口:
bash复制curl -k https://192.168.26.30:6443/healthz
如果返回 ok,说明 HAProxy 已经把请求转发到了真实的 API Server。虽然现在 API Server 还没有初始化,所以大概率会连接拒绝,这是正常的,因为 HAProxy 的 backend 还没有监听端口。我通常会等到 Kubernetes 初始化完成后再做这一步验证。
5. Docker 与 kubelet 握手:cgroup driver 统一最容易被忽略
因为我们的目标是 Docker 版而不是 containerd 版,所以容器运行时使用 Docker。Kubernetes 1.23.0 还带着 dockershim,kubelet 可以直接通过 /var/run/dockershim.sock 与 Docker 通信,这是这套方案最省事的地方。
5.1 安装 Docker CE 20.10.x
所有节点统一安装 Docker CE,版本选 20.10.24。安装方式可以是官方 yum 源,如果拉取慢,换成自己云账号下的 Docker 镜像源。
bash复制yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce-20.10.24 docker-ce-cli-20.10.24 containerd.io
安装完成后先不要急着启动,先改 Docker 配置文件。最核心的是把 Docker 的 cgroup driver 改成 systemd,同时设置日志轮转,避免容器日志把磁盘写满。
bash复制mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"registry-mirrors": ["https://你的镜像加速器地址"]
}
EOF
systemctl enable --now docker
docker info | grep -i cgroup
看到 Cgroup Driver: systemd 就说明配置生效了。
为什么要改成 systemd?因为绝大多数现代 Linux 发行版的 init 进程都是 systemd,它默认把进程放在 systemd 的 cgroup 层级下。如果 kubelet 使用 systemd 作为 cgroup driver,而 Docker 仍然使用 cgroupfs,两者会分别管理同一组 cgroup,造成资源统计混乱。更严重的是,在节点内存压力较高时,systemd 和 kubelet 可能各自去 kill 对方认为不重要的进程。Kubernetes 官方从 1.22 开始就建议 kubelet 和容器运行时统一使用 systemd driver,这个建议同样适用于 1.23.0。
5.2 让 kubelet 也用 systemd cgroup driver
kubeadm 默认生成的 kubelet 配置并不一定把 cgroup driver 写成 systemd,所以需要提前在 /etc/sysconfig/kubelet 里加上参数:
bash复制cat > /etc/sysconfig/kubelet <<EOF
KUBELET_EXTRA_ARGS="--cgroup-driver=systemd"
EOF
这一步如果不做,kubeadm init 时可能出现 kubelet 报错,错误信息大致是:kubelet cgroup driver "cgroupfs" is different from docker cgroup driver "systemd"。这不是网络问题,也不是证书问题,而是 cgroup driver 不匹配。我在第一次部署时在这里卡了一个多小时,因为 kubeadm init 一直显示等待 kubelet 启动,实际看 kubelet 日志才发现是这个原因。
5.3 把 kubelet 设为开机启动,但不急着启动
所有节点都执行:
bash复制systemctl enable kubelet
注意 kubelet 在没有任何配置的情况下无法启动成功,会无限重启,这个是预期中的。不用手动 systemctl start kubelet,等到 kubeadm init 的时候,kubeadm 会自动拉起 kubelet 并生成配置。
