☰
Kubernetes二进制集群从单master到多master高可用改造实战
2026/10/19 18:08:40 网站建设 项目流程

如果你维护过一套用二进制方式部署的 Kubernetes 集群,大概率会遇到这样一个阶段:集群只有一个 master 节点,apiserver、controller-manager、scheduler、etcd 全挤在一台机器上。平时测试没问题,可一旦这台机器重启、网络抖动、或者需要做系统升级,整个控制面就断了,业务 pod 哪怕还活着,你也没办法通过 kubectl 管理集群。

我最近就把这样一套单节点集群扩展成了多 master 高可用架构,核心动作是在原有基础上新增 master2,同时引入两台 LB 节点,用 nginx 做四层负载均衡,用 keepalived 提供 VIP 漂移。整个过程踩了不少坑,这篇就完整梳理一遍,从架构设计、证书调整、etcd 扩容,到 LB 配置、全局 kubeconfig 切换,全部用实际命令和配置说话。适合已经能独立部署单节点 Kubernetes、想继续把手里的集群改造成高可用的人。

1. 从单节点到高可用:整体架构与设计思路

1.1 先确认现有集群的状态

改架构之前,先别急着下载二进制文件。你手里的“单节点基础”具体是什么形态,决定了后面所有操作方式。我这次面对的集群是全部控制面组件在同一台机器上,etcd 也是单实例,证书是通过 openssl 手动签发的一套内部 CA,部署路径标准在 /etc/kubernetes 下。

先做一轮盘点,命令不多,但每一条都要看结果:

kubectl get nodes -o wide kubectl get cs ss -lntp | grep -E '6443|2379' cat /etc/kubernetes/apiserver 2>/dev/null || systemctl cat kube-apiserver

重点关注几个信息:

  • 当前 apiserver 的监听地址和端口,默认是 0.0.0.0:6443
  • etd 监听地址和 peer 端口,确认是 127.0.0.1:2379 还是内网 IP:2380
  • 当前各组件 kubeconfig 里的 server 字段指向的是哪个 IP
  • 证书目录的结构,有没有 ca.crt、ca.key,apiserver 证书里包含了哪些 IP

我这次的情况是 kubeconfig 指向 master1 的内网 IP 10.0.0.11,etcd 单实例只监听本机,所以后面很多地方要改。

1.2 目标架构与 IP 规划

架构最终定成这样:

角色主机名IP部署组件
原有 mastermaster110.0.0.11apiserver、controller-manager、scheduler、etcd、nginx、keepalived
新增 mastermaster210.0.0.12apiserver、controller-manager、scheduler、etcd
负载均衡器1lb110.0.0.13nginx、keepalived
负载均衡器2lb210.0.0.14nginx、keepalived
业务节点(原有)node110.0.0.15kubelet、kube-proxy、containerd
VIP-10.0.0.100由 keepalived 漂移

为什么 master1 上也放了 nginx 和 keepalived?我的习惯是让 master1 同时具备 LB 功能,这样即使两台独立 LB 都挂了,VIP 还能落在 master1 上,控制面不至于完全失去入口。当然这不是必须的,如果你希望角色干净一点,可以把 master1 上的 nginx 和 keepalived 去掉,只保留两台专用 LB。

还需要注意一个端口细节:nginx 的四层代理监听的是 6443,但 master 节点本身也有 apiserver 在监听 6443,同一台机器两个进程不能绑同一个端口。所以我让 master1 上的 nginx 监听 16443,LB1 和 LB2 上的 nginx 监听 6443,VIP 的 kubeconfig 访问端口统一用 6443。如果你严格把 LB 和 master 分开,就不用纠结这个,直接都监听 6443。

1.3 为什么是 nginx 四层转发 + keepalived

连接 apiserver 的流量本质上是长连接,TLS 握手后会有大量持续的数据交换。这种场景适合四层转发,也就是 nginx 的 stream 模块在 L4 做 TCP 透传,不需要解析 HTTP 内容,性能损耗极小。如果非要用七层反向代理,nginx 的 http 模块也能做,但需要额外处理 websocket、长连接、TLS 终结等问题,复杂度和风险都更高。

