说实话,一个 Kubernetes 集群被说"性能不行"的时候,很少是单点问题。我维护过不少集群,用户抱怨的往往是"接口变慢了""Pod 老重启""节点 CPU 被打满",但一层层追下去,原因可能出现在内核参数上、调度策略上、甚至在监控数据本身就没采对。这正是《Kubernetes 集群性能优化:从调优到监控》这个主题有意思的地方:调优不是改几个参数,监控也不是为了看几张图,两者本质上是一件事——把集群里每一层的行为量化清楚,再针对瓶颈做精准调整。这篇内容适合刚把集群跑起来、准备认真做生产化的人,也适合已经被线上问题折腾过几次、想把调优经验系统化的运维和开发同学。
1. 先想清楚:调优和监控为什么永远分不开
1.1 性能优化不是玄学,是一场有输入、有反馈的实验
我第一次做 Kubernetes 集群性能优化的时候,犯过一个典型错误:到处找"最佳实践",照着网上的文章把内核参数改了一遍,把 kubelet 配置调了一遍,重启完发现现象有缓解,但说不清到底是哪项改动起了作用,甚至不确定是不是只有心理作用。后来我才意识到,性能优化本质上是在做对照组实验。任何改动之前,必须有一个能反映问题的量化指标;改动之后,还要靠同一套指标来做验证。如果没有监控体系提前就位,优化动作就失去了参照物,出了问题也分不清是本次改动引入的还是本来就存在的。
这也是我把监控放在调优前面讲的原因。集群里任何一个"卡顿"现象,落到指标上都应该是可解释的:CPU 用量高不高、是不是被 CFS 限流了、内存是否接近驱逐线、etcd 的 fsync 耗时是否飙升、API Server 的请求延迟有没有毛刺。把这些指标先采起来,调优才有底气动手。
1.2 从用户抱怨到根因,其实是一条指标链路
我曾经帮一个业务团队排查严重超时问题。业务方描述得很模糊:一到下午高峰期,部分请求会卡好几秒。看 Pod CPU 用量,平均值不高,看节点负载也不像被打满。后来用了监控里的容器 CPU throttle 指标,才发现部分 Pod 的 CPU Limit 设得偏低,流量稍大就周期性被限流,外部表现就是请求排队和延迟毛刺。这种问题如果不看底层指标,单靠应用日志很难定位,因为应用本身没有报错,只是慢了。
所以你在构建自己的集群优化思路时,也要建立类似的链路思维:终端用户体验 -> 服务 RED 指标 -> Pod 资源指标 -> 节点系统指标 -> 控制面组件指标。每一层都有各自的角色,越往下越接近物理真相,越往上越接近业务感受。调优时通常从下往上排查,从内核和节点开始,确认底层没有瓶颈,再逐步看工作负载层。
2. 节点层调优:先把地基夯实,再谈上层建筑
2.1 内核参数:不要抄一堆,只改与集群强相关的几项
节点层调优最常见的错误是过度。Linux 内核参数有上千个,真正跟 Kubernetes 运行相关的其实集中在文件句柄、连接跟踪、本地端口范围、进程数这几个维度。我一般在所有集群节点上统一落一份/etc/sysctl.d/99-k8s.conf,内容不多,但每一项都有明确用途:
# /etc/sysctl.d/99-k8s.conf vm.swappiness = 10 vm.overcommit_memory = 1 fs.file-max = 20971520 fs.inotify.max_user_instances = 8192 fs.inotify.max_user_watches = 524288 net.core.somaxconn = 32768 net.core.netdev_max_backlog = 16384 net.ipv4.tcp_max_syn_backlog = 8096 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 1024 65535 net.netfilter.nf_conntrack_max = 1048576逐项解释一下:
vm.swappiness = 10:默认 60 在服务器上太高,会让内核倾向于把不常用的内存页换到 swap,而 K8s 节点上跑的大多是常驻型服务,swap 换页一旦发生,延迟很难看。调到 10 只是降低换页倾向,不会完全禁用 swap。vm.overcommit_memory = 1:让内核总是允许超过 CommitLimit 的内存申请。这里要注意,这个参数配合 K8s 的 cgroup 内存隔离是安全的,因为容器内存超限会在 cgroup 层被杀掉,而不是让整个节点直接 OOM,但前提是你得把 kubelet 的驱逐阈值设置好,见下一节。fs.inotify.max_user_watches、max_user_instances:这两项跟 ConfigMap/Secret 挂载更新有关。Kubelet 通过 inotify 监听挂载文件变化,集群里 Pod 数量多了、ConfigMap 多了,很容易触到默认的上限。之前遇到过一次现象:更新 ConfigMap 后部分 Pod 没有感知到变化,排查半天,最后发现是节点的 inotify watch 耗尽,kubelet 根本没收到事件。- 后面几个
net.*参数主要是应对高并发场景。Kubernetes 集群里 Service、Pod IP 变化很频繁,节点上 NAT 和连接越多,conntrack 表越容易被占满。nf_conntrack_max调大是常见做法,但调大以后还要配合tcp_tw_reuse这类参数,否则 TIME_WAIT 连接堆积仍然会导致端口耗尽。
改完之后记得sysctl --system生效,并用sysctl -a | grep <参数名>验证。注意,真正生产环境我一般还会配合节点健康检查脚本,防止有人手动改了系统参数后重启丢失。
提示:每次修改内核参数后,建议顺带看一下
dmesg -T | tail,有些参数与云厂商的虚拟化层冲突时会留下显式报错,能省掉后面大量的怀疑时间。
2.2 Kubelet 预留与驱逐阈值:别让节点被 Pod 打趴
Kubernetes 节点上跑的进程不止是 Pod,还有 kubelet、容器运行时、systemd 等系统服务。如果你不提前给它们预留资源,调度器会认为节点上的 CPU、内存全部可以分配给 Pod,一旦 Pod 用量上去了,系统组件先饿死,节点状态开始异常,Pod 被驱逐,最终整个节点进入恶性循环。
Kubelet 的--system-reserved和--kube-reserved就是干这个的。我在生产集群里一般会按节点规格分档设置,比如 16C32G 的工作节点,常见的配置是这样:
apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration evictionHard: memory.available: "500Mi" nodefs.available: "10%" imagefs.available: "15%" kubeReserved: cpu: "500m" memory: "1Gi" systemReserved: cpu: "500m" memory: "1Gi"这里的逻辑是:kubelet 向调度器汇报的可分配资源,等于节点总资源减去 kubeReserved 和 systemReserved 的预留量。预留得太多会浪费可调度资源,预留得太少则保护不了系统稳定性。我的经验是先从节点规格的 5%-8% 起步,跑一阵记录真实用量再微调,而不是拍脑袋定死。
驱逐阈值同样关键。当节点内存逼近memory.available,kubelet 会开始驱逐优先级最低的 Pod。evictionHard是硬性触发线,我这边还会配evictionSoft和evictionSoftGracePeriod,给系统留出温和处理的缓冲时间。特别提醒:这里的nodefs和imagefs分别对应 kubelet 的工作目录和容器镜像存储目录,如果你用 containerd,一般imagefs和nodefs在同一块磁盘,这块磁盘满了会有各种诡异问题,比如镜像拉不下来、Pod 无法启动,而这些现象在监控里往往看不出 CPU 或内存异常。
2.3 CPU Manager 和拓扑感知:要不要上,取决于你的业务
Kubernetes 默认的 CPU 分配是允许 Pod 在节点上所有可用的 CPU 核心间自由调度的,这在多数业务场景下没有问题。但对于延迟敏感型应用,比如网络转发组件、实时数据处理,上下文切换和 CPU 缓存命中率会带来明显抖动。此时可以给 kubelet 开启CPUManagerPolicy: static,让满足条件的 Guaranteed QoS Pod 独占一组物理核心。条件包括:Pod 中的所有容器都设置了 CPU request 等于 CPU limit,并且 CPU 请求值是整数。
开启后有两点要提醒。第一,static 策略只对整数核的 Guaranteed Pod 生效,小数核的 Pod 仍然走原来的共享池,所以你要提前规划哪些工作负载需要独占,哪些可以共享。第二,如果你的机器是 NUMA 架构,单开 CPU Manager 还不够,最好配合 Topology Manager 的single-numa-node策略,让 Pod 的 CPU、内存、设备都尽量落在同一个 NUMA 节点内。我实测过,某些对内存带宽敏感的任务,开启后延迟能下降 10%-20%,但代价是调度约束变强,碎片化会加剧,小规格节点上可能反而出现调度不下的情况。
不要为了"高级特性"而上这些能力。我见过不少集群开着 static CPU Manager,但业务全是普通的 Web API,效果不明显,还平白增加了排障复杂度。CPU Manager 这类特性的正确姿势是:先有明确的性能指标波动,再考虑针对性引入。
3. 控制面与网络层:瓶颈常常藏在看不见的地方
3.1 etcd 是整个控制面的命门
集群控制面的很多"慢",最后都能追到 etcd。你创建一个 Deployment、更新一次 Service,所有 API 对象的读写最终落到 etcd。如果 etcd 慢了,API Server 的请求就会排队,然后 kubectl 命令变慢、控制器调谐变慢、节点心跳上报变慢,最终表现成集群"整体迟钝"。
首先要保住磁盘性能。etcd 对 fsync 延迟极其敏感,官方建议 SSD 或 NVMe,而且不要跟容器镜像层、日志文件共享同一块高负载磁盘。我见过一个集群,etcd 和 containerd 的数据目录都在同一块云盘上,镜像拉取一频繁,etcd 的写入延迟直接飙到几百毫秒,整个控制面跟着抖动。监控里重点关注etcd_disk_wal_fsync_duration_seconds的 99 分位,这个值如果长期大于 10ms,就该排查磁盘或 IO 抢占问题了。
其次是 etcd 自身的维护。Kubernetes 默认开启了历史版本压缩,但压缩只是回收历史版本占用的空间,底层存储引擎在实际删除大量数据后会产生碎片,空间不会自动归还给文件系统。所以需要定期执行etcdctl defrag,释放碎片空间。操作 defrag 时要小心,它会造成一次短暂的阻塞,建议在业务低峰期执行,并且优先逐个节点做,不要所有 etcd 成员同时操作。另外,etcd 的--quota-backend-size默认 2GB,超限后 etcd 会进入只读保护模式,这个指标etcd_server_quota_backend_bytes长期接近上限,就是该考虑清理历史数据和压缩的明确信号。
3.2 API Server:并发、缓存和事件流
API Server 是控制面的入口,所有的kubectl、控制器、调度器、kubelet 都在跟它通信。默认的--max-requests-inflight=400、--max-mutating-requests-inflight=200对小集群够用,但节点数和 Pod 数上涨以后,大量 list/watch 请求会把它拖住。把这两个值调高到 1000/500 通常是安全的,前提是 apiserver 所在节点的 CPU 和内存有富余,否则只是把瓶颈从并发队列转移到了处理能力。
在调这个参数之前,我建议大家先看一下监控里apiserver_request_duration_seconds的分位数,尤其是apiserver_current_inflight_requests是否经常打满。如果打满,光调参数是治标不治本,往往是有哪个客户端写了个低效的 list 全量请求,或者某个控制器在频繁刷全量对象。从调优角度,可以先从源头排查过度热心的组件,再决定是否放大并发额度。
另一个容易被忽略的点是事件。Event 对象在 etcd 里的写入频率非常高,高变更率的集群里 Event 甚至会占据 etcd 大量空间。我习惯把 Event 单独拆出来存,比如用events.k8s.io/v1配合独立的事件存储后端,或者至少给 Event 设置较短的 TTL,防止它成为 etcd 容量瓶颈。
3.3 kube-proxy 模式与长连接优化
网络路径上的性能损耗经常被低估。默认的 kube-proxy 使用 iptables 做 Service 流量转发,每一条 Service 规则在 iptables 里都是一长串链。当集群里 Service、Endpoints 数量达到几千条时,新连接匹配规则的耗时明显上涨,规则变更时整表刷新还会导致瞬时丢包或延迟。
遇到这个规模,最直接的调优就是把 kube-proxy 切到 IPVS 模式。IPVS 底层用哈希表,规则数量变大后性能不会像 iptables 那样线性劣化。kube-proxy 的 mode 是启动参数,改完必须重建 kube-proxy DaemonSet。切换前建议先在测试集群验证一遍,确认你的 CNI 插件跟 IPVS 兼容,大多数主流 CNI 都支持,但有些自定义网络策略依赖 iptables 的标记链,切换后需要重新检查 NetworkPolicy 是否恢复正常。
节点上还有一个容易被忽视的隐性杀手:conntrack 表溢出。如果集群里短连接特别多,conntrack 表被占满,新连接会直接被丢弃,外部表现是"过一会儿又通一下,然后又开始超时"。除了前面说的调大nf_conntrack_max,还可以通过 Service 的externalTrafficPolicy: Local减少 DNAT 带来的连接跟踪开销。当然,Local 模式会把流量只转发到当前节点上的 Pod,绕开了一层转发,代价是负载不均的概率变大,要配合反亲和性让每个节点尽量都有副本。
4. 工作负载资源模型:把 Pod 的"胃口"告诉调度器
4.1 Request、Limit 重新审视:别再 average 一把梭
Pod 的资源请求值是调度器的核心依据,但很多团队把 request 和 limit 设得极其随意——所有容器一律 request 100m、limit 500m。这样带来的问题是:调度器以为节点还很空,疯狂把 Pod 往上堆,实际运行时内存或 CPU 却远超 request,节点很快被打到驱逐阈值;而 Limit 又可能偏低,引发前面提到的 CPU 限流。
正确做法是把 request 当作"正常工作负载的预测值",limit 当作"容忍突发的最大值"。要得到这两个值,不能靠猜,必须回到监控数据。用 Prometheus 查一下工作负载最近一两周的 CPU 和内存用量,取 P95 或 P99 作为 request 的依据,再留出 30%-50% 的余量作为 limit。比如一个 Java 服务稳定状态下内存用量在工作集 1.2Gi 左右,那么 request 给 1Gi 到 1.5Gi、limit 给 2Gi 是合理区间。
这里还有个 QoS 的概念值得强调。Pod 的 request 和 limit 相等时属于 Guaranteed,不等于时属于 Burstable,都没有设置则算 BestEffort。节点 Memory 压力触发驱逐时,最先被驱逐的是 BestEffort,然后是 Burstable,最后才是 Guaranteed。关键业务尽量做到 Guaranteed 或至少把 request 设置准确,这样节点资源紧张时它不会首当其冲被清理。
4.2 调度策略:让 Pod 长在合适的位置
请求值和 Limit 解决的是"能不能塞下"的问题,调度策略解决的是"塞在哪里更合理"的问题。最常用的是把 CPU 密集型服务跟内存型服务混部,或者通过反亲和性让同服务的多个副本分散到不同节点,避免一台节点挂了服务整体受影响。
以常见的无状态 API 服务为例,我会在 Deployment 里加三段约束:
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - api-gateway topologyKey: kubernetes.io/hostname topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule反亲和性负责让同一应用的 Pod 尽量不在同一台节点,拓扑分布约束负责让副本尽量均匀分散到不同可用区。maxSkew: 1的意思是可用区间副本数最多差 1,适合对容灾有要求的场景。代价是调度成功率会下降,如果集群节点数太少或副本数太多,可能部分 Pod 永远 Pending。建议先用preferredDuringSchedulingIgnoredDuringExecution这类软约束跑一段时间,观察分布效果,再决定要不要换成硬约束。
4.3 HPA 的正确姿势:指标没选对,扩缩容就失真
很多团队搭了 HPA,但扩缩容反应很怪,要么流量涨了副本半天起不来,要么流量一波动副本抖动得厉害。这通常不是 HPA 本身的问题,而是指标和配置的问题。
Kubernetes 1.23 之后推荐使用autoscaling/v2版本,它支持多指标和自定义指标。我的基本配置长这样:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api-gateway-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-gateway minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: 500 behavior: scaleDown: stabilizationWindowSeconds: 300同一时间配置 CPU 利用率和每秒请求数两个指标时,HPA 会取两者计算结果的较大值,避免单一指标失真。比如 CPU 因为限流一直上不去,但请求量已经翻倍,靠 CPU 指标不会扩,靠 QPS 指标就能兜底。
扩缩容抖动问题通过behavior里的stabilizationWindowSeconds解决。scaleDown 的稳定窗口建议设 5-10 分钟,防止流量短时下跌就立刻缩容,然后又因流量恢复而扩容,形成来回震荡。缩容策略也应该保守一些,比如每次只缩 20% 的副本,给新扩出的 Pod 留足启动和预热时间。我见过不少一扩容就有大量 502 的情况,那往往不是 HPA 的问题,而是服务启动后需要较长预热时间,扩容幅度又太激进,导致所有新 Pod 同时处于未就绪状态。
补充一点,资源指标这块依赖 metrics-server,它是只提供资源用量的轻量组件;如果要基于自定义业务指标做伸缩,比如消息积压数、队列深度,就需要 kube-prometheus 体系里的 prometheus-adapter。在瓶颈排查时,先确认kubectl top node和kubectl top pod数据正常,再排查自定义指标链路,这样能快速缩小问题范围。
5. 监控体系搭建:用指标把集群"照"清楚
5.1 组件清单:哪些该装、各自负责什么
搭建 Kubernetes 监控,我见过两种典型偏差:一种是只部署一套 Prometheus 就觉得自己"有监控了",实际上连 Pod 级别的指标都没有;另一种是恨不得把能装的 Exporter 全部装上,告警每天轰炸,真正出问题时反而没人看。
合理的组件分工应该是这样的:
| 组件 | 负责内容 | 典型指标示例 |
|---|---|---|
| metrics-server | 资源指标汇总,供 kubectl top/HPA 使用 | /metrics/resource |
| kube-state-metrics | 集群对象状态转换 | 副本数、Pod 状态、PVC 用量 |
| node-exporter | 节点物理指标 | CPU、内存、磁盘、网络、文件系统 |
| 内置 cAdvisor | 容器资源使用和隔离指标 | container_cpu_usage_seconds_total |
| kubelet 自带 metrics | kubelet 自身状态 | 卷操作、Pod 生命周期 |
| 控制面组件 metrics | API Server、etcd、调度器、控制器 | etcd_disk_wal_fsync_duration_seconds |
很多从零开始配监控的人会混淆 metrics-server 和 kube-state-metrics。前者只采集节点和 Pod 的当前资源用量,数据存在内存里,不落库,专门为kubectl top和 HPA 提供 API;后者则是把 Deployment、ReplicaSet、Pod、PVC 这些对象的状态暴露成指标,供告警和趋势查询使用。两者不是替代关系,是互补关系。
生产环境我建议直接用 kube-prometheus-stack 这套 Helm Chart,它把 Prometheus、Alertmanager、Grafana、各种规则和 dashboard 打包在一起,装完就有控制面的指标和告警。自己手动拼 Prometheus 不是不行,但后面升级规则、维护告警会比较痛苦。如果集群规模大、指标量多,还可以给 Prometheus 加 Thanos 做长期存储和全局查询,不过那是另一个话题了。
5.2 真正值得盯的核心指标:别被几百个指标淹没
Prometheus 里关于 Kubernetes 的指标有上千个,但日常优化和排查真正值得重点盯的不会超过几十个。我一直跟团队强调三层盯法:
业务层看 RED:Rate(请求速率)、Errors(错误数)、Duration(延迟)。这一层是离用户最近的信号,从 Service Mesh 或应用的 Prometheus client 里拿。业务指标异常时,才往下追资源层。
资源层看容器 CPU 和内存的用量、限制、节流。CPU 限流要看container_cpu_cfs_throttled_periods_total这个指标,只看container_cpu_usage_seconds_total会被 limit 掩盖真实瓶颈。内存要看container_memory_working_set_bytes而非container_memory_usage_bytes,后者包含文件缓存,不能直接反映容器真实内存压力。
系统层看节点 CPU 的 steal、IO 等待、磁盘使用率和 inode、网络丢包。云服务器上node_cpu_seconds_total{mode="steal"}如果长期占比超过 5%,说明宿主机本身存在资源争抢,这是云厂商超卖的信号,无论你在集群内部怎么调优都很难彻底解决,只能迁移实例或者换规格。
另外,控制面组件自身的指标在高负载排查中优先级最高。我会在 Grafana 里固定放几个面板:API Server 请求延迟分位数、etcd wal fsync 延迟、etcd leader 变化次数、调度器调度失败率。控制面任何一个指标出现持续恶化,都会在下游表现为 Pod 调度慢、变更慢,早发现能避免把根因误判到业务代码上。
5.3 告警规则与记录规则:减少噪音,盯住增量
告警配置这里,踩过大坑之后我有几条切身体会。一是不要在 Prometheus 的原始指标上直接写告警,先定义 recording rule,把常用查询固化下来。这样既能提前聚合,减少告警查询对 Prometheus 的压力,也能让不同告警之间复用一个计算结果。比如我通常会先算好每个命名空间的 CPU 总用量:
groups: - name: k8s-recording-rules rules: - record: namespace:cpu_usage_cores:sum expr: | sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (namespace)然后在告警规则里直接引用这个记录结果,表达更清晰,告警查询也更快。
二是在"进程重启"这类高频事件告警上加上增量判断。直接对kube_pod_container_status_restarts_total做阈值判断很容易误报,因为 Pod 可能在一个月前重启过,总次数一直大于 0。正确的写法是看一个时间窗口内的增量:
- alert: KubePodCrashLooping expr: increase(kube_pod_container_status_restarts_total[15m]) > 3 for: 5m labels: severity: warning annotations: summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} 在 15 分钟内重启超过 3 次"第三,告警要有路由和分级。配置 Alertmanager 的路由规则,把 warning 级别的告警发到工单或群里,critical 级别的告警走电话或紧急渠道。没有分级的结果就是告警疲劳,到真正出大事时大家已经习惯性忽略推送了。
6. 排查实录:把调优经验固化成可复用的手段
6.1 高频问题速查表
工具和参数都会过时,但排查思路可以沉淀成一张表。下面是我在工作中反复用到的清单,现象、优先排查项和常见解法都列出来,每次定位问题都从这张表开始往后想:
| 现象 | 高概率根因 | 优先查看的指标/命令 | 常见解法 |
|---|---|---|---|
| Pod 频繁重启 | 内存超限触发 OOMKilled | kubectl describe pod的 Last State | 调大内存 limit、优化内存占用、排查内存泄漏 |
| 服务延迟毛刺但 CPU 不高 | CFS 限流 | container_cpu_cfs_throttled_periods_total | 调高 CPU limit 或降低 request 对齐业务实际用量 |
| 节点 MemoryPressure 导致驱逐 | 节点总内存不足或 request 设置过高 | kubectl describe node的 Pressure 状态 | 扩容节点、调整 request、开启 HPA 及时缩容 |
| 创建 Pod 一直 Pending | 资源请求过大或调度约束冲突 | kubectl describe pod的 Events | 检查节点可分配资源、放宽调度亲和性约束 |
| kubectl 执行很慢 | API Server 并发打满或 etcd 慢 | apiserver_current_inflight_requests、etcd fsync 延迟 | 调大上限、排查低效 list/watch、清理 etcd 碎片 |
| Service 偶发超时、连接重置 | conntrack 表满或 iptables 规则膨胀 | conntrack -S、dmesg | 调大 conntrack 上限、改用 IPVS、启用externalTrafficPolicy: Local |
| 业务容器内存持续增长不回收 | JVM 或运行时堆外内存问题 | container_memory_working_set_bytes趋势 | 结合应用和 GC 日志定位,限制堆大小或调整 GC 参数 |
| 节点 NotReady | kubelet 卡死或磁盘满 | journalctl -u kubelet、df -h | 清理镜像/日志、检查 docker/containerd 存储目录 |
这张表不能替代你对具体组件的理解,但能帮你在告警刷屏时第一时间锁定排查方向,而不是从零开始翻文档。
6.2 一次 CPU 限流问题的完整追查过程
去年我处理过一个比较典型的案例,过程很适合用来展示调优和监控怎么协同工作。业务方上报说线上某个推送服务在每天 20 点准时有大量请求超时,开发先查了应用日志和调用链,发现下游依赖都很正常,等于把问题定位到了基础架构。
我打开 Grafana,先看这几个面板:这个服务的 CPU 用量只有 1.5 核,但它所在 Pod 的 limit 是 2 核,看起来远没到上限。接着看内存,工作集稳定在 800Mi 左右,也不紧张。然后我随手翻了container_cpu_cfs_throttled_periods_total这个指标,发现从每晚 19:50 左右开始,throttled periods 在快速上升——Pod 里的容器正在被强制限流。
原因在于这个服务的 request 只设了 100m,而它跑在 8 核的节点上,调度器给它的 CPU 份额非常低。虽然 limit 是 2 核,但 request 为 100m 意味着这个 Pod 被分配到的 CPU 权重只相当于 0.1 核,节点上其他 Pod 只要一忙,它的 CPU 时间就会被抢占。这解释了为什么 CPU 平均值看起来不高,业务却感觉卡顿:大量的 CPU 时间片被 CFS 调度器拿走了。
修复方式很直接:把 request 从 100m 上调到 1 核,limit 保持 2 核不变。调整后 Pod 获得了稳定的 CPU 份额,限流指标立即回落,当晚高峰期的请求延迟也恢复正常。这个案例后来被我写进了团队的资源规范:所有关键业务的 request 必须基于一个月的 P95 用量来设,不能用"占个位"的心态写个 100m 糊弄过去。
7. 几条越用越顺手的注意事项
围绕 Kubernetes 集群性能调优和监控,我最后再整理几条平时容易被忽略但实际帮助很大的经验。
第一,改动一次尽量只动一个变量。给集群做调优时,把内核参数、kubelet 配置、HPA 阈值分开灰度,一次只验证一项,不要把所有改动一起推到生产。如果你把十项改动一起发上去,出了问题根本没法回滚定位。
第二,基础指标要保留足够长的历史。内存类问题通常是缓慢累积的,如果没有 30 天以上的历史数据,你很难对照出从什么时候开始请求量、内存、重启数出现拐点。Prometheus 默认 15 天保留期可能不够,建议根据容量调整到 30 天以上,或者接上对象存储做长期归档。
第三,做变更前先看基线。任何一次调优前,先记下当前的关键指标基线值,比如 API Server 的 P99 延迟、etcd fsync 延迟、Pod 调度延迟、节点 CPU 均值,改完以后再对比同一指标。没有基线的优化报告和"感觉好了点"没有区别,也经受不住领导或客户的追问。
第四,调度器和控制面的日志值得定期扫一遍。kube-scheduler会记录每一个 Pod 的调度决策,scheduler_scheduling_attempt_duration_seconds和调度失败原因会直接反映集群容量和约束问题。很多 Pending 类问题在事件里只能看到模糊的 "didn't match node selector",但调度器日志里会有更详细的原因,排障时别只看事件,日志才是完整信息源。
最后想说的是,集群性能优化没有终点。业务在变、流量在涨、新的工作负载形态不断出现,今天合适的参数明天可能就成了瓶颈。把监控做成习惯,把每次调优当成一次带记录的实验,这套方法论本身,才是比任何单项调优手段都值钱的东西。