K8s单节点集群扩容实战:二进制部署master2与nginx+keepalived高可用架构
先说场景吧。很多团队搞 Kubernetes 集群第一步通常都是拿一台机器把单节点的集群先跑起来组件全部用二进制装好、证书手动签好验证业务没问题之后才回过头来想生产环境和灾备的事万一这台 master 挂了怎么办流量入口怎么抗住所以就有了这个标题里说的事——在已经存在的单节点基础上横向扩展出 master2前面再架两台机器跑 nginx 加 keepalived把 API Server 的流量收敛到一个虚拟 IP 上。这基本是所有自建 K8s 集群从能跑走向能扛的必经一步。先说清楚这套方案定位它不依赖 kubeadm 初始化而是纯二进制部署、手动维护证书和配置文件。好处是每个组件都是裸进程逻辑透明、方便排查也更容易定制参数代价是新增 master 节点时没有现成的kubeadm join可用必须手工同步证书、改 etcd 集群、注册 kubelet任何一个环节漏了都会导致新节点起不来或者连不上集群。本文适合已经有一套单节点二进制集群、正准备扩容的运维和开发同学参考也适合完全没接触过多节点部署、想理解 K8s 控制面组件之间是怎么协作的人。下面直接进入正题。1. 部署架构与整体思路1.1 目标架构解析这套方案做完之后集群的形态大致是这样3 个 master 节点原有的 master1 作为第一个控制面节点继续保留在此基础上新增 master2后续可能还会加 master3所以证书和配置最好一次做到位。2 个 LB 节点分别安装 nginx四层负载均衡和 keepalived提供虚拟 IP 漂移能力它们共同对外提供一个 VIP。所有客户端、组件、kubelet 都通过这个 VIP 访问 API Server。Worker 节点继续通过 VIP 接入控制面不需要因为 master 变化而修改配置。从流量路径看请求到达 VIP 后由当前 keepalived 的 master 节点上的 nginx 接收再按照配置把流量转发到 master1 和 master2 的 6443 端口。任何一个 LB 节点宕机VIP 会漂移到另一台任何一个 master 宕机nginx 会自动将其从上游摘除。控制面整体对外表现为一个稳定的 https 入口。这个架构最核心的思路是把控制面节点和流量入口节点彻底分开。master 只负责跑 apiserver、etcd、controller-manager、scheduler 这些控制面组件由前面的 nginx 统一代理LB 层只做流量转发不参与集群状态也不存储任何数据。这样任意一层的故障都不会直接带崩整个集群非常经典的解耦设计。1.2 为什么坚持二进制部署而不是重新用 kubeadm 搭一套我知道很多人会问这个问题。已经有单节点集群了直接把旧的干掉用 kubeadm init 重新来一个三 master 的高可用集群不是更省事吗理论上确实省事但实际生产里升级改造最忌讳的就是把线上环境推倒重来。很多团队的单节点集群里已经跑着业务、配了各种自定义的 admission webhook、存储插件、网络插件甚至手工调整过 apiserver 的启动参数。如果推倒重建这些都得上线前重新验证一遍回归成本极高。而二进制部署的集群组件之间耦合度低新增节点本质上是复制一个 master 的配置文件 证书再改少量分布式相关的参数对现有环境的影响面最小。另外二进制部署虽然繁琐但每个组件的启动参数、证书路径、监听端口都是明明白白写在 systemd unit 文件里的。出了问题直接看 process 列表就能定位是哪个组件异常不像 kubeadm 那样封装了一层报错信息绕来绕去。对于要长期维护集群的团队来说这种透明性比省那点初始化的功夫更值钱。当然如果原有集群是用 kubeadm 装的那直接 kubeadm join 会更顺完全没必要手工 curl 二进制下来折腾——选择哪个方案取决于现有集群的出身不要盲目套模板。1.3 版本选择与环境规划版本不建议太新。K8s 社区节奏快但生产环境追求的是稳定和资料可查一般选1.20到1.26之间比较稳妥。我这次演示基于1.23.xetcd 使用3.5.x这两个版本组合验证过大量生产案例兼容性很好。节点规划可以参考下面的表格实际根据你手里的机器调整 IP 即可节点角色主机名IP 地址建议配置旧控制面节点master1192.168.1.104C8G100G 系统盘新增控制面节点master2192.168.1.114C8G100G 系统盘负载均衡节点1lb01192.168.1.122C4G50G 系统盘负载均衡节点2lb02192.168.1.132C4G50G 系统盘虚拟 IPVIP-192.168.1.100keepalived 管理不绑定到固定机器Worker 节点规划看业务需要这里不展开。所有节点建议使用 CentOS 7.9 或 Ubuntu 20.04内核版本不低于 3.10网络插件常用的 flannel 和 calico 都能正常跑。如果机器配置允许master 节点的数据盘尽量用独立的etcd 的 IO 压力要比普通业务进程大得多。2. 前期准备LB 节点与 master2 环境初始化2.1 基础系统配置这一步属于磨刀不误砍柴工所有新节点都要做统一的基础设置。先把主机名改好避免后面看到一堆 localhost 分不清谁是谁# master2 上执行 hostnamectl set-hostname master2 # lb01 上执行 hostnamectl set-hostname lb01 # lb02 上执行 hostnamectl set-hostname lb02然后统一 hosts 解析。注意不仅是新节点要写旧的 master1 也要检查一遍最好所有节点上使用的都是同一份 hosts 内容这样证书里的 IP 和主机名才能对得上cat /etc/hosts EOF 192.168.1.10 master1 192.168.1.11 master2 192.168.1.12 lb01 192.168.1.13 lb02 EOF接着是防火墙和 SELinux。生产环境我不建议直接systemctl stop firewalld而是放行需要的端口但很多内网测试环境图省事就直接关了。如果你只想快速验证方案可以临时关闭systemctl stop firewalld systemctl disable firewalld sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config setenforce 0这里要提醒一句如果你是在需要合规审计的环境里操作别图省事关防火墙自己按端口维度把规则写好。K8s 控制面相关端口如下apiserver6443etcd2379客户端、2380集群间通信kubelet10250scheduler10259controller-manager10257LB 节点如果开启了 firewalld需要放行 VIP 的 6443 端口以及 keepalived 的 VRRP 协议通常是 112 端口。内核参数也建议提前统一。K8s 对网络转发要求比较高尤其是 calico 使用 BGP 模式时需要开启 IP 转发cat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-ip6tables 1 net.bridge.bridge-nf-call-iptables 1 net.ipv4.ip_forward 1 EOF sysctl -p /etc/sysctl.d/k8s.confvm.swappiness 也可以顺手调成 0 或 10减少内存交换带来的不稳定。每台新节点做完这些之后建议统一测试一下节点间的网络连通性ping一下所有 host检查 6443 端口是否通master 上LB 之间互相 ping VIP 网段是否正常。2.2 免密登录与文件分发准备新增 master2 需要拷贝主节点上的大量文件包括二进制、证书、配置文件。手打 scp 一个个传也可以但文件多了极易出错。我习惯在 master1 上生成一对部署专用的密钥分发给 master2 和两台 LB# 在 master1 上 ssh-keygen -t rsa -b 4096 -f /root/.ssh/deploy_key -N cat /root/.ssh/deploy_key.pub /root/.ssh/authorized_keys ssh-copy-id -i /root/.ssh/deploy_key.pub root192.168.1.11 ssh-copy-id -i /root/.ssh/deploy_key.pub root192.168.1.12 ssh-copy-id -i /root/.ssh/deploy_key.pub root192.168.1.13之后所有文件分发都用scp -i /root/.ssh/deploy_key指定密钥执行。用专用密钥而不是直接复用 root 密码登录至少能减少一部分误操作风险——比如脚本里不小心把密码打出来落在 shell history 里就很尴尬。同时确认一下 master2 上已经准备好 Kubernetes 二进制文件。最稳妥的方式是让 master2 和 master1 使用完全相同的二进制版本直接用 scp 从 master1 拷贝# 在 master1 上执行 scp -i /root/.ssh/deploy_key -r /opt/kubernetes/bin root192.168.1.11:/opt/kubernetes/bin如果你的单节点集群当初是把二进制放在/usr/local/bin之类的目录就先确认一下原来的路径。后面所有 systemd unit 文件的ExecStart路径要和你拷贝到的位置保持一致这一块前后不一致导致的启动失败是我见过最普遍的翻车点。2.3 目录结构与配置基线检查二进制部署最忌讳的是每个节点目录结构不一致。master1 上的目录结构可能是这样的/opt/kubernetes/ ├── bin/ │ ├── kube-apiserver │ ├── kube-controller-manager │ ├── kube-scheduler │ ├── kubelet │ └── kube-proxy ├── cfg/ │ ├── kube-apiserver.conf │ ├── kube-controller-manager.conf │ ├── kube-scheduler.conf │ ├── kubelet.conf │ └── kube-proxy.conf ├── ssl/ │ ├── ca.pem │ ├── ca-key.pem │ ├── server.pem │ ├── server-key.pem │ ├── kubelet.pem │ └── ... └── logs/在动手之前先把 master1 上这些目录完整列出来逐个确认每个目录里有哪些文件。然后对照检查 master2 是否也建立了相同目录mkdir -p /opt/kubernetes/{bin,cfg,ssl,logs}这个目录基线非常重要。后面你写 systemd unit 文件时ExecStart里引用的--config参数路径、证书路径全部依赖这套目录结构。如果 master2 上少建了一个目录启动时就会报 file not found排查起来非常绕。3. master2 加入集群证书、etcd 与控制面组件同步3.1 证书配置的两种处理方式单节点集群新增 master2最大的坑之一就是证书。如果你的 apiserver 证书签发时只写了 master1 的 IP 和 hostname那么 master2 上的 kubelet 或者其他组件访问 apiserver 时证书校验会直接失败报 x509 错误。处理方式分两种一种是从 master1 拷贝一套现成的证书过来。这种方式最快但只适用于 apiserver 并没有开启严格的 client cert 校验、并且你已经知道证书的 IP SAN 里没有 master2 的场景。实际情况中很多单节点集群的 server.pem 只包含 master1 的 IP那直接拷贝过来是过不了 client 校验的。另一种方式是重新签发证书把 master2 的 IP 写进 apiserver 的 server 证书 SAN。这也是我推荐的处理方式。证书签发用你原有的 CA 来签这样整个集群信任链不变。签发完之后把新的 server.pem、server-key.pem 分发给所有 master 节点。很多团队早期图省事直接用 kubeadm init 生成了一整套证书然后用 kubeadm 的证书密钥导出到二进制部署的集群里这确实可行但不建议深究因为 kubeadm 签发的证书有效期通常只有一年到期之后的续签又是一套功夫。自己维护 CA 和证书可以用openssl x509 -req按需签发过期时间自己定主动权在手里。比如openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial \ -out server.pem -days 3650 -extensions v3_req -extfile server-csr.confserver-csr.conf里的subjectAltName必须包含 master1、master2 的 IP还有 VIP 地址。下面是一个典型的配置[ v3_req ] keyUsage keyEncipherment, dataEncipherment, digitalSignature extendedKeyUsage serverAuth subjectAltName alt_names [ alt_names ] IP.1 192.168.1.10 IP.2 192.168.1.11 IP.3 192.168.1.100 DNS.1 master1 DNS.2 master2 DNS.3 kubernetes.default.svc.cluster.local因为通过 VIP 访问 apiserver证书里不写 VIP 的话所有外部的 kubectl 都会报证书校验失败。这是个很容易被忽略的细节一定要提前加进去。证书签发完在 master1 和 master2 上各存一份cp server.pem /opt/kubernetes/ssl/server.pem cp server-key.pem /opt/kubernetes/ssl/server-key.pem sysctl restart kube-apiserver # 先在 master1 上重启3.2 etcd 集群扩容从单节点到双成员etcd 是整个集群状态的存储也是新增 master 时最容易出问题的组件。单节点集群的 etcd 配置文件里initial-cluster通常只有master1http://192.168.1.10:2380这一条。要加 master2就要把它改成两个成员。这里要注意千万不要直接手动改 etcd 的静态配置文件然后重启那样 etcd 会认为集群拓扑变了容易产生数据不一致。正确的方式是在 etcd 集群运行状态下通过 etcdctl 动态添加成员。假设 etcdctl 的 API 版本是 v3export ETCDCTL_API3 etcdctl --endpointshttps://192.168.1.10:2379 \ --cacert/opt/kubernetes/ssl/ca.pem \ --cert/opt/kubernetes/ssl/server.pem \ --key/opt/kubernetes/ssl/server-key.pem \ member add master2 https://192.168.1.11:2380执行成功后etcdctl 会输出一段提示信息其中包括ETCD_INITIAL_CLUSTER环境变量。这段信息非常关键它告诉你在 master2 的 etcd 配置文件里应该怎样填写 initial-cluster。很多人在这一步就随便写所以 master2 上的 etcd 一直起不来。把 master1 的 etcd 配置拿出来找到类似这一段ETCD_INITIAL_CLUSTERmaster1https://192.168.1.10:2380,master2https://192.168.1.11:2380 ETCD_INITIAL_CLUSTER_STATEexisting注意ETCD_INITIAL_CLUSTER_STATE的值在动态添加成员后必须是existing不能在 master2 上写成new否则 etcd 节点会初始化成一个全新的空集群。同时修改 master1 上的 etcd 配置确保两边的 initial-cluster 信息一致然后重启 master1 的 etcdsystemctl restart etcd重启 master1 的 etcd 时可能会短暂影响控制面的读取所以在业务低峰期操作比较好。etcd 起来之后用 member list 确认两个成员都 healthyetcdctl member list正常情况下会看到两个成员并且 leader 存在follower 状态正常。如果你在这一步遇到etcdserver: request timed out大概率是 master2 上的 etcd 还没有启动master1 在等待它心跳。这时候只需要继续把 master2 的 etcd 配置写好并启动即可。3.3 同步 kube-apiserver 与控制面组件配置master2 上的 kube-apiserver 配置和 master1 相比只有少数参数不同监听 IP 从192.168.1.10改成192.168.1.11其他像 etcd 连接地址、service CIDR、证书路径等都是一样的。最简单的方式是先拷贝 master1 的配置文件再全局替换 IPscp -i /root/.ssh/deploy_key \ root192.168.1.10:/opt/kubernetes/cfg/kube-apiserver.conf \ /opt/kubernetes/cfg/kube-apiserver.conf sed -i s/192.168.1.10/192.168.1.11/g /opt/kubernetes/cfg/kube-apiserver.conf但用 sed 全局替换有一定风险。如果你在 apiserver 配置里写了证书 SAN 或某些 URL 参数不该替换的也被替换了就会出问题。所以更稳妥的方式是手动编辑配置文件只改这两处--advertise-address改为本机 IP--bind-address保持0.0.0.0或改为本机 IP其余的--client-ca-file、--etcd-servers、--service-cluster-ip-range都保持与 master1 一致。controller-manager 和 scheduler 的配置相对简单它们通过 kubeconfig 连接本机 apiserver所以--kubeconfig里的 server 地址如果写的是https://192.168.1.10:6443也要改成https://192.168.1.11:6443或https://127.0.0.1:6443。用本机回环地址更好少一层网络转发也避免 apiserver 地址变化导致这两个组件掉线。controller-manager 和 scheduler 的 systemd unit 文件通常长这样[Unit] DescriptionKubernetes Controller Manager Documentationhttps://github.com/kubernetes/kubernetes [Service] ExecStart/opt/kubernetes/bin/kube-controller-manager \ --config/opt/kubernetes/cfg/kube-controller-manager.conf Restarton-failure RestartSec5 [Install] WantedBymulti-user.target配置文件细节确认完之后复制到 master2 对应位置然后逐个启动systemctl daemon-reload systemctl enable --now kube-apiserver systemctl enable --now kube-controller-manager systemctl enable --now kube-scheduler启动之后不要急着继续先确认进程状态正常再进入下一步。检查命令systemctl status kube-apiserver ss -lntp | grep 6443 curl -k https://192.168.1.11:6443/healthz如果 healthz 返回 ok说明 apiserver 本身和服务发现都正常。注意这里 curl 加-k是因为我们访问的是 IP 而不是证书里的 DNS跳过校验只是用于验证服务存活并不代表客户端也能通过证书校验。3.4 kubelet 与 kube-proxy 注册控制面组件起来之后还差 kubelet 和 kube-proxy。这两个组件是直接跑在每个节点上的负责管理 Pod 和网络规则。master2 的 kubelet 配置文件同样是拷贝 master1 的之后改 IP。但这里有个关键点kubelet 启动时会向 apiserver 发起 CSR证书签名请求然后等待 controller-manager 自动批准如果配置了 auto-approve或者管理员手动批准。二进制部署的集群往往没有自动批准机制所以 master2 上的 kubelet 可能一直处于 Pending 状态。先拷贝配置并启动 kubeletscp -i /root/.ssh/deploy_key \ root192.168.1.10:/opt/kubernetes/cfg/kubelet.conf \ /opt/kubernetes/cfg/kubelet.conf # 修改 kubelet.conf 里的 server 地址为 https://192.168.1.11:6443然后启动systemctl enable --now kubelet纯二进制部署时kubelet 通常使用 bootstrap kubeconfig 来向 apiserver 发起 CSR。你可以在 master1 上查看证书签名请求kubectl get csr如果看到 master2 发来的 CSR 处于 Pending 状态手动批准即可kubectl certificate approve csr-name如果是用--register-with-taints配置了 master 节点默认的污点要确认 kubelet 的 systemd unit 里是否带了--node-labelsnode-role.kubernetes.io/master这类的标注。有些单节点集群配置了特殊标签拷贝过来不改的话master2 上 kubelet 会以 worker 身份注册控制面节点身份就丢了。kube-proxy 配置相对简单。拷贝配置文件改 apiserver 地址为 VIP 或本机 IP然后systemctl enable --now kube-proxy这里我建议 kubelet 和 kube-proxy 的 apiserver 地址都指向 VIP192.168.1.100因为 kubelet 需要通过 apiserver 上报节点状态。如果配置指向 master1 的 IP当 master1 故障而 VIP 正常工作kubelet 与 apiserver 的通信会中断节点状态就会处于 NotReady。指向 VIP 则天然具备故障转移能力。3.5 验证 master2 已加入集群在 master1 上执行kubectl get nodes输出应该包含 master1 和 master2状态都是 Ready。再检查组件状态kubectl get cs如果 controller-manager 和 scheduler 都显示 Healthy说明控制面组件和 apiserver 的连接没有问题。接着检查 etcd 健康状态etcdctl endpoint health --cluster这个命令会输出所有 etcd 成员的健康状态。如果全部是 healthy说明 etcd 集群本身扩容成功。此时整个集群的控制面已经具备了两个 master但流量入口还分散着接下来就该上 LB 层了。4. 两台 LB 节点搭建 nginx keepalived 高可用负载均衡4.1 为什么要用 L4 而不是 L7 负载均衡在给 K8s 做入口负载均衡时最常见的争论是用 L4四层 TCP/UDP还是 L7七层 HTTP/HTTPS。如果前面已经有专业的硬件负载均衡或者云厂商的 SLB 产品通常直接用 L4 转发 TCP 到 6443 端口最稳妥性能和稳定性都有保障。对于自建的 LB 节点我强烈建议用 nginx 的 stream 模块做四层转发而不是用 http 模块做反向代理。原因是 Kubernetes 的 apiserver 与客户端之间是长连接HTTP/2 或 WebSocketL7 代理会做协议解析和连接复用apiserver 看到的客户端 IP 会被代理替换而且遇到长连接比如 exec、logs、port-forward时L7 层需要额外配置超时参数非常容易把连接断开。而 L4 转发基本就是端口透传客户端与 apiserver 直接建立 TCP 连接nginx 只做流量分发不关心 HTTP 内容行为最接近一个高通量的 TCP 路由器。其实还有一个隐藏问题K8s 的 kubectl、kubelet 等组件访问 apiserver 时证书校验很重要。如果走 L7 的 HTTP 代理nginx 与 apiserver 之间建立了自己的 TLS 连接客户端拿到的证书并不是 apiserver 的证书虽然可以通过proxy_ssl_server_name之类的参数规避但还是要额外处理。直接用 L4 透传TLS 层完全不动证书链路自然就不会出问题。4.2 keepalived 高可用设计与配置两台 LB 节点最核心的就是 keepalived 提供 VIP。这个 VIP 由两台机器抢来抢去正常情况下绑定在其中一台的网卡上如果这台机器挂了另一台会在几个 VRRP 通告周期内抢占过来整个过程对客户端透明。keepalived 配置并不复杂但有几个容易踩的坑。首先VIP 必须和业务网段在同一二层网络内否则 VRRP 广播无法正常工作VIP 也 ping 不通。配置示例# /etc/keepalived/keepalived.conf global_defs { router_id LVS_K8S # 禁止 keepalived 在启动时执行脚本检查避免不必要的输出 script_user root enable_script_security } vrrp_instance VI_K8S { state BACKUP # 两台都设为 BACKUP避免同时抢占 interface eth0 virtual_router_id 101 priority 110 # 两台不同lb01 110lb02 100 advert_int 1 authentication { auth_type PASS auth_pass 8u3k8s } unicast_src_ip 192.168.1.12 # 本机 IP unicast_peer { 192.168.1.13 # 对端 IP } virtual_ipaddress { 192.168.1.100/24 dev eth0 label eth0:1 } track_script { check_apiserver } priority 110 }注意不要把两个节点都配成state MASTER这样即便有优先级机制在网络抖动时也容易造成脑裂。更好的做法是两台都配成BACKUP通过 priority 大小决定谁优先抢占。lb01 配置 priority 110lb02 配置 priority 100正常情况下 lb01 持有 VIPlb01 故障后lb02 因为优先级低会等一段时间再抢占避免频繁切换。unicast_src_ip和unicast_peer是很多教程会漏掉的。如果你的交换机禁用了组播某些云环境就是这样VRRP 组播包根本发不出去keepalived 两侧互相看不到对方VIP 永远漂移不起来。使用单播可以让 keepalived 在非组播环境正常工作这也是在云服务器上部署时必备的配置。还需要加一个健康检查脚本。单纯的 VRRP 只能发现节点宕机如果 nginx 进程异常或者 apiserver 全部不可达VIP 是不会自动漂移的。这会导致客户端连上了 VIP但 VIP 背后的 nginx 已经无法转发。所以必须写个脚本定期探活本地 nginx 或后端 apiserver#!/bin/bash # /etc/keepalived/check_apiserver.sh if curl -k https://192.168.1.100:6443/healthz /dev/null 21; then echo apiserver is ok exit 0 else echo apiserver is down exit 1 fi这里探测的是 VIP 地址因为在 LB 节点上 nginx 是绑定在 VIP 上的如果 VIP 不在本机探测必然失败脚本会触发 keepalived 降低权重配合nopreempt可以避免反复切换。脚本需要可执行权限chmod x /etc/keepalived/check_apiserver.sh4.3 nginx 四层 stream 配置nginx 需要编译安装 stream 模块或者使用发行版里已经包含 stream 模块的版本一般通过nginx-mod-stream包安装。stream 配置与 http 平级推荐放在单独的文件里用 include 引入。# /etc/nginx/nginx.conf 添加以下内容 stream { log_format proxy $remote_addr - [$time_local] $protocol $status $bytes_sent $bytes_received $session_time $upstream_addr; access_log /var/log/nginx/stream-access.log proxy; upstream kube_apiserver { server 192.168.1.10:6443 max_fails3 fail_timeout10s; server 192.168.1.11:6443 max_fails3 fail_timeout10s; } server { listen 192.168.1.100:6443; proxy_pass kube_apiserver; proxy_timeout 300s; proxy_connect_timeout 3s; } }关键点在于proxy_timeout。这里指的是连接空闲超时而不是总超时。K8s 里有大量长连接场景比如 kubectl exec 会话、kubelet 的 pod sync如果超时设太短空闲的连接会被 nginx 断开客户端就会遇到 connection reset。我见过不少集群在 LB 层放了 nginx 之后port-forward 总是莫名断掉最后定位就是这里的超时被默认值害了。设成 300s 甚至更大都不为过。另外listen必须绑定 VIP 的 IP而不是0.0.0.0。这是因为 keepalived 管理 VIP 后nginx 如果监听所有地址那么在非 VIP owner 节点上访问也会被本机 nginx 接收但它无法转发因为本机不是 owner 且 VIP 没有漂移过来行为就变得很诡异。绑定 VIP 后只有 VIP 漂移到哪台哪台的 nginx 才真正提供服务逻辑非常干净。配置完成后nginx -t systemctl restart nginx4.4 配置 health check 脚本与启动验证keepalived 的健康检查脚本我建议既检查本地 nginx 进程也检查后端 apiserver。因为 nginx 进程活着但转发失效比 nginx 挂了更难察觉。把脚本改成#!/bin/bash if ! pgrep -x nginx /dev/null; then exit 1 fi # 从本机任意网卡访问后端 apiserver 进行探测 for i in 1 2; do if curl -k https://192.168.1.10:6443/healthz /dev/null 21 || curl -k https://192.168.1.11:6443/healthz /dev/null 21; then exit 0 fi done exit 1注意这个脚本中探测的是 master 的 IP 而不是 VIP。如果在 LB 的 keepalived 健康检查脚本里探测 VIP 地址而 VIP 就在本机nginx 又绑定在 VIP 上这个探测能够通过一旦 nginx 挂了探测也自然会失败。两种写法各有其适应场景我用的是探测后端 apiserver 判断 nginx 进程因为它的逻辑更贴近真实需求只要有一个 apiserver 活着且本机 nginx 活着那么这个 LB 节点就应当持有 VIP继续提供服务。启动验证顺序是这样的# 先在 lb01 和 lb02 上分别启动 keepalived systemctl enable --now keepalived然后看 VIP 在哪里ip addr show | grep 192.168.1.100正常情况下只有一台机器有这个 IP通常是 lb01。接着在任意一台机器上验证连通性curl -k https://192.168.1.100:6443/healthz返回ok就说明整个链路 LB - apiserver 已经打通。5. 高可用验证故障注入与恢复演练5.1 验证 VIP 漂移模拟 LB 节点宕机高可用架构交付前一定要做故障注入测试不然高可用只是纸面上的。最直接的就是把 lb01 的 keepalived 停掉# 在 lb01 上执行 systemctl stop keepalived然后到 lb02 上查看是否抢占了 VIPip addr show | grep 192.168.1.100正常情况下等待 1 到 3 个 VRRP 通告周期VIP 出现在 lb02 上。再用curl -k https://192.168.1.100:6443/healthz验证请求恢复正常。这就是主备切换的基本流程。注意观察 keepalived 日志tail -f /var/log/messages日志中会出现Entering BACKUP STATE和Entering MASTER STATE之类的记录。如果日志里反复切换说明 keepalived 的优先级或者健康检查脚本有问题需要排查。常见原因是两台机器上 script 的返回结果不一致导致 VRRP 权重抖动VIP 频繁漂移。生产环境里VIP 漂移虽然正常但过于频繁的切换比不切换更糟。每次切换都会让所有长连接中断kubelet 会重新上报节点状态对集群造成一次不小的抖动。所以 keepalived 的参数尽量保守priority 的差距不要太大advert_int 保持在 1 秒以上避免脑裂和抢占有延迟的叠加。5.2 验证后端故障模拟 master1 的 apiserver 宕机LB 层的高可用验证完还要验证后端 apiserver 的高可用。在 master1 上把 kube-apiserver 停掉systemctl stop kube-apiserver此时通过 VIP 访问集群还是正常的因为 nginx 会把请求转发到 master2 的 6443 端口。验证kubectl get nodes正常情况下输出 master1 状态变成 NotReady或者完全不显示master2 状态 Ready集群控制面仍可读写。这里有一个细节值得注意kubelet 上报节点状态是有时间间隔的master1 上的 kubelet 还会持续向 master2 的 apiserver 上报自己的心跳所以 master1 的节点状态可能暂时显示 Ready过一段时间才变成 NotReady。这属于正常现象。如果你发现 master1 的 apiserver 停掉后通过 VIP 访问直接 connection refused问题通常出在 nginx 的 upstream 配置——max_fails和fail_timeout可能需要更短或者 nginx 的zone配置缺失导致后端状态无法被正确判定为 down。调整后重新加载 nginx 配置即可。5.3 验证 kubelet 通过 VIP 接入确保所有 worker 节点包括 master 节点的 kubelet 和 kube-proxy 配置里的 server 地址都是https://192.168.1.100:6443。检查一下grep -r 192.168.1.100 /opt/kubernetes/cfg/如果 kubelet 的 kubeconfig 里还是 master1 的 IP建议改掉否则 master1 apiserver 故障时该节点上的 kubelet 会直接失联Pod 大概率会被系统重新调度到其他节点造成不必要的漂移。在三个 master 上都执行systemctl restart kubelet kube-proxy重启后观察节点状态是否恢复正常。这个操作可能会暂时中断该节点存量 Pod 与 apiserver 之间的通信建议分批做不要三个 master 同时重启。我见过有人把三条命令写到一个 for 循环里一起执行结果整个集群 apiserver 短暂全部失联虽然最终恢复但过程非常吓人。6. 常见问题与排查记录6.1 etcd 成员添加失败日志显示requested member already exists大概率是之前已经 add 过了但没有完成后续操作etcd 的 cluster state 里还留着这个旧信息。解决方式是移除旧 member再重新 addetcdctl member remove member-id etcdctl member add master2 https://192.168.1.11:2380还有一种情况是 master2 本机的 etcd 数据目录里已经有数据了。比如你之前错误地以new状态启动过一次 etcd数据目录写入了一半内容。此时需要清掉数据目录再启动rm -rf /var/lib/etcd/* systemctl start etcd这个操作要谨慎确认 master2 上没有需要保留的数据再执行。etcd 数据目录清理是此生不会再救回来的操作即使集群已经通过 add member 方式加回来了之前残留的旧数据也可能干扰集群一致性。所以理论上如果 master2 从来不是 etcd 成员它不应该有 etcd 数据目录。你可以在启动前检查这个目录是否存在如果存在且不确定来源最好先移走而不是直接删。6.2 apiserver 证书 x509 错误证书的 SAN 不包含当前访问地址新 master 加入后kubectl 从外部访问 VIP 出来的报错是这样error: x509: certificate is valid for 192.168.1.10, master1, not 192.168.1.100这说明 server.pem 里没有签 VIP。解决方法是重新签发 apiserver 的 server 证书加上 192.168.1.100 这个 IP。注意签发完要同时更新两台 master 上的证书文件并重启 apiserversystemctl restart kube-apiserver如果改完之后还是报错注意 kubectl 使用的 kubeconfig 可能缓存了旧的证书信息把 ~/.kube/config 里的certificate-authority-data替换成新 CA 的内容或者直接删除并使用集群内的 admin.conf 重新生成即可。6.3 keepalived 抢占模式导致 VIP 反复横跳如果你看到 keepalived 日志里频繁出现Entering MASTER STATE和Entering BACKUP STATE并且 VIP 在两台机器上反复切换多半是因为两台机器的 priority 设置太接近或者健康检查脚本有误报。推荐统一把两台都设成nopreempt模式BACKUP 状态 nopreempt这样即使高优先级节点恢复后也不主动抢占 VIP等到下一次主节点故障时再切换。对客户端来说主动抢占带来的长连接重连完全没有必要非抢占模式更稳定。6.4 nginx 配置了轮询但 apiserver 连接不稳定表现是 kubectl 偶尔能通偶尔 connection refused。检查 nginx 错误日志tail -f /var/log/nginx/error.log如果全是connect() failed (111: Connection refused)说明某一个 apiserver 地址已经不可用但 nginx 还不知道。加重时可以通过zone配置开启共享内存让 nginx 维护更精确的后端健康状态upstream kube_apiserver { zone kube-apiserver 64k; server 192.168.1.10:6443 max_fails3 fail_timeout10s; server 192.168.1.11:6443 max_fails3 fail_timeout10s; }加zone之后nginx 会为这个 upstream 分配共享内存用于保存健康检查状态多个 worker 进程共享。这样某个后端的失败计数能被正确累计不会因为 worker 进程各自为政而忽略故障。6.5 kubelet 一直处于 NotReady排查思路从下往上systemctl status kubelet看进程是否存活journalctl -u kubelet -f看报错内容kubectl get csr看是否证书没批kubectl describe node master2看上报状态最常见的原因就是 CSR 没批。二进制部署不会自动批准需要手动 approve 之后 kubelet 才能拿到证书。第一次加 master2 时这一步一定会遇到批完后 kubelet 才能成功鉴权并上报状态。如果 CSR 一直处于 Pending 且数量在持续增加说明 kubelet 没有正确的 bootstrap token 或 kubeconfig 参数。检查 kubelet.conf 里的certificate-authority是否指向了正确的 CA 文件以及server的地址是否能通。6.6 高可用验证时发现主节点 apiserver 挂了VIP 却还是转发到它这个现象说明 nginx 对后端的健康检查没有生效。检查 two thingsnginx 是否加载了 stream 模块并开启了 upstream 健康检查版本不同stream 模块的health_check支持情况不同默认可能没有开启keepalived 脚本里检查的是否是后端 apiserver 的连通性而不是单纯检查本机 nginx 进程二进制部署时nginx 四层转发有个特性默认不主动发健康检查包只根据max_fails和fail_timeout在转发失败时标记后端不可用。如果一直没有人发起请求故障后端不会被标记为 down。所以上面的 keepalived 脚本中的主动探活反而是更可靠的一环。写到最后的一点实际体会这套部署方案做完之后我最大的感受是二进制部署的高可用 K8s 集群并不复杂复杂的是保持每个节点的一致性。目录结构、证书 SAN、etcd member、kubelet 配置、nginx upstream任何一个细节只要和 master1 差一点故障就会以最诡异的面貌出现。所以实操时建议建立一份配置清单每同步一个文件就打一个勾每启动一个组件就验证一次接口别等服务全部起来再统一排查否则你根本不知道是哪一层出了问题。另外如果你在新增 master2 的过程中发现 master1 的 apiserver 偶尔会出现etcdserver: request timed out不要慌。检查一下 etcd 网络连接和磁盘 IO。如果你用的是机械硬盘etcd 的 fsync 延迟会很大控制面性能会有明显波动。这种情况下优先把 etcd 数据目录迁到 SSD比调整任何参数都管用。最后再分享一个小技巧这套方案里所有配置尽量做到用文件定义一切。apiserver、etcd、kubelet 的配置全部落到对应的*.conf或 systemd 文件里禁止直接在命令行上临时改参数启动。后续做节点扩容时只需要拷文件再改 IP就能在半小时内完成一个 master 的加入。如果一开始就图省事手工敲命令启动进程那记录配置状态就成了纯靠记忆迟早会出错。