keepalived 负责 VIP 的高可用。它的原理是基于 VRRP 协议,在多个节点之间选主,VIP 由主节点持有,主节点故障后备份节点自动接管。VIP 跟 DNS 或者客户端写死的 IP 相比有两个直接好处:一是客户端只需要记住一个地址;二是故障切换对客户端透明,kubeconfig 里的 server 一直是同一个 VIP。

在实际部署中,很多教程会让你用 haproxy 代替 nginx。我选 nginx 是因为集群里本身可能已经有其他 nginx 实例,运维更熟悉,配置也直观,stream 配置块只有几行就能搞定。两个方案都能用,没必要在这种简单场景里引入额外组件。

2. 二进制部署需要提前准备的组件与证书体系

2.1 新增 master2 要准备哪些二进制文件

Kubernetes 二进制部署最大的特点就是所有组件都是独立进程,不依赖容器运行时去拉镜像。新增 master2 需要准备的组件和原有 master1 完全对应:

  • kube-apiserver
  • kube-controller-manager
  • kube-scheduler
  • etcd
  • kubelet(如果你打算让 master2 也运行业务 pod)
  • kube-proxy
  • kubectl

版本必须和 master1 保持一致。用下面的命令先确认真实版本:

kube-apiserver --version etcd --version kubelet --version

下载二进制时要注意架构(amd64/arm64),下载完放到 /usr/local/bin 并加执行权限。版本不一致比较常见的一个问题是 apiserver 和 etcd 之间兼容性,比如新版 apiserver 对旧版 etcd 的某些 API 不兼容,启动后直接报错。确认版本一致之后再往下走。

2.2 证书体系怎么扩展:SAN 是重点

二进制部署的集群,证书体系通常是自己用 openssl 或 cfssl 搭的。新增 master2 后,apiserver 提供给客户端访问的地址变多了,包括:

  • master1 IP 10.0.0.11
  • master2 IP 10.0.0.12
  • VIP IP 10.0.0.100
  • 各主机名
  • kubernetes.default.svc 这类集群内部域名

之前单节点部署时,apiserver 证书里很可能只有 master1 的 IP 和主机名。如果你不加修改直接用这套证书启动 master2,外部客户端用 https://10.0.0.100:6443 访问 apiserver 时,TLS 握手会失败,报错类似 x509: certificate is valid for 10.0.0.11, not 10.0.0.100。这个问题的根因就是证书 SAN 里没有新的 IP。

所以必须重新签发 apiserver 证书,而且要保证 master1 和 master2 共用同一份 CA,这样 kubelet、kube-proxy 等客户端在验证 apiserver 时才会信任它。

具体做法是在 master1 上重新生成 apiserver 的 CSR,把 SAN 扩展完整。如果你用的是 cfssl,JSON 配置大致是这样的:

{ "CN": "kubernetes", "hosts": [ "10.0.0.11", "10.0.0.12", "10.0.0.100", "127.0.0.1", "192.168.0.1", "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.local" ], "key": { "algo": "rsa", "size": 2048 } }

如果你用 openssl,就修改 openssl.cnf 的 [alt_names] 段,把所有 IP 和域名都追加进去:

subjectAltName = IP:10.0.0.11, IP:10.0.0.12, IP:10.0.0.100, IP:127.0.0.1, IP:192.168.0.1, DNS:kubernetes.default.svc

签发完成后,把新的 apiserver.crt 和 apiserver.key 同时分发到 master1 和 master2 对应目录,并重启两边的 apiserver。

这里我吃过一次亏:签发新证书时只加了 master2 的 IP,忘了 VIP。结果 LB 配好了,从 LB 访问 6443 直接报证书不匹配,排查了半个小时才反应过来。SAN 列表里的 VIP 一定不能漏,这是整个架构能跑通的必要条件。

2.3 etcd 从单实例到多实例

二进制部署的集群里,etcd 通常也是单实例。我们的目标架构里有 master1 和 master2 两个控制面节点,所以 etcd 至少需要变成两个成员的集群。

有两种选择:

  1. master1 和 master2 各跑一个 etcd,形成双成员集群
  2. 再加一个独立节点或复用 worker 节点,形成三成员集群

