【K8S 运维实战】23-性能调优全链路

性能调优:节点、调度、网络全链路

一句话定位:集群跑得慢不是某一处的问题,本文带你从内核参数到 etcd 全链路定位瓶颈、压测验证、落地基线配置。

写在前面

做过线上 K8s 运维的同学大概都遇到过这种场景:业务方反馈"Pod 起得慢"、"接口延迟抖动"、"调度卡住",你登上去 topkubectl 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"

同时配 systemReservedkubeReserved,确保系统进程有保护。

坑 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 至少 65535
  • nf_conntrack_max 至少 1M,hashsize = max / 4
  • ip_local_port_range 扩到 1024 65535
  • tcp_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 包含 podskube-reservedsystem-reserved
  • 大节点开 reservedSystemCPUs 把系统进程绑到固定 CPU

调度器:

  • 节点数 < 1000:默认 percentageOfNodesToScore=50 即可
  • 节点数 > 1000:降到 30,并监控调度延迟
  • 大规模集群 NodeResourcesFitMostAllocated 减少碎片
  • 关键业务用 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=10000
  • auto-compaction-retention=1h + 定期 defrag
  • wal-dir 独立盘(可选,生产强烈建议)
  • 监控 etcd_disk_wal_fsync_duration_seconds P99 < 10ms
  • etcd CPU 至少 2 核,内存 4Gi+

压测验证:

  • 新集群上线前必跑 cluster-loader2,基线见 2.6 节
  • 大版本升级前(如 1.29 → 1.30)必跑压测对比
  • 日常每周跑一次小型压测(创建 1000 Pod),观察回归
  • Prometheus 持续采集 apiserver_request_duration_secondsscheduler_scheduling_algorithm_duration_secondsetcd_disk_wal_fsync_duration_seconds

五、小结

性能调优不是"调一个参数就快了",而是"每一层都别拖后腿"。这一篇覆盖了节点内核、kubelet、调度器、CNI、etcd 五层的基线配置和踩坑路径,核心思路是:

  1. 先有基线:节点 sysctl + kubelet 参数 + etcd 配置统一打基线,不要每个集群一份
  2. 再有监控:Prometheus 持续采集 P99 延迟,阈值告警
  3. 最后压测:上线前必跑 cluster-loader2,验证基线达成
  4. 持续优化:每次大版本升级、节点规格变更、业务流量翻倍,都要回归压测

下一篇我们讲资源优化,让集群容量跟着流量走------HPA、VPA、Cluster Autoscaler、KEDA 怎么组合出多级弹性方案。

思考题

  1. percentageOfNodesToScore 设成 10% 后,你预期会出现什么调度问题?如何监控?
  2. ipvs 模式下 strictARP=true 为什么是 MetalLB 的前置依赖?如果不开会怎样?
  3. etcd 的 snapshot-count 调大(如 50000)和调小(如 5000)分别有什么副作用?
  4. 节点 tcp_tw_reuse=2 vs =1 在实际效果上有什么区别?为什么官方推荐 2 而不是 1?

延伸阅读

相关推荐
重生的黑客16 小时前
Linux 进程程序替换与自定义 Shell:从 exec 函数族到命令行解释器
linux·运维·服务器·shell
北极糊的狐16 小时前
阿里云服务器-命令2-Linux 系统实时资源监视器 top 命令详解(进程级实时资源监控)
linux·运维·服务器
三言老师17 小时前
clear与history历史命令管理实操
linux·运维·服务器·网络·centos
味悲17 小时前
Linux 环境下 DNS 服务器搭建
linux·运维·服务器
扶疏52517 小时前
Prometheus+Prometheus Adapter+metrics-server+Ingress实现k8s水平自动扩缩容(HPA)
容器·kubernetes·prometheus
greenbbLV17 小时前
中小公司积分商城选型:SaaS与私有化优劣对比分析
大数据·运维·人工智能
张忠琳18 小时前
【NPU】Ascend Docker Runtime v26.0.1 系统级架构分析
云原生·容器·kubernetes·npu·docker-runtime
这是谁的博客?18 小时前
【中阶·融合】如何隔离多租户 AI 推理平台的 GPU 资源:从 Namespace 到 MIG/Kata 的五层纵深防御
人工智能·ai·云原生·kubernetes·gpu·多租户·ai安全
feasibility.18 小时前
wsl安装Ubuntu方法(含网络不稳定处理)
linux·运维·windows·ubuntu
汽车网络安全爱好者18 小时前
Public Key Infrastructure(二)— 深入理解 X.509 证书:从 RFC 5280 到 OpenSSL 实践
运维·服务器·算法·网络安全·汽车·密码学·可信计算技术