Kubernetes v1.36.3 高可用集群 —— 部署与原理完整指南
前言
本文是一份从零手动搭建 Kubernetes v1.36.3 高可用集群的完整实战指南。不同于简单的“一键安装”教程,本文深入剖析了每个核心组件的工作原理、通信机制和部署细节,旨在帮助读者不仅“知其然”,更“知其所以然”。
目标读者:
- 希望深入理解 Kubernetes 高可用架构的运维工程师和架构师
- 需要搭建生产级 K8s 集群的 DevOps 团队
- 准备 Kubernetes CKA/CKAD 认证,想掌握底层原理的学习者
- 对传统单 Master 集群不满足,希望构建真正高可用环境的实践者
你将获得:
- 7节点完整拓扑:包含 3 Master + 2 Worker + 2 LB 的经典高可用架构
- 组件深度解析:从容器运行时到控制平面,每个组件的作用与交互关系
- 通信全链路:证书体系、TLS 握手、watch 机制等关键通信路径
- 逐行可执行命令:所有脚本和配置均来自实际生产环境,已逐台验证
- 故障切换验证:HAProxy + Keepalived 的 VIP 漂移、apiserver 摘除等真实场景测试
无论你是初次接触 K8s 高可用部署,还是希望优化现有集群架构,本文提供的原理讲解 + 实战步骤 + 排错经验都将为你提供完整的技术栈参考。让我们从理解架构开始,一步步构建这个稳定可靠的 Kubernetes 高可用集群。
适用对象:希望从头手动搭建一套与本环境完全一致的 7 节点 K8s v1.36.3 高可用集群的人。
本文所有命令均来自本环境实际使用的脚本(k8s-ha/scripts/*、k8s-ha/configs/*),并已对照运行中的 7 台虚拟机逐台核对。
完成时间:2026-08-09
目录
-
- 2.1 容器运行时层:containerd / runc / CRI
- 2.2 kubelet(节点代理)
- 2.3 kube-apiserver(API 网关 + 集群大脑)
- 2.4 etcd(分布式键值数据库)
- 2.5 kube-scheduler(调度器)
- 2.6 kube-controller-manager(控制器管理器)
- 2.7 kube-proxy(Service 转发,IPVS 模式)
- 2.8 CoreDNS(集群 DNS)
- 2.9 Calico(CNI 网络插件)
- 2.10 HAProxy(控制平面负载均衡)
- 2.11 Keepalived(VIP 高可用)
- 2.12 控制平面 HA 设计小结(stacked etcd)
第 0 章 总览
0.1 集群拓扑
物理网络 192.168.1.0/24 ┌──────────────────────────────┐
网关 192.168.1.1 │ VIP 192.168.1.100:6443 │ ← 控制平面统一入口
宿主机(装 VMware) 192.168.1.88 │ Keepalived VRRP id 51 │
└───────────────┬──────────────┘
VIP 浮动于 lb / lb2 之间
┌──────────────────────┐ 单播 VRRP ┌──────────────────────┐
│ lb 192.168.1.10 │◄──────────►│ lb2 192.168.1.16 │
│ MASTER pri 150 │ │ BACKUP pri 100 │
│ HAProxy TCP :6443 │ │ HAProxy TCP :6443 │
│ 状态页 :8404/stats │ │ 状态页 :8404/stats │
└───────────┬──────────┘ └──────────┬───────────┘
└───────────┬──────────────────────┘
│ 两台 HAProxy 后端完全相同
┌───────────────────────┼───────────────────────┐
┌─────────▼────────┐ ┌──────────▼───────┐ ┌───────────▼──────┐
│ master1 .11 │ │ master2 .12 │ │ master3 .13 │
│ apiserver/etcd │ │ apiserver/etcd │ │ apiserver/etcd │
│ scheduler/cm │ │ scheduler/cm │ │ scheduler/cm │
└──────────────────┘ └──────────────────┘ └──────────────────┘
┌────────────────────┴────────────────────┐
┌─────────▼─────────┐ ┌──────────▼────────┐
│ worker1 .14 │ │ worker2 .15 │
│ kubelet/containerd│ │ kubelet/containerd│
└───────────────────┘ └───────────────────┘
0.2 节点清单
| 主机名 | IP | 规格 | 角色 | 关键组件 |
|---|---|---|---|---|
| lb | 192.168.1.10 | 2C/2G/20G | 负载均衡(主) | HAProxy 2.4.30 + Keepalived 2.2.4(MASTER, pri 150) |
| lb2 | 192.168.1.16 | 2C/2G/20G | 负载均衡(备) | HAProxy 2.4.30 + Keepalived 2.2.4(BACKUP, pri 100) |
| master1 | 192.168.1.11 | 2C/4G/20G | 控制平面 1 | kube-apiserver / etcd / scheduler / controller-manager / kube-proxy / calico-node |
| master2 | 192.168.1.12 | 2C/4G/20G | 控制平面 2 | 同上 |
| master3 | 192.168.1.13 | 2C/4G/20G | 控制平面 3 | 同上 |
| worker1 | 192.168.1.14 | 2C/4G/20G | 计算节点 1 | kubelet / kube-proxy / calico-node |
| worker2 | 192.168.1.15 | 2C/4G/20G | 计算节点 2 | 同上 |
| VIP | 192.168.1.100 | — | 控制平面入口 | Keepalived 虚拟 IP,浮动于 lb / lb2 |
登录账号:tantan / 791018(已配免密 sudo;宿主机对 7 台均可 SSH 免密)。
0.3 软件版本(已逐台 SSH 核对)
| 组件 | 版本 | 部署形态 |
|---|---|---|
| Kubernetes | v1.36.3(deb 包 1.36.3-1.1,已 apt-mark hold) | kubeadm / kubelet / kubectl |
| etcd | 3.6.8(镜像 etcd:3.6.8-0) | 控制平面静态 Pod,堆叠式 3 成员 |
| CoreDNS | v1.14.2 | Deployment 2 副本 |
| containerd | 1.7.18 | 系统服务(CRI 运行时) |
| runc | 1.7.18 | containerd 底层 OCI 运行时 |
| crictl | v1.32.0 | 命令行调试工具 |
| Calico(CNI) | v3.32.1 | DaemonSet calico-node + Deployment calico-kube-controllers |
| kube-proxy | IPVS 模式,rr 调度,strictARP=true | DaemonSet |
| HAProxy | 2.4.30 | 系统服务(仅 lb / lb2) |
| Keepalived | 2.2.4 | 系统服务(仅 lb / lb2) |
| OS / 内核 | Ubuntu 22.04.5 LTS / 6.8.0-90 | cgroup v2 |
注:
lb与lb2上残留有旧版kubeadm/kubelet/kubectl v1.32.7二进制(历史清理残留),它们不运行任何 K8s 组件,仅作为负载均衡器使用,因此不影响集群。全新搭建时 lb/lb2 不需要安装 kube* 工具。
0.4 网段规划
| 网段 | 用途 |
|---|---|
192.168.1.0/24 | 节点网络(虚拟机桥接物理网,与宿主机/网关同二层) |
10.244.0.0/16 | Pod 网络(刻意避开 192.168.x,否则与物理网冲突) |
10.96.0.0/12 | Service(ClusterIP)网络 |
第 1 章 七台虚拟机各安装了哪些 K8s 相关软件
1.1 lb / lb2(负载均衡器,只装 2 个组件)
| 软件 | 包/版本 | 作用 |
|---|---|---|
| HAProxy | haproxy 2.4.30 | TCP 层四层负载均衡,把 :6443 的请求轮询转发给 3 台 master 的 apiserver;提供 :8404/stats 状态页 |
| Keepalived | keepalived 2.2.4 | 通过 VRRP 协议在两台 LB 间浮动 VIP 192.168.1.100;并监控 HAProxy 存活做主备切换 |
二者通过
check_haproxy.sh(检测 haproxy 进程)联动:HAProxy 挂掉 → VIP 漂到另一台。
1.2 master1 / master2 / master3(控制平面,每节点装)
| 软件 | 版本 | 部署形态 | 作用 |
|---|---|---|---|
| kubeadm | v1.36.3 | 命令行工具 | 集群初始化与节点加入的"引导器" |
| kubelet | v1.36.3 | 系统服务 | 节点上的"节点代理",管 Pod 生命周期 |
| kubectl | v1.36.3 | 命令行工具 | 集群操作客户端 |
| containerd | 1.7.18 | 系统服务(CRI) | 容器运行时 |
| runc | 1.7.18 | 二进制 | OCI 容器底层运行时 |
| crictl | v1.32.0 | 命令行工具 | 容器调试 |
| kube-apiserver | v1.36.3 | 静态 Pod | 集群唯一入口 API 服务 |
| kube-scheduler | v1.36.3 | 静态 Pod | 调度器(3 台选 1 主) |
| kube-controller-manager | v1.36.3 | 静态 Pod | 控制器管理器(3 台选 1 主) |
| etcd | 3.6.8 | 静态 Pod | 集群键值数据库(3 成员堆叠) |
| kube-proxy | v1.36.3 | DaemonSet(IPVS) | Service 流量转发 |
| calico-node | v3.32.1 | DaemonSet | CNI 网络插件(每节点一个) |
| calico-kube-controllers | v3.32.1 | Deployment | Calico 控制面(路由/策略同步) |
| CoreDNS | v1.14.2 | Deployment | 集群 DNS |
1.3 worker1 / worker2(计算节点,每节点装)
| 软件 | 版本 | 部署形态 | 作用 |
|---|---|---|---|
| kubelet | v1.36.3 | 系统服务 | 节点代理 |
| kubectl(可选) | v1.36.3 | 命令行工具 | 客户端(便于排障,非必需) |
| containerd | 1.7.18 | 系统服务 | 容器运行时 |
| runc | 1.7.18 | 二进制 | OCI 运行时 |
| crictl | v1.32.0 | 命令行工具 | 调试 |
| kube-proxy | v1.36.3 | DaemonSet(IPVS) | Service 转发 |
| calico-node | v3.32.1 | DaemonSet | CNI 插件 |
worker 节点不运行 kube-apiserver / etcd / scheduler / controller-manager / CoreDNS,镜像也更省(只拉
kube-proxy与pause)。
第 2 章 组件原理:每个组件的作用与工作原理
2.1 容器运行时层:containerd / runc / CRI
- containerd:行业标准的容器运行时,实现了 Kubernetes 的 CRI(Container Runtime Interface)。它负责拉取镜像、管理容器生命周期、镜像存储、网络命名空间。Kubernetes 不直接操作容器,而是通过 CRI 让 containerd 干活。
- runc:OCI 规范的底层运行时,真正调用 Linux 内核
clone()/cgroups/namespaces创建进程。containerd 通过runc把镜像变成运行中的容器。 - pause 容器(
pause:3.10.2):每个 Pod 的第一个容器,只持有一个网络命名空间(持有 Pod 的 IP),其余同 Pod 容器通过pause共享网络栈。sandbox_image指向阿里云镜像,避免拉registry.k8s.io失败。 - crictl:CRI 兼容的调试工具,等价于 docker 的"运维版",用于
crictl ps / images / logs / exec。
关键配置:
SystemdCgroup = true:让容器 cgroup 交给 systemd 管,与 kubelet 的cgroupDriver: systemd一致(否则 kubelet 起不来或告警)。- 镜像加速:containerd 的
certs.d为docker.io / registry.k8s.io / quay.io / ghcr.io分别配了国内镜像(daocloud.io等),否则境外镜像拉不动。
2.2 kubelet(节点代理)
每个节点(包括 master)都跑一个 kubelet。它是节点上的"大管家":
- 向 apiserver 注册本节点,持续上报节点状态(CPU/内存/就绪)。
- 监听 apiserver 分配给本节点的 Pod,调用 CRI 让 containerd 把 Pod 跑起来。
- 做探针(liveness/readiness/startup),失败就重启容器。
- 挂载 ConfigMap/Secret/PV。
- 静态 Pod(Static Pod)机制:kubelet 会监控
/etc/kubernetes/manifests/目录,里面的 yaml 直接由 kubelet 以"脱离 apiserver 调度"的方式启动——这正是控制平面的 kube-apiserver / etcd / scheduler / controller-manager 的运行方式。好处:即使 apiserver 还没起来,etcd/apiserver 也能先被 kubelet 拉起,形成"先有鸡还是先有蛋"的自举闭环。
KUBELET_EXTRA_ARGS=--node-ip=<本机IP>:强制 kubelet 上报固定 IP,避免多网卡时选错。
2.3 kube-apiserver(API 网关 + 集群大脑)
- 集群唯一的对外/对内操作入口。所有组件(kubectl、kubelet、scheduler、controller-manager、kube-proxy)都只跟它通信。
- 把收到的对象(Pod/Service/Deployment…)写入 etcd 并从中读取。
- 做认证(证书/Token)、鉴权(RBAC)、准入控制(admission webhook)。
- 本身无状态、可水平扩展:3 台 master 各跑一个 apiserver,前端用 HAProxy 做负载均衡,VIP 提供统一地址,所以"挂掉一台 apiserver,集群照常工作"。
--service-node-port-range 30000-32767、证书 SAN 包含了 VIP/各 master IP/域名,保证从任意地址访问都证书可信。
2.4 etcd(分布式键值数据库)
- Kubernetes 的"唯一真实数据源(source of truth)",所有集群状态都存这里。
- 采用 Raft 共识算法 保证 3 成员数据一致:写入需多数派(2/3)确认;某成员宕机不影响读写(只要活着的 ≥2)。
- 堆叠式(stacked)部署:etcd 作为静态 Pod 与 apiserver 同机,每个 master 一个 etcd 成员,3 成员互为 peer 通过
2380端口复制数据,2379端口供本机 apiserver 访问。- 优点:部署简单、节点数少。
- 代价:master 整机宕机 = 同时丢了 apiserver 和一个 etcd 成员,但 3 取 2 仍可用。
- 数据目录
/var/lib/etcd,务必放可靠磁盘。
2.5 kube-scheduler(调度器)
- 监听 apiserver 里"尚未绑定节点"的 Pod,按资源请求、亲和性、污点容忍等规则选出最合适的节点,把结果写回 apiserver(
Pod.Spec.NodeName)。 - 3 台各跑一个,但通过 leader election(租约) 同一时刻只有 1 个真正干活,其余 standby,避免重复调度。
2.6 kube-controller-manager(控制器管理器)
- 一堆控制器的集合:Node 控制器(管节点上下线)、Replication 控制器(维持副本数)、Endpoints 控制器、ServiceAccount 控制器等。
- 核心逻辑:"实际状态"与"期望状态"不一致就调谐(reconcile)。例如 Deployment 期望 3 副本,少一个它就创建。
- 同样 leader election,3 台选 1 主。
2.7 kube-proxy(Service 转发,IPVS 模式)
- 每个节点一个,监听 apiserver 的 Service/Endpoints 变化,把 ClusterIP/NodePort 的流量转发到后端 Pod。
- 本集群用 IPVS 模式(不是 iptables):
- 在节点上创建
kube-ipvs0虚拟网卡,把每个 Service 的 ClusterIP 配上去。 - 用内核
ip_vs模块做负载均衡(调度算法rr轮询),性能远高于 iptables 链式规则,且规则数量与 Service 数无关。 strictARP=true:让节点对 VIP 网段响应 ARP 时不被 Calico 抢答,避免 ARP 冲突(CNI 与 kube-proxy 共存必须的配合)。
- 在节点上创建
2.8 CoreDNS(集群 DNS)
- 把 Service 名(如
mysvc.default.svc.cluster.local)解析成 ClusterIP。 - 也是 kube-proxy 的"客户":Pod 里
/etc/resolv.conf的 nameserver 指向10.96.0.10(CoreDNS ServiceIP),CoreDNS 再通过 apiserver 查询 Service/Endpoints。 - 跨节点 Pod 互访、Ingress、Service Mesh 都依赖它。
2.9 Calico(CNI 网络插件)
- 负责 Pod 网络:给每个 Pod 分配
10.244.0.0/16里的 IP,并保证跨节点 Pod 能互通。 - 本集群用 IPIP
CrossSubnet模式:calico-nodeDaemonSet 在每台节点创建一个tunl0隧道口。- 同二层网段(都在 192.168.1.0/24)的 Pod 间直接路由、不封装;跨子网才走 IPIP 封装。鉴于所有节点同二层,实际几乎零封装开销。
- Calico 用 BGP/路由表把"哪个 Pod 子网在哪一节点"广播出去,节点间靠 Linux 路由表转发。
calico-kube-controllers负责把 K8s 的 NetworkPolicy、Pod/Service 变化同步进 Calico 的数据面。IP_AUTODETECTION_METHOD=can-reach=192.168.1.1:自动探测用哪块网卡做 Calico 后端,避免选错(如选到 docker0)。
2.10 HAProxy(控制平面负载均衡)
- 在 lb/lb2 上监听
:6443,backend 是 3 台 master 的:6443,balance roundrobin轮询。 mode tcp:四层转发,不解析 HTTP,对 apiserver 的 TLS 透明。- 长连接超时设到 4 小时(
timeout client/server/tunnel 4h):因为 kubelet/controller 与 apiserver 之间是 watch 长连接,超时太短会导致每分钟断流、集群抖动。 check每 3 秒探一次后端 apiserver 的:6443,失败则把该 master 摘出(这正是"摘除 master1 apiserver 后 15 秒内 HAProxy 标记 DOWN"的来源)。
2.11 Keepalived(VIP 高可用)
- 在两台 LB 之间用 VRRP 协议协商谁持有 VIP
192.168.1.100。 - 单播模式(unicast):直接指定对端 IP,不依赖交换机组播(云/受限交换机环境下更稳)。
state MASTER/BACKUP+priority决定初始角色:lb=150、lb2=100。track_script chk_haproxy:每 2 秒查一次 HAProxy 进程,连挂 3 次(约 6s)就把本机优先级减weight(-60):- lb 正常 150;HAProxy 挂 → 150-60=90 < lb2 的 100 → VIP 漂到 lb2。
- lb 恢复 → 回到 150 > 100 → 抢回 VIP(非抢占配置
nopreempt下按优先级自然回归)。
auth_pass k8sha99:VRRP 报文认证,防误抢。
2.12 控制平面 HA 设计小结(stacked etcd)
VIP:6443 (Keepalived)
│
┌────────┴────────┐
HAProxy(lb) HAProxy(lb2) ← 互备,VIP 浮动
│ │ │ │ │ │
└──┬─┴─┬──────────────┘ │
▼ ▼ ▼
master1 master2 master3
(apiserver+etcd) (apiserver+etcd) (apiserver+etcd)
│ │ │
└── etcd Raft 复制(2380)──┘
- apiserver 3 副本 × etcd 3 成员,任意单点故障(1 台 master 宕机、1 台 LB 宕机)集群仍可用。
- 客户端/节点只认 VIP,HAProxy 把请求摊到健康的 apiserver。
第 3 章 通信细节:组件之间的相互关系与通信
3.1 总体通信骨架
kubectl / 节点 kubelet
│ (HTTPS, 经 VIP:6443)
▼
[Keepalived VIP] ──► HAProxy(lb/lb2):6443 ──roundrobin──► masterN:6443 (kube-apiserver)
│
┌──────────┴──────────┐
etcd:2379 (本机) watch/心跳
│ │
etcd peer 复制:2380 scheduler / controller-manager
│
写回 apiserver
3.2 证书与 TLS 体系(谁信任谁)
Kubernetes 全栈走 mTLS(双向 TLS)。kubeadm 生成一套 PKI,核心有:
- CA(根证书):
/etc/kubernetes/pki/ca.crt+ca.key。它给所有其他证书签名。apiserver、kubelet、etcd、scheduler、controller-manager 都信任这个 CA。 - apiserver 证书:SAN 包含
192.168.1.100(VIP)、192.168.1.10~13、127.0.0.1、10.96.0.1、k8s-vip、各主机名、kubernetes.default.svc...。这就是为什么从 VIP 或任意 master IP 访问 apiserver 都不会证书报错。 - etcd 证书:
serverCertSANs/peerCertSANs包含 3 个 master IP 与主机名,使 etcd 成员间互信、apiserver 连 etcd 也互信。 - kubelet 客户端证书:kubelet 用
kubelet-client证书连 apiserver;apiserver 用kubelet-api证书反向连 kubelet(如kubectl exec/logs)。 --certificate-key(a1b2c3...):kubeadm 用它加密 etcd/CA 等证书,在加入新 control-plane 时通过--upload-certs临时放到集群 Secret 里,让 master2/3 能拿到同一套 PKI(否则每个 master 各签各的,etcd 无法互信)。
3.3 关键通信路径逐条说明
(1) 客户端 → apiserver(经 VIP)
kubectl读 kubeconfig,server: https://192.168.1.100:6443。- 请求先到 Keepalived 当前持有 VIP 的那台 LB,HAProxy 在
:6443接收,按 roundrobin 选一个健康 master 转发(TCP 透传,不改 TLS)。 - apiserver 校验客户端证书/Token → RBAC → 操作 etcd。
(2) apiserver ↔ etcd(本机 localhost)
- 每台 master 的 apiserver 只连本机 etcd:
--etcd-servers=https://127.0.0.1:2379,走 mTLS。 - 写请求由本机 etcd 通过 Raft 复制到另两个成员的
:2380,需多数派确认。
(3) etcd 成员之间(2380 peer)
- 3 个 etcd 通过
:2380(peer port)建立 Raft 复制通道,用etcd-peer证书互信。 - leader 处理写,follower 同步;leader 心跳/选举超时触发重新选主。
(4) kubelet ↔ apiserver(watch + 心跳)
- kubelet 启动后用客户端证书连 apiserver(或 VIP),建立长连接 watch 本节点应跑的 Pod。
- 每 10 秒发一次节点状态心跳;apiserver 超过
node-monitor-grace-period没收到就标记节点 NotReady。 - 这正是 HAProxy 长连接超时设 4 小时的原因——watch 连接不能随便断。
(5) scheduler / controller-manager ↔ apiserver(watch + leader election)
- 二者都 watch apiserver(未调度 Pod / 实际状态变化)。
- 通过 apiserver 里的 Lease 对象(
kube-node-lease命名空间 +control-planeendpoint)做 leader 选举:谁抢到 lease 谁就是 active,其余阻塞;active 挂了 lease 过期,其他人接手。所以 3 台 master 上这俩组件"看起来都在跑,但只有一个真干活"。
(6) kube-proxy ↔ apiserver(watch Service/Endpoints)
- 每个节点的 kube-proxy watch apiserver 的 Service 和 Endpoints 变化,更新本机
ip_vs规则:把 ClusterIP 绑到kube-ipvs0,并写后端 Pod IP。
(7) Calico ↔ apiserver(watch + BGP)
calico-nodewatch Pod/Node/NetworkPolicy;通过 Felix 写本机路由表;节点间用 BGP(或 IPIP 隧道)把 Pod 子网路由广播出去,使跨节点 Pod 互通。calico-kube-controllerswatch K8s 对象,把期望网络状态同步到 Calico datastore(这里直接存 etcd/K8s CRD)。
(8) Pod 间通信(跨节点)
- Pod A(节点 X)发往 Pod B(节点 Y,IP
10.244.x.y):- 出 Pod 走
pause的网络命名空间,路由判定目的在10.244.0.0/16且不在本机。 - 查节点路由表 → 下一跳是节点 Y(经
tunl0或直接二层),Calico 已写好这条路由。 - 同二层(本环境)直接路由到节点 Y 的
ens33;Calico 在 Y 上再把包递给 Pod B 的cali*网卡。 - 全程源/目的都是 Pod IP,Service 的 ClusterIP 只在进出节点时被 kube-proxy 做一次 DNAT。
- 出 Pod 走
(9) Service 访问流(ClusterIP / NodePort)
- 集群内 Pod 访问
myapp:8080→ 解析到 ClusterIP10.96.x.y(CoreDNS)→ 命中kube-ipvs0→ IPVS 按 rr 选一个后端 Pod IP 做 DNAT → Pod。 - 外部访问 NodePort
30xxx→ 任一层节点 IP:30xxx→ kube-proxy 的 ip_vs 规则 → 后端 Pod。
(10) DNS 解析流
- Pod 内
getent hosts mysvc→ 查10.96.0.10:53(CoreDNS)→ CoreDNS 查 K8s Service API → 返回 ClusterIP。 - 外部域名 → CoreDNS 走转发(默认 forward 到节点
/etc/resolv.conf的上游,即223.5.5.5等)。
(11) 控制平面主备切换(HAProxy 摘点)
- 某 master apiserver 进程死 → HAProxy 的
tcp-check连不上:6443→fall 3次后标记 DOWN → 新请求不再分发给它 → 集群经 VIP 仍可用,scheduler/cm 通过另两台继续工作,etcd 仍 2/3 多数派可用。 - apiserver 恢复 → HAProxy
rise 2重新标 UP。
(12) LB 主备切换(VIP 漂移)
- lb 的 HAProxy 挂 →
chk_haproxy.sh连失败 → Keepalived 优先级降到 90 → 低于 lb2(100) → VRRP 抢位 → VIP 漂到 lb2 →kubectl(指向 VIP)无感切换。 - lb 恢复 → 优先级回 150 → 重新持有 VIP(非抢占环境下按优先级自然回归)。
3.4 一句话关系图(依赖链)
Keepalived 管 VIP
└─ HAProxy 用 VIP 做 apiserver 负载均衡
└─ apiserver 是唯一入口,背后是 etcd(Raft)
├─ kubelet 听 apiserver 跑 Pod(经 containerd/runc)
├─ scheduler/cm 经 apiserver 做调度与调谐(leader 选举)
├─ kube-proxy 经 apiserver 维护 Service 转发(IPVS)
└─ Calico 经 apiserver 维护 Pod 网络(路由/BGP)
CoreDNS 是 apiserver 的"常客",给 Service 提供名字
第 4 章 从零手动部署(详细步骤)
前置:一台装好 VMware Workstation 的 Windows 宿主机(IP 192.168.1.88),能联网(可选代理
http://192.168.1.88:7890)。下面所有ssh命令从宿主机执行。
本指南以桥接模式 + 阿里云/道客镜像为主路径,不依赖国外源,照做即可联网拉取。
步骤 0:准备 7 台 Ubuntu 22.04 虚拟机
| 节点 | 主机名 | IP | vCPU | 内存 | 磁盘 |
|---|---|---|---|---|---|
| 1 | lb | 192.168.1.10 | 2 | 2G | 20G |
| 2 | lb2 | 192.168.1.16 | 2 | 2G | 20G |
| 3 | master1 | 192.168.1.11 | 2 | 4G | 20G |
| 4 | master2 | 192.168.1.12 | 2 | 4G | 20G |
| 5 | master3 | 192.168.1.13 | 2 | 4G | 20G |
| 6 | worker1 | 192.168.1.14 | 2 | 4G | 20G |
| 7 | worker2 | 192.168.1.15 | 2 | 4G | 20G |
- 安装 Ubuntu 22.04.5,磁盘用 ext4,安装时先不连网或装完再配静态 IP。
- 创建用户
tantan(密码791018),加 sudo;或装完用下面的sshsetup脚本统一处理。 - 7 台都用 桥接(Bridged) 网卡接入
192.168.1.0/24,网关192.168.1.1。
强烈建议:先装好 1 台"模板机",用 VMware 冷克隆 出其余 6 台,再各自改 IP/主机名(参考 README 第九节的克隆注意事项:必须重置
machine-id、/etc/ssh/ssh_host_*、解除 NetworkManager 的 MAC 绑定)。lb2 是 lb 的克隆机并改造成 BACKUP,详见本文 4.2。
步骤 1:所有 K8s 节点(master×3 + worker×2)通用前置
在 master1/2/3、worker1/2 共 5 台 上执行(lb/lb2 跳过此步,它们只需 HAProxy/Keepalived)。
1.1 换阿里云源 + 关自动更新 + 装 openssh
# 以 tantan 登录后切 root
sudo -i
cat > /etc/apt/sources.list <<'EOF'
deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
EOF
# 永久禁用 unattended-upgrades,避免抢 dpkg 锁
systemctl mask unattended-upgrades.service apt-daily.timer apt-daily-upgrade.timer 2>/dev/null
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "0";
APT::Periodic::Unattended-Upgrade "0";
APT::Periodic::Download-Upgradeable-Packages "0";
APT::Periodic::AutocleanInterval "0";
EOF
apt-get update
apt-get install -y openssh-server
systemctl enable --now ssh
1.2 内核模块 + sysctl(K8s 与 IPVS 必需)
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay; modprobe br_netfilter
cat > /etc/modules-load.d/ipvs.conf <<'EOF'
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF
for m in ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack; do modprobe $m; done
cat > /etc/sysctl.d/99-kubernetes.conf <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
vm.swappiness = 0
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 524288
EOF
sysctl --system
# 关 swap(K8s 要求;内存充足可不依赖 swap)
swapoff -a
sed -i -E '/\sswap\s/s/^([^#])/#\1/' /etc/fstab
1.3 部署 containerd(CRI 运行时)
apt-get install -y containerd
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
# 关键两处:SystemdCgroup + 国内 pause 镜像
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sed -i 's#sandbox_image = "registry.k8s.io/pause:3.8"#sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.10.2"#' /etc/containerd/config.toml
# 配置镜像加速(docker.io / k8s.io / quay.io / ghcr.io)
mkdir -p /etc/containerd/certs.d/docker.io /etc/containerd/certs.d/registry.k8s.io \
/etc/containerd/certs.d/quay.io /etc/containerd/certs.d/ghcr.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://docker.io"
[host."https://docker.m.daocloud.io"]
capabilities = ["pull", "resolve"]
[host."https://registry-1.docker.io"]
capabilities = ["pull", "resolve"]
EOF
cat > /etc/containerd/certs.d/registry.k8s.io/hosts.toml <<'EOF'
server = "https://registry.k8s.io"
[host."https://k8s.m.daocloud.io"]
capabilities = ["pull", "resolve"]
[host."https://registry.k8s.io"]
capabilities = ["pull", "resolve"]
EOF
cat > /etc/containerd/certs.d/quay.io/hosts.toml <<'EOF'
server = "https://quay.io"
[host."https://quay.m.daocloud.io"]
capabilities = ["pull", "resolve"]
[host."https://quay.io"]
capabilities = ["pull", "resolve"]
EOF
cat > /etc/containerd/certs.d/ghcr.io/hosts.toml <<'EOF'
server = "https://ghcr.io"
[host."https://ghcr.m.daocloud.io"]
capabilities = ["pull", "resolve"]
[host."https://ghcr.io"]
capabilities = ["pull", "resolve"]
EOF
# 若宿主机有代理(可选,本环境为 192.168.1.88:7890)
# cat > /etc/systemd/system/containerd.service.d/http-proxy.conf <<EOF
# [Service]
# Environment="HTTP_PROXY=http://192.168.1.88:7890"
# Environment="HTTPS_PROXY=http://192.168.1.88:7890"
# Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,.svc,.cluster.local"
# EOF
systemctl daemon-reload
systemctl restart containerd
systemctl enable containerd
cat > /etc/crictl.yaml <<'EOF'
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 30
debug: false
EOF
验证:
ctr -v/crictl info应正常,SystemdCgroup在 config.toml 中为true。
1.4 安装 kubeadm / kubelet / kubectl v1.36.3
apt-get install -y apt-transport-https ca-certificates curl gpg conntrack socat ebtables ipset ipvsadm
mkdir -p /etc/apt/keyrings
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
chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg
cat > /etc/apt/sources.list.d/kubernetes.list <<'EOF'
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/deb/ /
EOF
apt-get update
apt-get install -y kubelet=1.36.3-1.1 kubeadm=1.36.3-1.1 kubectl=1.36.3-1.1
apt-mark hold kubelet kubeadm kubectl
systemctl enable kubelet
注意 kubeadm v1.36 的配置 API 是
kubeadm.k8s.io/v1beta4。下面 init 配置必须匹配。
1.5 静态 IP + 主机名 + hosts(每台按自己 IP 改)
以 master1(192.168.1.11) 为例(其余节点把 IP/主机名换掉即可):
hostnamectl set-hostname master1
cat > /etc/hosts <<'EOF'
127.0.0.1 localhost
127.0.1.1 master1
::1 localhost ip6-localhost ip6-loopback
192.168.1.10 lb
192.168.1.16 lb2
192.168.1.11 master1
192.168.1.12 master2
192.168.1.13 master3
192.168.1.14 worker1
192.168.1.15 worker2
192.168.1.100 k8s-vip kube-apiserver.k8s.local
EOF
nmcli con add type ethernet ifname ens33 con-name k8s-net \
ipv4.method manual ipv4.addresses 192.168.1.11/24 \
ipv4.gateway 192.168.1.1 ipv4.dns "223.5.5.5,119.29.29.29" \
ipv4.route-metric 100 ipv6.method disabled connection.autoconnect yes
nmcli con up k8s-net
提示:本环境用
netcfg.sh一键完成 1.5(变量NODE_NAME/NODE_IP),可批量推到各节点。
步骤 2:部署负载均衡器 lb 与 lb2(HAProxy + Keepalived)
在 lb(192.168.1.10) 上:
apt-get install -y haproxy keepalived psmisc
# 允许绑定非本地 VIP
cat > /etc/sysctl.d/98-haproxy.conf <<'EOF'
net.ipv4.ip_nonlocal_bind = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
# HAProxy 配置
cat > /etc/haproxy/haproxy.cfg <<'EOF'
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 20000
defaults
log global
mode tcp
option dontlognull
option redispatch
option tcpka
retries 3
timeout connect 10s
timeout client 4h
timeout server 4h
timeout tunnel 4h
timeout check 5s
maxconn 18000
frontend k8s-apiserver
bind *:6443
mode tcp
option tcplog
default_backend k8s-apiserver-backend
backend k8s-apiserver-backend
mode tcp
option tcp-check
balance roundrobin
default-server inter 3s downinter 5s rise 2 fall 3 slowstart 60s maxconn 3000 maxqueue 256 weight 100
server master1 192.168.1.11:6443 check
server master2 192.168.1.12:6443 check
server master3 192.168.1.13:6443 check
frontend stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
stats admin if TRUE
EOF
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl enable --now haproxy
# Keepalived(MASTER)
mkdir -p /etc/keepalived
cat > /etc/keepalived/check_haproxy.sh <<'EOF'
#!/bin/bash
if ! /usr/bin/killall -0 haproxy >/dev/null 2>&1; then exit 1; fi
exit 0
EOF
chmod +x /etc/keepalived/check_haproxy.sh
cat > /etc/keepalived/keepalived.conf <<'EOF'
! Configuration File for keepalived -- lb (MASTER)
global_defs {
router_id LVS_K8S_LB
script_user root
enable_script_security
vrrp_skip_check_adv_addr
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight -60
fall 3
rise 2
timeout 2
}
vrrp_instance VI_K8S {
state MASTER
interface ens33
virtual_router_id 51
priority 150
advert_int 1
unicast_src_ip 192.168.1.10
unicast_peer { 192.168.1.16 }
authentication { auth_type PASS; auth_pass k8sha99 }
virtual_ipaddress { 192.168.1.100/24 dev ens33 label ens33:vip }
track_script { chk_haproxy }
}
EOF
systemctl enable --now keepalived
lb2(192.168.1.16) 与 lb 几乎一致,仅 3 处不同(即本环境的 lb2-setup.sh 逻辑):
apt-get install -y haproxy keepalived psmisc
# (HAProxy 配置、check_haproxy.sh、sysctl 同 lb,一字不差)
cat > /etc/keepalived/keepalived.conf <<'EOF'
! Configuration File for keepalived -- lb2 (BACKUP)
global_defs {
router_id LVS_K8S_LB2
script_user root
enable_script_security
vrrp_skip_check_adv_addr
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight -60
fall 3
rise 2
timeout 2
}
vrrp_instance VI_K8S {
state BACKUP
interface ens33
virtual_router_id 51
priority 100
advert_int 1
unicast_src_ip 192.168.1.16
unicast_peer { 192.168.1.10 }
authentication { auth_type PASS; auth_pass k8sha99 }
virtual_ipaddress { 192.168.1.100/24 dev ens33 label ens33:vip }
track_script { chk_haproxy }
}
EOF
systemctl enable --now keepalived haproxy
切换原理:lb 正常优先级 150;HAProxy 挂 →
weight -60→ 90 < lb2 的 100 → VIP 漂到 lb2。weight必须让降权后低于备机,否则永不切换(早期-40即因 150-40=110>100 而失效)。
验证:ip -4 addr show ens33 | grep 192.168.1.100 在 lb 上能看到 VIP;nc -z 192.168.1.100 6443 && echo VIP_OK。
步骤 3:初始化 master1(kubeadm init)
在 master1 上创建初始化配置 /etc/kubernetes/kubeadm-init.yaml:
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
bootstrapTokens:
- token: "abcdef.0123456789abcdef"
description: "kubeadm bootstrap token"
ttl: "24h0m0s"
usages: [signing, authentication]
groups:
- system:bootstrappers:kubeadm:default-node-token
localAPIEndpoint:
advertiseAddress: 192.168.1.11
bindPort: 6443
certificateKey: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"
nodeRegistration:
name: master1
criSocket: unix:///run/containerd/containerd.sock
imagePullPolicy: IfNotPresent
taints:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
kubeletExtraArgs:
- name: node-ip
value: "192.168.1.11"
timeouts:
controlPlaneComponentHealthCheck: 6m0s
kubeletHealthCheck: 5m0s
discovery: 5m0s
etcdAPICall: 2m0s
kubernetesAPICall: 2m0s
tlsBootstrap: 5m0s
upgradeManifests: 5m0s
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.3
clusterName: k8s-ha
controlPlaneEndpoint: "192.168.1.100:6443"
imageRepository: registry.aliyuncs.com/google_containers
certificatesDir: /etc/kubernetes/pki
networking:
dnsDomain: cluster.local
serviceSubnet: 10.96.0.0/12
podSubnet: 10.244.0.0/16
apiServer:
certSANs:
- "192.168.1.100"
- "192.168.1.10"
- "192.168.1.11"
- "192.168.1.12"
- "192.168.1.13"
- "127.0.0.1"
- "10.96.0.1"
- "k8s-vip"
- "lb"
- "master1"
- "master2"
- "master3"
- "localhost"
- "kubernetes"
- "kubernetes.default"
- "kubernetes.default.svc"
- "kubernetes.default.svc.cluster.local"
extraArgs:
- name: authorization-mode
value: "Node,RBAC"
- name: service-node-port-range
value: "30000-32767"
controllerManager:
extraArgs:
- name: bind-address
value: "0.0.0.0"
- name: node-cidr-mask-size
value: "24"
scheduler:
extraArgs:
- name: bind-address
value: "0.0.0.0"
etcd:
local:
dataDir: /var/lib/etcd
serverCertSANs: ["192.168.1.11","192.168.1.12","192.168.1.13","master1","master2","master3"]
peerCertSANs: ["192.168.1.11","192.168.1.12","192.168.1.13","master1","master2","master3"]
dns: {}
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
failSwapOn: false
maxPods: 110
clusterDomain: cluster.local
clusterDNS: ["10.96.0.10"]
rotateCertificates: true
serverTLSBootstrap: false
evictionHard:
imagefs.available: "5%"
memory.available: "100Mi"
nodefs.available: "5%"
nodefs.inodesFree: "3%"
systemReserved:
cpu: "200m"
memory: "256Mi"
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: rr
strictARP: true
clusterCIDR: "10.244.0.0/16"
确保 apiserver/etcd 不走代理(避免连本机被代理截胡):
export no_proxy="localhost,127.0.0.1,::1,10.0.0.0/8,10.96.0.0/12,10.244.0.0/16,192.168.0.0/16,.svc,.cluster.local"
export NO_PROXY="$no_proxy"
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
kubeadm init --config /etc/kubernetes/kubeadm-init.yaml --upload-certs --v=5
成功后:
mkdir -p /root/.kube /home/tantan/.kube
cp -f /etc/kubernetes/admin.conf /root/.kube/config
cp -f /etc/kubernetes/admin.conf /home/tantan/.kube/config
chown -R tantan:tantan /home/tantan/.kube
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl get nodes # 此时应只有 master1,状态 NotReady(还没装 CNI)
生成后续 join 命令(保存到文件,给 master2/3 和 worker 用):
CERT_KEY=$(kubeadm init phase upload-certs --upload-certs | tail -1)
JOIN=$(kubeadm token create --print-join-command)
echo "$JOIN --control-plane --certificate-key $CERT_KEY" > /tmp/join-cp.txt
echo "$JOIN" > /tmp/join-worker.txt
cat /tmp/join-cp.txt # 形如:kubeadm join 192.168.1.100:6443 --token abcdef.xxx --discovery-token-ca-cert-hash sha256:yyy --control-plane --certificate-key zzz
cat /tmp/join-worker.txt
关键:join 命令里的地址是 VIP:6443,所以新节点会经由 HAProxy 找到任意一台健康 master,HA 从加入那一刻就生效。
步骤 4:加入 master2、master3(control-plane)
在 master2(192.168.1.12) 与 master3(192.168.1.13) 上,先设好 no_proxy,再执行步骤 3 生成的 control-plane join 命令:
export no_proxy="localhost,127.0.0.1,::1,10.0.0.0/8,10.96.0.0/12,10.244.0.0/16,192.168.0.0/16,.svc,.cluster.local"
export NO_PROXY="$no_proxy"
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
kubeadm join 192.168.1.100:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:<master1的hash> \
--control-plane --certificate-key <CERT_KEY> \
--cri-socket unix:///run/containerd/containerd.sock --node-name master2 --v=5
加入后固定 node-ip(master2 用 192.168.1.12,master3 用 192.168.1.13):
cat > /etc/default/kubelet <<EOF
KUBELET_EXTRA_ARGS=--node-ip=192.168.1.12
EOF
systemctl daemon-reload; systemctl restart kubelet
# 复制 kubeconfig 便于本地 kubectl
mkdir -p /root/.kube /home/tantan/.kube
cp -f /etc/kubernetes/admin.conf /root/.kube/config
cp -f /etc/kubernetes/admin.conf /home/tantan/.kube/config
chown -R tantan:tantan /home/tantan/.kube
master2/3 加入时会从 master1 通过
--certificate-key拉取同一套 PKI(CA/etcd 证书),因此 3 台 etcd 能互信组成 Raft。
步骤 5:加入 worker1、worker2
在 worker1(192.168.1.14)、worker2(192.168.1.15) 上执行 worker join 命令(不带 --control-plane):
export no_proxy="localhost,127.0.0.1,::1,10.0.0.0/8,10.96.0.0/12,10.244.0.0/16,192.168.0.0/16,.svc,.cluster.local"
export NO_PROXY="$no_proxy"
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
kubeadm join 192.168.1.100:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:<master1的hash> \
--cri-socket unix:///run/containerd/containerd.sock --node-name worker1 --v=5
cat > /etc/default/kubelet <<EOF
KUBELET_EXTRA_ARGS=--node-ip=192.168.1.14
EOF
systemctl daemon-reload; systemctl restart kubelet
此时
kubectl get nodes应看到 5 个节点,但都是NotReady——因为还没装 CNI 网络插件。
步骤 6:部署 Calico CNI
在 master1(有 kubeconfig)上:
export KUBECONFIG=/etc/kubernetes/admin.conf
CALICO_VER=v3.32.1
cd /root
# 下载 manifest(多处兜底源)
for url in \
"https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VER}/manifests/calico.yaml" \
"https://cdn.jsdelivr.net/gh/projectcalico/calico@${CALICO_VER}/manifests/calico.yaml" \
"https://ghproxy.net/https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VER}/manifests/calico.yaml" ; do
curl -fsSL --max-time 90 -o /root/calico.yaml "$url" && break
done
# 适配本集群
sed -i -e 's|# - name: CALICO_IPV4POOL_CIDR|- name: CALICO_IPV4POOL_CIDR|' \
-e 's|# value: "192.168.0.0/16"| value: "10.244.0.0/16"|' /root/calico.yaml
# IPIP 改为 CrossSubnet(同二层不封装,跨子网才封装)
sed -i '/name: CALICO_IPV4POOL_IPIP/{n;s|value: "Always"|value: "CrossSubnet"|}' /root/calico.yaml
kubectl apply -f /root/calico.yaml
# 网卡自动探测:选能到达网关的那块网卡,避免选错
kubectl -n kube-system set env daemonset/calico-node IP_AUTODETECTION_METHOD=can-reach=192.168.1.1
kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
kubectl -n kube-system rollout status daemonset/calico-node --timeout=600s
kubectl -n kube-system rollout status deployment/calico-kube-controllers --timeout=300s
kubectl wait --for=condition=Ready nodes --all --timeout=300s
装完 Calico 后,节点陆续变
Ready,Pod 网络打通。
步骤 7:验证集群
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl get nodes -o wide
# 期望:5 节点 Ready,版本 v1.36.3,ROLES 含 control-plane / <none>
kubectl get pods -n kube-system -o wide
# 期望:etcd-master1/2/3、kube-apiserver-master1/2/3、kube-scheduler-*、
# kube-controller-manager-*、kube-proxy-*、calico-node-*、coredns-* 全部 Running
kubectl get csr # 应无 Pending(证书已自动签发)
# 跑一个测试 Deployment 验证调度 + 网络
kubectl create deployment nginx --image=registry.aliyuncs.com/google_containers/nginx:1.27 --replicas=4
kubectl rollout status deploy/nginx
kubectl get pods -o wide # 跨节点分布
kubectl expose deploy nginx --port=80 --type=ClusterIP
kubectl run curltest --rm -it --image=registry.aliyuncs.com/google_containers/curl -- sh -c 'curl -s nginx | head'
HA 故障切换验证(可选但推荐):
# 1) 摘除 master1 的 apiserver,观察 HAProxy 与集群
ssh tantan@192.168.1.10 "sudo systemctl stop haproxy" # 约 10s 后 VIP 漂到 lb2
kubectl get nodes # 经 VIP 仍正常
ssh tantan@192.168.1.10 "sudo systemctl start haproxy" # 约 12s 后 VIP 抢回
# 2) 停 master1 apiserver 进程
ssh tantan@192.168.1.11 "sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/ && sleep 20"
kubectl get nodes # 经 VIP 仍正常,15s 内 HAProxy 标记 master1 DOWN
ssh tantan@192.168.1.11 "sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/ && sleep 20"
kubectl get nodes # master1 apiserver 自动恢复
步骤 8(可选):宿主机自动化 —— 开机自启 / 关机挂起 / 桌面控制台
这部分不属于 K8s 本身,但本环境已配套(详见 README 第十、十一节):
- 两个计划任务
K8sVM-Autostart/K8sVM-Autosuspend(S4U + Administrator 身份)实现宿主机开机拉起 7 台、关机挂起 7 台。 - 桌面图标
K8s 虚拟机控制台.lnk一键 start/suspend。 - 脚本:
k8s-ha/scripts/vm-power.ps1、vm-panel.ps1、make-shortcut.ps1。
附录 A:加入新节点 / 证书续期 / 常见坑
A.1 加入新的 worker
kubeadm token create --print-join-command # master1 上生成,24h 有效
# 新 worker 上执行该命令(加 --node-name 与 kubelet --node-ip)
A.2 加入新的 control-plane
kubeadm init phase upload-certs --upload-certs # 拿 certificate-key
kubeadm token create --print-join-command # 拿 join 命令
# 拼接: <join> --control-plane --certificate-key <key>
A.3 证书续期
控制平面证书默认 1 年到期(本集群约 2027-08-08)。到期前:
kubeadm certs renew all # 在每台 master 上
# 然后重启各控制平面静态 Pod:临时移走 /etc/kubernetes/manifests/ 下对应 yaml 再放回
A.4 常见坑(本环境实际踩过)
SystemdCgroup=false:kubelet 会报错或反复重启。containerd 必须SystemdCgroup = true且与 kubeletcgroupDriver: systemd一致。- pause 镜像拉不到:把
sandbox_image改成本地可达的镜像(阿里云pause:3.10.2),否则每个 Pod 的 sandbox 起不来。 - kube-proxy 用 iptables 而非 IPVS:本集群显式
mode: ipvs+ 加载ip_vs*内核模块;若用 iptables 也是可行的,但大集群下 IPVS 性能更好。 - Keepalived
weight配错:-40导致 150-40=110 > 备机 100,永远不会切换;必须 ≤ -50(本环境 -60)。 - HAProxy 超时太短:长连接
timeout client/server必须足够大(本环境 4h),否则 watch 频繁断流、集群抖动。 - VIP 与物理网段冲突:Pod 网段必须避开节点物理网段(本环境用
10.244.0.0/16而非默认的192.168.0.0/16)。 - Calico 选错网卡:用
IP_AUTODETECTION_METHOD=can-reach=192.168.1.1锁定,避免选到 docker0/lo。 - apiserver/etcd 走代理:加入/初始化时必须
unset http_proxy并设no_proxy含192.168.0.0/16,.svc,.cluster.local,否则 kubeadm 连本机 apiserver 被代理截胡而失败。 - swap 未关:
kubeadm init会因 swap 开启而失败(除非failSwapOn: false,本环境已设置)。 - hosts 不一致 / 主机名解析失败:7 台
/etc/hosts必须互相包含,且主机名唯一,否则 etcd TLS 与节点注册会出问题。
附:本指南与脚本/配置的对应关系
| 本文章节 | 对应脚本 / 配置 |
|---|---|
| 步骤 1.1 | scripts/aptbase.sh |
| 步骤 1.2~1.4 | scripts/reset-install.sh |
| 步骤 1.5 | scripts/netcfg.sh |
| 步骤 2 | scripts/lb-setup.sh、scripts/lb2-setup.sh、scripts/lb-keepalived-update.sh、configs/haproxy.cfg、configs/keepalived.conf |
| 步骤 3 | scripts/init-master1.sh、configs/kubeadm-init.yaml |
| 步骤 4~5 | scripts/join-tpl.sh(本地渲染每节点实例) |
| 步骤 6 | scripts/calico.sh、configs/calico-v3.32.1.yaml |
| 步骤 7 | scripts/verify.sh、scripts/status.sh |
| 步骤 8 | scripts/vm-power.ps1、scripts/vm-panel.ps1、scripts/make-shortcut.ps1 |

273

被折叠的 条评论
为什么被折叠?