双成员 etcd 在原理上不如三成员稳,因为当其中一个成员挂掉,剩余一个只有一半的投票权,无法选出 leader,整个集群不可写。但在资源和节点受限的实验室环境下,双成员也够用,毕竟另一台 master 还活着。我这次选的是三成员方案,新增了一个单独的 etcd 节点 10.0.0.16,只跑 etcd,避免和 kubelet 的资源竞争。

不管选哪种,最核心的一步是把原本单节点的 etcd 改造成集群。

初始部署时 etcd 启动命令里配置的是 --initial-cluster=master1=https://10.0.0.11:2380 --initial-cluster-state=new。现在要新增成员,必须改成:

--initial-cluster=master1=https://10.0.0.11:2380,master2=https://10.0.0.12:2380,etcd3=https://10.0.0.16:2380 \ --initial-cluster-state=existing

注意最后这个 existing,它的意思是 etcd 进程启动时不再尝试自己初始化一个新集群,而是作为已有集群中的一员去接入。如果这里保持为 new,新启动的 etcd 会尝试再次初始化集群,大概率会跟现有 etcd 互相冲突,轻则成员列表混乱,重则数据目录直接损坏。

操作顺序也很重要。先在 master1 上修改 etcd 启动参数并重启,让 master1 以 existing 模式加入原有的单节点集群。然后在 master2 和 etcd3 上同样修改配置并启动,最后用 etcdctl 确认集群状态:

etcdctl --endpoints=https://10.0.0.11:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ member list

如果输出里能看到三个成员并且都是 started,这一步就算完成了。

2.4 新节点需要拷贝哪些证书

master2 上要放四类证书文件:

  • /etc/kubernetes/pki/ca.crt 和 ca.key,集群的根 CA
  • /etc/kubernetes/pki/apiserver.crt 和 apiserver.key,重新签发后的新证书
  • /etc/kubernetes/pki/etcd/ca.crt,etcd 通讯用 CA
  • /etc/kubernetes/pki/etcd/server.crt 和 server.key,etcd 服务端证书,需要在 SAN 里包含 master2 的 IP

如果用 kubeadm 初始化的集群,证书在 /etc/kubernetes/pki 下已经分好了。手动部署的集群需要自己确认。最简单的方式是直接在 master1 上把这些文件用 scp 或内网共享目录拷贝到 master2,然后逐一核对校验值,确保证书没有在传输过程中损坏。

3. 两台 LB:nginx 四层负载均衡与 keepalived 高可用实操

3.1 nginx stream 四层代理配置

nginx 默认编译一般不会带 stream 模块,先检查一下:

nginx -V 2>&1 | grep stream

如果没有,需要重新编译安装一个带 --with-stream 的版本,或者直接用发行版自带的 nginx 包,通常默认都带。我用的系统是 CentOS 系的,yum 安装的 nginx 自带 stream 模块,省了这一步。

nginx 的关键配置写在 /etc/nginx/nginx.conf 顶层,不能在 http 块里写,因为四层转发不是 HTTP 协议。配置如下:

stream { log_format proxy '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time "$upstream_addr"'; access_log /var/log/nginx/k8s-apiserver.log proxy; upstream k8s_apiserver { server 10.0.0.11:6443 max_fails=3 fail_timeout=30s; server 10.0.0.12:6443 max_fails=3 fail_timeout=30s; } server { listen 6443; proxy_pass k8s_apiserver; proxy_connect_timeout 5s; proxy_timeout 300s; } }

注意几个参数:

  • max_fails 和 fail_timeout 控制后端健康检查。如果 apiserver 连续 3 次失败,nginx 会将其标记为不可用,并等待 30 秒后再尝试。这里我调过多次,实测 max_fails=3 fail_timeout=30s 是比较合适的,太灵敏容易误杀正在启动的 apiserver,太迟钝又会让故障恢复变慢。
  • proxy_timeout 300s 是关键。kubectl 执行某些操作时会长时间占用连接,尤其是 exec 和 logs,如果 proxy_timeout 太短,连接会被 nginx 强行断开,表现是 kubectl 操作做到一半突然卡死。
  • 日志里记录了 upstream_addr,这是排查问题的重要信息。apiserver 请求来自哪个后端、每个连接耗时多少,在日志里一眼就能看出来。

如果你的 LB 节点和 master 节点分开部署,listen 6443 没有问题。如果 LB 和 master 在同一台机器,listen 要改成 16443 或者其他不冲突端口。

