课程目录(第 18 章 / 共 33 章)
生产集群部署:从节点规划到 kubespray
把集群从本地几个容器搬到真实机房:规划节点与磁盘、用 fio 验证 etcd 磁盘、用 kubespray 完成高可用部署,并学会后续的扩缩容与升级。
学完这一章,你将能够
- ✓说得出生产集群与本地 kind 的差别,并写出节点、磁盘与端口的规划
- ✓用 fio 验证 etcd 磁盘的 fsync 延迟是否达标
- ✓用 kubespray 的 inventory 与 group_vars 部署一个高可用集群并完成验证
- ✓会安全地扩容节点、按组件升级集群
从 kind 到生产集群:差在哪里
前面十几章里,你的集群很可能是 kind 创建的:几个 Docker 容器扮演节点,控制面和 worker 挤在同一台笔记本里。它足够用来学习,但一旦要接管真实流量,几乎所有前提都变了。
生产集群最核心的三个变化是:节点是多台真实机器、控制面必须高可用、磁盘是会被写坏的。任何一环偷懒,都会在某天凌晨变成事故。
| 维度 | kind(本地学习) | 生产集群 |
|---|---|---|
| 节点 | 1~3 个 Docker 容器 | 独立物理机或虚拟机 |
| 控制面 | 单实例,删了重建 | 3 节点高可用,容忍 1 台故障 |
| etcd 磁盘 | 宿主机磁盘,可能还是笔记本 SSD | 独占 NVMe,禁止与其他 I/O 共享 |
| 网络 | 容器网桥 + 端口映射 | 真实路由与 VIP,内网 10Gb 起步 |
| 升级 | kind delete cluster 重建 | 逐节点滚动升级,业务不中断 |
节点规划:控制面、worker 与磁盘
规划的第一件事是数机器。控制面必须用奇数台,因为 etcd 靠多数派(quorum)投票,3 台能容忍 1 台故障,5 台能容忍 2 台,2 台反而比 1 台更脆弱。所以生产的起步答案是 3 台。
| 角色 | 数量建议 | 规格参考 | 说明 |
|---|---|---|---|
| 控制面 + etcd | 3(最多 7) | 4C 8G 起 | etcd 独占 NVMe,节点之间低延迟 |
| worker(CPU) | 按业务量 | 8C 16G 起 | 按 requests 总量除以单机可分配量估算 |
| worker(GPU) | 按业务量 | 按卡型 | 通常单独打标签、单独污点 |
| 负载均衡 / VIP | 2 台或复用控制面 | 小规格 | 给 apiserver 一个稳定入口 |
| 存储节点 | 3 起 | 独立数据盘 | 跑 Rook/Ceph 时另算 |
worker 数量没有公式,但有两条经验:先把所有工作负载的 requests 加起来,按 70% 的分配率折算(其余留给系统组件与突发);节点规格尽量一致,否则调度会偏向大机器。
第二件事是把磁盘拆开。这是新手最容易忽略、代价也最大的一步。
| 挂载点 | 用途 | 要求 |
|---|---|---|
/ | 操作系统 | SSD,50~100GB 足够 |
/var/lib/etcd | etcd 的 WAL 与数据库 | 独占一块 NVMe,禁止机械盘、NAS、SAN、iSCSI、Ceph RBD、NFS |
/var/lib/containerd | 镜像与容器可写层 | SSD,容量按镜像数量估算 |
为什么 etcd 不能和别人共享磁盘
etcd 的每次写都要把 WAL 同步落盘(fsync)才敢确认。如果同一块盘上还有日志刷盘、镜像拉取、数据库压测,fsync 延迟就会被拖长,表现是 apiserver 突然变慢、甚至频繁重新选主。给 etcd 一块独占盘,是最便宜的保险。
网络与端口清单
节点之间至少要有千兆、推荐万兆内网互联,并且全部节点互通。控制面节点之间的网络尤其重要:etcd 的心跳与日志复制都走它。
防火墙不要「先关了再说」,而是按清单放行。下面这张表是自建集群最常见的一套端口。
| 端口 | 协议 | 组件 | 用途 |
|---|---|---|---|
| 6443 | TCP | kube-apiserver | kubectl 与所有节点访问控制面的入口 |
| 2379-2380 | TCP | etcd | 客户端请求与 peer 复制 |
| 10250 | TCP | kubelet | apiserver 访问 kubelet、exec、logs |
| 30000-32767 | TCP | NodePort | Service 对外暴露端口段 |
| 8472 / 4789 | UDP | VXLAN | Flannel、Cilium、Calico 的封装 |
| 179 | TCP | BGP | Calico 与物理网络做 BGP 对等 |
还有一条软性要求:所有节点时间必须同步。用 chrony 或 systemd-timesyncd 都行,偏差控制在毫秒级,否则证书校验和事件时间线都会出问题。
etcd 最怕磁盘慢:部署前用 fio 验一遍
磁盘性能是这个模块里唯一能在部署前量化验证的东西。方法很直接:用 fio 模拟 etcd 写 WAL 的行为,测 fsync 延迟。
# fio 版本建议 >= 3.5,旧版本不输出 fdatasync 百分位
sudo mkdir -p /var/lib/etcd
sudo fio --rw=write --ioengine=sync --fdatasync=1 \
--directory=/var/lib/etcd \
--size=22m --bs=2300 \
--name=etcd-test三个关键参数:--fdatasync=1 让 fio 每次写完都做一次 fdatasync,复刻 etcd 的落盘动作;--bs=2300 匹配 etcd WAL 条目的典型大小;--size=22m 产生约一万个样本,让 p99 统计有意义。
看输出里的 sync percentiles (usec) 段:
sync percentiles (usec):
| 99.00th =[ 2376] ← 2.4ms,健康;接近 10000 就是余量不足判据只有一条:99.00th 小于 10000 usec(10ms)算合格。上面这组 2.4ms 属于很健康;如果 p99 已经在 9ms 附近,说明余量不足,真实 etcd 还叠加了其他 I/O,建议换盘再上。参考口径是:小集群至少 50 顺序写 IOPS,大集群推荐 500 以上。
动手:给你的磁盘打一次分
- 准备一台目标节点,用
lsblk和df -h确认哪块盘将承载/var/lib/etcd。 - 装上 fio:
sudo apt install -y fio或sudo dnf install -y fio。 - 跑上面的命令,记下
99.00th的数值。 - 再跑一次,把
--directory换成/tmp(系统盘),对比两者差距。如果差距很大,说明把 etcd 放系统盘一定会拖慢控制面。
部署之后还要持续盯着运行时指标:etcd_disk_wal_fsync_duration_seconds 的 p99 应低于 10ms,etcd_disk_backend_commit_duration_seconds 的 p99 应低于 25ms,etcd_server_leader_changes_seen_total 应长期为 0;etcd 3.6+ 再看一眼 etcd_disk_wal_write_duration_seconds,它覆盖 write() 系统调用本身的延迟——出现过 fsync 正常但 write() 因内核问题极慢的案例。
注意 10ms 是运行目标,不是告警线:etcd 官方告警规则要到 WAL fsync p99 超过 500ms 才 warning、1s 才 critical,告警响的时候集群早就严重劣化了。另外 --bs=2300 只是经验值,可以在自己的集群上用 sudo strace -p $(pgrep -x etcd) -e write 复核真实 WAL 条目大小;同节点有 I/O 竞争时用 sudo ionice -c2 -n0 -p $(pgrep -x etcd) 给 etcd 提优先级。参考:reference/k8s-in-action/k8s/etcd-disk-performance.md
用 kubespray 部署集群
kubespray 是官方社区维护的 Ansible 部署工具,它把 kubeadm、etcd、CNI、CoreDNS 等组件串成一套可重复执行的剧本。相比手工 kubeadm,它更适合「以后还要反复扩容和升级」的场景。
准备安装机
找一台能 SSH 免密登录所有节点的机器作为安装机(可以是控制面之一,也可以单独一台):
export KUBESPRAY_VERSION="2.31.0"
wget https://github.com/kubernetes-sigs/kubespray/archive/refs/tags/v${KUBESPRAY_VERSION}.tar.gz
tar zxf v${KUBESPRAY_VERSION}.tar.gz
cd kubespray-${KUBESPRAY_VERSION}
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt写 inventory
复制一份示例 inventory,再改成你的机器:
cp -r inventory/sample inventory/mycluster编辑 inventory/mycluster/inventory.ini,主机名只是标签,ansible_host 才是真实地址:
[kube_control_plane]
mn-10-0-0-1 ansible_host=10.0.0.1 etcd_member_name=etcd1
mn-10-0-0-2 ansible_host=10.0.0.2 etcd_member_name=etcd2
mn-10-0-0-3 ansible_host=10.0.0.3 etcd_member_name=etcd3
[etcd:children]
kube_control_plane
[kube_node]
cn-10-0-1-1 ansible_host=10.0.1.1
cn-10-0-1-2 ansible_host=10.0.1.2
[k8s_cluster:children]
kube_control_plane
kube_node关键 group_vars
只需改几个文件里的少量字段。group_vars/k8s_cluster/k8s-cluster.yml:
kube_version: v1.31.5
kube_network_plugin: cilium
kube_service_addresses: 172.23.0.0/16
kube_pods_subnet: 172.24.0.0/13
kube_proxy_mode: ipvs
container_manager: containerdgroup_vars/k8s_cluster/addons.yml 里打开 kube-vip,给控制面一个高可用地址:
kube_vip_enabled: true
kube_vip_arp_enabled: true
kube_vip_address: 10.0.0.100
kube_vip_interface: eth0group_vars/all/etcd.yml 确认 etcd 的数据目录和配额:
etcd_data_dir: /var/lib/etcd
etcd_deployment_type: host
etcd_quota_backend_bytes: "8589934592"etcd_deployment_type: host 表示 etcd 以 systemd 服务跑在宿主机上,这是当前版本的默认值。配额一项要留意:kubespray 2.31 的默认值是 2147483648(2GiB),etcd 官方把 8GiB 视为常规环境的上限建议,所以这里显式写成 8GiB 是一个常见做法;配额调大也会增加 etcd 的内存占用,机器只有 4GB 内存时不要照抄。
Pod 与 Service 网段别和机房冲突
kube_pods_subnet 和 kube_service_addresses 一旦定下来,后期修改代价很高。规划时先问清楚机房、VPN、公司内网用掉了哪些网段,避开它们。
执行部署并验证
source .venv/bin/activate
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root cluster.yml从任意控制面节点取 kubeconfig,并把 server 改成 VIP 地址:
mkdir -p ~/.kube
scp root@10.0.0.1:/etc/kubernetes/admin.conf ~/.kube/config
# 把 server 字段改成 https://10.0.0.100:6443
sed -i 's#https://10.0.0.1:6443#https://10.0.0.100:6443#' ~/.kube/config
kubectl get nodes -o wide怎么算成功?kubectl get nodes 里每台机器都是 Ready、版本一致,kube-system 下 apiserver、scheduler、controller-manager、etcd、CoreDNS、CNI 的 Pod 全部 Running,且 CNI 的 DaemonSet 副本数等于节点数:
kubectl get pods -n kube-system高可用控制面与 VIP
高可用的意思是:任意一台控制面挂掉,集群仍然能接受请求。先看一张完整的拓扑,把每个角色的位置对上号:
kubectl / kubelet / 控制器
│ https://10.0.0.100:6443
┌─────────▼──────────┐
│ VIP 10.0.0.100 │ kube-vip(ARP 或 BGP)
└─────────┬──────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ cp1 │ │ cp2 │ │ cp3 │
│ apiserver│ │ apiserver│ │ apiserver│
│ etcd1 │◄──►│ etcd2 │◄──►│ etcd3 │ 多数派:3 选 2
└────┬─────┘ └────┬─────┘ └────┬─────┘
└───────┬───────┴───────┬───────┘
▼ ▼
┌─────────┐ ┌─────────┐
│ worker1 │ │ worker2 │ kubelet + CNI
└─────────┘ └─────────┘节点只认 VIP,不关心背后是哪台控制面;etcd 之间的日志复制走控制面之间的内网,和业务流量分开。高可用由两部分组成:etcd 的多数派——3 个成员里只要有 2 个活着就能选出 leader,所以控制面必须 3 台起,且最好跨机柜或跨可用区;apiserver 前面的稳定入口——kubeconfig、kubelet、controller 都指向这个 VIP,由 kube-vip(ARP 或 BGP 模式)或外部 HAProxy/Keepalived、F5 承担,它只把连接送到还活着的 apiserver,不参与 etcd 投票。
判断高可用是否真的生效,不要只看机器数量,而是随便关掉一台控制面,再用kubectl get nodes和kubectl get --raw='/readyz?verbose'验证集群还能用。
升级与扩缩容节点
扩容:把新节点加进 inventory,然后只对它执行 scale 剧本,避免全量重跑。
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root playbooks/facts.yml
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root scale.yml --limit=cn-10-0-1-3
kubectl get nodes -o wide下线节点:先驱逐业务,再从集群里摘掉。
kubectl drain cn-10-0-1-3 --ignore-daemonsets --delete-emptydir-data
kubectl delete node cn-10-0-1-3升级:kubespray 的剧本是幂等的,改掉 kube_version 后重跑即可;先升控制面再升 worker,只更新单个组件时用 tag 缩小范围。
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root \
-e kube_version=v1.32.0 cluster.yml --limit=kube_control_plane
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root \
-e kube_version=v1.32.0 cluster.yml --limit=kube_node
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root cluster.yml --tags=coredns升级前一定要用 etcdctl snapshot save 备份 etcd,并确认节点时间同步、磁盘空间充足。
控制面扩容不能用 scale.yml
scale.yml 是给 worker 用的:它按「新增节点」处理并重写 etcd 成员配置,控制面从 1 台扩到 3 台时很容易把 ETCD_INITIAL_CLUSTER 写坏。正确做法是把新节点当成「一个坏掉的成员来恢复」,用 recover-control-plane.yml:新节点同时登记进 kube_control_plane 和 broken_etcd、broken_kube_control_plane 两个组。
[kube_control_plane]
mn-10-0-0-1 ansible_host=10.0.0.1 etcd_member_name=etcd1
mn-10-0-0-2 ansible_host=10.0.0.2 etcd_member_name=etcd2
[broken_etcd]
mn-10-0-0-2 ansible_host=10.0.0.2 etcd_member_name=etcd2
[broken_kube_control_plane]
mn-10-0-0-2 ansible_host=10.0.0.2ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root playbooks/facts.yml
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root \
recover-control-plane.yml --limit=etcd,kube_control_plane -e etcd_retries=10etcd_member_name 必须唯一,且两个组里的写法要一致。跑完*务必把 `broken_ 两个组从 inventory 删掉**,否则以后每次执行 playbook 都会把这些节点当成故障节点处理。用 etcdctl ... member list 验证:每个成员都是 started、成员数为奇数,才算成功。**控制面缩容比扩容更危险**,必须先移除 etcd 成员、等集群重新选出 leader,再下线机器,顺序反了会直接丢多数派。参考:reference/k8s-in-action/k8s/kubespray/kubespray-2.28.0/control-plane-scale.md`
换版本前先核对这几条破坏性变更
kubespray 的破坏性变更通常来自它带上的 Kubernetes 与 etcd,而不是它自己。以 2.30 → 2.31(默认 K8s 1.35、etcd 3.6)为例:
| 变更 | 影响 | 上线前怎么确认 |
|---|---|---|
| cgroup v1 默认拒绝启动 | K8s 1.35 起 kubelet 在 cgroup v1 节点上直接启动失败 | stat -fc %T /sys/fs/cgroup 必须是 cgroup2fs,再 grep -r unified_cgroup_hierarchy /etc/default/grub* 确认没被手工降级 |
| etcd 3.5 → 3.6 | 大版本升级,升级与回退都要按官方流程,不能只换二进制 | 升级前 etcdctl snapshot save,确认数据目录与配额 |
kube_proxy_mode: ipvs 被废弃 | K8s 1.35 起启动即告警,未来移除 | 新集群直接选 nftables,老集群把迁移排进计划 |
| ingress-nginx / dashboard addon 被移除 | 这两个 addon 开关不再生效 | 入口改用 Gateway API 或其它控制器,看板换 Headlamp |
stat -fc %T /sys/fs/cgroup # cgroup2fs 才安全
ansible-playbook -i inventory/mycluster/inventory.ini -b -u=root cluster.yml --list-tags参考:reference/k8s-in-action/k8s/kubespray/kubespray-2.31.0/kubespray-changes.md
OS 与 VIP 准备:跑 playbook 之前先对齐基线
节点装好系统之后、执行 cluster.yml 之前,有几项必须在所有节点上对齐,它们都属于事后返工代价极高的类型。先把节点列表写进一个文件(如 all 里写 mn-10-0-0-[1-3]、cn-10-0-1-[1-2]),然后用 pdsh 批量执行,别一台台 SSH:
pdsh -w ^all ip link show eth0 | grep mtu # 期望全部 mtu 1500,不一致会让 CNI 封装出怪问题
pdsh -w ^all hostnamectl --static # 主机名必须全局唯一,克隆虚拟机最容易重复
pdsh -w ^all timedatectl # 时区与时同步状态两条最容易被忽略的基线:网卡名要统一,CNI 依赖固定接口名,物理机建议用 netplan 的 match.macaddress + set-name 固定成 eth0;节点间 `ping` 延迟大于 0.05ms 就关掉 CPU 省电模式,用 tuned-adm profile latency-performance,否则 etcd 心跳与选举会抖。
内网环境还要在 group_vars/all/all.yml 里补三个变量,否则装完才发现 CoreDNS 起不来或时间漂移:upstream_dns_servers(节点访问不到外网 DNS 时,CoreDNS 与 NodeLocal DNS 会直接起不来)、ntp_servers(内网 NTP,时间漂移会导致证书校验失败)、http_proxy / https_proxy 与 no_proxy(节点不能出网时拉不到镜像)。参考:reference/k8s-in-action/k8s/kubespray/kubespray-2.31.0/README.md
如果 apiserver 入口不用 kube-vip,而是 keepalived + haproxy 这套传统做法,下面几个参数必须核对:
interface eth0 # 换成真实网卡名
virtual_router_id 52 # 0-255 随机取;同网段有别人的 keepalived 必须避开
virtual_ipaddress # VIP 掩码必须与物理网络一致,否则可能不可达:10.0.0.100/24
unicast_peer # 用单播而非组播(云上、跨机柜常禁组播),列出其余控制面 IP
track_script chk_haproxy # haproxy 进程没了就交出 VIP参考:reference/k8s-in-action/os/os.md、reference/k8s-in-action/os/keepalived.md
常见坑与排错
部署类问题的排查顺序很固定:先看节点是否注册成功,再看 kubelet 与容器运行时的日志,最后才怀疑网络和存储。下面这张表把最常见的几种症状对上原因。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
kubectl 偶发超时 | etcd 磁盘 fsync 变慢或同盘有其他 I/O | etcd_disk_wal_fsync_duration_seconds 的 p99、iostat -xm 1 | 把 etcd 挪到独占 NVMe,迁走同盘的压测与日志刷盘 |
某台节点一直 NotReady | kubelet 或容器运行时版本不匹配、swap 没关、CNI Pod 没起来 | kubectl describe node <name>、journalctl -u kubelet -n 100 | 统一版本、swapoff -a、检查 CNI DaemonSet 副本数 |
ansible-playbook 卡在某台机器 | SSH 免密没配好、时间不同步、防火墙拦了端口 | ansible -i inventory/mycluster/inventory.ini all -m ping、chronyc tracking | 修免密与时间同步,对照本章端口表逐条放行 |
| 新加的节点不接活 | 缺少业务需要的标签,或节点上有污点 | kubectl get nodes --show-labels、kubectl describe node <name> | 补标签、确认污点与容忍是否匹配 |
| 控制面频繁重新选主 | 控制面之间网络抖动,或 etcd 盘太慢 | etcd_server_leader_changes_seen_total 持续增长 | 检查跨机柜网络与磁盘延迟,必要时重排部署位置 |
| apiserver 报证书错误 | 节点时间偏差过大或证书已过期 | date 与 kubeadm certs check-expiration | 修好 chrony 后再排查证书 |
常见坑
- etcd 磁盘慢导致 apiserver 抖动:症状是
kubectl偶发超时、etcd_server_leader_changes_seen_total增长。先看 fsync p99,再看同节点是否跑了 I/O 密集型负载。 - kubelet 与容器运行时版本不匹配:containerd 或 CRI-O 太旧会导致节点
NotReady,报错通常在journalctl -u kubelet里;时间不同步则表现为证书校验失败与事件时间错乱。两者都在部署前统一。 - swap 未关闭:kubelet 默认拒绝启动。用
swapoff -a并注释/etc/fstab里的 swap 行。 - 防火墙未放行端口:节点之间不通,表现为 CNI Pod 起不来或 apiserver 连不上 kubelet。对照本章的端口表逐条放行。
- 主机名或 machine-id 重复:从模板克隆的虚拟机最容易出这个问题,会导致节点注册混乱。
自测题
自测:为什么控制面要 3 台,2 台反而比 1 台更危险?(点击展开答案)
etcd 靠多数派(quorum)决定能否写入,2 台的多数派是 2,也就是任何一台挂掉就失去多数派,集群立刻无法写入、无法选主。你付出了两台机器的成本和两倍的故障概率,却换来了零容忍能力。1 台至少是「坏了就全坏」,逻辑简单;3 台才第一次获得「坏 1 台仍然可用」的收益,5 台能容忍 2 台。所以生产的最小正确答案是 3 台,并且分散到不同机柜或可用区。
自测:fio 的 99.00th 是 9ms,判据是小于 10ms,为什么还要谨慎?(点击展开答案)
因为判据是上线门槛,不是安全线。这个 9ms 只是裸盘上空跑出来的数字,真实 etcd 还要同时处理 WAL 写入、后端 compaction、快照传输,叠加之后 p99 很容易突破 10ms,表现就是 apiserver 偶发超时和 leader 频繁切换。经验做法是留出一倍以上余量(比如 p99 在 2~4ms),或者干脆换一块更好的盘。反过来,如果 p99 已经接近 10ms,那就不该把 etcd 放上去。
自测:kube-vip 提供了 VIP,它参与 etcd 投票吗?(点击展开答案)
不参与。VIP 只是 apiserver 的入口,负责把客户端连接送到还活着的控制面节点上;etcd 的成员身份、日志复制和投票完全由 etcd 自己完成,和 VIP 无关。理解这一点很重要:VIP 挂了只影响你「怎么连」apiserver,不会让 etcd 失去多数派;反过来,etcd 失去多数派时,即使 VIP 正常,集群也无法接受写操作。
小结
- 生产集群和 kind 的差别在节点、高可用与磁盘三件事上,规划阶段就要定下来。
- 控制面用奇数台(3 起),etcd 数据盘必须独占 NVMe,部署前用 fio 确认 fsync p99 小于 10ms。
- 端口、时间同步、swap 是三张必须提前处理的清单。
- kubespray 的日常操作就是改 inventory 与 group_vars,然后跑
cluster.yml、scale.yml或带 tag 的部分更新。 - 高可用由 etcd 多数派加 apiserver 的 VIP 共同保证,验收方式是真的关掉一台机器试试。
练习
- 给你手上一台机器规划一份最小生产集群清单:几台控制面、几台 worker、每台的磁盘怎么分,并说明理由。
- 在目标磁盘上跑一遍 fio,把 p99 结果和本章的判据对照,判断这块盘能不能放 etcd。
- 想一下:如果机房只给你 2 台机器做控制面,为什么说这比 1 台更危险?
fio 的用法和第 20 章 生产存储里的性能验证是同一套思路,可以对照着看。集群装好了,但 Pod 之间还不能互通——接着看第 19 章 集群网络:CNI 怎么选、NetworkPolicy 怎么写;集群真出问题时的通用顺序在排障手册。