Skip to content

第二部分 大规模集群配置与自动化部署

当集群规模从几十个节点走向数千个节点时,"能跑起来"与"能持续稳定地跑下去"之间隔着一条鸿沟。第一部分我们解决了架构设计与容量规划的问题——把控制平面、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 镜像安装                 │
└──────────────────────────────────────────────────────────────┘
        每一层都可独立重跑、独立验证、独立回滚

各层的关键设计点:

  1. L1 裸机供给(provisioning):通过 PXE + Kickstart/Preseed/Autoinstall 或专用供给工具安装 OS。产出是一台能 SSH、带正确主机名与网络配置的机器。
  2. L2 OS 初始化:把内核参数、时间同步、swap、防火墙、镜像源等收敛为一份可幂等执行的基线(Ansible role 或 cloud-init)。这一层必须对"重跑"安全。
  3. L3 RKE2 安装:使用 RKE2 官方 install.sh,配合离线 artifact(INSTALL_RKE2_ARTIFACT_PATH),避免数千节点同时去公网拉包。
  4. L4 配置下发/etc/rancher/rke2/config.yamlregistries.yaml 一律由模板渲染,模板进 Git,禁止人工 SSH 上去改。
  5. L5 校验:节点加入后自动跑健康检查(kubelet 端口、etcd 成员状态、CNI Pod、DNS 解析、实际调度一个冒烟 Pod),通过才打"可承载业务"标签。

1.3 工具选型对比

工具定位优势局限适用场景
AnsibleL2/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 swap

RKE2/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/meminfo

irqbalance 用于把网卡/磁盘中断均匀分散到多核,避免所有中断挤在 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-bytes2GB8GB对象/事件增长后默认配额很快触发 alarm
auto-compaction-retention(压缩由 K8s 侧 5m 触发)periodic 6h控制 etcd DB 体积,降低备份与恢复耗时
max-requests-inflight4003000数千节点 kubelet 心跳 + 控制器风暴
max-mutating-requests-inflight2001000大批量 Pod 创建/删除时的写并发
event-ttl1h1h(显式声明)数千节点 event 写入量是 etcd 主要压力之一
concurrent-*-syncs1~55~40控制器吞吐,但要配合 kube-api-qps 防自伤
node-monitor-grace-period40s40~60s过短会在网络抖动时引发大面积 NotReady 误判
kubelet kube-api-qps550默认 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.com

3.3 分角色节点池配置

数千节点集群必然异构。通过"基础模板 + 角色差异块"组织(Ansible 中即 group_vars):

节点池规模建议配置差异要点
control-plane5(跨 3 机房)完整 server 模板;NoSchedule taint;etcd 快照目录挂独立 SSD
etcd 专用5disable-apiserver: truedisable-controller-manager: truedisable-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' | head

4.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_PATHinstall.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=falseregistry-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.yml

ansible.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=600s

6.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_pool

6.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 节点加入的速率控制

这是大规模部署最容易翻车的环节。 节点加入对控制面的冲击来源:

  1. 每个新节点:kubelet 注册(写 Node 对象)→ CSR 签发 → DaemonSet 控制器为其创建一批 Pod → 镜像拉取 → Endpoint/事件风暴;
  2. etcd 接收所有写入;apiserver 接收所有 watch/list 重连;
  3. 镜像仓库承受并发拉取。

经验基线(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_fsync p99 是最灵敏的早期指标,比 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=1200

CAPI 的 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-labelskubelet 注册时携带池级稳定标签: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).log

8.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 中可持续运行。