配置完执行 nginx -t 检查语法,没问题后 systemctl restart nginx。用 curl 验证:

curl -k https://10.0.0.13:6443/version

能返回 JSON 格式的版本信息,说明四层转发已经通了。

3.2 keepalived VIP 配置

keepalived 的配置文件在 /etc/keepalived/keepalived.conf。两台 LB 的配置几乎一样,区别只在优先级和角色。

LB1 上的配置:

! Configuration File for keepalived global_defs { router_id LVS_DEVEL } vrrp_script chk_nginx { script "/etc/keepalived/check_nginx.sh" interval 2 weight -20 fall 3 rise 2 } vrrp_instance VI_1 { state MASTER interface ens33 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 10.0.0.100/24 dev ens33 } track_script { chk_nginx } }

LB2 上的配置大同小异,state 改成 BACKUP,priority 改成 90,其他保持一致。

这里说实话有一个容易踩的点:很多人以为把两台都配成 BACKUP 更好,避免抢占。但 VRRP 协议不支持双 BACKUP 架构,如果两台都是 BACKUP,没有主节点,VIP 永远无法绑定。所以必须有一台是 MASTER。

track_script 里指向的 check_nginx.sh 是自定义的健康检查脚本,内容如下:

#!/bin/bash if ss -lnt | grep -q ':6443'; then exit 0 else exit 1 fi

脚本的作用是探测 nginx 的四层端口是否在监听。如果 nginx 挂了,keepalived 会降低本节点的权重,VIP 自动漂移到另一台 LB。注意脚本一定要加执行权限:

chmod +x /etc/keepalived/check_nginx.sh

另外提一句,如果系统防火墙开启了,需要放行 VRRP 协议。iptables 或 firewalld 默认会拦截 VRRP 组播报文,导致两台 LB 互相感知不到对方,出现双主现象。放行方式:

firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept' firewall-cmd --reload

这一步很容易被忽略,但遗漏之后的问题非常隐蔽,因为看起来 keepalived 服务都正常,VIP 却有可能在某一时刻同时出现在两台机器上。对于 apiserver 这类状态敏感的服务来说,双主会带来不可预测的写冲突。手动测试时可以用 ip addr show 检查 VIP 实际落在哪台机器上。

设置完成后,两台 LB 上分别执行 systemctl enable --now keepalived,然后用 ip addr show 确认 VIP 绑定的位置。正常情况下 vip 会绑定在 MASTER 节点,也就是 LB1 上。把 LB1 上的 keepalived 停掉,几秒后 VIP 应该漂移到 LB2。

3.3 LB 整体联动验证

LB 配置完成之后不要急着继续搭 master2,先把这一层验证扎实。我习惯做三组测试:

第一组,用 VIP 访问 apiserver:

curl -k https://10.0.0.100:6443/version

第二组,停掉 master1 的 apiserver,再访问 VIP:

systemctl stop kube-apiserver curl -k https://10.0.0.100:6443/version

此时流量应该全部转发到 master2。注意 master2 此时可能还没起来,所以需要在 master2 部署完之后再做这组测试。如果你在只有 master1 单节点的情况下先行验证 LB,停掉 apiserver 之后 curl 超时是正常的。

第三组,看 nginx 日志确认负载均衡效果:

tail -f /var/log/nginx/k8s-apiserver.log

请求会交替出现在 master1 和 master2 的上游地址中。如果所有请求都指向同一个节点,检查两台 apiserver 是否都正常监听 6443。

4. master2 节点完整加入流程与全局连接切换

4.1 在 master2 上部署 etcd 并加入集群

先确认 master1 上 etcd 的 systemd unit 内容和证书目录结构,保证 master2 的配置风格一致。然后按顺序操作 master2 的 etcd:

  • 创建 /etc/etcd 配置目录和 /var/lib/etcd 数据目录
  • 将 etcd 的 CA 证书、服务端证书、客户端证书拷贝到 /etc/kubernetes/pki/etcd
  • 编写 systemd unit,关键参数如下:
