性能调优:节点、调度、网络全链路
一句话定位:集群跑得慢不是某一处的问题,本文带你从内核参数到 etcd 全链路定位瓶颈、压测验证、落地基线配置。
写在前面
做过线上 K8s 运维的同学大概都遇到过这种场景:业务方反馈"Pod 起得慢"、"接口延迟抖动"、"调度卡住",你登上去 top、kubectl top nodes 看一圈,CPU、内存都不高,但问题就是复现。这种情况八成不是单点瓶颈,而是全链路某一段被卡住了------可能是节点内核参数没调、kubelet 限流命中、调度器打分慢、CNI 数据面走过 iptables 慢路径,也可能是 etcd 写入延迟抖动。
性能调优的关键不是"会改参数",而是"会定位瓶颈在哪一层"。这一篇我把生产里真实用过的调优路径整理出来,从节点内核 → kubelet → 调度器 → CNI 数据面 → etcd → 压测验证,一条链走完,并给出可以直接落地的基线配置。
核心问题
集群跑得慢,瓶颈到底在哪?
这个问题没有标准答案,但有一个标准的排查路径:
业务慢
├─ 应用层?(pprof / 火焰图)
├─ Pod 调度慢?(kubectl get events / 调度器日志)
├─ 网络慢?(CNI 数据面 / kube-proxy 模式)
├─ 节点资源紧张?(kubelet 限流 / CPU throttling)
├─ API Server 慢?(apiserver sloc / etcd)
└─ etcd 慢?(fsync 延迟 / 磁盘 IO)
本文聚焦下面四层:节点(内核 + kubelet)、调度器、CNI 数据面、etcd,最后给一套压测验证方法。
一、原理剖析
1.1 节点内核参数:网络栈是第一道瓶颈
K8s 节点上跑着大量短连接(Liveness/Readiness 探针、Sidecar 互调)和长连接(gRPC、数据库),内核网络栈的几个参数默认值在容器密度上去后会瞬间被打满。
#mermaid-svg-Tulpqm2TbReQwCOt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tulpqm2TbReQwCOt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tulpqm2TbReQwCOt .error-icon{fill:#552222;}#mermaid-svg-Tulpqm2TbReQwCOt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tulpqm2TbReQwCOt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tulpqm2TbReQwCOt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tulpqm2TbReQwCOt .marker.cross{stroke:#333333;}#mermaid-svg-Tulpqm2TbReQwCOt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tulpqm2TbReQwCOt p{margin:0;}#mermaid-svg-Tulpqm2TbReQwCOt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tulpqm2TbReQwCOt .cluster-label text{fill:#333;}#mermaid-svg-Tulpqm2TbReQwCOt .cluster-label span{color:#333;}#mermaid-svg-Tulpqm2TbReQwCOt .cluster-label span p{background-color:transparent;}#mermaid-svg-Tulpqm2TbReQwCOt .label text,#mermaid-svg-Tulpqm2TbReQwCOt span{fill:#333;color:#333;}#mermaid-svg-Tulpqm2TbReQwCOt .node rect,#mermaid-svg-Tulpqm2TbReQwCOt .node circle,#mermaid-svg-Tulpqm2TbReQwCOt .node ellipse,#mermaid-svg-Tulpqm2TbReQwCOt .node polygon,#mermaid-svg-Tulpqm2TbReQwCOt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tulpqm2TbReQwCOt .rough-node .label text,#mermaid-svg-Tulpqm2TbReQwCOt .node .label text,#mermaid-svg-Tulpqm2TbReQwCOt .image-shape .label,#mermaid-svg-Tulpqm2TbReQwCOt .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tulpqm2TbReQwCOt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tulpqm2TbReQwCOt .rough-node .label,#mermaid-svg-Tulpqm2TbReQwCOt .node .label,#mermaid-svg-Tulpqm2TbReQwCOt .image-shape .label,#mermaid-svg-Tulpqm2TbReQwCOt .icon-shape .label{text-align:center;}#mermaid-svg-Tulpqm2TbReQwCOt .node.clickable{cursor:pointer;}#mermaid-svg-Tulpqm2TbReQwCOt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tulpqm2TbReQwCOt .arrowheadPath{fill:#333333;}#mermaid-svg-Tulpqm2TbReQwCOt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tulpqm2TbReQwCOt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tulpqm2TbReQwCOt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tulpqm2TbReQwCOt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tulpqm2TbReQwCOt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tulpqm2TbReQwCOt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tulpqm2TbReQwCOt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tulpqm2TbReQwCOt .cluster text{fill:#333;}#mermaid-svg-Tulpqm2TbReQwCOt .cluster span{color:#333;}#mermaid-svg-Tulpqm2TbReQwCOt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tulpqm2TbReQwCOt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tulpqm2TbReQwCOt rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tulpqm2TbReQwCOt .icon-shape,#mermaid-svg-Tulpqm2TbReQwCOt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tulpqm2TbReQwCOt .icon-shape p,#mermaid-svg-Tulpqm2TbReQwCOt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tulpqm2TbReQwCOt .icon-shape .label rect,#mermaid-svg-Tulpqm2TbReQwCOt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tulpqm2TbReQwCOt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tulpqm2TbReQwCOt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tulpqm2TbReQwCOt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端连接
tcp_max_syn_backlog
SYN 队列
somaxconn
Accept 队列
应用 listen
连接关闭
tcp_tw_reuse
TIME_WAIT 复用
新建连接
ip_local_port_range
源端口范围
conntrack 表
nf_conntrack_max
转发到 Pod
关键参数对应的瓶颈现象:
| 参数 | 默认值 | 瓶颈现象 |
|---|---|---|
net.core.somaxconn |
4096(新内核)/128(老内核) | 应用 accept 不够快时溢出,连接被 reset |
net.ipv4.tcp_max_syn_backlog |
1024 | SYN 队列满,新连接被丢 |
net.ipv4.tcp_tw_reuse |
0/2 | TIME_WAIT 堆积,源端口耗尽 |
net.ipv4.ip_local_port_range |
32768-60999 | 节点作为客户端时端口耗尽 |
net.netfilter.nf_conntrack_max |
65536(低)/262144(高) | conntrack 表满,新连接被丢,日志 nf_conntrack: table full |
fs.file-max |
65536~ | 文件描述符耗尽,Too many open files |
net.ipv4.neigh.default.gc_thresh3 |
1024 | ARP 表满,跨节点 Pod 通信偶发失败 |
1.2 kubelet 参数:节点资源分配的"水线"
kubelet 决定了节点上能塞多少 Pod、什么时候驱逐、给系统留多少资源。常见踩坑是默认 max-pods=110 但节点规格大(如 64C256G),Pod 密度上不去;或者没配 system-reserved / kube-reserved,导致节点系统进程和 K8s 组件抢资源,引发雪崩。
节点总容量 = Allocatable + system-reserved + kube-reserved + eviction-hard
│
└─ 这部分是"硬保留"
Pod 用不到,系统稳
pods-per-core 控制单节点 Pod 密度上限(通常设 10-30),--max-pods 是硬上限。eviction-hard 是触发驱逐的硬阈值(如 memory.available<10%),生产必须配,否则节点会 OOM 全员被杀。
1.3 调度器调优:打分阶段是热点
默认调度器在 Filter 阶段会扫描所有节点,在 Score 阶段对所有候选节点打分。当节点数 > 1000 时,调度延迟显著上升。percentageOfNodesToScore 控制调度器扫描的节点比例,默认在 1.26+ 是 50%,大规模集群可以调到 30% 甚至更低,但代价是调度结果次优。
调度器内部还有一个热点:kube-scheduler 的 cache 在节点变更时会触发全量同步,大规模集群下 watch 的资源量大,容易内存抖动。
1.4 CNI 数据面:iptables 模式是大集群杀手
kube-proxy 有两种主流模式:
- iptables:规则数 = O(Services × Pods),大规模集群规则数上十万,iptables 顺序匹配,延迟随规则数线性增长。
- ipvs:基于 hash 表,查找复杂度 O(1),适合大规模 Service 场景。
- eBPF(Cilium):绕过 kube-proxy,直接在 socket 层做 LB,延迟最低,但要求内核 5.4+。
#mermaid-svg-Q5defs3Mug0LM6Za{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Q5defs3Mug0LM6Za .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5defs3Mug0LM6Za .error-icon{fill:#552222;}#mermaid-svg-Q5defs3Mug0LM6Za .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Q5defs3Mug0LM6Za .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Q5defs3Mug0LM6Za .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Q5defs3Mug0LM6Za .marker.cross{stroke:#333333;}#mermaid-svg-Q5defs3Mug0LM6Za svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Q5defs3Mug0LM6Za p{margin:0;}#mermaid-svg-Q5defs3Mug0LM6Za .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Q5defs3Mug0LM6Za .cluster-label text{fill:#333;}#mermaid-svg-Q5defs3Mug0LM6Za .cluster-label span{color:#333;}#mermaid-svg-Q5defs3Mug0LM6Za .cluster-label span p{background-color:transparent;}#mermaid-svg-Q5defs3Mug0LM6Za .label text,#mermaid-svg-Q5defs3Mug0LM6Za span{fill:#333;color:#333;}#mermaid-svg-Q5defs3Mug0LM6Za .node rect,#mermaid-svg-Q5defs3Mug0LM6Za .node circle,#mermaid-svg-Q5defs3Mug0LM6Za .node ellipse,#mermaid-svg-Q5defs3Mug0LM6Za .node polygon,#mermaid-svg-Q5defs3Mug0LM6Za .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q5defs3Mug0LM6Za .rough-node .label text,#mermaid-svg-Q5defs3Mug0LM6Za .node .label text,#mermaid-svg-Q5defs3Mug0LM6Za .image-shape .label,#mermaid-svg-Q5defs3Mug0LM6Za .icon-shape .label{text-anchor:middle;}#mermaid-svg-Q5defs3Mug0LM6Za .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Q5defs3Mug0LM6Za .rough-node .label,#mermaid-svg-Q5defs3Mug0LM6Za .node .label,#mermaid-svg-Q5defs3Mug0LM6Za .image-shape .label,#mermaid-svg-Q5defs3Mug0LM6Za .icon-shape .label{text-align:center;}#mermaid-svg-Q5defs3Mug0LM6Za .node.clickable{cursor:pointer;}#mermaid-svg-Q5defs3Mug0LM6Za .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Q5defs3Mug0LM6Za .arrowheadPath{fill:#333333;}#mermaid-svg-Q5defs3Mug0LM6Za .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Q5defs3Mug0LM6Za .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Q5defs3Mug0LM6Za .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q5defs3Mug0LM6Za .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Q5defs3Mug0LM6Za .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q5defs3Mug0LM6Za .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Q5defs3Mug0LM6Za .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Q5defs3Mug0LM6Za .cluster text{fill:#333;}#mermaid-svg-Q5defs3Mug0LM6Za .cluster span{color:#333;}#mermaid-svg-Q5defs3Mug0LM6Za div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Q5defs3Mug0LM6Za .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Q5defs3Mug0LM6Za rect.text{fill:none;stroke-width:0;}#mermaid-svg-Q5defs3Mug0LM6Za .icon-shape,#mermaid-svg-Q5defs3Mug0LM6Za .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q5defs3Mug0LM6Za .icon-shape p,#mermaid-svg-Q5defs3Mug0LM6Za .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Q5defs3Mug0LM6Za .icon-shape .label rect,#mermaid-svg-Q5defs3Mug0LM6Za .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q5defs3Mug0LM6Za .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Q5defs3Mug0LM6Za .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Q5defs3Mug0LM6Za :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} iptables
ipvs
eBPF
Pod 访问 Service
kube-proxy 模式
顺序匹配规则
延迟随规则数增长
hash 表查找
O1
socket 层短路
不经 iptables
Pod
1.5 etcd:集群的"心脏"
etcd 是 K8s 的唯一持久化存储,所有写操作(创建 Pod、更新 Endpoint、Leader 选举)都走 etcd。etcd 慢,整个集群就慢。关键瓶颈:
- fsync 兂延迟:每次写都要 fsync 到磁盘,磁盘 IO 抖动直接放大成 API 延迟。
- quota-backend-bytes:默认 2GB,满了会触发只读告警,集群不可写。
- snapshot-count:默认 10000 次写触发一次 snapshot,snapshot 期间写性能下降。
生产建议 etcd 用独立 SSD(NVMe 最好),quota-backend-bytes 调到 8GB,snapshot-count 调到 10000-50000 之间。
二、实战操作
2.1 节点内核基线 sysctl 配置
这是生产里用了 3 年的基线,适用于 5.4+ 内核、节点规格 16C64G 起、Pod 密度 50+ 的场景。
bash
# /etc/sysctl.d/99-k8s-node.conf
# 网络栈
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 2
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_max_tw_buckets = 1048576
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_window_scaling = 1
# conntrack(需要 nf_conntrack 模块)
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_buckets = 262144
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
# 文件描述符
fs.file-max = 2097152
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192
fs.nr_open = 1048576
# ARP / 邻居表(大规模集群必调)
net.ipv4.neigh.default.gc_thresh1 = 4096
net.ipv4.neigh.default.gc_thresh2 = 8192
net.ipv4.neigh.default.gc_thresh3 = 16384
# 转发与路由
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
# 内存(避免容器 swap)
vm.swappiness = 0
vm.overcommit_memory = 1
vm.max_map_count = 262144
应用配置:
bash
sysctl --system
# 验证
sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse net.netfilter.nf_conntrack_max
注意 nf_conntrack_buckets 是只读的,需要在模块加载时设置:
bash
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
通过 systemd 持久化(节点重启生效):
bash
cat > /etc/modules-load.d/nf_conntrack.conf <<'EOF'
nf_conntrack
EOF
cat > /etc/modprobe.d/nf_conntrack.conf <<'EOF'
options nf_conntrack hashsize=262144
EOF
2.2 kubelet 推荐参数(生产基线)
以 16C64G 节点为例,适合跑 40-80 个 Pod:
yaml
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
staticPodPath: /etc/kubernetes/manifests
podLogsDir: /var/log/pods
maxPods: 110
podsPerCore: 20
podPidsLimit: 4096
# 驱逐阈值(硬阈值,生产必配)
evictionHard:
memory.available: "500Mi"
nodefs.available: "10%"
nodefs.inodesFree: "5%"
imagefs.available: "15%"
# 软驱逐(给 Pod 优雅退出时间)
evictionSoft:
memory.available: "750Mi"
nodefs.available: "12%"
evictionSoftGracePeriod:
memory.available: "1m30s"
nodefs.available: "2m"
evictionMaxPodGracePeriod: 30
evictionPressureTransitionPeriod: 5m
# 资源保留(节点独占,Pod 用不到)
kubeReserved:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "1Gi"
systemReserved:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "1Gi"
enforceNodeAllocatable:
- pods
- kube-reserved
- system-reserved
# 镜像 GC
imageGCHighThresholdPercent: 80
imageGCLowThresholdPercent: 70
# 证书轮转
rotateCertificates: true
# 拓扑管理(配合 NUMA / CPU Manager)
topologyManagerPolicy: best-effort
kubelet 启动参数(关键几个):
bash
/usr/bin/kubelet \
--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--kubeconfig=/etc/kubernetes/kubelet.conf \
--config=/var/lib/kubelet/config.yaml \
--container-runtime=remote \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
--node-ip=${NODE_IP} \
--max-pods=110 \
--v=2
大节点(64C256G+)专用调整:
yaml
maxPods: 250
podsPerCore: 30
# 大节点务必开 CPU Manager static,绑核提升性能
cpuManagerPolicy: static
reservedSystemCPUs: "0,1" # 系统进程绑到 CPU0/1
2.3 调度器调优
修改 kube-scheduler 的 configmap(静态 Pod 模式):
yaml
# kubectl -n kube-system edit configmap kube-scheduler-config
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
profiles:
- schedulerName: default-scheduler
plugins:
filter:
enabled:
- name: NodeUnschedulable
- name: NodeName
# ... 其他默认插件
score:
disabled:
- name: NodeResourcesFit # 默认 LeastAllocated,大规模换 MostAllocated
enabled:
- name: NodeResourcesFit
weight: 5
config:
nodeResourcesFit:
scoringStrategy:
type: MostAllocated # 让 Pod 集中调度,减少碎片
# 关键参数:扫描节点比例
percentageOfNodesToScore: 50 # 小集群 100,大集群可降到 30
注意:percentageOfNodesToScore 太低会导致调度结果次优,生产建议:
- 节点数 < 100:
100(全扫) - 100-1000:
50 - 1000-5000:
30 -
5000:考虑分片调度器
2.4 CNI 数据面优化
ipvs 模式切换(从 iptables 切到 ipvs):
bash
# 修改 kube-proxy configmap
kubectl -n kube-system edit configmap kube-proxy
# 把 mode 改成 ipvs
mode: "ipvs"
ipvs:
scheduler: "lc" # least-connection,默认 rr 也可以
excludeCIDRs: []
strictARP: true # MetalLB 必需
minSyncPeriod: 0s
syncPeriod: 30s
tcpTimeout: 0s
tcpFinTimeout: 0s
udpTimeout: 0s
滚动重启 kube-proxy:
bash
kubectl -n kube-system rollout restart daemonset kube-proxy
# 验证
kubectl -n kube-system exec kube-proxy-xxxx -- ipvsadm -Ln | head
Cilium eBPF 替换 kube-proxy(彻底绕过 iptables):
bash
helm install cilium cilium/cilium --version 1.15.6 \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=API_SERVER_IP \
--set k8sServicePort=6443 \
--set bgpControlPlane.enabled=false \
--set ipv4.enabled=true \
--set hubble.enabled=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distrib,icmp}" \
--set prometheus.enabled=true
# 验证
kubectl -n kube-system exec ds/cilium -- cilium status --verbose
kubectl -n kube-system exec ds/cilium -- cilium service list
2.5 etcd 调优
修改 etcd 静态 Pod 配置 /etc/kubernetes/manifests/etcd.yaml:
yaml
spec:
containers:
- command:
- etcd
- --advertise-client-urls=https://NODE_IP:2379
- --listen-client-urls=https://127.0.0.1:2379,https://NODE_IP:2379
- --listen-peer-urls=https://NODE_IP:2380
- --initial-advertise-peer-urls=https://NODE_IP:2380
- --data-dir=/var/lib/etcd
- --snapshot-count=10000 # 快照频率
- --quota-backend-bytes=8589934592 # 8GB 配额
- --max-request-bytes=33554432 # 32MB 单请求上限
- --auto-compaction-retention=1 # 1 小时自动压缩
- --auto-compaction-mode=periodic
- --max-txn-ops=10240
# 性能相关
- --heartbeat-interval=250 # ms,默认 100,跨机房调大
- --election-timeout=2500 # ms,默认 1000
- --tick-interval=100
# 后端与 WAL 分离(关键!)
- --wal-dir=/var/lib/etcd/wal # WAL 放 NVMe
# TLS
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --cert-file=/etc/kubernetes/pki/etcd/server.crt
- --key-file=/etc/kubernetes/pki/etcd/server.key
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd-data
- mountPath: /var/lib/etcd/wal
name: etcd-wal # 独立 NVMe 盘
验证 etcd 性能:
bash
# 检查 db 大小
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
endpoint status -w table
# 检查 fsync 延迟(关键指标)
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
check perf --load="s" --prefix="benchmark"
# 持续监控
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
watch / --prefix --rev=0 --keepalive-time=30s
2.6 性能压测方法
kubemark:模拟大规模 Pod 的压测工具,验证控制平面容量。
bash
# 1. 准备物理集群作为控制平面
# 2. 部署 hollow-node(模拟 kubelet/kube-proxy)
cat > kubemark-hollow-node.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: hollow-node-config
namespace: kubemark
data:
hollow-node.yaml: |
apiVersion: v1
kind: Pod
metadata:
name: hollow-node-{{.NodeIndex}}
namespace: kubemark
spec:
containers:
- name: hollow-node
image: registry.k8s.io/kubemark:v1.30.0
command: ["/kubemark"]
args:
- --kubeconfig=/kubeconfig/kubemark.kubeconfig
- --master=https://API_SERVER:6443
- --v=2
resources:
requests:
cpu: 100m
memory: 200Mi
EOF
# 启动 1000 个 hollow node
kubectl create ns kubemark
kubectl apply -f kubemark-hollow-node.yaml
# 用脚本批量生成 Pod
cluster-loader2:更主流的压测工具。
bash
# 下载 kubernetes/perf-tests
git clone https://github.com/kubernetes/perf-tests.git
cd perf-tests/clusterloader2
# 配置文件
cat > config.yaml <<'EOF'
NODES: 1000
CLUSTER_NAME: perf-test
PROMETHEUS_SCRAPE_APISERVER: true
TEST_CONFIG_NODE_MODE: separate
TEST_CONFIG_MAP_NODE_GROUPS:
- name: default
count: 1000
TEST_CONFIG_MAP_POD_GROUPS:
- name: pod-group-1
pods_per_group: 100
group_count: 10
EOF
# 执行压测
go run cmd/clusterloader.go \
--testconfig=config.yaml \
--nodes=1000 \
--provider=local \
--kubeconfig=$KUBECONFIG \
--report-dir=./report
关键指标基线(1000 节点集群,Pod 创建 1 万个):
| 指标 | 目标值 | 说明 |
|---|---|---|
| Pod 启动 P99 延迟 | < 5s | 从创建到 Running |
| Pod 启动 P50 延迟 | < 2s | |
| apiserver 请求 P99 | < 1s | 除 list 外 |
| etcd fsync P99 | < 10ms | |
| 调度延迟 P99 | < 500ms | |
| 60s 内启动 Pod 数 | > 5000 | 触发限流除外 |
三、踩坑与排查
坑 1:节点 TCP 连接大量 TIME_WAIT,业务偶发 EADDRNOTAVAIL
现象 :某服务 Pod 日志报 connect: cannot assign requested address,节点上 ss -s 显示 TIME_WAIT 20 万+。
定位:
bash
# 看 TIME_WAIT 数量
ss -s | grep TIME-WAIT
# 看端口范围
sysctl net.ipv4.ip_local_port_range
# 看源端口是否耗尽
cat /proc/sys/net/ipv4/ip_local_port_range
# 输出 32768 60999,约 2.8 万个端口
20 万 TIME_WAIT 严重堆积,且源端口只有 2.8 万,被占满后新连接无法分配源端口。
原因 :Pod 作为客户端高频访问外部服务,没有长连接复用;tcp_tw_reuse 默认 0,TW 不复用。
解决:
bash
# 节点级
sysctl -w net.ipv4.ip_local_port_range="1024 65535" # 扩大端口范围
sysctl -w net.ipv4.tcp_tw_reuse=2 # 1=本地复用,2=循环复用(5.4+ 内核)
sysctl -w net.ipv4.tcp_max_tw_buckets=1048576 # 上限保护
# 应用侧(根因)
# - 改成连接池 / 长连接(HTTP keep-alive、gRPC)
# - 降低 QPS 或加 retry with backoff
坑 2:节点 conntrack 表满,跨节点 Pod 通信丢包
现象:集群偶发跨节点 Pod 通信超时,节点 dmesg 报:
nf_conntrack: nf_conntrack: table full, dropping packet
定位:
bash
# 看 conntrack 表使用量
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# 比如 262144 / 262144 = 100% 满
# 看具体条目
conntrack -L | wc -l
conntrack -L | awk '{print $5}' | sort | uniq -c | sort -rn | head
发现大量 TIME_WAIT=120s 的条目堆积。
解决:
bash
# 临时
sysctl -w net.netfilter.nf_conntrack_max=1048576
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize # hashsize = max/4
# 缩短 TIME_WAIT 超时(默认 120s 太长)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
# 持久化
echo "options nf_conntrack hashsize=262144" > /etc/modprobe.d/nf_conntrack.conf
坑 3:kubelet 默认驱逐阈值太松,节点 OOM 全员被杀
现象:某节点内存使用 95% 后突然所有 Pod 被 SIGKILL,kubelet 日志:
The node was low on resource: memory. Container xxx was using ...
原因 :默认 eviction-hard 只配了 memory.available<100Mi,阈值太低,节点接近 OOM 才触发驱逐,触发时已经来不及优雅退出,被内核 OOM killer 直接杀。
解决:
yaml
evictionHard:
memory.available: "500Mi" # 剩余 500Mi 就驱逐,给系统留足
nodefs.available: "10%"
imagefs.available: "15%"
evictionSoft:
memory.available: "750Mi"
evictionSoftGracePeriod:
memory.available: "1m30s"
同时配 systemReserved 和 kubeReserved,确保系统进程有保护。
坑 4:ipvs 模式下 Service 偶发 502
现象:切到 ipvs 后,某 Service 偶发 502,后端 Pod 健康但被剔除。
定位 :ipvs 默认使用 rr 调度,健康检查由 kube-proxy 同步,但 --min-sync-period 默认 0,某些版本在 Endpoint 频繁变更时同步延迟。
解决:
yaml
ipvs:
scheduler: "lc"
strictARP: true
syncPeriod: 30s
minSyncPeriod: 1s # 最小同步间隔,避免抖动
同时升级到 kube-proxy 1.30+,该版本修复了多个 ipvs 同步 bug。
坑 5:etcd fsync 延迟抖动,API Server 504
现象 :集群偶发 kubectl get pod 卡 10s+, apiserver 日志报 etcd 超时。
定位:
bash
# etcd 暴露的 Prometheus 指标
# etcd_disk_wal_fsync_duration_seconds
# etcd_disk_backend_commit_duration_seconds
# 查看磁盘 IO
iostat -xz 1
# 注意 %util、await,await > 10ms 就要警惕
发现 etcd 数据盘 await 高达 50ms,fsync P99 200ms+。
原因:etcd 和容器镜像存储共用一块盘,镜像 pull 时 IO 抖动。
解决:etcd 数据 + WAL 分到独立 NVMe 盘,容器镜像存储单独一块。生产环境 etcd 忬盘延迟 P99 必须 < 10ms。
四、最佳实践
节点基线:
- 所有节点统一 sysctl 配置,通过 Ansible / DaemonSet(
node-problem-detector+sysctl-setter)分发 net.core.somaxconn/tcp_max_syn_backlog至少 65535nf_conntrack_max至少 1M,hashsize= max / 4ip_local_port_range扩到1024 65535tcp_tw_reuse=2(5.4+ 内核)fs.file-max至少 2M,容器内ulimit -n同步调大
kubelet:
- 节点 > 16C:
max-pods=110,pods-per-core=20 - 节点 > 64C:
max-pods=250,pods-per-core=30,cpuManagerPolicy=static - 必配
evictionHard+systemReserved+kubeReserved enforceNodeAllocatable包含pods、kube-reserved、system-reserved- 大节点开
reservedSystemCPUs把系统进程绑到固定 CPU
调度器:
- 节点数 < 1000:默认
percentageOfNodesToScore=50即可 - 节点数 > 1000:降到 30,并监控调度延迟
- 大规模集群
NodeResourcesFit用MostAllocated减少碎片 - 关键业务用
PriorityClass+PodTopologySpread保证调度
CNI 数据面:
- 节点数 > 500:必须切 ipvs,不能用 iptables
- 节点数 > 2000 且内核 5.4+:上 Cilium eBPF,彻底绕过 iptables
- ipvs
scheduler=lc,strictARP=true(用 MetalLB 必须) - 监控
sync_proxy_rules_iptables_equivalent_rules_total/sync_proxy_rules_iptables_equivalent_last_duration_seconds
etcd:
- 独立 NVMe SSD,etcd 数据盘与容器镜像盘分离
quota-backend-bytes=8589934592(8GB)snapshot-count=10000auto-compaction-retention=1h+ 定期defragwal-dir独立盘(可选,生产强烈建议)- 监控
etcd_disk_wal_fsync_duration_secondsP99 < 10ms - etcd CPU 至少 2 核,内存 4Gi+
压测验证:
- 新集群上线前必跑 cluster-loader2,基线见 2.6 节
- 大版本升级前(如 1.29 → 1.30)必跑压测对比
- 日常每周跑一次小型压测(创建 1000 Pod),观察回归
- Prometheus 持续采集
apiserver_request_duration_seconds、scheduler_scheduling_algorithm_duration_seconds、etcd_disk_wal_fsync_duration_seconds
五、小结
性能调优不是"调一个参数就快了",而是"每一层都别拖后腿"。这一篇覆盖了节点内核、kubelet、调度器、CNI、etcd 五层的基线配置和踩坑路径,核心思路是:
- 先有基线:节点 sysctl + kubelet 参数 + etcd 配置统一打基线,不要每个集群一份
- 再有监控:Prometheus 持续采集 P99 延迟,阈值告警
- 最后压测:上线前必跑 cluster-loader2,验证基线达成
- 持续优化:每次大版本升级、节点规格变更、业务流量翻倍,都要回归压测
下一篇我们讲资源优化,让集群容量跟着流量走------HPA、VPA、Cluster Autoscaler、KEDA 怎么组合出多级弹性方案。
思考题
percentageOfNodesToScore设成 10% 后,你预期会出现什么调度问题?如何监控?- ipvs 模式下
strictARP=true为什么是 MetalLB 的前置依赖?如果不开会怎样? - etcd 的
snapshot-count调大(如 50000)和调小(如 5000)分别有什么副作用? - 节点
tcp_tw_reuse=2vs=1在实际效果上有什么区别?为什么官方推荐 2 而不是 1?
延伸阅读
- Kubernetes 官方: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/control-plane-flags/
- etcd 调优指南: https://etcd.io/docs/v3.5/tuning/
- Cilium 性能白皮书: https://cilium.io/blog/2021/05/11/cni-benchmark/
- kube-proxy ipvs 模式文档: https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/
- cluster-loader2: https://github.com/kubernetes/perf-tests/tree/master/clusterloader2
- 内核参数权威解释: https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt