在已有十几个微服务的内网环境里,我最近又把 Kubernetes 高可用集群用 Kubeadm 完整地搭了一遍。为什么说“又”?因为这东西看着是照着官方文档敲命令,但真落到生产,从版本对齐、镜像来源、容器运行时适配到前端的负载均衡层,每一步都能卡住一批人。这篇就把我这次用 Kubeadm + Docker(注意,是 cri-dockerd 适配后的 Docker,不是 K8s 1.24 之前那种直连方式)搭建多 master 高可用集群的完整过程记录下来,包括架构设计、参数选择、完整命令和最后的高可用故障演练,希望能帮你少走几条弯路。
1. 这套方案的设计取舍:为什么是 Kubeadm + Docker + HAProxy
很多人一提到生产级 K8s 就想到云厂商托管、RKE2、k3s 或者二进制部署。但在裸金属或内网环境里,Kubeadm 依然是官方推荐、社区资料最全、排错手段最透明的方案。二进制部署灵活,但证书签发、静态 Pod 编排、etcd 集群引导这些全都要手做,维护成本高;k3s 把内置 etcd、SQLite、sqlite 的开关全封装了,适合边缘,但生产场景要精细调整组件参数时反而绕。Kubeadm 的好处是能用一组声明式配置文件把 kube-apiserver、kube-controller-manager、kube-scheduler 和 etcd 装成静态 Pod,结构清清楚楚,出问题也容易回溯。
容器运行时选型上,标题既然写的是 Docker,那我把话说透:Kubernetes 从 1.24 起正式移除了 dockershim,kubelet 不再直接认 docker.sock。想保留 Docker 工作流,必须在 Docker 之上再跑一个 cri-dockerd 把 CRI 请求转换成 Docker API。我在生产环境里实际这么干了,cri-dockerd 0.3.x 在 K8s 1.28 下运行很稳定,Docker 的 build、tag、save、load 流程也照常用,团队迁移成本最低。
高可用入口选了 HAProxy + Keepalived,而不是云厂商 LB,原因很简单:内网环境里没有云 LB 可用,硬件 F5 又贵。HAProxy 在用户态做四层转发,性能足够支撑中小规模集群的 API Server 流量;Keepalived 提供 VIP 漂移,保证前端入口和 kubelet 里的 apiserver 地址不会因为某台机器挂掉而失效。这套组合的好处是纯软件、无状态、配置透明,K8s 控制面本身并不关心 VIP 是怎么来的,它只要求 controlPlaneEndpoint 能稳定指向某一个可访问的 API Server 地址。
网络插件选了 Calico 而不是 Flannel,是因为 Calico 可以按需开启 NetworkPolicy,天然支持 BGP 路由和 VXLAN 跨子网封装,后续如果做跨集群互联或策略隔离,不用再换插件。Flannel 更简单,但功能边界明显,不适合我这种后续要上策略的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主机与网络规划:角色分配、网段设计、版本对齐
2.1 主机角色与资源规划
高可用集群的最基本要求是控制面不能单点,etcd 至少要能凑出多数派。所以控制节点我用了 3 台,工作节点先放 3 台,后续随时扩容。具体规划如下:
| 角色 | 主机名 | IP 地址 | 配置 |
|---|---|---|---|
| 负载均衡主节点 | lb1 | 10.0.0.10(VIP) | 4C8G |
| 负载均衡节点 | lb2 | 10.0.0.9 | 4C8G |
| 控制节点1 | master1 | 10.0.0.11 | 8C16G |
| 控制节点2 | master2 | 10.0.0.12 | 8C16G |
| 控制节点3 | master3 | 10.0.0.13 | 8C16G |
| 工作节点1 | worker1 | 10.0.0.21 | 8C16G |
| 工作节点2 | worker2 | 10.0.0.22 | 8C16G |
| 工作节点3 | worker3 | 10.0.0.23 | 8C16G |
说明一下,实战里负载均衡节点也可以和 master 复用,但组件多一层抢占损耗,还是分开更干净。所有节点操作系统我统一用 Ubuntu 22.04 LTS,内核 5.15,兼容性最好;CentOS 7 的内核版本和 iptables 行为差异较大,容易被坑。
2.2 网段设计与端口规划
K8s 集群内部至少涉及两层虚拟网络:Pod 网段和 Service 网段。这两个网段不能和物理机房网段冲突,否则路由会被内网核心交换机直接吞掉,导致 Pod 间通信黑洞。
- Pod 网段:
/16 - Service 网段:
/16 - 物理管理网段:
10.0.0.0/24
Pod 网段决定了集群最多能放多少 Pod,Calico 默认按 /26 划分 block,/16 够用;Service 网段是虚拟 IP,只存在 kube-proxy 的转发表里,尺寸可以小一点。
控制面组件需要放行的端口也不要漏:
- kube-apiserver:6443,控制平面之间、worker 访问、负载均衡健康检查都要放行
- etcd:2379(客户端)、2380(peer 通信),只在 master 之间互通
- kubelet:10250,所有节点之间都要通
- NodePort 端口段:30000~32767,按业务需要放行
- HAProxy 监听:6443,要和 apiserver 同一端口,前端 client 只用这一个入口
2.3 版本对齐的硬约束
版本是最容易出错的地方。Kubeadm、kubelet、kubectl 三个二进制必须使用完全相同的版本,Docker 和 cri-dockerd 也有对应的兼容矩阵。我这次用的组合是:
| 组件 | 版本 |
|---|---|
| 操作系统 | Ubuntu 22.04 |
| Docker | 24.0.7 |
| cri-dockerd | 0.3.9 |
| Kubernetes | 1.28.2 |
| kubeadm / kubelet / kubectl | 1.28.2-00 |
| Calico | 3.26.4 |
| 网络模式 | VXLAN CrossSubnet |
提示:Docker 版本不要盲目追新,cri-dockerd 的兼容列表是固定的,如果你在 join 阶段看到 cri-dockerd 报错的日志,大概率是版本组合不在支持矩阵里。先在官方 release 页确认组合再动手。
3. 基础环境配置:内核参数、Docker 安装、cri-dockerd 接入
3.1 所有节点统一初始化
这一步很多人会漏,但几乎决定了后续 kubeadm init 会不会莫名其妙失败。在 所有节点(包括 lb 节点)上执行:
bash复制# 关闭 swap,kubelet 默认不允许 swap 开启
swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab
# 加载内核模块
cat <<EOF | tee /etc/modules-load.d/k8s.conf
br_netfilter
overlay
EOF
modprobe br_netfilter
modprobe overlay
# 配置内核转发参数
cat <<EOF | 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
sysctl --system
net.ipv4.ip_forward 是 Kubernetes Service 转发的基础;br_netfilter 让 iptables 规则能作用于 Linux 网桥,这直接影响 later 阶段 Pod 间通信是否能通。swap 更是 kubelet 的硬门槛,不关掉,kubelet 会直接拒绝启动,日志里写得很清楚。
3.2 安装 Docker 并配置镜像加速
Docker 安装我直接用阿里云 Docker CE 源,内网环境可以提前把 deb 包拉到本地,避免每台机器逐一下载:
bash复制apt-get update
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce=5:24.0.7-1~ubuntu.22.04~jammy docker-ce-cli containerd.io docker-compose-plugin
安装完先改 daemon.json,把 cgroup driver 切到 systemd,同时加镜像加速:
json复制{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"registry-mirrors": ["https://docker.m.daocloud.io"]
}
cgroup driver 必须和 kubelet 保持一致。Kubernetes 官方推荐 systemd,因为它和 systemd 的资源管理不会打架,防止双写 cgroupfs。镜像加速地址你换成自己能访问的即可,关键是让 docker pull 不卡死。
3.3 安装 cri-dockerd 并把 kubelet 指向它
cri-dockerd 的作用是接住 kubelet 发来的 CRI 请求,再转成 Docker API。没有它,kubelet 无从发现 Docker 里运行的容器。
bash复制# 下载 cri-dockerd 二进制包,版本要和 K8s 对齐
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.9/cri-dockerd-0.3.9.amd64.tgz
tar xvf cri-dockerd-0.3.9.amd64.tgz
install -m 755 cri-dockerd/cri-dockerd /usr/local/bin/cri-dockerd
# 生成 systemd unit 文件
cat > /etc/systemd/system/cri-docker.service <<EOF
[Unit]
Description=CRI Interface for Docker Application Container Engine
Documentation=https://docs.mirantis.com
After=network-online.target firewalld.service docker.service
Wants=network-online.target
Requires=cri-docker.socket
[Service]
Type=notify
ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint unix:///var/run/cri-dockerd.sock --pod-infra-container-image registry.aliyuncs.com/google_containers/pause:3.9
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
StartLimitBurst=3
StartLimitInterval=60s
LimitNOFILE=infinity
LimitNPROC=infinity
InheritEnvironment=yes
Delegate=yes
[Install]
WantedBy=multi-user.target
EOF
cat > /etc/systemd/system/cri-docker.socket <<EOF
[Unit]
Description=CRI Docker Socket for the API
PartOf=cri-docker.service
[Socket]
ListenStream=/var/run/cri-dockerd.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
[Install]
WantedBy=sockets.target
EOF
systemctl daemon-reload
systemctl enable --now cri-docker cri-docker.socket
这里我把 pause 镜像显式指定成了阿里云镜像仓库里的 pause:3.9,避免 kubelet 默认去 registry.k8s.io 拉 pause 镜像时超时。这个镜像常被忽略,但它是每个 Pod Sandbox 都依赖的基础镜像。
3.4 安装 kubeadm、kubelet、kubectl
所有集群节点都要装这三个二进制:
bash复制curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
echo "deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-cache madison kubeadm | grep 1.28.2
apt-get install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00
apt-mark hold kubelet kubeadm kubectl
apt-mark hold 一定要做,否则哪天系统 apt upgrade 会把 kubectl 升级到不兼容版本,node 版本不齐会导致集群状态告警。这个坑我亲眼见过,整个 node 的 kubelet 版本变成 1.29,其余都是 1.28,kube-apiserver 里不断刷版本不一致错误。
4. 高可用入口:Keepalived + HAProxy 两级守护
4.1 为什么需要两级组件而不是一个 IP
控制面多 master 后,kubelet、kubectl、kube-proxy 都要访问 API Server,它们不能只连某一台 master 的 6443,否则那台挂了,整个集群控制面入口就断了。所以它们必须指向一个 VIP。但 VIP 本身只是 IP 地址,没有服务监听是不行的,HAProxy 就是挂在 VIP 后面的四层转发器,把 6443 流量轮询转发到 3 台 master。
Keepalived 负责让 VIP 在 HAProxy 故障时自动漂移。两台 lb 节点上各跑一个 keepalived 实例,正常情况下 VIP 在 lb1,lb1 挂掉后 lb2 接管,客户端仍然访问同一个 IP。
4.2 HAProxy 配置
在 lb1 和 lb2 上安装并配置:
bash复制apt-get install -y haproxy keepalived
HAProxy 配置 /etc/haproxy/haproxy.cfg:
text复制global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend k8s-api
bind *:6443
mode tcp
default_backend k8s-master
backend k8s-master
mode tcp
balance roundrobin
option httpchk GET /livez
server master1 10.0.0.11:6443 check fall 3 rise 2
server master2 10.0.0.12:6443 check fall 3 rise 2
server master3 10.0.0.13:6443 check fall 3 rise 2
注意健康检查用的是 /livez,这是 kube-apiserver 自带的存活探针路径,比 TCP 端口探测更准确。TCP 探测只知道端口开着,但 apiserver 如果卡死,端口也可能一直处于监听状态。
4.3 Keepalived 配置
Keepalived 配置里最关键的是健康检查脚本,脚本只检查 HAProxy 进程,不检查整机存活:
bash复制cat > /etc/keepalived/check_haproxy.sh <<'EOF'
#!/bin/bash
if pgrep -x haproxy >/dev/null; then
exit 0
else
exit 1
fi
EOF
chmod +x /etc/keepalived/check_haproxy.sh
Keepalived 配置文件 /etc/keepalived/keepalived.conf:
text复制global_defs {
router_id LVS_K8S
}
vrrp_script check_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight 2
}
vrrp_instance VI_K8S {
state BACKUP
interface ens18
virtual_router_id 51
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass k8s-ha-pass
}
virtual_ipaddress {
10.0.0.10/24 dev ens18
}
track_script {
check_haproxy
}
}
两台 lb 节点的 priority 可以一高一低,比如 lb1 是 100,lb2 是 99,正常情况下 VIP 在 lb1。两边都要用 nopreempt,这样就算 lb1 恢复也不会把 VIP 抢回来,避免频繁切换带来连接抖动。这个细节是我从生产故障里学到的,默认抢占模式下核心理由是新 master 加入时若 VIP 来回跳,HAProxy 连接会被重置。
配置完成后先启动 haproxy,再启动 keepalived:
bash复制systemctl enable --now haproxy
systemctl enable --now keepalived
验证方式:
bash复制ip addr show | grep 10.0.0.10
curl -k https://10.0.0.10:6443/livez
/livez 返回 200 OK,说明入口层已经站在你这边了。
5. 第一个控制节点初始化:kubeadm init 与镜像准备
5.1 准备 kubeadm 配置文件
不要在 master1 上直接跑一长串参数版 kubeadm init,把配置写进 YAML 里,后面加第二、第三个 master 和排错时都能复用。我用的 kubeadm-config.yaml:
yaml复制apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.28.2
imageRepository: registry.aliyuncs.com/google_containers
controlPlaneEndpoint: "10.0.0.10:6443"
networking:
podSubnet: "/16"
serviceSubnet: "/16"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
containerRuntimeEndpoint: unix:///var/run/cri-dockerd.sock
controlPlaneEndpoint 就是这次高可用的灵魂,它让所有组件都通过 VIP 访问 API Server,而不是直连 master1 的 IP。imageRepository 指向阿里云镜像仓库,解决国内网络拉取 registry.k8s.io 镜像超时的问题。
5.2 预拉取镜像
kubeadm init 时如果镜像没拉好,它会卡在镜像拉取阶段等很久,最后超时。我习惯先手动把镜像拉下来:
bash复制kubeadm config images pull --config=/etc/kubernetes/kubeadm-config.yaml
这一步会拉 apiserver、controller-manager、scheduler、etcd、pause 和 coredns 镜像。拉完用 docker images 确认一下,看到 REPOSITORY 都是 registry.aliyuncs.com/google_containers/ 开头就对了。
5.3 执行 init
bash复制kubeadm init --config=/etc/kubernetes/kubeadm-config.yaml --upload-certs
--upload-certs 会把控制面证书上传到 kubeadm 管理的 secret 里并生成一个 certificate key,后面其他 master join 时可以直接用这个 key,而不用手动拷贝证书文件。但注意这个 key 默认只有 2 小时有效期,超期后重新执行 kubeadm init phase upload-certs --upload-certs 获取新 key。
init 成功后会输出三段关键内容:
- 加入 worker 节点的完整
kubeadm join命令 - 加入控制面节点的
kubeadm join --control-plane命令和--certificate-key - 下一步配置 kubectl 的提示
把这两条 join 命令保存下来,后面要用。如果当时没存,下面看第 6.2 节怎么找回。
初始化完成后配置 kubectl:
bash复制mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
先验证单节点状态:
bash复制kubectl get nodes
此时 master1 应该处于 NotReady,这是正常的,因为网络插件还没装,容器能跑但节点的 CNI 配置还没有,Pod 拿不到 IP。
6. 其余控制节点和 worker 节点加入集群
6.1 加入第二个、第三个控制面节点
在 master2 上,如果 init 输出里的 certificate key 还在有效期内,直接执行;如果过期了,在 master1 上重新拿 key:
bash复制# master1 上执行,生成新的 certificate key
kubeadm init phase upload-certs --upload-certs
然后在 master2 上:
bash复制kubeadm join 10.0.0.10:6443 \
--token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH> \
--control-plane \
--certificate-key <CERTIFICATE_KEY>
--discovery-token-ca-cert-hash 是集群 CA 的指纹,保证了 join 时不会连到冒名集群。token 过期了可以重新创建:
bash复制kubeadm token create --print-join-command
这条命令会把当前 token 和 hash 拼好直接打印出来,复制即可。master3 重复同样的操作。
join 之后在 master1 上确认:
bash复制kubectl get nodes
kubectl get pods -n kube-system -o wide
如果 join 时报 connection refused 或者卡在等待 apiserver,多半是 6443 端口被防火墙挡了,或者 VIP 的 6443 没有转发到别的 master。
6.2 worker 节点加入
worker 节点不需要传到证书,只需要执行不带 --control-plane 的 join:
bash复制kubeadm join 10.0.0.10:6443 \
--token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH>
加入后你会发现 node 状态还是 NotReady,因为此时只装了 kubelet,没有任何网络插件。所以下一步必须在整个集群层面装好 Calico 之前,worker 节点的 kubelet 会反复启动失败吗?不会,它只是报 CNI 未配置,这是预期行为。
worker join 完成后,在 master1 上看到三台 master、三台 worker 全部出现在 kubectl get nodes 列表里,但状态是 NotReady。此时继续部署 Calico。
7. Calico 网络插件部署与网络策略验证
7.1 用 Operator 方式安装 Calico
我不推荐直接 apply 一个大而全的 manifest,出了问题反而不好拆。用 Operator 方式安装,后续升级和故障排查都有现成 CRD 可以看。
在 master1 上:
bash复制kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.4/manifests/tigera-operator.yaml
然后写 calico-custom-resources.yaml:
yaml复制apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
ipPools:
- name: default-ipv4-ippool
blockSize: 26
cidr: /16
encapsulation: VXLANCrossSubnet
natOutgoing: true
nodeSelector: all()
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
name: default
spec: {}
这里面有两个细节必须和 kubeadm 配置对齐:
cidr必须等于 kubeadm 配置里的podSubnet,否则 Calico 会创建和集群预期完全不相干的网络encapsulation: VXLANCrossSubnet意思是跨子网流量走 VXLAN 封装,同子网直接走 BGP 路由,兼顾性能和二层网络灵活性
如果物理网络本身是三层隔离的,也可以用 VXLAN 全封装模式,但性能和故障定位都会差一些。
7.2 验证节点 Ready
等待片刻后:
bash复制kubectl get pods -n calico-system
kubectl get nodes
一个大前提要知道:Calico 的 Pod 全部 Running 后,节点才会从 NotReady 变成 Ready。如果 Calico 的 calico-node 一直 CrashLoopBackOff,最常的原因是 IP 池配置和 kubeadm 的 podSubnet 不一致。
此时再检查一下 kubectl 是否能正常读取集群状态:
bash复制kubectl get nodes -o wide
看到所有节点 Ready 后,才算是集群真正建网成功。
7.3 快速做一次 Pod 网络连通性测试
创建一个临时的测试负载:
bash复制kubectl create deployment nginx-test --image=nginx:1.25
kubectl expose deployment nginx-test --port=80
kubectl run test-pod --image=busybox:1.36 --rm -it -- sh
在进入容器后:
sh复制wget -O- http://<nginx-test-pod-ip>:80
能拿到 Welcome to nginx! 就说明 Pod 网段和 kube-proxy 的转发链路是通的。如果失败,先看 kube-proxy 的日志,再确认节点之间的 8472(VXLAN)或 179(BGP)端口放行。
8. 高可用实测演练:拔线、停服、VIP 漂移
8.1 演练前先确认当前 VIP 所属节点
在 lb1 上执行:
bash复制ip addr show | grep 10.0.0.10
确认 VIP 在 lb1。然后在 lb2 上执行相同命令,如果也能看到 VIP,那就出现了脑裂,检查 keepalived 配置里的 advert_int 和 auth_pass 是否一致。
8.2 模拟控制面节点故障
我把 master1 直接关机,模拟整机掉电。此时观察:
- apiserver 是否仍然可用:在任意一台能执行 kubectl 的机器上
kubectl get nodes,如果 VIP 已经漂移且 HAProxy 还有存活后端,命令正常返回 - VIP 是否漂移:lb2 上
ip addr show | grep 10.0.0.10,出现则说明漂移成功 - etcd 集群是否选主:由于是 3 节点 etcd,master1 挂掉后剩余 2 节点仍占多数派,可以正常对外服务,但要注意此时系统已经处于“只剩一票冗余”的状态,不能再接受第二台 master 宕机
- API 负载均衡是否继续工作:再用 curl 请求 VIP 的
/livez,返回 200
等 master1 恢复后,kubelet 会重新拉起静态 Pod,节点重新加入 etcd 集群。整个过程中 VIP 不应该发生反复跳变,这是因为我们配置了 nopreempt。
8.3 模拟负载均衡节点故障
在 lb1 上停掉 haproxy:
bash复制systemctl stop haproxy
看 keepalived 的健康检查脚本是否触发:
bash复制tail -50 /var/log/syslog | grep Keepalived
正常会看到 VRRP 状态从 MASTER 切到 BACKUP,lb2 在几秒内接管 VIP。整个过程中 kubectl 的 API 请求几乎无感,因为 Kubeadm 生成的 client 配置本身就是长连接,HAProxy 做了连接复用。
这次演练之前我和同事都觉得简单,但实际发现健康检查脚本只检查 haproxy 进程会有个隐患:如果 haproxy 进程死锁但没退出,脚本会误判,VIP 不漂移。所以后来我把脚本升级成 curl 检查 VIP 上的 6443 端口:
bash复制#!/bin/bash
curl -k -s --connect-timeout 3 https://10.0.0.10:6443/livez >/dev/null 2>&1 || exit 1
改成端口级健康检查后,故障识别的准确率高很多。
9. 实施中遇到的坑与排查思路
9.1 kubelet cgroup driver 不一致
典型的报错是:
text复制failed to run Kubelet: misconfiguration: kubelet cgroup driver: "cgroupfs" is different from docker cgroup driver: "systemd"
这是 Docker 的 native.cgroupdriver=systemd 和 kubelet 的 cgroupDriver: systemd 没有同时配置导致的。我的做法是 kubeadm 配置文件里显式写了 cgroupDriver: systemd,Docker 的 daemon.json 也保持一致,两边都以 systemd 为准。
9.2 kubeadm init 卡在拉取镜像
这几乎是首次部署最容易碰到的问题。如果没配置 imageRepository,kubeadm 默认从 registry.k8s.io 拉镜像,网络不通就一直卡住。我之前的解决方式是在 kubeadm 配置里指定阿里云镜像仓库,并且提前用 kubeadm config images pull 预拉。
9.3 镜像拉取慢但不确定是哪个镜像
可以用下面的方式直接看当前节点已经拉取了哪些镜像、缺哪些:
bash复制docker images | grep google_containers
kubeadm config images list --config=/etc/kubernetes/kubeadm-config.yaml
两边对比,缺哪个就单独 docker pull 哪个。
9.4 node 一直是 NotReady 且 calico-node 报错
先看 Pod 日志:
bash复制kubectl logs -n calico-system daemonset/calico-node
如果是 BIRD is not ready,大概率是节点间 179 端口没放行。如果是 IP 地址分配失败,检查 Calico IPPool 的 cidr 和 kubeadm 的 podSubnet 是否一致。
9.5 etcd 集群健康检查
控制面集群运行时间长了以后,我会定期检查 etcd 健康:
bash复制kubectl get --raw='/healthz?verbose'
如果能拿到 healthz check passed,说明 etcd、apiserver 等组件都是 OK 的。如果出现 etcd server: request timed out,需要立刻看 etcd Pod 的 CPU 和磁盘 IO,etcd 对磁盘延迟极其敏感,慢盘会拖垮整个控制面。
9.6 证书过期与续期
Kubeadm 自签证书默认一年有效期。控制面节点的证书在过期前要手动续期:
bash复制kubeadm certs renew all
注意续期后要重启 kubelet 和静态 Pod,让 apiserver、controller-manager、scheduler 重新加载新证书。我这里踩过一次:续了证书忘了重启静态 Pod,结果 apiserver 还在用旧证书,客户端已经开始报证书过期。
9.7 节点 IP 与主机名不一致导致 kubelet 注册失败
Kubelet 默认用主机名注册节点,如果机器的主机名和 DNS 解析不一致,或者主机名含大写字母,kubelet 注册时可能创建出多个节点记录。我的做法是规划阶段就统一主机名:
bash复制hostnamectl set-hostname master1
并且 /etc/hosts 里加上所有节点的 IP 和主机名映射,保证每台机器解析结果一致。
10. 最后的经验与小技巧
集群铺完到现在跑了几周,最深的体会有两条。第一条,高可用不是搭完就算数,而是把故障场景实际演一遍,我上面提到的拔线演练请一定要做,只停留在文档层面的高可用根本算不上高可用。第二条,整个部署过程中尽量把配置收敛到几个 YAML 文件里,kubeadm-config.yaml、calico-custom-resources.yaml、haproxy.cfg、keepalived.conf,这些文件要放进配置管理仓库。我这次能快速复现环境和排错,全靠把当时的配置原样存了下来。
另外分享一下我现在每次部署都会做的事:在 worker 节点上加一个 /etc/systemd/system/kubelet.service.d/90-local.conf,写入节点专用参数,比如单独给 kubelet 设置 --max-pods=110 或设置 --node-labels,这样后续扩容时只需要调整这个文件,不用改 kubeadm 设置。小改动,但排错的时候能少绕很多弯。
如果以后要在这套集群上继续做东西,我建议下一步优先把监控补齐,比如 kube-prometheus 套件,再配合 Loki 采集日志。只有让集群状态可视化,高可用才不再是“看上去可用”。
