目标:用 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-vip192.168.211.10keepalived 虚拟 IP,漂移在 lb1/lb2 之间
lb1 / lb2192.168.211.11 / .12keepalived + HAProxy 负载均衡节点
master1/2/3192.168.211.31 / .32 / .33控制平面(etcd + apiserver 等)
worker1/2/3192.168.211.34 / .35 / .36工作节点
harbor192.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 逐一核对
2lb 没开机join 卡在 preflight(No route to host)入口依赖,先保证 VIP 可达
3HAProxy 后端网段写错(模板是 192.168.10.x,实际 192.168.211.x)join / token create 报 EOF照抄模板必须核对网段!
4certificate-key 换了没同步旧 join 命令失败重新 upload-certs 后必须用新 key
5worker join 带 --control-plane行为错误worker 去掉 --control-plane 和 --certificate-key
6calico IP 池没改路由冲突/节点异常CALICO_IPV4POOL_CIDR 与 podSubnet 一致且避开节点网段
7DaemonSet 镜像只导部分节点部分节点 ImagePullBackOffcalico-node / metallb-speaker 全节点导入
8controller 没 ready 先 apply 地址池webhook connection refused先等 controller 1/1 再 apply
9健康检查只做 TCP 探测节点进程卡死仍被认为存活option httpchk GET /livez + check ssl

6. 灾难演练(验收 HA 是否真的可用)

  1. 确认 VIP 持有者ip a | grep 192.168.211.10(lb1 或 lb2)

  2. 开监控:master1 上 watch -n 1 "kubectl get nodes"

  3. 拔网线测试:VMware 里直接把持有 VIP 的 lb 断电

  4. 见证:kubectl 卡顿 1~2 秒后自动恢复 = VIP 已漂移,无缝接管

  5. 挂 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 环境

更多推荐