干运维这些年,Kubernetes 集群我部署过的没有一百套也有大几十套。周围朋友聊起"高可用",大多数人的第一反应是控制平面三个副本、etcd 奇数节点、ingress 多实例,这些当然都对,但总有一个问题被放在最后才想起来——节点如果真的失联了,到底要不要动它的电源?怎么判断它是真死了还是假死?如果网络抖动导致误判,直接把一台还在运行线上业务的物理机断电,后果谁担得起?
这其实就是 fence 机制要解决的命题。Kubernetes 的高可用部署从来不是把组件跑多份就完事,控制平面冗余、etcd 选主、VIP 漂移、节点隔离,是一整套环环相扣的设计。尤其是自建机房、裸金属这类没有云厂商 API 兜底的场景,fence 做得好不好,直接决定集群在极端故障下是"优雅降级"还是"脑裂双写"。
这篇文章不绕弯子,直接讲清楚三件事:高可用集群架构为什么要依赖 fence,裸金属环境下怎么把 fence 真正落地,以及我在实际部署和故障演练中踩过的那些坑。
1. 高可用不只是多副本:为什么必须谈 fence
1.1 控制平面冗余不等于高可用
Kubernetes 控制平面的几个组件,本身是可以做冗余的。kube-apiserver 无状态,前面挂负载均衡就行;kube-controller-manager 和 kube-scheduler 走 leader election,同一时刻只有一个实例在真正干活,谁拿锁谁干活;etcd 是 raft 协议,3 个节点能容忍挂 1 个,5 个节点能容忍挂 2 个。
这些机制保证了"节点优雅关机"或者"进程干净退出"时集群可以继续跑。比如某个控制平面节点执行 kubectl drain + shutdown,etcd 成员正常离开 raft 集群,kubelet 停止心跳,一切按流程走。
但真正的生产故障从来不走流程。
我印象最深的一类场景是:某个控制平面节点的操作系统内核 hang 住了,网络栈完全瘫掉,kubelet 发不出心跳,etcd 成员虽然进程还在但已经无法和另外两个节点通信。从集群角度看,这个节点属于"无法确认状态"。这时候问题就来了——调度器检测到节点 NotReady,会把上面的 pod 重新调度到别的节点,但如果原来节点上的容器还在运行呢?如果它挂着共享存储还在写数据呢?
你会发现,仅靠 Kubernetes 原生的"踢出"机制,根本保证不了"那个节点真的不会再产生数据"。
1.2 Fence 要解决的核心难题:脑裂与脏状态
fence 这个词直译过来是"围栏",在集群领域的意思是在无法确认节点存活状态时,主动把它的权力收回。最常见的手段就是 STONITH——Shoot The Other Node In The Head,翻译得直白一点:既然没法确认你死没死,那就先用物理手段打死你,再重新分配资源。
为什么要物理手段?因为软件手段不可靠。节点内核都 hang 了,你 ssh 上去 kill 进程没有任何意义,因为它根本执行不了命令。操作系统层失效的情况下,唯一可靠的是带外管理通道:服务器的 BMC 管理口、IPMI 协议、远程电源控制。
我从直觉上打个比方:宿舍几个人住,A 出门了,大家半天联系不上他。这时候 B 想用 A 的桌子,正确的做法不是直接搬桌子,而是先确认 A 是不是真的不在——要么打电话联系上他,要么破门进去看到他确实不在。fence 就相当于"破门确认"这一步,只有确认了,资源才能安全地转移出去。
没有 fence 的集群,在遇到"边界情况"时会出现两种危险状态:
- 脑裂:网络分区导致新 leader 已经选出来,旧 leader 还在运行,两个副本同时接受写入。
- 脏状态:节点实际活着但被判定死亡,工作负载被调度到新节点,新旧节点同时访问同一份数据。
这俩情况一旦出现,比集群"挂掉"还难恢复——因为数据可能已经写脏了,恢复起来要对账、要回滚,代价远远大于一次主动断电。
理解了这个前提,下面的架构设计和部署步骤才有意义。所有高可用的部署动作,本质上都是在为"快速、确定地处理故障"服务的,而 fence 就是最后一道兜底手段。
2. 部署前必须想清楚的架构选型
2.1 节点规划与硬件层面的考虑
如果你打算在物理机上做 Kubernetes 高可用,节点规划得按"控制平面一套、etcd 一套、接入层一套、业务节点一套"的维度去思考。
先说数量。控制平面我推荐 3 个节点起步,生产环境优先 3 个或者 5 个。原因是 etcd 依赖 raft quorum,3 节点允许挂 1 个,5 节点允许挂 2 个,奇数节点保证了在故障时不会出现票数持平的情况。有些团队为了省钱用 2 个控制平面加 1 个仲裁节点,也能跑,但运维复杂度明显上升,我不太建议。
硬件层面有三条要求,缺一条 fence 就做不彻底:
- 每台物理机必须有独立的带外管理接口(IPMI/BMC),能独立于操作系统执行开关机操作。
- 管理网口和生产网络、存储网络物理隔离。BMC 的 IP 走独立网段,别跟 kube-apiserver 流量混在一起。
- 系统盘和数据盘分离。etcd 所在的数据盘必须用独立物理盘或者高性能存储池,避免系统盘 IO 抖动直接影响 etcd 心跳。
这里补充一个容易忽略的点:工作节点要不要也纳入 fence 范围?我的做法是,凡是可能挂载共享存储的物理节点都纳入。原因在于,云环境里遇到节点假死,云 API 可以强制停机,物理机环境没有这个能力。只有让每个物理节点都具备"被远程断电"的条件,后面才能讨论"要不要断"。
2.2 接入层高可用方案:VIP 与负载均衡的选择
kubeconfig 里的 server 地址不能写单个节点的 IP,必须是一个统一入口。这个入口在物理机环境里就是 VIP 加负载均衡。
我多次用的是这套组合:keepalived 管 VIP,HAProxy 管四层转发。
- 两台机器(或者三台)组成 keepalived 组,通过 VRRP 协议宣告 VIP。
- HAProxy 前端监听 VIP 的 6443 端口,后端指向所有控制平面节点的 6443 端口。
- 健康检查用 TCP 检测,只要 apiserver 的 6443 端口有响应,就认为这个后端存活。
选 keepalived 而不是其他方案,主要是成熟稳定、配置简单。三行配置就能起一个 VRRP 实例,故障切换时间取决于 VRRP 的宣告周期,默认 1 秒左右,完全满足 Kubernetes 客户端对 apiserver 的连接重试要求。
不过 keepalived 本身也需要 fence。如果 keepalived 的 master 节点假死,backup 节点收不到 VRRP 通告,VIP 会漂移过去。但如果原 master 只是网络隔离但进程还活着,VIP 就会短暂出现在两个节点上,这就是 brain split。虽然 keepalived 的免费 ARP 机制能在一定程度上纠正这个问题,但两台机器同时持有 VIP 的窗口期依然存在。这时候 fence 的作用就是第一时间把原 master 的网络通路或者电源断掉。
2.3 远程电源管理与 fence 设备选型
fence 设备的选型直接决定隔离成功率,我把它单独拿出来说。
物理机环境最常见的 fence 手段是这几类:
| 手段 | 通道 | 适用场景 | 可靠性 |
|---|---|---|---|
| IPMI/BMC 电源控制 | 带外管理网口 | 绝大多数物理服务器 | 高,独立于操作系统 |
| 交换机端口隔离 | 带外网络设备 | 网络分区故障 | 高,但需要网管权限 |
| iLO / iDRAC 远程控制 | 带外管理网口 | 特定品牌服务器 | 高 |
| 硬件 Watchdog | 带内但独立于 CPU | 部分服务器可选 | 中,需 BIOS 支持 |
我首推 IPMI 2.0 标准接口的 BMC 远程电源控制。原因很直接:所有主流服务器品牌都支持 IPMI 协议,fence_ipmilan 这类现成工具可以直接调用,Linux 下的 ipmitool 命令也是标配。做隔离时执行一条ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power off就能断电,简单可靠。
硬件 Watchdog 是另一种思路:节点通过定期喂狗证明自己活着,一旦内核 hang 住,看门狗硬件自动触发重启。它的好处是不依赖外部网络,坏处是无法做"远程决策"——你不能判断这个节点是不是该隔离,它只能自救,不能保护集群。
至于交换机端口隔离,我遇到过一些团队用,核心思路是故障时将节点的业务网口 shutdown,节点还在但对外网络已断。这个手段在"网络故障"场景下好用,但在"节点电源故障导致无法恢复"时就无效了。所以我的通用做法还是:电源控制为主,交换机隔离为辅。
3. 高可用集群搭建全过程实录
3.1 基础环境与依赖组件准备
下面进入实操。先给出一份我常用的规划清单,方便对照。
| 角色 | 主机名 | 配置建议 | 数量 |
|---|---|---|---|
| 接入层 | lb1 / lb2 | 4C8G,双网卡 | 2 |
| 控制平面 | k8s-m1 / m2 / m3 | 8C16G,SSD 数据盘 | 3 |
| 工作节点 | k8s-w1 / w2 / w3 | 按业务需求 | 3 |
VIP 规划为 192.0.2.100,业务网段 192.0.2.0/24,管理网段 10.10.10.0/24(BMC 专用),另外每个机型的 /etc/hosts 都要维护好主机名对应关系。
基础环境统一做这几件事:
- 系统:主流 Linux 发行版即可,内核 5.x 以上。
- 容器运行时:containerd,配置 systemd cgroup 驱动。
- 时间同步:chronyd 强制开启,集群节点时间偏差超过 100ms 就会导致选举异常。
- 内核参数:
net.bridge.bridge-nf-call-iptables=1、net.ipv4.ip_forward=1、vm.swappiness=0。 - 关闭 swap,Kubernetes 从 1.22 之后直接拒绝带 swap 的节点加入。
这里特别强调一下 IPMI 的配置检查。很多新服务器默认的 BMC IP 是 192.168.0.120 这种固定地址,需要进 BIOS 或者用厂商工具改成规划好的管理网段地址。我踩过最疼的坑就是:设备配置好了,fence 的时候才发现 BMC 的账号只有查看权限没有电源控制权限,结果断电失败。所以在开始部署之前,一定先把 BMC 的账号权限验证一遍——执行查询电源状态的命令能通,还要执行一次开关机测试。
3.2 用 kubeadm 搭出多控制平面集群
接入层先部署,顺序不能乱。在 lb1 和 lb2 上装 keepalived 和 haproxy,核心配置如下:
keepalived 的配置:
vrrp_instance VI_1 { state BACKUP interface eth1 virtual_router_id 51 priority 100 advert_int 1 unicast_src_ip 192.0.2.11 unicast_peer { 192.0.2.12 } virtual_ipaddress { 192.0.2.100/24 } }注意我用的是 BACKUP 加 priority 的方式而不是 MASTER/BACKUP,两台机器 priority 分别是 100 和 90,这样优先级高的会抢占 VIP,谁优谁是主,逻辑清晰。 unicast_peer 是重点,VRRP 默认走组播,很多物理交换机禁止组播,配置成单播对端地址更稳。
haproxy 的配置:
listen k8s-apiserver bind 192.0.2.100:6443 mode tcp timeout client 60s timeout server 60s balance roundrobin server k8s-m1 192.0.2.21:6443 check server k8s-m2 192.0.2.22:6443 check server k8s-m3 192.0.2.23:6443 check只用 TCP 模式,健康检查check是每 2 秒一次 TCP 连接,简单可靠。不要把七层 HTTP 检查加进来,万一 apiserver 因为 etcd 慢暂时返回 503,TCP 健康检查仍然认为它活着,反而更符合实际需求。
然后初始化第一个控制平面节点:
kubeadm init \ --control-plane-endpoint=192.0.2.100:6443 \ --kubernetes-version=v1.28.2 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=10.96.0.0/12重点说--control-plane-endpoint。kubeadm 会把这段地址写进 apiserver 证书的 SAN,同时写进生成的 kubeconfig 文件。也就是说,以后 kubelet、kubectl、控制平面组件访问 apiserver,统一走这个 VIP,不直接访问单节点 IP。
init 完成后,其他两个控制平面节点用同样的 token 和证书执行kubeadm join 192.0.2.100:6443 --control-plane即可。注意控制平面 join 需要先拿到第一个节点生成的 certificate key,不然会要求手动分发证书。
CNI 我习惯用 Calico,部署时把 pod 网段调成和 init 时一致。这里有个细节:Calico 的 BGP 模式下,所有节点的网卡要和 pod 网段互通,物理机环境一般建议关闭 BGP 对端自动发现,手动指定 peer 列表,避免广播风暴。
3.3 给集群装上 fence:STONITH 资源与隔离策略配置
集群起来了,接入层也通了,但还没有真正"防脑裂"。这一步才是本文的核心——给整个架构装上 fence。
我把它拆成两层来配置。
第一层:接入层节点的 fence
在 lb1 和 lb2 上搭一套轻量级集群管理工具(我用的是 pacemaker + corosync),让这两台机器除了跑 keepalived 和 haproxy,同时承担 STONITH 的编排工作。
pacemaker 集群初始化的常规过程是:安装 pcs、corosync、pacemaker、fence-agents,然后建集群、启用 STONITH。注意这里需要 fenced 设备的信息,每个节点的 BMC 地址、账号、密码都要提前准备好。
pcs stonith create stonith-lb1 fence_ipmilan \ pcmk_host_list=lb1 \ ipaddr=10.10.10.11 \ login=admin passwd=xxx \ lanplus=1 \ action=off这里 action 设成 off 而不是 reboot,原因是 reboot 依赖 BMC 正常执行上下电时序,在极端情况下不如直接断电可靠。fence 完,pacemaker 确认节点已经掉电,才允许 VIP 资源在剩余节点上启动。
然后是 VIP 资源的定义:
pcs resource create ClusterIP ocf:heartbeat:IPaddr2 \ ip=192.0.2.100 cidr_netmask=24 \ op monitor interval=5s这个资源交给 pacemaker 管理,不再由 keepalived 单独管理。好处是 pacemaker 会在节点隔离完成后,才把 VIP 移到别的节点,杜绝了"旧节点还没死透、新节点就拿到 VIP"的情况。
第二层:Kubernetes 控制平面节点的 fence
控制平面节点我不用 pacemaker,而是写了一个轻量守护脚本跑在三个控制平面节点上。逻辑不复杂:这个脚本通过 kubelet 心跳判断节点健康状态,检测到某个控制平面节点持续 NotReady 超过阈值,就调用ipmitool对其 BMC 执行断电。
脚本的核心过滤逻辑是:
# 检测节点 NotReady 状态,并打点到判定日志 kubectl get node -o json | jq -r ' .items[] | select(.status.conditions[] | select(.type=="Ready" and .status!="True")) | .metadata.name'流程大概是:第一次检测到 NotReady 后,不立刻动作,等待 30 秒再查一次;如果依然 NotReady,再查 etcd 的健康状态,确认此节点确实不在 raft 集群的当前成员里;然后才执行 BMC 断电。
有个关键点是守护脚本自己不能成为单点。我把它同时跑在三个控制平面节点上,并且通过 etcd 租约做互斥——同一时间只要一个脚本执行 fence 操作,避免三个节点同时给同一个故障节点发断电指令。这里其实不需要什么复杂算法,用 etcd 的lease功能就能实现:每个脚本尝试拿一个 10 秒租约,拿到的人执行。
3.4 高可用与隔离能力的联合演练
部署完成不等于真的高可用。我每交付一个集群,都会做三类故障演练,全部过一遍才敢说这个集群能用。
场景一:主控制平面节点宕机
直接执行poweroff模拟最纯粹的故障。预期行为:
- etcd 重新选主,其他两个节点形成 quorum。
- apiserver 通过 VIP 依然可以被访问,客户端基本无感知。
- 受损节点上的 pod 被重新调度到健康节点,但需要等 5 分钟的默认驱逐时间。
- fence 脚本判定 NotReady 后执行供电切断,确认节点已完全离线。
实际执行时,你会发现"断电成功"这件事比想象中重要。因为 kubelet 的节点心跳超时默认是 40 秒,加上驱逐宽限期,整个转移过程通常要 6 分钟左右。fence 的介入不是为了让这个过程变快,而是为了确保这个过程是安全的。
场景二:控制平面节点网络完全隔离
用交换机的端口 shutdown 模拟,网线还在但网络不通。这种故障最容易被误判成节点死亡,因为进程都活着、负载也不高。
预期行为:
- kubelet 心跳丢失,节点被标记 NotReady。
- etcd 与故障节点的连接超时,raft 触发新一轮选主。
- fence 脚本通过 BMC 通道确认节点电源状态正常,但因为网络隔离导致集群确认不了存活状态,依然执行断电。
这里我要强调一个细节:网络隔离之后,fence 脚本自己也连不上正常情况下应该能访问的 apiserver 怎么办?所以脚本的判断依据不能只依赖 apiserver,还要能通过 BMC 通道和故障节点通信。通常我在每个节点上都配了独立的 BMC 网络接口,fence 脚本通过管理网段直接连 BMC,跟业务网段完全隔离。
场景三:内核 hang 模拟
这个演练需要小心,控制不好容易影响同机柜的其他机器。一般用echo c > /proc/sysrq-trigger触发内核 panic,或者直接挂载一个 NFS 卷后强制卸载来模拟 IO hang。
内核 hang 场景下,fence 是唯一有效的处理方式。因为操作系统本身已经失去响应,ssh 进不去、进程杀不掉、kubelet 也不发心跳了,只有断电能解决。这个场景我还观察过另外一个现象:即使你不做 fence,硬件 watchdog 或者服务器的固件机制有时候也会自动重启机器。但自动重启和受控断电带来的结果完全不一样——重启后节点可能带着旧状态重新加入集群,反而更容易出问题。所以有计划的受控 fence 比系统自行重启更安全。
整个演练过程都有一个收尾动作:确认 fence 完成后,节点怎么加回。控制平面节点重新上电后,kubelet 会自动重新连接 apiserver,etcd 需要手动把旧成员删掉再重新加入。这套流程我在下一节展开。
4. 生产环境常见故障与排查笔记
4.1 etcd 集群因单节点异常失去 quorum
现象:apiserver 日志大量报etcdserver: request timed out,客户端访问 kube-apiserver 直接超时。
排查步骤:
- 先看 etcd 集群成员列表:
etcdctl member list -w table。 - 再看每个成员的健康状态:
etcdctl endpoint health --cluster。 - 最后看日志定位是哪个节点出了问题。
遇到可疑节点时,我习惯第一时间查它的磁盘 IO 和网络连接状态。现在的主机,大量的 etcd 异常并不来自磁盘损坏,而是来自系统盘 IO 抖动——因为 etcd 的 fsync 要求非常严格,写盘超过几百毫秒就会导致 raft 心跳超时。
这个场景下 fence 的介入时机是:确认节点不能再加入集群,需要把它彻底隔离掉,让剩余节点尽快恢复 quorum。我建议不要等太久,看到连续多次 health check 失败就准备隔离。因为 etcd 在失去 quorum 后是只读状态,所有写操作全部阻塞,apiserver 会陷入"连得上但什么都做不了"的状态,直接影响线上业务。
4.2 VIP 漂移失败与脑裂的 fence 干预
比较难排查的问题:VIP 出现在两个节点上,集群里同时有两个 apiserver 被访问。
为什么会出现这种情况?大多是 VRRP 的通信问题。我遇到过几种原因:交换机丢弃了 VRRP 的组播报文;节点防火墙把 VRRP 协议拦了;跨机柜的网络设备没放行 keepalived 的单播端口。
解决办法是在 keepalived 里配置unicast_peer,明确指定对端 IP,不要让 VRRP 走组播。同时交换机上把 VRRP 组播 MAC01:00:5e:00:00:12放行。配合 pacemaker 的场景下,pacemaker 会在检测到资源冲突时自动执行 stonith——这也是为什么我把 VIP 交给 pacemaker 管理而不是 keepalived 管理的原因,前者能在确认节点死亡后才漂移,后者只管广播宣告。
如果已经出现脑裂,第一件事不是修网络,而是用 fence 把一侧的电源断掉,让 VIP 归属重新收敛。
4.3 误隔离问题的定位与规避
fence 是一把双刃剑,误隔离比不隔离更磨人。我梳理了几个高发原因:
- BMC IP 配置错误或账号权限不足,导致 fence 指令投递失败。
- BMC 网络抖动,导致 fence 操作超时,pacemaker 误判节点未响应。
- 判断阈值设得太短,把一次正常的 kubelet 重启或网络闪断当成了节点死亡。
规避办法就一句话:fence 之前多用几个信号源确认节点状态。信号源可以是 kubelet 心跳的连续性、BMC 的电源状态、网络设备的端口状态。我实际使用的判定组合是:kubectl 的 NotReady 状态持续 30 秒以上 + BMC 的chassis status能正常返回 + 该节点在 etcd 成员中的健康检查连续失败 3 次以上。三重确认之后才执行断电。
有一次我亲眼见过当阈值设太短时,某节点的 kubelet 因为 OOM 被 systemd 重启,期间那 15 秒丢了几次心跳,fence 脚本就直接断电了。后来我把所有 fence 动作都加了可配置的确认延迟,默认是 60 秒,牺牲一点故障切换速度,换来了误杀率大幅下降。
4.4 故障恢复后的节点重建流程
这里按控制平面节点重建来说,这是最复杂的恢复流程。
步骤一:确认旧节点已被 fence 断电,并且 BMC 状态显示完全关机。
步骤二:在健康的控制平面节点上,把旧节点从集群中移除。
kubectl delete node k8s-m2步骤三:移除 etcd 成员。注意是在健康节点上操作 etcdctl:
etcdctl member remove <member_id>步骤四:清理旧节点的所有数据。这一步非常关键,我之前遇到过一次恢复后集群反复出问题,根因就是旧节点上残留的 etcd 数据和证书被重新初始化后一起带进了集群,导致 raft 日志冲突。要清干净的数据包括/etc/kubernetes、/var/lib/etcd、/var/lib/kubelet。
步骤五:重新执行kubeadm join --control-plane,把节点加回来。
步骤六:重建 fence 配置,重新验证 BMC 的电源控制指令。
整个流程做完,还要回到第 3.4 节的演练步骤,重新跑一遍故障注入,确认加回来的节点确实已经能正常参与选举、能正常被 fence。
5. 几点从实战里沉淀出的认知
fence 这个事,说起来只是"断电"两个字,做起来全是细节。我最后分享几点认知,算是这十几年反复踩坑之后的体会。
第一,fence 和 Kubernetes 原生驱逐机制是配合关系,不是替代关系。Kubernetes 的节点 NotReady、pod 驱逐、污点容忍,这些都是"逻辑层面的隔离",它解决的是"调度系统不再向故障节点分发新任务"。但是物理层面的隔离——确保节点上的进程真的不能再访问共享存储、不能再产生网络流量——必须靠 fence 来兜底。
第二,fence 的超时和阈值设置要沉淀成一套标准参数。我从不用默认值。默认的 NotReady 容忍时间是 5 分钟,默认的 kubelet 心跳间隔是 10 秒,这些在生产环境里会导致故障恢复时间太长。我一般会把 node-monitor-grace-period 调整到 40 秒,pod-eviction-timeout 根据业务容忍度调整到 3~5 分钟,fence 判定前的确认延迟设为 60 秒。这组参数结合起来,既避免了误杀,也保证了故障切换不会拖太久。
第三,日志是 fence 排障的第一入口。我在所有参与 fence 的节点上都单独配了一份独立的日志文件,专门记录 fence 的触发时间、判定依据、执行结果。出了故障,第一件事就是把这条时间线的日志拉出来对齐,而不是去翻 kubelet 日志大海捞针。
第四,所有的 fence 动作一定要有干跑模式。上线前先在测试环境跑几轮完整的故障演练,确认断电指令、VIP 漂移、etcd 选主、pod 重调度这四个环节每次都能按预期收敛。演练时故意制造一些复杂条件——比如同时断掉两个节点的网络、或者在 etcd 恢复过程中执行 fence——往往能暴露出很多架构设计时没想清楚的问题。
最后再分享一个容易忽略的小技巧:fence 之后的节点重启,最好安排一个冷启动等待时间。服务器断电后再上电,BMC 和固件初始化需要时间,如果立即给节点加电,部分机器会因为硬件初始化未完成而反复重启。我一般会在 fence 脚本里加一个延时,确保电源完全切断到再次通电之间至少间隔 5 分钟,这个细节帮我解决了不少"fence 后节点起不来"的诡异问题。
高可用集群从来不是配置出来的,是演练出来的。把 fence 做好,你才敢在半夜接到告警电话的时候,安心地远程按下一个断电按钮。