[Unit] Description=etcd After=network.target [Service] User=root Type=notify ExecStart=/usr/local/bin/etcd \ --name=master2 \ --data-dir=/var/lib/etcd \ --listen-client-urls=https://10.0.0.12:2379,https://127.0.0.1:2379 \ --advertise-client-urls=https://10.0.0.12:2379 \ --listen-peer-urls=https://10.0.0.12:2380 \ --initial-advertise-peer-urls=https://10.0.0.12:2380 \ --initial-cluster=master1=https://10.0.0.11:2380,master2=https://10.0.0.12:2380,etcd3=https://10.0.0.16:2380 \ --initial-cluster-state=existing \ --initial-cluster-token=etcd-cluster \ --cert-file=/etc/kubernetes/pki/etcd/server.crt \ --key-file=/etc/kubernetes/pki/etcd/server.key \ --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt \ --peer-key-file=/etc/kubernetes/pki/etcd/peer.key \ --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt \ --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt [Install] WantedBy=multi-user.target

启动顺序上有个小讲究:先在 master1 上把 etcd 的启动参数改成包含全部成员的 existing 模式并重启,再启动 master2 和 etcd3。如果顺序反了,新节点会一直尝试连接 master1 的 2380,而 master1 又没有以集群模式对外提供 peer 服务,加入会一直超时。

启动完成后,回到 master1 上执行 member list,确认三个 etcd 成员都已就绪。任何节点出现 unhealthy 状态,都不要继续往下走,先把 etcd 集群修好。etcd 是控制面的地基,地基不稳后面全是问题。

4.2 在 master2 上启动 apiserver、controller-manager、scheduler

master2 上的三个控制面组件,启动参数基本照搬 master1,但有几个地方必须修改。

apiserver 的关键参数如下:

ExecStart=/usr/local/bin/kube-apiserver \ --advertise-address=10.0.0.12 \ --bind-address=0.0.0.0 \ --secure-port=6443 \ --service-cluster-ip-range=192.168.0.0/16 \ --service-node-port-range=30000-32767 \ --etcd-servers=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.16:2379 \ --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt \ --etcd-certfile=/etc/kubernetes/pki/etcd/client.crt \ --etcd-keyfile=/etc/kubernetes/pki/etcd/client.key \ --client-ca-file=/etc/kubernetes/pki/ca.crt \ --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \ --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \ --service-account-key-file=/etc/kubernetes/pki/sa.pub \ --service-account-signing-key-file=/etc/kubernetes/pki/sa.key \ --service-account-issuer=https://kubernetes.default.svc.cluster.local \ --authorization-mode=Node,RBAC \ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key \ --allow-privileged=true

apiserver 的 --service-cluster-ip-range 必须和 master1 完全一致。如果这里配成不同的网段,两个 apiserver 对 Service ClusterIP 的理解就不一样,集群会出现各种幽灵 IP。

controller-manager 和 scheduler 的配置简单很多,关键在于启动参数里有一个 --kubeconfig 或者 --master 指向的位置。二进制部署时这里不能再写死 master1 的 IP,要改成 VIP:

--kubeconfig=/etc/kubernetes/controller-manager-kubeconfig.yaml --leader-elect=true

scheduler 同理:

--kubeconfig=/etc/kubernetes/scheduler-kubeconfig.yaml --leader-elect=true

这里的 kubeconfig 里 server 字段必须写成 https://10.0.0.100:6443。因为两台 controller-manager 要竞争同一个 leader 锁,如果 master2 上的 controller-manager 还在连接 10.0.0.11 或 10.0.0.12 的某个地址,而 master1 恢复时又不断电,两边就像在两条平行线上各自选举,集群里会同时出现两个 active 的 controller-manager。

配置文件里:

apiVersion: v1 kind: Config clusters: - cluster: server: https://10.0.0.100:6443 certificate-authority: /etc/kubernetes/pki/ca.crt name: kubernetes contexts: - context: cluster: kubernetes user: controller-manager name: system:controller-manager current-context: system:controller-manager users: - name: controller-manager user: client-certificate: /etc/kubernetes/pki/controller-manager.crt client-key: /etc/kubernetes/pki/controller-manager.key

scheduler kubeconfig 结构一样,只是证书换成 scheduler.crt 和 scheduler.key。

4.3 原有节点与新增节点的 kubeconfig 统一切换到 VIP

