K8s 高可用集群搭建实战:Keepalived + HAProxy + Calico + MetalLB
目标:用 kubeadm 搭建一套「控制平面高可用 + 业务入口可暴露」的生产级 K8s 集群。 适用于:裸金属 / VMware 虚拟机环境(没有云厂商 LB 的场景)。
0. 为什么搭这套架构
单台 master 的集群有一个致命问题:apiserver 是集群大脑,它挂了,kubectl 全废、kubelet 失联、整个集群不可用。这就是"单点故障"。
高可用要解决两个单点:
| 单点 | 风险 | 解决方案 |
|---|---|---|
| 控制平面(apiserver/etcd) | 大脑死了集群全瘫 | 3 台 master 各跑一份 + VIP + 负载均衡 |
| 业务入口(LoadBalancer Service) | 业务 Pod 有了,外部进不来 | MetalLB 分配外部 IP |
一句话记忆:Keepalived+HAProxy 保"大脑"的命,MetalLB 开"业务"的门。
1. 架构总览
┌─────────────────────────────────────┐ │ 第 1 层:控制平面入口 │ │ Keepalived 漂移 VIP 192.168.211.10 │ │ HAProxy 16443 → master 的 6443 │ │ (kubectl / kubelet / join 走这里)│ └──────────────────┬──────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ master1 master2 master3 │ │ apiserver + etcd + scheduler + c-m │ │ etcd 多数派投票,挂 1 台无感 │ └──────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ worker1 worker2 worker3 │ │ 业务 Pod 在这跑,Calico 提供 Pod 网络 │ └──────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ 第 2 层:业务入口 │ │ MetalLB 给 LoadBalancer Service 发 IP │ │ 外部用户访问这个 IP 就能进业务 │ └─────────────────────────────────────────┘
2. 节点规划
| 角色 | IP | 说明 |
|---|---|---|
| k8s-vip | 192.168.211.10 | keepalived 虚拟 IP,漂移在 lb1/lb2 之间 |
| lb1 / lb2 | 192.168.211.11 / .12 | keepalived + HAProxy 负载均衡节点 |
| master1/2/3 | 192.168.211.31 / .32 / .33 | 控制平面(etcd + apiserver 等) |
| worker1/2/3 | 192.168.211.34 / .35 / .36 | 工作节点 |
| harbor | 192.168.211.101 | 私有镜像仓库(hb.reg.com) |
所有节点 /etc/hosts 都要配好主机映射,例如:
cat >> /etc/hosts <<EOF 192.168.211.10 lb.kubex.com k8s-vip 192.168.211.11 lb1 192.168.211.12 lb2 192.168.211.31 master1 192.168.211.32 master2 192.168.211.33 master3 192.168.211.34 worker1 192.168.211.35 worker2 192.168.211.36 worker3 192.168.211.101 hb.reg.com harbor EOF
3. 搭建流程(按层推进)
3.1 第一层:负载均衡(仅 lb1、lb2)
3.1.1 内核参数:允许绑定非本机 IP
cat > /etc/sysctl.d/haproxy.conf <<EOF net.ipv4.ip_nonlocal_bind = 1 net.ipv4.ip_forward = 1 EOF sysctl --system
类比:
ip_nonlocal_bind让服务可以监听"还没落到自己身上的 VIP"——像快递员可以先代收不是写自己名字的包裹。
3.1.2 HAProxy:四层转发 apiserver
apt install haproxy
cp /etc/haproxy/haproxy.cfg{,.bak}
cat > /etc/haproxy/haproxy.cfg <<EOF
global
log /dev/log local0
log /dev/log local1 notice
daemon
maxconn 4000
defaults
mode tcp
log global
retries 3
timeout queue 1m
timeout connect 10s
timeout client 1h
timeout server 1h
frontend k8s-apiserver
bind *:16443
mode tcp
default_backend k8s-masters
backend k8s-masters
mode tcp
balance roundrobin
option httpchk GET /livez HTTP/1.0
server master1 192.168.211.31:6443 check ssl verify none inter 2000 fall 3 rise 2
server master2 192.168.211.32:6443 check ssl verify none inter 2000 fall 3 rise 2
server master3 192.168.211.33:6443 check ssl verify none inter 2000 fall 3 rise 2
EOF
systemctl enable --now haproxy
关键点:
-
监听 16443(自定义端口,避开 6443 防混淆),
mode tcp四层转发 -
健康检查不要只用 TCP 探测:
option httpchk GET /livez+check ssl verify none是真实请求 apiserver 的存活探针,节点进程卡死(TCP 还通)时也能识别并摘除 -
fall 3 rise 2:连续 3 次失败标记 DOWN,2 次成功恢复
3.1.3 Keepalived:VIP 漂移
apt install keepalived
cat > /etc/keepalived/keepalived.conf <<EOF
global_defs {
router_id LVS_DEVEL
}
vrrp_script check_haproxy {
script "killall -0 haproxy"
interval 3
weight -20
}
vrrp_instance VI_1 {
state BACKUP
nopreempt
interface ens33 # 注意核对网卡名!
virtual_router_id 51
priority 100 # lb1 高,lb2 低
advert_int 1
authentication {
auth_type PASS
auth_pass k8s_ha
}
unicast_src_ip 192.168.211.11 # 本机 IP
unicast_peer {
192.168.211.12 # 对方 IP
}
virtual_ipaddress {
192.168.211.10 # K8s VIP
}
track_script {
check_haproxy
}
}
EOF
systemctl enable --now keepalived
lb2 配置相同,仅 priority 改 90、unicast 对调。
关键点:
-
单播模式(
unicast_src_ip+unicast_peer):VMware 里组播经常不通,单播最稳 -
nopreempt 非抢占:master 恢复后不抢回 VIP,避免网络震荡
-
track_script 联动:haproxy 进程死了就
weight -20降优先级,VIP 必须跟着走人 -
验证:
ip a | grep 192.168.211.10能看到 /32 的 VIP
3.2 第二层:容器底座(全部 6 台节点)
containerd 2.x(Ubuntu 26.04 自带仓库即可装):
apt update && apt install -y containerd mkdir -p /etc/containerd/ containerd config default > /etc/containerd/config.toml # 1) cgroup 驱动改为 systemd(必须,与 kubelet 一致) sed -i 's#SystemdCgroup = false#SystemdCgroup = true#g' /etc/containerd/config.toml # 2) 镜像加速(国内网络) sed -i "s#registry.k8s.io/pause#registry.aliyuncs.com/google_containers/pause#g" /etc/containerd/config.toml systemctl restart containerd
kubeadm / kubelet / kubectl(阿里云源):
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/deb/ /" | tee /etc/apt/sources.list.d/kubernetes.list apt-get update apt-get install -y kubelet kubeadm kubectl systemctl enable kubelet # 只 enable 不要 start,等 kubeadm 接管 apt-mark hold kubelet kubeadm kubectl
内核模块与参数:
cat > /etc/modules-load.d/k8s.conf <<EOF overlay br_netfilter EOF modprobe overlay && modprobe br_netfilter 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 sysctl --system
3.3 镜像准备(离线环境必做)
先看 kubeadm 到底要哪些镜像,再逐一核对导入:
kubeadm config images list # registry.k8s.io/kube-apiserver:v1.36.4 # registry.k8s.io/kube-controller-manager:v1.36.4 # registry.k8s.io/kube-scheduler:v1.36.4 # registry.k8s.io/kube-proxy:v1.36.4 # registry.k8s.io/coredns/coredns:v1.14.2 # registry.k8s.io/pause:3.10.2 # registry.k8s.io/etcd:3.6.8-0
逐台导入(containerd 用 ctr,注意 -n k8s.io 命名空间,否则 kubelet 看不见):
ctr -n k8s.io images import /root/k8s-v1.36.4-images.tar ctr -n k8s.io images list | grep -E 'kube-|coredns|pause|etcd' # 验证
踩坑:漏一个镜像(比如 kube-proxy),kubeadm init 就会"卡在拉取镜像"——先
kubeadm config images list逐一对照,再初始化。
3.4 初始化控制平面(仅 master1)
用 YAML 声明式初始化(生产推荐):
mkdir -p /opt/k8s && cd /opt/k8s cat > kubeadm-config.yaml <<'EOF' apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: "v1.36.4" controlPlaneEndpoint: "lb.kubex.com:16443" # 关键!指向 VIP imageRepository: "registry.aliyuncs.com/google_containers" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: "systemd" EOF kubeadm init --config=kubeadm-config.yaml --upload-certs
关键点:
-
controlPlaneEndpoint指向 VIP 而不是任何一台 master——所有组件都通过 VIP 找 apiserver,单台 master 挂了入口不死 -
podSubnet=10.244.0.0/16要跟后面 Calico 的CALICO_IPV4POOL_CIDR一致 -
--upload-certs:把控制平面证书上传到集群,供其他 master join 时下载
初始化成功后配置 kubectl:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes # 先看到 master1(此时 NotReady 正常,还没装网络插件)
3.5 组建完整集群(master2/3 + worker1/2/3)
master2、master3 加入控制平面(必须带 --control-plane 和 --certificate-key):
kubeadm join lb.kubex.com:16443 --token <token> \ --discovery-token-ca-cert-hash sha256:<hash> \ --control-plane --certificate-key <certificate-key>
worker1/2/3 加入(去掉 --control-plane 和 --certificate-key):
kubeadm join lb.kubex.com:16443 --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>
重新获取 join 参数的两种方式:
# worker 用:直接生成标准 join 命令 kubeadm token create --print-join-command # master 用:先重新上传证书拿新 certificate-key,再手动拼上 --control-plane kubeadm init phase upload-certs --upload-certs
关键点(最容易翻车):
-
⚠️ token 有效期:
kubeadm init默认生成的 bootstrap token 永久有效(可用--token-ttl调) -
⚠️ certificate-key 有效期只有 2 小时!重新
upload-certs会生成新 key,旧 join 命令里的 key 立即作废——重新生成后必须用新 key -
⚠️ 加入后
kubectl get nodes看到 6 个节点全NotReady是正常的——还没装 CNI 网络插件
3.6 部署 Calico 网络插件(仅 master1)
先解决镜像:calico-node 是 DaemonSet(每台节点都要跑),所以 6 台节点全部要导入镜像:
ctr -n k8s.io images import /root/calico-images_v3.32.1.tar.gz ctr -n k8s.io images list | grep calico # quay.io/calico/cni:v3.32.1 # quay.io/calico/node:v3.32.1 # quay.io/calico/kube-controllers:v3.32.1 # quay.io/calico/typha:v3.32.1
下载并修改 manifest(CALICO_IPV4POOL_CIDR 必须改成你 init 时的 podSubnet,并关闭 IPIP/VXLAN 用 BGP):
curl -L -o /root/calico.yaml https://raw.githubusercontent.com/projectcalico/calico/v3.32.1/manifests/calico-typha.yaml # 修改 calico.yaml: # CALICO_IPV4POOL_CIDR = 10.244.0.0/16 ← 必须与 kubeadm 的 podSubnet 一致! # CALICO_IPV4POOL_IPIP = Off # CALICO_IPV4POOL_VXLAN = Never kubectl apply -f /root/calico.yaml kubectl get nodes # 等一会儿,6 台全部 Ready
关键点:
-
IP 池网段绝不能和节点网段重叠!默认池是
192.168.0.0/16,会覆盖192.168.211.0/24节点网段,必须显式改掉 -
节点 NotReady 的典型原因就是 CNI 没装:describe 报
cni plugin not initialized
3.7 部署 MetalLB(最后一块拼图)
同样先解决镜像(speaker 也是 DaemonSet,6 台都要):
ctr -n k8s.io images import /root/metallb-images.tar ctr -n k8s.io images list | grep metallb # quay.io/metallb/controller:v0.16.1 # quay.io/metallb/speaker:v0.16.1
安装控制器(仅 master1):
curl -L -o /root/metallb-native.yaml https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml kubectl apply -f /root/metallb-native.yaml
配置地址池(必须避开所有节点 IP 和 VIP):
# /root/metallb-ip-pool.yaml apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: k8s-pool namespace: metallb-system spec: addresses: - 192.168.211.200-192.168.211.220 # 避开 .10/.11/.12/.31-36/.101 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: l2 namespace: metallb-system spec: ipAddressPools: - k8s-pool
# 等 controller pod Ready 后再 apply(否则报 webhook connection refused) kubectl -n metallb-system get pods kubectl wait --for=condition=ready pod -l app=metallb,component=controller -n metallb-system kubectl apply -f /root/metallb-ip-pool.yaml
验证闭环:
kubectl create deploy myapp --image hb.reg.com/k8s/nginx:1.27.2 --replicas 3 kubectl expose deploy myapp --port 80 --type LoadBalancer --name myapp-svc kubectl get svc myapp-svc # EXTERNAL-IP 出现 192.168.211.200-220 之一 = 成功 curl http://<分配的IP> # 返回 nginx 页面 = 全链路打通
4. 核心原理:为什么这套架构"高可用"
4.1 VIP 漂移(Keepalived/VRRP)
-
lb1 和 lb2 每秒互发心跳(
advert_int 1) -
lb1 挂了,lb2 连续收不到心跳(默认 3 个周期)→ 判定 lb1 死亡 → VIP 飘到自己身上
-
对外无感知,kubectl/kubelet 只管连 VIP,不管 VIP 在谁身上
4.2 控制平面高可用(kubeadm + etcd)
-
3 台 master 各跑一份 apiserver + etcd,共用同一个
controlPlaneEndpoint(VIP) -
etcd 写操作需要多数派(≥2/3)确认——挂 1 台照常工作,挂 2 台才不可用(一致性优先,不是 bug)
-
kubectl 连 VIP → HAProxy 轮询到还活着的 apiserver
4.3 业务入口(MetalLB)
-
controller(Deployment):从地址池分配 IP
-
speaker(DaemonSet):每台节点用 ARP(L2 模式)宣告这个 IP
-
类比:MetalLB 是"裸金属上的云厂商 LB",云上集群由云平台发 IP,本地集群由它发 IP
5. 避坑清单(血泪总结)
| # | 坑 | 现象 | 解法 |
|---|---|---|---|
| 1 | 镜像漏导(如 kube-proxy) | kubeadm init 卡在拉镜像 | kubeadm config images list 逐一核对 |
| 2 | lb 没开机 | join 卡在 preflight(No route to host) | 入口依赖,先保证 VIP 可达 |
| 3 | HAProxy 后端网段写错(模板是 192.168.10.x,实际 192.168.211.x) | join / token create 报 EOF | 照抄模板必须核对网段! |
| 4 | certificate-key 换了没同步 | 旧 join 命令失败 | 重新 upload-certs 后必须用新 key |
| 5 | worker join 带 --control-plane | 行为错误 | worker 去掉 --control-plane 和 --certificate-key |
| 6 | calico IP 池没改 | 路由冲突/节点异常 | CALICO_IPV4POOL_CIDR 与 podSubnet 一致且避开节点网段 |
| 7 | DaemonSet 镜像只导部分节点 | 部分节点 ImagePullBackOff | calico-node / metallb-speaker 全节点导入 |
| 8 | controller 没 ready 先 apply 地址池 | webhook connection refused | 先等 controller 1/1 再 apply |
| 9 | 健康检查只做 TCP 探测 | 节点进程卡死仍被认为存活 | 用 option httpchk GET /livez + check ssl |
6. 灾难演练(验收 HA 是否真的可用)
-
确认 VIP 持有者:
ip a | grep 192.168.211.10(lb1 或 lb2) -
开监控:master1 上
watch -n 1 "kubectl get nodes" -
拔网线测试:VMware 里直接把持有 VIP 的 lb 断电
-
见证:kubectl 卡顿 1~2 秒后自动恢复 = VIP 已漂移,无缝接管
-
挂 master 测试:关掉 master2,
kubectl get nodes应无感(master2 被 HAProxy 自动摘除,etcd 多数派仍成立)
7. 一句话记忆
两个门:Keepalived+HAProxy 保"大脑"(VIP 漂移),MetalLB 开"大门"(业务 IP);三台 master 一条心(etcd 多数派),全网 Calico 一线牵(Pod 网络)。
搭建于 Ubuntu 26.04 + K8s v1.36.4 + containerd 2.x 环境
更多推荐
所有评论(0)