主题
第二部分 大规模集群配置与自动化部署
当集群规模从几十个节点走向数千个节点时,"能跑起来"与"能持续稳定地跑下去"之间隔着一条鸿沟。第一部分我们解决了架构设计与容量规划的问题——把控制平面、etcd、网络、存储的拓扑定下来。本部分解决的是下一个问题:如何用自动化的方式,把数千台物理机/虚拟机,标准化、可复制、可回滚地变成一个 RKE2 集群,并在此后的生命周期里防止配置漂移。本部分的所有命令与配置基于 RKE2 v1.28+,部分示例直接取自生产仓库
rke2/(server-config.yaml、agent-config.yaml、install-server.sh、install-agent.sh)。
第 1 章 大规模部署总体策略
1.1 为什么数千节点不能靠手工
先算一笔账。假设一个熟练工程师手工部署一个节点需要 15 分钟(装 OS 除外,仅 RKE2 安装、配置、校验),3000 个节点就是 750 工时——一个人干一年。更要命的不是慢,而是:
| 手工部署的问题 | 在大规模下的后果 |
|---|---|
| 配置靠人记、靠复制粘贴 | 节点间配置漂移,故障时无法定位"这台机器为什么不一样" |
| 无幂等性 | 重复执行产生不一致状态,无法安全重试 |
| 无速率控制 | 批量加入时冲击 etcd 与 apiserver,可能直接把控制平面打垮 |
| 无校验闭环 | 装完不知道好不好,问题滞后发现,定位成本指数上升 |
| 无审计 | 谁在什么时候改了什么,无从追溯 |
大规模部署的第一原则:任何节点在任何时刻的状态,都必须能由一份版本化的声明式描述 + 一套自动化流水线重建出来。手工只允许出现在流水线本身的开发与调试环节。
1.2 自动化分层模型
数千节点的部署必须拆成分层的流水线,每层职责单一、输入输出明确:
┌──────────────────────────────────────────────────────────────┐
│ L5 校验层 节点健康/配置一致性/基线扫描/准入门禁 │
├──────────────────────────────────────────────────────────────┤
│ L4 配置层 config.yaml / registries.yaml / sysctl │
│ 以模板 + GitOps 方式下发 │
├──────────────────────────────────────────────────────────────┤
│ L3 RKE2 安装层 install.sh + artifact tarball + systemd │
├──────────────────────────────────────────────────────────────┤
│ L2 OS 初始化层 内核参数/时间同步/swap/防火墙/镜像源 │
├──────────────────────────────────────────────────────────────┤
│ L1 裸机供给层 PXE/IPMI/DHCP → OS 镜像安装 │
└──────────────────────────────────────────────────────────────┘
每一层都可独立重跑、独立验证、独立回滚各层的关键设计点:
- L1 裸机供给(provisioning):通过 PXE + Kickstart/Preseed/Autoinstall 或专用供给工具安装 OS。产出是一台能 SSH、带正确主机名与网络配置的机器。
- L2 OS 初始化:把内核参数、时间同步、swap、防火墙、镜像源等收敛为一份可幂等执行的基线(Ansible role 或 cloud-init)。这一层必须对"重跑"安全。
- L3 RKE2 安装:使用 RKE2 官方 install.sh,配合离线 artifact(
INSTALL_RKE2_ARTIFACT_PATH),避免数千节点同时去公网拉包。 - L4 配置下发:
/etc/rancher/rke2/config.yaml、registries.yaml一律由模板渲染,模板进 Git,禁止人工 SSH 上去改。 - L5 校验:节点加入后自动跑健康检查(kubelet 端口、etcd 成员状态、CNI Pod、DNS 解析、实际调度一个冒烟 Pod),通过才打"可承载业务"标签。
1.3 工具选型对比
| 工具 | 定位 | 优势 | 局限 | 适用场景 |
|---|---|---|---|---|
| Ansible | L2/L3/L4/L5 配置与编排 | 无 agent、幂等模块丰富、serial 滚动控制天然支持 | 无状态、无裸机供给能力、超大规模需优化(forks/pipelining) | 数百~数千节点的配置与批量部署主力 |
| Terraform | 云资源/虚机供给 | 声明式、状态管理、云厂商生态好 | 不做 OS 内部配置;裸机场景弱 | 云上大规格虚机批量创建(配合 cloud-init) |
| Cluster API (CAPI) | K8s 风格的生命周期管理 | Machine/MachineDeployment 声明式管理节点,扩缩容即调副本数 | 需要 bootstrap 集群;运维心智成本高 | 已有 K8s 团队、希望"用 K8s 管 K8s" |
| Metal³ | 裸机供给 (L1) | 基于 Ironic,K8s 原生管理裸机 | 组件多(Ironic/inspection/dnsmasq),调优复杂 | 大规模裸机池,与 CAPI 组合 |
| Tinkerbell | 裸机供给 (L1) | 轻量、工作流(workflow)模型灵活 | 社区规模小于 Metal³;生产案例较少 | 中小规模裸机、需要自定义供给流程 |
| Rancher 自动注册 | L3~L5 一体化 | 注册命令/注册令牌自动生成,节点自动打标签、自动纳管 | 引入 Rancher 控制面本身的依赖 | 已用 Rancher 做多集群管理的组织 |
典型组合建议:
- 纯虚拟机/云环境:Terraform(建机) + cloud-init(L2/L3 引导) + Ansible(L4/L5)。
- 大规模裸机:Metal³ 或 Tinkerbell(L1) + Ansible(L2~L5),或全量交给 Rancher + CAPI。
- 已采购 Rancher:直接用 Rancher 的 cluster registration,自定义节点用 registration command 自动加入,省去自建 L3/L4。
注意事项:工具不是越多越好。每一层只选一个工具并写清楚边界("Terraform 只负责机器存在,Ansible 只负责机器内部"),否则两个工具同时改一处配置,必然打架。
1.4 本章最佳实践
- 部署流水线代码化后,先在 3 节点环境全量演练,再到 50 节点灰度,最后放大到生产批次。每一档都要记录耗时与 etcd/apiserver 指标基线。
- 所有批次操作必须支持"一键暂停":Ansible 的
serial+--limit,或 CAPI 的 MachineDeployment 暂停 rollout。 - 把"节点加入速率"作为流水线的一级参数(见第 6 章),而不是藏在脚本里的魔法数字。
第 2 章 操作系统层标准化
2.1 镜像定制:Packer 与 cloud-init
数千节点如果每次从 ISO 装起再做初始化,L1+L2 会成为瓶颈。正确做法是预烘焙(baking)镜像:把稳定的、不易变的内容打进镜像,把易变的内容留给 cloud-init/Ansible。
打进镜像(Packer): 留给 cloud-init / Ansible:
- 内核版本与内核参数 - 主机名、IP(静态注入)
- 容器运行时依赖、rke2 tarball - node-name / node-labels
- chrony、关闭 swap - cluster token(运行时注入)
- 安全加固基线(ssh、审计) - 环境差异(机房、可用区)Packer 模板(Rocky Linux 9,QEMU/KVM 示例,截选):
hcl
# rke2-node-rocky9.pkr.hcl
source "qemu" "rocky9-rke2" {
iso_url = "https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.4-x86_64-minimal.iso"
iso_checksum = "sha256:..."
disk_size = "100G"
ssh_username = "xxx"
ssh_password = "xxx"
shutdown_command = "shutdown -P now"
}
build {
sources = ["source.qemu.rocky9-rke2"]
provisioner "shell" {
scripts = [
"scripts/01-sysctl-baseline.sh", # 见 2.2
"scripts/02-disable-swap.sh",
"scripts/03-chrony.sh",
"scripts/04-prefetch-rke2.sh", # 预置 rke2 artifact 到 /opt/rke2-artifacts
"scripts/99-cleanup.sh"
]
}
}对应的 cloud-init user-data(Ubuntu 示例,首次启动时执行):
yaml
#cloud-config
hostname: ${NODE_NAME}
fqdn: ${NODE_NAME}.dc1.example.com
preserve_hostname: false
write_files:
- path: /etc/rancher/rke2/config.yaml
permissions: "0600"
content: |
server: https://rke2-vip.dc1.example.com:9345
token: ${CLUSTER_TOKEN}
node-ip: ${NODE_IP}
kube-proxy-arg:
- "proxy-mode=ipvs"
system-default-registry: harbor.dc1.example.com
runcmd:
- curl -fsSL http://artifact.dc1.example.com/repo/rke2/1.28/install.sh -o /tmp/install.sh
- INSTALL_RKE2_VERSION=v1.28.12+rke2r1 INSTALL_RKE2_ARTIFACT_PATH=/opt/rke2-artifacts INSTALL_RKE2_TYPE=agent sh /tmp/install.sh
- systemctl enable rke2-agent --now最佳实践:token 不要打进镜像。token 通过 cloud-init、虚拟机 CD-ROM(NoCloud datasource)或 Ansible 在运行时注入;镜像里只放版本固定的 artifact。
2.2 内核参数基线
大规模集群的节点级内核参数基线(/etc/sysctl.d/90-rke2.conf),按"为什么调"分组:
conf
# ── 连接跟踪:大规模 Service/iptables 规则下 conntrack 表极易打满 ──
net.netfilter.nf_conntrack_max = 2097152
net.netfilter.nf_conntrack_buckets = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 3600
# ── inotify:每个容器镜像层、configmap/secret 挂载都消耗 watch ──
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192
fs.file-max = 2097152
# ── 网络栈:高并发 Service 转发 ──
net.ipv4.tcp_max_syn_backlog = 3240000
net.core.somaxconn = 32768
net.core.netdev_max_backlog = 65536
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 10
net.ipv4.ip_local_port_range = 10240 65000
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# ── K8s 必需 ──
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# ── 虚内存:大内存节点防止过度换页抖动 ──
vm.max_map_count = 262144
vm.swappiness = 0
vm.overcommit_memory = 1
vm.panic_on_oom = 0
kernel.panic = 10
kernel.panic_on_oops = 1应用并持久化:
bash
cat > /etc/sysctl.d/90-rke2.conf <<'EOF'
# ... 上述内容 ...
EOF
modprobe br_netfilter overlay nf_conntrack
sysctl --system
# 校验关键值
sysctl net.netfilter.nf_conntrack_max fs.inotify.max_user_watches注意事项:
nf_conntrack_max调大的同时要关注内存——每条 conntrack 约 300 字节,200 万条约占 600MB。还要确认 kube-proxy 的conntrack-max-per-core与 sysctl 不冲突(kube-proxy 会自行设置 conntrack 上限,大规模集群建议显式配置--conntrack-max-per-core=0交由 sysctl 控制,或两者对齐)。RKE2 中通过 config.yaml 的kube-proxy-arg下发。
2.3 时间同步
etcd 对时钟漂移极其敏感(lease、watch 都依赖时钟),数千节点必须统一 chrony/NTP 源:
bash
# Rocky Linux 9
dnf install -y chrony
cat > /etc/chrony.conf <<'EOF'
server ntp1.dc1.example.com iburst
server ntp2.dc1.example.com iburst
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
EOF
systemctl enable chronyd --now
chronyc sources -v
chronyc tracking | grep -E 'System time|Leap'Ansible 巡检任务(确认全集群偏移在阈值内):
bash
ansible all -m shell -a "chronyc tracking | awk '/System time/{print \$4}'" \
| awk '{if ($1+0 > 0.050) print "WARN clock offset", $0}'最佳实践:etcd 专用节点要求时钟偏差 < 50ms;业务节点 < 500ms。把时钟偏移纳入节点准入校验(L5),超标的节点不允许加入集群。
2.4 关闭 swap
bash
swapoff -a
# 从 fstab 永久移除(注意要用注释而非删除行,保留审计痕迹)
sed -i.bak '/swap/ s/^\(.*\)$/#\1/' /etc/fstab
# 验证
swapon --show && free -m | grep -i swapRKE2/Kubernetes 自 v1.22 起支持 swap(NodeSwap 特性门控),但在大规模生产集群中仍建议直接关闭:swap 会让 kubelet 的驱逐阈值失真,OOM 行为变得不可预测,排查问题时多一层变量。
2.5 防火墙策略:firewalld vs 直放 iptables
RKE2 组件、kube-proxy(ipvs/iptables 模式)、CNI 都会动态操作 iptables/nftables 规则。firewalld 在规则数量大时(数千节点 × 每节点数千条规则)会成为性能与一致性问题源。
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| firewalld 全量托管 | 语义化 zone、变更原子 | 与 K8s 动态规则并存时规则膨胀;大规模下 firewalld 重载慢 | 不建议用于承载 K8s 流量的节点 |
| 完全关闭防火墙 | 零干扰 | 无主机层防护,等保/安全合规难过 | 仅在宿主机已有上游 ACL/安全组时可接受 |
| firewalld 仅管管理面,K8s 流量放行 | 兼顾合规与性能 | 需要精确维护端口清单 | 推荐 |
推荐做法:firewalld 只开管理端口(ssh、监控),K8s 组件所需端口按数千节点统一规划永久放行:
bash
# RKE2 所需端口基线(参见官方文档,v1.28+)
# server: 6443(apiserver) 9345(注册) 2379-2380(etcd) 10250(kubelet) 10257/10259
# agent : 10250(kubelet) 30000-32767(nodeport, 仅对需要的网段)
# CNI canal/calico: 8472/udp(vxlan) 179/tcp(bgp) 51820/udp(wireguard)
firewall-cmd --permanent --new-zone=rke2 || true
firewall-cmd --permanent --zone=rke2 --add-source=10.0.0.0/8 # 集群内网段整体放行
firewall-cmd --permanent --zone=rke2 --add-port=6443/tcp
firewall-cmd --permanent --zone=rke2 --add-port=9345/tcp
firewall-cmd --permanent --zone=rke2 --add-port=2379-2380/tcp
firewall-cmd --permanent --zone=rke2 --add-port=10250/tcp
firewall-cmd --permanent --zone=rke2 --add-port=8472/udp
firewall-cmd --permanent --zone=rke2 --set-target=ACCEPT
firewall-cmd --reload注意事项:数千节点的端口规划必须"全网段维度"做,不要逐节点临时开端口。常见事故:新机房网段没加进 zone,加入的节点 9345 注册失败、etcd learner 卡住——批量加入时这类问题会成批出现(参见 k8s-issues/ 目录下 master 加入被防火墙拦截的真实案例)。
2.6 Huge Pages 与 irqbalance
大内存节点(如 512GB+,承载数据库/AI 训练)建议预分配 HugePages:
bash
# 2MB 大页 × 65536 = 128GB
echo 'vm.nr_hugepages = 65536' >> /etc/sysctl.d/91-hugepages.conf
sysctl --system
grep HugePages_Total /proc/meminfoirqbalance 用于把网卡/磁盘中断均匀分散到多核,避免所有中断挤在 CPU0 上拖垮高吞吐节点:
bash
dnf install -y irqbalance
systemctl enable irqbalance --now
# 校验网卡中断分布
grep eth0 /proc/interrupts | awk '{print NF-3" 个 CPU 在承接中断"}'最佳实践:对 RDMA/25G+ 网卡节点,irqbalance 策略需与网卡驱动 RSS 队列数对齐;GPU 训练节点常反过来用中断亲和性(irqaffinity)把中断钉在非计算核上。这些是节点池级别的差异化配置,应该体现在第 3 章的"分角色节点池"模板里,而不是全局一刀切。
第 3 章 RKE2 大规模 config.yaml 模板集
RKE2 的全部静态配置集中在 /etc/rancher/rke2/config.yaml。大规模集群的模板设计原则:一个基础模板 + 按角色叠加差异块,模板进 Git,节点上永远只出现渲染产物。
3.1 Server 配置模板(控制平面)
以下为数千节点集群的 server 模板(templates/server-config.yaml.j2),在仓库基础版本(rke2/server-config.yaml)之上叠加大规模所需参数:
yaml
# /etc/rancher/rke2/config.yaml (server)
node-name: {{ inventory_hostname }}
node-ip: {{ node_ip }}
advertise-address: {{ node_ip }}
tls-san:
- {{ node_ip }}
- {{ inventory_hostname }}
- rke2-vip.dc1.example.com # 固定注册地址,所有 agent 经此接入
cluster-cidr: 10.12.0.0/16
service-cidr: 10.13.0.0/16
cluster-domain: cluster.local
# ── etcd 大规模参数 ──
etcd-arg:
- "quota-backend-bytes=8589934592" # 8GB:大规模集群对象多,默认 2GB 会触发 NOSPACE 告警
- "auto-compaction-mode=periodic"
- "auto-compaction-retention=6h" # 周期性压缩,避免历史版本撑爆存储
- "max-request-bytes=10485760" # 10MB,容忍大 ConfigMap/CRD
- "heartbeat-interval=500" # 跨机房/高延迟场景放宽心跳
- "election-timeout=5000"
etcd-snapshot-retention: 5
etcd-snapshot-schedule-cron: "0 */6 * * *"
# ── apiserver 大规模参数 ──
kube-apiserver-arg:
- "max-requests-inflight=3000" # 默认 400,数千节点事件风暴时不够
- "max-mutating-requests-inflight=1000" # 默认 200
- "enable-aggregator-routing=true"
- "event-ttl=1h" # event 是 etcd 压力大户,缩短保留
- "audit-log-maxage=7"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=200"
- "audit-log-mode=batch" # 异步批量,不阻塞请求路径
- "audit-log-batch-buffer-size=10000"
- "audit-log-batch-max-size=1"
- "default-watch-cache-size=1000" # 大集群加大 watch 缓存
- "watch-cache-sizes=nodes#5000,pods#20000,events#1000"
# ── controller-manager 大规模参数 ──
kube-controller-manager-arg:
- "node-monitor-period=5s"
- "node-monitor-grace-period=40s" # 默认 40s;网络抖动频繁的环境可加大防误判
- "pod-eviction-timeout=5m"
- "concurrent-replicaset-syncs=10"
- "concurrent-deployment-syncs=10"
- "concurrent-service-syncs=5"
- "concurrent-endpoint-syncs=10" # 大规模 Service/Endpoint 更新频繁
- "concurrent-gc-syncs=40" # 级联删除并发
- "kube-api-qps=100" # controller-manager 自身对 apiserver 的限速
- "kube-api-burst=200"
- "large-cluster-size-threshold=100" # 超过此节点数进入大集群扩缩容模式
# ── scheduler ──
kube-scheduler-arg:
- "kube-api-qps=100"
- "kube-api-burst=200"
# ── kubelet(server 角色也运行 kubelet) ──
kubelet-arg:
- "max-pods=250"
- "kube-api-qps=50"
- "kube-api-burst=100"
- "serialize-image-pulls=false" # 并行拉镜像,批量启动时关键
- "registry-qps=10"
- "registry-burst=20"
- "image-gc-high-threshold=80"
- "image-gc-low-threshold=70"
- "eviction-hard=memory.available<500Mi,nodefs.available<10%"
- "system-reserved=cpu=500m,memory=1Gi"
- "kube-reserved=cpu=500m,memory=1Gi"
kube-proxy-arg:
- "proxy-mode=ipvs"
- "conntrack-max-per-core=0" # 交由 sysctl 统一控制(见 2.2)
system-default-registry: harbor.dc1.example.com
# 控制平面不跑业务
node-taint:
- "node-role.kubernetes.io/control-plane=true:NoSchedule"
profile: cis # 需要 CIS 加固时开启关键参数的取舍说明:
| 参数 | 默认值 | 大规模建议 | 理由 |
|---|---|---|---|
| quota-backend-bytes | 2GB | 8GB | 对象/事件增长后默认配额很快触发 alarm |
| auto-compaction-retention | (压缩由 K8s 侧 5m 触发) | periodic 6h | 控制 etcd DB 体积,降低备份与恢复耗时 |
| max-requests-inflight | 400 | 3000 | 数千节点 kubelet 心跳 + 控制器风暴 |
| max-mutating-requests-inflight | 200 | 1000 | 大批量 Pod 创建/删除时的写并发 |
| event-ttl | 1h | 1h(显式声明) | 数千节点 event 写入量是 etcd 主要压力之一 |
| concurrent-*-syncs | 1~5 | 5~40 | 控制器吞吐,但要配合 kube-api-qps 防自伤 |
| node-monitor-grace-period | 40s | 40~60s | 过短会在网络抖动时引发大面积 NotReady 误判 |
| kubelet kube-api-qps | 5 | 50 | 默认 5 QPS × 3000 节点都嫌多;但单节点 50 足够且要避免再调低 |
注意事项:
max-requests-inflight不是越大越好——它决定 apiserver 内存上限。每个 inflight 请求消耗数 MB 内存,3000 并发需为 apiserver 预留 16GB+ 内存。正确的兜底是第 4 章的 APF,而不是无限制放大全局并发。
3.2 Agent 配置模板
yaml
# /etc/rancher/rke2/config.yaml (agent)
server: https://rke2-vip.dc1.example.com:9345
token: {{ cluster_token }}
node-name: {{ inventory_hostname }}
node-ip: {{ node_ip }}
kubelet-arg:
- "max-pods=250"
- "kube-api-qps=50"
- "kube-api-burst=100"
- "serialize-image-pulls=false"
- "registry-qps=10"
- "registry-burst=20"
- "eviction-hard=memory.available<500Mi,nodefs.available<10%"
- "system-reserved=cpu=500m,memory=2Gi"
- "kube-reserved=cpu=500m,memory=2Gi"
- "node-labels=nodepool=general,dc=dc1,rack={{ rack_id }}"
kube-proxy-arg:
- "proxy-mode=ipvs"
system-default-registry: harbor.dc1.example.com3.3 分角色节点池配置
数千节点集群必然异构。通过"基础模板 + 角色差异块"组织(Ansible 中即 group_vars):
| 节点池 | 规模建议 | 配置差异要点 |
|---|---|---|
| control-plane | 5(跨 3 机房) | 完整 server 模板;NoSchedule taint;etcd 快照目录挂独立 SSD |
| etcd 专用 | 5 | disable-apiserver: true、disable-controller-manager: true、disable-scheduler: true,仅跑 etcd;NVMe + 独立网卡 |
| infra 节点 | 3~6/机房 | 承载 ingress/monitoring/logging;node-labels=nodepool=infra + taint infra=true:NoSchedule |
| 业务通用节点 | 主体 | 3.2 的 agent 模板 |
| 大内存节点 | 按需 | kubelet system-reserved 加大、HugePages(见 2.6)、memory-manager-policy=Static |
| GPU 节点 | 按需 | node-labels 带 nvidia.com/gpu.product、containerd 配置 nvidia runtime、驱逐阈值放宽 nodefs |
etcd 专用节点 config.yaml 差异块:
yaml
# group_vars/etcd/config.yaml 差异
disable-apiserver: true
disable-controller-manager: true
disable-scheduler: true
disable-cloud-controller: true
etcd-expose-metrics: true
kubelet-arg:
- "max-pods=32" # etcd 节点只跑系统组件
node-taint:
- "node-role.kubernetes.io/etcd=true:NoExecute"最佳实践:
node-labels在 config.yaml 里写"池级标签"(nodepool/dc/rack),"实例级标签"(GPU 型号、磁盘类型)交给 node-feature-discovery 或注册后补丁。kubelet 只能携带node-restriction.kubernetes.io/等白名单前缀以外的普通标签,且kubelet 注册后修改 config.yaml 中的 node-labels 不会更新已有 Node 对象——已加入节点要改标签必须kubectl label或删节点重加。
第 4 章 APF(API Priority and Fairness)详解与配置
4.1 为什么数千节点下必须调优 APF
没有 APF 时,apiserver 只有两道全局闸门:max-requests-inflight(读)与 max-mutating-requests-inflight(写)。这是无差别限流——当 3000 个 kubelet 的心跳、一个失控的控制器循环、和集群管理员抢救故障的 kubectl 同时涌进来时,它们挤同一个队列。结果往往是:
- 失控组件把全局并发打满,etcd 心跳请求被饿死 → leader 切换 → 雪崩;
- 管理员的
kubectl get nodes排在几千个 list 请求后面,排障都排不了。
APF(v1.20 beta,v1.29 GA)把"一个池子"改成"多个带优先级和独立配额的队列":
请求进入
│
▼
FlowSchema 匹配(按 user/verb/resource/namespace 分类)
│
▼
PriorityLevelConfiguration(优先级)
├── exempt ── 不限流(system:masters、leader-election)
├── workload-high ── 大配额(kubelet 心跳、node 状态更新)
├── workload-low ── 中配额(普通控制器)
├── global-default── 小配额(兜底)
└── catch-all ── 极小配额(未匹配的一切)4.2 开启与验证
RKE2 v1.28 默认已启用 APF(上游 APIPriorityAndFairness 自 v1.20 默认开启)。验证:
bash
kubectl get flowschemas
kubectl get prioritylevelconfigurations
# 观测各优先级的排队与拒绝情况
kubectl get --raw /metrics | grep -E 'apiserver_flowcontrol_(rejected|dispatched)_requests_total' | head4.3 实战:为大规模集群定制 FlowSchema
场景:集群里有自研的"批量作业控制器",曾经失控产生过 list 风暴。为它建一个独立的低优先级桶,限制其并发与排队:
yaml
# apf-batch-controller.yaml
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: PriorityLevelConfiguration
metadata:
name: batch-controller-limited
spec:
type: Limited
limited:
nominalConcurrencyShares: 20 # 占 apiserver 总并发份额的一小部分
lendablePercent: 0
limitResponse:
type: Queue # 超限排队而不是直接拒绝
queuing:
queues: 16
handSize: 4
queueLengthLimit: 100
---
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
name: batch-controller-flow
spec:
matchingPrecedence: 500 # 越大优先级越低(系统内置 1~10000)
priorityLevelConfiguration:
name: batch-controller-limited
distinguisherMethod:
type: ByUser
rules:
- subjects:
- kind: ServiceAccount
serviceAccount:
name: batch-controller
namespace: batch-system
resourceRules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["list", "watch"]
namespaces: ["*"]另一个大规模常见需求:保护节点心跳。内置 system-nodes FlowSchema 已把 kubelet 流量导向 workload-high,但 3000+ 节点时建议确认其份额:
bash
kubectl get prioritylevelconfiguration workload-high -o yaml | \
grep -E 'nominalConcurrencyShares|queues|queueLengthLimit'
# 如节点心跳出现 rejected_requests_total 增长,可调大 system-nodes 的 shares:
kubectl edit prioritylevelconfiguration workload-high判断是否"该调了"的指标:
promql
# 某优先级开始拒绝请求 = 需要加份额或治理源头
sum by (priority_level) (rate(apiserver_flowcontrol_rejected_requests_total[5m])) > 0
# 排队时延超过 1s = 份额不足
histogram_quantile(0.99,
sum by (priority_level, le) (rate(apiserver_flowcontrol_request_wait_duration_seconds_bucket[5m]))) > 1注意事项:
exempt优先级(system:masters)完全不排队不限流。大规模集群里绝不要把控制器、CI 系统挂到 exempt 或 system:masters 证书下——一个失控的 exempt 客户端没有任何机制能拦住它。同样注意:调大nominalConcurrencyShares前先看 apiserver 内存水位,并发份额最终都折算成内存。
4.4 面试视角的一句话总结
APF 的本质是把 apiserver 从"一个全局信号量"改造成"按流量身份分类的多队列公平调度器",大规模集群里它是防雪崩的最后一道防线:先靠客户端限速(kube-api-qps)管好自己人,再靠 APF 保证坏人/失控者饿不死关键流量。
第 5 章 镜像与安装源的大规模方案
5.1 问题:数千节点同时安装/拉镜像会发生什么
- 3000 节点同时从
get.rke2.io下载 install.sh 与 tarball → 出口带宽打爆、被源站限速; - 业务高峰数千个新 Pod 同时拉镜像 → Docker Hub rate limit(未认证 100 次/6h/IP)、上游 registry 过载;
- 离线/半离线机房根本到不了公网。
结论:安装源与镜像源必须完全私有化,公网只出现在私有源自己的同步环节。
5.2 私有 artifact 源
参考仓库 rke2/install-server.sh 给出了标准模式——内网 HTTP 源 + 本地 artifact 路径安装:
bash
# 内网 artifact 源上准备的目录结构
/repo/rke2/1.35.6/install.sh
/repo/rke2/rke2-1.35.6/rke2.linux-amd64.tar.gz
/repo/rke2/rke2-1.35.6/rke2-images.linux-amd64.tar.gz # 组件镜像
/repo/rke2/rke2-1.35.6/sha256sum-amd64.txt
# 节点上(摘自 install-server.sh)
curl -fsSL ${INSTALL_URL} -o /tmp/install-rke2.sh
curl -fsSL ${ARTIFACT_URL}/rke2.linux-amd64.tar.gz -o /opt/rke2-artifacts/
curl -fsSL ${ARTIFACT_URL}/sha256sum-amd64.txt -o /opt/rke2-artifacts/
INSTALL_RKE2_VERSION=${RKE2_VERSION} \
INSTALL_RKE2_ARTIFACT_PATH=${ARTIFACT_DIR} \
INSTALL_RKE2_TYPE=server \
/tmp/install-rke2.sh三个关键环境变量:
| 变量 | 作用 |
|---|---|
INSTALL_RKE2_ARTIFACT_PATH | install.sh 从此本地目录取二进制与镜像,不联网下载 |
INSTALL_RKE2_VERSION | 锁定版本,杜绝"新节点装了新版本"的漂移 |
INSTALL_RKE2_MIRROR | 不使用本地 artifact 时,把下载源改到内网镜像(如自建的 GitHub release mirror) |
对镜像预热场景,直接把 images tarball 放到 install.sh 约定的位置即可:
bash
# install.sh 启动时会自动从 /var/lib/rancher/rke2/agent/images/ 导入
mkdir -p /var/lib/rancher/rke2/agent/images/
cp /opt/rke2-artifacts/rke2-images.linux-amd64.tar.gz /var/lib/rancher/rke2/agent/images/5.3 system-default-registry 与 registries.yaml
system-default-registry 让 RKE2 的系统组件镜像(coredns、canal、ingress 等)从私有 Harbor 拉取,而不需要改任何 chart:
yaml
# config.yaml(server 与 agent 都要配)
system-default-registry: harbor.dc1.example.com业务镜像的认证与 mirror 规则由 /etc/rancher/rke2/registries.yaml 管理(v1.28 模板):
yaml
# /etc/rancher/rke2/registries.yaml
mirrors:
docker.io:
endpoint:
- "https://harbor.dc1.example.com" # docker.io 统一走 Harbor 代理缓存
"harbor.dc1.example.com":
endpoint:
- "https://harbor.dc1.example.com"
"registry-k8s-io.dc1.example.com": # registry.k8s.io 的内网镜像
endpoint:
- "https://registry-k8s-io.dc1.example.com"
configs:
"harbor.dc1.example.com":
auth:
username: xxx
password: ${HARBOR_ROBOT_SECRET}
tls:
ca_file: /etc/pki/tls/certs/harbor-ca.crt注意事项:
registries.yaml修改后 containerd 会热加载(v1.5+),但 RKE2 需要重启 rke2-server/rke2-agent 才能完全生效(旧行为),生产上统一按"改完重启服务"执行更安全。另外 Harbor 需为 docker.io / registry.k8s.io / gcr.io / quay.io 各建一个 proxy cache project,并在 Harbor 上确认 robot 账号配额不会被 3000 节点拉爆(Harbor 的 per-IP rate limit、存储 GC 策略都要提前规划)。
5.4 预拉取镜像策略:DaemonSet 预热
新节点加入后的前几分钟是"镜像饥饿期"——所有 DaemonSet(CNI、监控、日志 agent)同时拉镜像。用预热 DaemonSet 在节点就绪后立刻并行拉齐基础镜像:
yaml
# image-prewarm.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: image-prewarm
namespace: kube-system
spec:
selector:
matchLabels: {app: image-prewarm}
updateStrategy:
type: RollingUpdate
rollingUpdate: {maxUnavailable: 20%} # 不要一次全节点重拉
template:
metadata:
labels: {app: image-prewarm}
spec:
priorityClassName: system-node-critical
tolerations: [{operator: Exists}]
initContainers:
- name: pull-base
image: harbor.dc1.example.com/library/pause:3.9
command: ["sh", "-c", |
"crictl pull harbor.dc1.example.com/library/calico-node:v3.27.3;
crictl pull harbor.dc1.example.com/library/node-exporter:v1.7.0;
crictl pull harbor.dc1.example.com/library/fluent-bit:2.2"]
containers:
- name: pause
image: harbor.dc1.example.com/library/pause:3.9
resources:
requests: {cpu: 1m, memory: 8Mi}
limits: {cpu: 10m, memory: 16Mi}配合 3.x 章的 serialize-image-pulls=false 与 registry-qps/burst,新节点从 Ready 到"基础组件全 Running"的耗时可从 5~10 分钟压到 1~2 分钟。
第 6 章 Ansible 批量部署实战
6.1 Playbook 总体结构
rke2-deploy/
├── ansible.cfg
├── inventories/
│ └── dc1/
│ ├── hosts.ini
│ └── group_vars/
│ ├── all/main.yml # 版本、registry、token(Vault)
│ ├── rke2_server/main.yml
│ ├── rke2_etcd/main.yml
│ └── rke2_agent/main.yml
├── roles/
│ ├── os_baseline/ # L2:sysctl/chrony/swap/firewalld
│ │ ├── tasks/main.yml
│ │ ├── handlers/main.yml
│ │ └── templates/90-rke2.conf.j2
│ ├── rke2_common/ # artifact 下载 + registries.yaml
│ ├── rke2_server/ # 首个 server + 后续 server 加入
│ ├── rke2_agent/
│ └── healthcheck/ # L5 校验
└── playbooks/
├── site.yml
├── add-nodes.yml
└── rolling-config.ymlansible.cfg 的大规模优化项:
ini
[defaults]
forks = 100 # 控制面机器够强可到 200;配合 serial 控制每批实际并发
host_key_checking = False
timeout = 30
retry_files_enabled = False
stdout_callback = yaml
gathering = smart
[ssh_connection]
pipelining = True # 显著减少 SSH 往返
ssh_args = -o ControlMaster=auto -o ControlPersist=600s6.2 Inventory 分组
ini
# inventories/dc1/hosts.ini
[rke2_server]
cp-[01:05].dc1.example.com node_ip=192.168.122.19[1:5]
[rke2_etcd]
etcd-[01:05].dc1.example.com
[rke2_agent]
worker-[0001:3000].dc1.example.com
[rke2_agent:vars]
rke2_type=agent
[gpu_pool]
gpu-[01:16].dc1.example.com
[rke2_cluster:children]
rke2_server
rke2_etcd
rke2_agent
gpu_pool6.3 OS 初始化 role(关键 tasks)
yaml
# roles/os_baseline/tasks/main.yml
- name: 关闭 swap 并持久化
ansible.posix.swap:
state: absent
- name: 下发 sysctl 基线
template:
src: 90-rke2.conf.j2
dest: /etc/sysctl.d/90-rke2.conf
mode: "0644"
notify: reload sysctl
- name: 加载内核模块
community.general.modprobe:
name: "{{ item }}"
state: present
loop: [br_netfilter, overlay, nf_conntrack]
- name: 配置 chrony
template:
src: chrony.conf.j2
dest: /etc/chrony.conf
notify: restart chronyd
- name: 配置 firewalld rke2 zone(server/agent 端口集不同)
ansible.posix.firewalld:
port: "{{ item }}"
zone: rke2
permanent: true
immediate: true
state: enabled
loop: "{{ rke2_firewall_ports }}"yaml
# roles/os_baseline/handlers/main.yml
- name: reload sysctl
command: sysctl --system
- name: restart chronyd
service: {name: chronyd, state: restarted}6.4 rke2_server / rke2_agent role(关键 tasks)
yaml
# roles/rke2_agent/tasks/main.yml
- name: 分发 rke2 artifact(幂等:sha256 变了才传)
copy:
src: "{{ item }}"
dest: /opt/rke2-artifacts/
checksum: "sha256:{{ lookup('file', item + '.sha256') }}"
loop: "{{ rke2_artifacts }}"
- name: 获取 install.sh
get_url:
url: "{{ artifact_base_url }}/install.sh"
dest: /tmp/install-rke2.sh
mode: "0755"
- name: 渲染 config.yaml
template:
src: config-agent.yaml.j2
dest: /etc/rancher/rke2/config.yaml
mode: "0600"
notify: restart rke2-agent
- name: 渲染 registries.yaml
template:
src: registries.yaml.j2
dest: /etc/rancher/rke2/registries.yaml
mode: "0600"
notify: restart rke2-agent
- name: 安装 rke2 agent(artifact 已存在时 install.sh 幂等跳过)
command: /tmp/install-rke2.sh
environment:
INSTALL_RKE2_VERSION: "{{ rke2_version }}"
INSTALL_RKE2_ARTIFACT_PATH: /opt/rke2-artifacts
INSTALL_RKE2_TYPE: agent
args:
creates: /usr/local/bin/rke2 # 幂等锚点:已安装则不重跑
- name: 启动 rke2-agent
service: {name: rke2-agent, enabled: true, state: started}6.5 健康检查 handler / role
节点加入后必须验证才算完成。用 Ansible 的 wait + uri 模块在控制面侧校验:
yaml
# roles/healthcheck/tasks/main.yml —— 在首个 server 上 delegate 执行
- name: 等待节点 Ready
command: >
kubectl get node {{ inventory_hostname }}
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
register: node_ready
until: node_ready.stdout == "True"
retries: 30
delay: 10
delegate_to: "{{ groups['rke2_server'][0] }}"
- name: 校验节点基础 Pod(CNI/镜像预热)Running
command: >
kubectl -n kube-system get pods
--field-selector spec.nodeName={{ inventory_hostname }}
-o jsonpath='{range .items[*]}{.status.phase}{" "}{end}'
register: pod_phases
until: "'Pending' not in pod_phases.stdout and 'ContainerCreating' not in pod_phases.stdout"
retries: 20
delay: 15
delegate_to: "{{ groups['rke2_server'][0] }}"
- name: 打准入标签(通过校验才允许承载业务)
command: >
kubectl label node {{ inventory_hostname }}
admission.example.com/verified=true --overwrite
delegate_to: "{{ groups['rke2_server'][0] }}"业务调度侧配合 nodeAffinity 或 nodeSelector admission.example.com/verified=true,实现"未校验不承载"。
6.6 滚动执行与 3000 节点加入的速率控制
这是大规模部署最容易翻车的环节。 节点加入对控制面的冲击来源:
- 每个新节点:kubelet 注册(写 Node 对象)→ CSR 签发 → DaemonSet 控制器为其创建一批 Pod → 镜像拉取 → Endpoint/事件风暴;
- etcd 接收所有写入;apiserver 接收所有 watch/list 重连;
- 镜像仓库承受并发拉取。
经验基线(etcd 在 NVMe、apiserver 3 副本、每节点约 10 个 DaemonSet Pod 的集群):
| 批次大小 | 加入耗时/批 | etcd 写 QPS 峰值 | 风险评级 |
|---|---|---|---|
| 50 节点 | ~5 min | +800 | 低 |
| 200 节点 | ~8 min | +3500 | 中(需观察 event 写入与 etcd fsync 延迟) |
| 500 节点 | ~12 min | +9000,etcd disk sync 偶现 >100ms | 高,不推荐 |
推荐策略:每批 100~200 节点,批间冷却 2~3 分钟,全程盯 etcd 指标。
yaml
# playbooks/add-nodes.yml
- name: 批量加入 agent 节点(每批 150)
hosts: rke2_agent_new
serial: 150
max_fail_percentage: 1 # 一批中失败超 1% 立即停止,防止带病放大
roles:
- os_baseline
- rke2_common
- rke2_agent
- healthcheck
post_tasks:
- name: 批间冷却,等待控制面消化
pause: {minutes: 3}
run_once: true # serial 模式下 run_once 确保每批只等一次伴随监控(加入期间必须盯):
promql
# etcd 磁盘写延迟:p99 > 25ms 说明批次太猛,缩小 serial
histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
# apiserver 拒绝率:APF 开始拒人说明并发超配
sum(rate(apiserver_flowcontrol_rejected_requests_total[5m]))
# etcd leader 切换:>0 立即暂停
changes(etcd_server_leader_changes_seen_total[15m])6.7 幂等性与重试设计要点
- 幂等锚点:安装任务用
creates: /usr/local/bin/rke2;配置文件靠 handler 触发重启;token/证书类操作用stat前置判断。 - 失败恢复:任何一批失败后,
--limit 'rke2_agent_new:&!already_done'或用ansible --start-at-task从断点续跑;绝不用"删掉重跑全量"。 - 重试:网络类 task 配
retries/delay(如 get_url、kubectl wait);安装类任务不要盲目重试——失败后先收集journalctl -u rke2-agent再决定。 - 幂等验证:同一 playbook 对同一主机连跑两次,第二次 changed 应为 0(重启类 handler 除外)。
最佳实践:把"加入风暴"当成容量测试来做——先在 staging 用 200 台压出 etcd 延迟曲线,确定生产的批次上限。etcd 的
wal_fsyncp99 是最灵敏的早期指标,比 CPU/内存更早暴露问题。
第 7 章 节点自动注册与自动扩容
7.1 cloud-init + agent token 自动加入
对不可变基础设施(虚机镜像 + cloud-init),节点第一次开机即自动完成加入,无需 Ansible 触发:
yaml
#cloud-config
write_files:
- path: /etc/rancher/rke2/config.yaml
permissions: "0600"
content: |
server: https://rke2-vip.dc1.example.com:9345
token: ${TOKEN}
node-ip: $(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)
system-default-registry: harbor.dc1.example.com
kubelet-arg:
- "node-labels=nodepool=general,provisioner=cloud-init"
runcmd:
- /opt/bootstrap/install-rke2-agent.sh # 镜像内预置,参考 rke2/install-agent.sh要点:
- token 管理:server token 是集群的"根密钥"之一。生产上用 Vault/云 KMS 下发,或用"短期 bootstrap token + 注册后轮换"模式;cloud-init user-data 可能被 API 读取,不要长期放真 token。
- 固定注册地址:
server:必须指向 VIP/LB(9345),而不是某一台 server,否则该 server 故障时新节点无法加入。 - 失败重试:bootstrap 脚本要循环等待注册成功并上报状态(如打到内部 CMDB),否则会出现"机器在、集群不知道"的孤儿节点。
7.2 Cluster API 与 Rancher 机器供给简介
Cluster API(CAPI) 把节点生命周期抽象成 K8s 资源:Machine(一台机器)、MachineDeployment(一组可滚动更新的机器)、KubeadmConfigTemplate/RKE2ConfigTemplate(引导配置)。扩容即:
bash
kubectl scale machinedeployment workers-pool-a --replicas=1200CAPI 的 RKE2 provider(bootstrap-rke2 / controlplane-rke2)会生成与第 3 章等价的 config.yaml 并注入 cloud-init。适合需要"多集群 + 声明式扩缩容 + 不可变节点(坏了直接换而不是修)"的组织。
Rancher 自动注册则更简单:创建集群后拿到注册命令:
bash
curl -fL https://rancher.example.com/system-agent-install.sh | \
sudo sh -s - --server https://rancher.example.com \
--label 'nodepool=infra' --taint 'infra=true:NoSchedule'system-agent 自动完成 RKE2 安装、注册、标签注入。代价是节点生命周期由 Rancher 控制面接管,需评估 Rancher HA 与网络分区时的行为。
7.3 自动发现与标签注入
三种标签来源的分工:
| 来源 | 机制 | 适合标签 |
|---|---|---|
config.yaml node-labels | kubelet 注册时携带 | 池级稳定标签:nodepool、dc、rack |
| NFD (node-feature-discovery) | 注册后 DaemonSet 探测硬件 | GPU 型号、CPU 特性、NVMe、RDMA |
| 准入控制器/CMDb 同步 | 控制器周期性补标签 | 业务归属、成本中心、维保信息 |
bash
# config.yaml 中注册时注入(注意 kubelet 白名单前缀限制)
kubelet-arg:
- "node-labels=nodepool=gpu,dc=dc1,rack=r12,node.kubernetes.io/instance-type=a100-80g"注意事项:kubelet 自带标签不能覆盖
kubernetes.io/、k8s.io/保留前缀(node-restriction 准入会拒绝),实例类型这类"权威标签"建议由 NFD 或云 controller 注入,kubelet 标签只做调度粗分。
第 8 章 配置管理与漂移控制
8.1 问题:为什么集群建好后还会"烂掉"
大规模集群的配置漂移来源:某次紧急排障手工改了一台节点的 config.yaml;某批新节点用了旧模板;registries.yaml 被某个安装脚本覆盖。漂移的后果在数月后爆发:升级时行为不一致、故障时查不到"它为什么和别人不一样"。
原则:config.yaml 的唯一事实来源(source of truth)是 Git;节点上的文件只能由流水线生成;任何手工修改要么被回滚,要么被回收进 Git。
8.2 GitOps 管理 config.yaml:ArgoCD/Flux + HelmChart CRD
RKE2 会把 /var/lib/rancher/rke2/server/manifests/ 或 HelmChart CRD 定义的内容自动部署。我们可以反过来用 GitOps 工具管理"集群自身的配置文件下发组件"。
架构:
Git 仓库 (cluster-config/)
├── base/ # 全部角色共享配置
├── overlays/
│ ├── server/ # server config.yaml 模板
│ ├── agent-general/
│ └── agent-gpu/
└── 由 ArgoCD ApplicationSet 按集群/角色渲染
│
▼
集群内 DaemonSet (config-renderer)
按节点标签从 ConfigMap 渲染 /etc/rancher/rke2/config.yaml
校验变更 → systemctl restart rke2-agent → 上报状态HelmChart CRD 示例(RKE2 原生方式部署 config-renderer):
yaml
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: node-config-renderer
namespace: kube-system
spec:
repo: https://charts.dc1.example.com
chart: node-config-renderer
version: 1.4.0
targetNamespace: kube-system
valuesContent: |-
configVersion: "2026.07-r3" # 每次 Git 提交递增,节点据此判断是否需要重渲染简化替代:如果暂不想引入集群内渲染组件,用 Ansible 定时(cron/AWX schedule)跑
rolling-config.yml+serial: 5%也能达到收敛目的——GitOps 解决的是"声明与收敛闭环",工具可以替换。
8.3 配置一致性校验
无论是否上 GitOps,都要有一个基线扫描作业,定期回答"现网和 Git 差多少":
bash
#!/bin/bash
# config-drift-scan.sh:对比节点实际 config.yaml 与期望哈希
EXPECTED_SHA=$(sha256sum templates-rendered/agent-config.yaml | awk '{print $1}')
ansible rke2_agent -m shell \
-a "sha256sum /etc/rancher/rke2/config.yaml | awk '{print \$1}'" \
| awk -v exp="$EXPECTED_SHA" '$NF != exp {print "DRIFT:", $0}'K8s 侧的漂移用 ArgoCD 的 drift detection(Application OutOfSync)+ 定期 kubectl diff;集群外配置(sysctl、firewalld、chrony)用 Ansible --check --diff 模式定期空跑:
bash
ansible-playbook playbooks/site.yml --check --diff --tags os_baseline \
-l 'rke2_agent' | tee drift-report-$(date +%F).log8.4 基线扫描与安全合规
大规模集群建议把以下扫描固化为 CI 阶段:
| 扫描项 | 工具 | 频率 |
|---|---|---|
| config.yaml 漂移 | Ansible --check / 自研哈希对比 | 每日 |
| sysctl/防火墙基线 | Ansible --check --diff | 每日 |
| CIS 基准 | kube-bench(RKE2 profile: cis 对应版本) | 每周 |
| 集群资源漂移 | ArgoCD OutOfSync 告警 | 实时 |
| 证书有效期 | rke2 certificate rotate --check / 自研脚本 | 每周 |
| etcd 健康 | etcdctl endpoint status/health 全成员 | 每分钟(监控) |
bash
# etcd 成员健康巡检(在任一 server 上)
/var/lib/rancher/rke2/bin/etcdctl \
--cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt \
--cert=/var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
--key=/var/lib/rancher/rke2/server/tls/etcd/server-client.key \
--endpoints=https://127.0.0.1:2379 \
endpoint status --cluster -w table最佳实践:漂移修复要"自动收敛 + 人工确认"分级——标签、注释类自动修;config.yaml 变更必须走 PR + 灰度批次(serial 5%),因为改错一个内核参数可能导致整批节点 kubelet 起不来。
第 9 章 常见面试题
Q1. 3000 节点批量加入集群,你会怎么控制节奏?依据什么指标决定批次大小?
要点:分层限速——Ansible serial 100~200/批 + 批间冷却 2~3 分钟;镜像预热 DaemonSet 与 serialize-image-pulls=false 缩短单节点饥饿期。批次大小的依据是控制面指标而非拍脑袋:etcd wal_fsync_duration p99(>25ms 缩批)、leader_changes(>0 暂停)、apiserver APF 拒绝率、event 写入 QPS。先在 staging 压测定上限,生产留 50% 余量。还要设 max_fail_percentage 让失败自动熔断。
Q2. max-requests-inflight 调到 3000 之后,为什么还需要 APF?
要点:inflight 是全局无差别信号量,解决不了"重要流量被失控流量饿死"的问题——所有请求共享一个池子,先到先得的反面是关键请求排队。APF 按身份/动词/资源分类到不同优先级队列,各自有并发份额与排队策略,保证 etcd 心跳、leader election、管理员操作不被普通风暴挤死。另外 inflight 直接折算 apiserver 内存,不能无限放大,APF 的 lend/borrow 机制才是精细治理手段。
Q3. 大规模集群中 etcd 必须调哪几个参数?不调会怎样?
要点:quota-backend-bytes(默认 2GB,大规模对象+事件很快触发 NOSPACE alarm,etcd 进入只读);auto-compaction-mode/retention(不压缩则 DB 持续膨胀,备份恢复耗时线性增长,碎片影响读写延迟);高延迟链路调 heartbeat-interval/election-timeout;配合缩短 event-ttl 控制写入源头。配套动作:定期 etcdctl defrag(滚动逐成员做,先 defrag 非 leader)、快照异地备份。
Q4. 为什么生产上推荐 system-default-registry + Harbor,而不是让镜像里写死内网地址?
要点:system-default-registry 只改一处配置即可切换 RKE2 全部系统组件镜像源,chart/manifest 零侵入,升级时不会漏改;业务镜像通过 registries.yaml 的 mirrors 机制做 docker.io → Harbor proxy cache,开发者的镜像名保持不变,避免了"把 YAML 里的镜像地址全部改写成内网地址"带来的不可移植性和环境间差异。同时私有化解决 Docker Hub rate limit 与离线机房问题。
Q5. kubelet 的 serialize-image-pulls 默认值是什么?大规模场景为什么要改?
要点:默认 true,即串行拉镜像——一个节点同时调度 10 个 Pod 时排队一个一个拉,新节点/故障迁移后的启动时间被拉长数倍。大规模下改 false 并行拉取,并用 registry-qps/registry-burst(默认 5/10,建议 10/20)限制对 registry 的冲击,配合 Harbor proxy cache 与镜像预热 DaemonSet,把冷启动时间压到分钟级以内。
Q6. 节点加入集群后修改 config.yaml 里的 node-labels,为什么不生效?正确做法是什么?
要点:node-labels 只在 kubelet 首次注册(创建 Node 对象)时写入;已存在的 Node 对象不会被 kubelet 后续更新标签。正确做法:kubectl label 在线改;或删除 Node 对象(节点需 cordon/drain)让 kubelet 重新注册。生产上池级标签写 config.yaml,实例级标签用 NFD,业务标签用控制器/CMDb 同步——分层管理避免频繁重注册。
Q7. RKE2 离线安装有哪几种方式?各自适用什么场景?
要点:(1) INSTALL_RKE2_ARTIFACT_PATH:把 rke2 二进制 tarball + sha256 预置到本地目录,install.sh 不联网,适合 Ansible 批量分发(可校验哈希、幂等);(2) INSTALL_RKE2_MIRROR:install.sh 从自建 release 镜像下载,适合轻量内网源;(3) 镜像 tarball 放 /var/lib/rancher/rke2/agent/images/ 实现组件镜像离线导入。数千节点场景三种通常组合使用:artifact 走 (1),images 走 (3),再叠加 system-default-registry 指向 Harbor 兜底运行时拉取。
Q8. 数千节点的防火墙上,你如何处理 K8s 动态规则与主机防火墙的关系?
要点:不让 firewalld 托管 K8s 动态规则(规则膨胀、重载慢、与 kube-proxy/CNI 冲突);推荐 firewalld 独立 zone 只管管理面 + 按集群网段维度静态放行 K8s 必需端口(6443/9345/2379-2380/10250/8472 等),NodePort 只对需要的上游网段开放。全网段统一规划、模板化下发,避免新机房网段漏配导致批量加入失败。合规上靠上游 ACL/安全组 + 主机审计补足。
Q9. 如何发现并修复数千节点集群的配置漂移?
要点:发现——Git 为唯一事实来源,用 Ansible --check --diff 每日空跑扫 OS 层基线,哈希对比扫 config.yaml/registries.yaml,ArgoCD OutOfSync 扫集群内资源;修复分级——低风险(标签、注释)自动收敛,高风险(config.yaml、sysctl)必须走 PR 评审 + serial 5% 灰度滚动下发并带健康检查 handler,防止一次错误配置打残整批节点。紧急手工修改要求事后 24h 内回收进 Git,否则下次收敛时被回滚。
Q10. controller-manager 的 concurrent-*-syncs 调大有什么副作用?与 kube-api-qps 是什么关系?
要点:并发 sync 提升控制器吞吐(大规模下 Endpoint/RS/GC 处理不过来会导致 Service 更新延迟、删除残留),但每个 sync worker 都会向 apiserver 发请求——并发调大而不调 kube-api-qps/burst(默认 20/30)会被自身限速卡住,调了 qps 又可能冲击 apiserver。正确姿势:syncs、kube-api-qps、apiserver inflight/APF 三者联动调,并以 apiserver 延迟与 APF 拒绝率作为天花板信号。典型大规模基线:syncs 5~40、controller-manager qps 100/burst 200。
本部分小结:大规模部署的本质是"把每一次变更都变成一次可灰度、可回滚、可观测的发布"。分层流水线(裸机 → OS → RKE2 → 配置 → 校验)解决了"怎么装",模板集与 APF 解决了"控制面怎么扛住",私有源与镜像预热解决了"数千节点同时动",Ansible 速率控制解决了"不把自己打垮",GitOps 与漂移控制解决了"装完以后不变形"。下一部分将进入运维与观测:如何让这个数千节点的系统在 7×24 中可持续运行。