这是整个改造里最需要细心的一步。master2 上的三个控制面组件可以通过 VIP 访问 apiserver,但如果 master1 上还是用原来的 IP 连接自己,而 master1 恰好挂掉,那 master1 上的 kubelet、kube-proxy、controller-manager 就全部失去与 API server 的连接,高可用还是纸上谈兵。

所以全局的 kubeconfig 都要切换。需要改的文件包括:

  • /etc/kubernetes/controller-manager-kubeconfig.yaml
  • /etc/kubernetes/scheduler-kubeconfig.yaml
  • /etc/kubernetes/kubelet.conf
  • /etc/kubernetes/kube-proxy.kubeconfig(如果 kube-proxy 独立使用配置文件)
  • ~/.kube/config(管理员 kubectl 配置)

切换方法很简单,把所有 server 字段从 10.0.0.11 改成 10.0.0.100:

sed -i 's#https://10.0.0.11:6443#https://10.0.0.100:6443#g' /etc/kubernetes/*.yaml /etc/kubernetes/*.conf

然后逐一重启对应服务:

systemctl restart kube-apiserver kube-controller-manager kube-scheduler kubelet kube-proxy

注意顺序:因为 kubelet 和管理面组件自身需要通过 apiserver 通信,而 apiserver 重启后需要几秒才能重新监听,所以整个切换过程中出现短暂连接失败是正常的,服务会自动重试。如果重启完两分钟后节点状态还是 NotReady,再通过 systemctl status 查看具体报错。

worker 节点上的 kubelet.conf 和 kube-proxy.kubeconfig 也要做同样的修改。如果你的 worker 不直接访问 apiserver,而只通过 kubelet 访问,其实 kubelet.conf 里的 server 字段改了就行。

还有一个容易忽略的点:apiserver 自身的 --advertise-address 不能改成 VIP。advertise-address 告诉 kubelet 等组件用什么地址来回连 apiserver。如果你把 master1 的 advertise-address 写成 VIP,而当前 VIP 在 LB2 上,那么 kubelet 对 master1 的探活和日志回传都会跑到 LB2 上去,网络路径绕了一圈不说,LB2 的 nginx 还不一定能转发 kubelet 的 10250 端口流量。所以 advertise-address 保持本机 IP,不要动。

4.4 验证集群高可用

所有服务都起来之后,先看整体状态:

kubectl get nodes -o wide kubectl get cs kubectl get leases -n kube-system

get cs 的输出可能不再显示各个组件的健康状态,这跟新版 Kubernetes 移除了 master 组件健康检查接口有关,不必担心。可以通过下面的方式手动检查各组件日志是否报错。

关键验证是故障演练。我实际做过的测试有三个:

  1. 停掉 master1 上的 apiserver,执行 kubectl get nodes。正常情况是命令不中断,由 master2 继续服务。注意如果 kubectl 客户端里有对特定证书的缓存,连接重建需要几秒,耐心等待即可。

  2. 停掉 master1 上的 controller-manager 和 scheduler,观察集群是否有新的 Deployment 能被正常调度。正常情况下 master2 上的 controller-manager 会通过 leader 选举自动接管,调度和控制器功能不受影响。

  3. 停掉 LB1 上的 keepalived,VIP 漂移到 LB2,再次执行 kubectl get nodes,依然正常。

这三组测试全部通过,说明高可用架构真正生效了。只做第一组,不测 controller-manager 和 scheduler,很容易出现一种假象:apiserver 是好的,但整个控制面的协调工作停摆了,业务 pod 依然无法正常被调度。

5. 高可用部署中的常见坑与定位技巧

5.1 VIP 连不上、证书不匹配、端口不通

这类问题在刚部署完的阶段出现频率非常高,建议先排除几个最常见的点。

症状可能原因排查命令
curl https://VIP:6443 超时nginx 未监听、keepalived 未启动、防火墙拦截ss -lntp | grep 6443;ip addr show
curl 返回证书错误apiserver 证书 SAN 缺少 VIP 或 master2 IPopenssl x509 -in apiserver.crt -noout -text
kubectl get nodes 报 connect: connection refusedkubeconfig 的 server 还是旧 IPkubectl config view --minify
两台 LB 同时持有 VIPVRRP 报文被防火墙拦截journalctl -u keepalived | tail -30

证书问题尤其容易误判。我第一次在 master2 上启动 apiserver 时,日志一直报 x509: certificate is valid for 10.0.0.11, not 10.0.0.12。排查了半天才发现 apiserver 的证书还是老版本,根本原因是我重新签发证书后没有同步到 master2。这类问题用 openssl 看证书 SAN 非常快。

5.2 etcd 反复加入失败、报错 request timed out

新增 etcd 节点时最常见的报错是:

etcdserver: request timed out

这个报错通常有两类原因。一是 peer 端口 2380 不通,通过 telnet 验证:

telnet 10.0.0.12 2380

如果端口不通,检查 etcd 的 listen-peer-urls 和防火墙。二是 --initial-cluster-state 配置不一致,新节点状态是 existing,而原有节点还是 new,两边无法对齐。

另外一个隐蔽的问题是 etcd 的 --data-dir 里如果有之前残留的旧数据,etcd 启动时可能会尝试用旧集群配置初始化,导致 member 信息不一致。处理办法是备份数据目录后清空,让 etcd 以全新成员身份加入。

etcd 集群加入过程我强烈建议一步一验证:

etcdctl --endpoints=https://10.0.0.11:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ endpoint health --cluster

输出全部 healthy 再继续下一台,不要一次性把三台都启动完才检查,那样出了问题很难定位是哪一台加入失败。

5.3 apiserver 健康检查与 leader 选举细节

高可用架构下,控制面组件能否平稳切换,很大程度取决于 leader 选举配置是否正确。

controller-manager 和 scheduler 必须保持 --leader-elect=true。默认值确实是 true,但有些脚本在部署时为了简化会加上 --leader-elect=false,这在单节点时工作正常,多节点时就是灾难。两个 controller-manager 同时运行,会造成 Deployment、Service 等资源的重复 reconcile,虽然多数场景下不会产生严重的资源损坏,但行为会变得不可预测。

查看当前 leader 是谁:

kubectl get leases -n kube-system kubectl get endpoints kube-controller-manager -n kube-system -o yaml

输出里会显示持有者名称,正常情况下只能有一个 holderIdentity。

另外,controller-manager 和 scheduler 的 kubeconfig 里 server 指向 VIP 之后,要保持调度组件和 apiserver 之间的网络路径短且稳定。如果 VIP 漂移到远端的 LB,而两个 master 距离较远,leader 续约的网络延迟会增大,极端情况下可能出现频繁的 leader 切换。这种情况下可以把 LB 改成在 master1 和 master2 本机各放一套,形成分层结构。

5.4 版本不一致引发的兼容性问题

二进制部署不像 kubeadm 那样有版本强校验,换版本容易出问题。比如 apiserver 4.x 版本去连接 3.x 版本的 etcd,Kubernetes 官方规定了 apiserver 与 etcd 的版本兼容矩阵,跨度过大时 apiserver 可以直接拒绝启动。

我在实际部署中遇到过一次:master1 是某 k8s 版本,由于下载源问题,给 master2 下载到了另一个 patch 版本,启动时 apiserver 直接退出。解决办法很简单,从 master1 上把对应二进制文件拷到 master2,或者检查下载版本的 checksum 后再上传。补丁版本不一致通常不影响正常运行,但版本跨度大时,最好保持完全一致,省得给自己埋雷。

6. 压测与日常维护注意事项

6.1 高可用集群建议做一次压力测试

部署完成不等于高可用生效。我建议至少跑一轮基础压测再上线,核心目的是验证 nginx 四层转发的连接稳定性,以及 keepalived 切换时对长连接的影响。

简单的压测方式是用 ab 或者 wrk 从外部访问 VIP 的 apiserver 端口:

wrk -t4 -c100 -d30s https://10.0.0.100:6443/version

这个场景主要看连接是否建立,但不建议对 apiserver 做过于激烈的 QPS 压测,直接把 CPU 打满会影响控制面稳定性。更合理的做法是模拟实际业务操作,比如并发创建、删除 Deployment、访问 Service 等。观察 nginx 日志里的连接耗时,重点看有没有大量 5 秒以上的连接。

如果发现 nginx 日志中有很多 upstream 连接重置的报错,先看后端 apiserver 是否因负载过高而主动断连。一个常见的坑是 nginx 默认的 proxy_timeout 太短,长连接在 60 秒被 nginx 断开,而 kubectl exec 这类操作往往超过这个时间。把 proxy_timeout 调到 300 秒甚至 600 秒能解决大部分问题。

6.2 VIP 切换时的注意点

keepalived 的 VIP 切换理论上在几秒内完成,但要注意 Kubernetes 客户端可能已经缓存了旧的 TCP 连接。kubectl 是每次命令新建连接,影响不大。但如果有一些长驻进程通过 client-go 持续访问 apiserver,比如 controller-manager、scheduler 自身的连接,它们会通过重试重新握手,不需要额外干预。

真正需要注意的是,不要在 VIP 切换的瞬间去执行真的需要写入集群的操作,比如 kubectl apply。虽然 Kubernetes 有重试机制,但极短时间内可能遇到连接拒绝或超时,表现为操作失败。如果必须在这个窗口期操作,重试一次即可。

6.3 证书过期时间要提前排期

二进制部署的集群,证书有效期通常是自己签定的,常见的是一年或三年。证书过期后整个集群不可用,而且有些证书过期了不一定立刻显眼地报错。比如 apiserver 的客户端证书过期,kubectl 可能报 x509: certificate has expired or is not yet valid,这个错误很容易识别。但 kubelet 的证书过期,节点状态会变成 NotReady,且日志里不一定会直接提示证书过期。

建议集群部署完成后立刻做一个证书到期时间清单:

openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -enddate openssl x509 -in /etc/kubernetes/pki/kubelet.crt -noout -enddate

有条件的话把过期提醒写进监控系统。二进制部署没有 kubeadm 的自动轮换机制,证书管理必须靠人工。

6.4 备份策略要有版本意识

高可用不代表数据不会丢。etcd 集群三节点只是避免了单点故障,如果人为误删了一个 namespace 或者执行了野生的 delete pod --all,数据还是会被正常同步删除。我习惯每天凌晨做一次 etcd 快照:

ETCDCTL_API=3 etcdctl --endpoints=https://10.0.0.100:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /data/backup/etcd-snapshot-$(date +%F).db

恢复时用 etcdctl snapshot restore 恢复到一台新节点,再通过 etcd 集群重建的方式接入其他成员。这个过程建议单独做一次演练,不要等到真出事才第一次尝试恢复。

6.5 日志与监控要补齐

二进制部署的集群,日志都有 systemd journal 管理。通过 journalctl 查看每个组件的状态:

journalctl -u kube-apiserver -f journalctl -u etcd -f

建议把日志输出到文件并接入采集系统,方便故障时回溯。apiserver 的审计日志、nginx 的访问日志、keepalived 的状态切换日志,这些都是排查高可用问题的一手材料。

7. 我实际操作中总结的几点经验

整套流程走下来,最大的感受是:二进制部署高可用集群这件事,难度不在某个单独的技术点上,而在所有组件之间的连接关系梳理。apiserver 要连 etcd,controller-manager 要连 apiserver,kubelet 要连 apiserver,LB 要连 apiserver,每一环都要指向正确的地址,每一份证书都要覆盖正确的 IP 和域名。

证书 SAN 里加 VIP 这个细节,如果从一开始就规划好,后面能省很多排查时间。建议在最初部署单节点集群时就预留好 VIP 和 master2 的 IP,哪怕当时还没有这两台机器。证书签一次用一年,如果忘了加,后面再重新签发要同时重启两个 master 的 apiserver,所有使用旧证书的客户端也要重新加载,涉及面很广。

另一个体会是,keepalived 的 VIP 切换测试不要只做一次。VRRP 协议在持续运行中可能因为网络抖动、防火墙策略变更而出现异常。我现在的做法是每月手动切换一次 VIP,把 LB1 的 keepalived 临时停掉观察切换,再恢复,全程只需要几十秒,但能确保整个高可用链路长期处于可用状态。

最后想说的是,二进制部署虽然比 kubeadm 繁琐得多,但正因为所有组件都是手动启动、手动配证书、手动接连接,你对集群内部的工作方式会比用一键部署的人清楚得多。这套知识在排查生产环境问题时特别值钱,哪怕之后你切回 kubeadm 管理集群,遇到类似问题也能快速定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询