kube-apiserver 内存问题排查实战:2万+ Secret 导致的性能优化
本文记录了一次 kube-apiserver 内存过高问题的完整排查过程,从现象分析到定位根因,最终通过清理 Rancher 产生的 2.6 万个历史 Secret 成功解决问题。
问题描述
生产环境的 kube-apiserver 出现以下异常现象:
- 内存占用高:RSS 常驻 2GB+,经常因为 OOM 被重启
- Goroutines 数量大:达到 10k+ 级别
- etcd 压力大:虚拟内存 11.9GB,RSS 800MB
初步怀疑是连接数过多导致,但深入排查后发现另有隐情。
问题分析阶段
一、Goroutines 数量真的是罪魁祸首吗?
关键结论:10k goroutines 本身不会直接导致 OOM
1.1 Goroutine 内存占用分析
每个 goroutine 的栈空间是按需增长的:
- 初始栈大小约 2KB
- 10k goroutines ≈ 20MB(初始状态)
- 即使部分扩容,也就几十到百 MB 级别
结论:10k goroutines 占用的内存远低于 2GB,不是内存问题的直接原因。
1.2 Goroutines 数量的真正含义
大量 goroutines 说明:
- 并发连接/请求很多
- 每个 HTTP/2/WebSocket/Watch 连接至少对应 1 个甚至多个 goroutine
- 但这只是侧面反映并发高,不是内存爆涨的根因
二、Client-go 会在服务端缓存资源数据吗?
重要澄清:client-go 的缓存机制
2.1 在 kube-apiserver 进程内部
- apiserver 也使用 client-go,但主要用于:
- Leader 选举
- Webhook 配置获取
- 与聚合 API/CRD 交互
- 不会为每个连接维护完整的资源缓存副本
2.2 apiserver 处理外部请求时
- client-go 在客户端(controller-manager/operator 等)维护 Informer 缓存
- 缓存数据存储在客户端进程,而非 apiserver
- apiserver 只负责:
- 从 etcd 拉数据
- 做认证/授权/mutating/validating
- 序列化后发送给客户端
结论:不是 "每个连接都缓存一套数据" 导致内存问题。
三、kube-apiserver 内存高的真正原因
3.1 etcd result 缓存和索引
当集群资源数量多(10w+ Pod/ConfigMap/CRD)或对象大时,这部分内存线性增长。
3.2 请求队列和序列化缓冲
高并发 list/watch 产生大量:
- 临时对象(序列化/反序列化)
- 响应体缓冲区
- 压缩缓冲区
3.3 大对象/大量 CRD
CRD 对象的大 spec/status 字段会导致:
- etcd 读大量数据
- apiserver 多次拷贝/临时对象分配
- 内存和 GC 压力大
3.4 Webhook 负载
每个创建 / 更新经过 webhook 链:
- 请求/响应需要反复序列化
- 如果 webhook 慢,会堆积 goroutine 和请求对象
排查实战:使用 pprof 定位问题
一、启用 profiling
1.1 确认 profiling 已开启
kubectl -n kube-system describe pod kube-apiserver-xxx | grep profiling
默认为 true,如果没有 --profiling=false 就是开启状态。
1.2 访问 pprof 接口
方法一:通过 kubectl proxy
kubectl proxy --address=127.0.0.1 --port=8001
方法二:直接在 Pod 内访问
kubectl -n kube-system exec -it kube-apiserver-xxx -- sh
curl -k https://127.0.0.1/debug/pprof/
二、抓取 Heap Profile(分析内存)
# 安装 Go(推荐 1.20+)
wget https://go.dev/dl/go1.21.6.linux-amd64.tar.gz
tar xzf go1.21.6.linux-amd64.tar.gz -C /usr/local
export PATH=/usr/local/go/bin:$PATH
# 抓取 heap profile
go tool pprof http://127.0.0.1/debug/pprof/heap
# 在 pprof 交互界面
(pprof) top 20
2.1 分析结果示例
Showing nodes accounting for 662.79MB, 72.74% of 911.22MB total
flat flat% sum% cum cum%
214.79MB 23.57% 23.57% 316.30MB 34.71% k8s.io/api/core/v1.(*Secret).Unmarshal
98.39MB 10.80% 34.37% 184.53MB 20.25% k8s.io/apimachinery/pkg/apis/meta/v1.(*ObjectMeta).Unmarshal
78.26MB 8.59% 42.96% 78.26MB 8.59% bytes.growSlice
65.65MB 7.20% 50.16% 65.65MB 7.20% k8s.io/apimachinery/pkg/apis/meta/v1.(*FieldsV1).Unmarshal
关键发现:Secret.Unmarshal 占用 214MB,是最大的内存热点!
三、抓取 Goroutine Profile(分析协程)
# 文本形式(可直接阅读)
curl "http://127.0.0.1/debug/pprof/goroutine?debug=2" -o goroutine.txt
# 或用 pprof 交互查看
go tool pprof http://127.0.0.1/debug/pprof/goroutine
(pprof) top
3.2 分析 Goroutine 分布
# 统计各类 goroutine 数量
grep -c "k8s.io/apiserver/pkg/storage/cacher" goroutine.txt # 14014
grep -c "go.etcd.io/etcd/client/v3" goroutine.txt # 760
grep -c "secrets" goroutine.txt # 0
关键发现:1.4 万 goroutine 都在 storage/cacher 模块,说明 apiserver 在为大量资源维护缓存和 Watch 分发。
根因定位:Rancher Secret 泄漏
一、检查 Secret 数量分布
kubectl get secrets -A | wc -l
# 输出:29222
kubectl get secrets -A | awk '{print $1}' | sort | uniq -c | sort -nr | head -20
1.1 惊人的发现
26228 cattle-impersonation-system
232 it00463
198 it00496
153 it00594
关键问题:
- 集群总共 2.9 万个 Secret
- 其中 2.6 万(90%)都在
cattle-impersonation-systemnamespace - 这是 Rancher 为用户 impersonation 创建的 Secret
二、为什么 Secret 多会导致内存高?
2.1 业务视角 vs 技术视角的差异
业务视角:"只用最新的几个 Secret"
技术视角:
- 只要有组件做 List/Watch,就会处理全部 2.6 万条
- 初始 Informer List 会把所有 Secret 从 etcd 拉出并反序列化
- 每 Secret 都要经过
Secret.Unmarshal、ObjectMeta.Unmarshal等
2.2 内存占用链条
2.6万 Secret → etcd 存储
↓
某组件做 List/Watch
↓
apiserver 批量反序列化
↓
Secret.Unmarshal 热点(214MB)
↓
堆内存增长 + GC 压力
↓
RSS 涨到 2GB+
三、Goroutine 与 Secret 的关联
从 goroutine profile 可以看到大量:
(*Cacher).dispatchEvents(*cacheWatcher).process(*Reflector).ListAndWatch
这些是 apiserver 为资源维护的缓存和 Watch 分发协程,Secret 数量多 + Watch 多 = goroutine 爆炸。
解决方案
一、分析 Secret 类型
查看一个典型的 Secret:
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
namespace: cattle-impersonation-system
name: cattle-impersonation-u-qccvs-token-8vccq
annotations:
kubernetes.io/service-account.name: cattle-impersonation-u-qccvs
ownerReferences:
- kind: ServiceAccount
name: cattle-impersonation-u-qccvs
判断:这是 Rancher 为用户会话创建的临时 token Secret。
二、清理历史 Secret(分步骤)
步骤 1:确认对应 ServiceAccount 是否存在
# 检查 SA 是否还存在
kubectl -n cattle-impersonation-system get sa cattle-impersonation-u-qccvs
# 查找无对应 SA 的 Secret
kubectl -n cattle-impersonation-system get secret -o json | \
jq -r '.items[] | select(.metadata.ownerReferences == null) | .metadata.name'
步骤 2:小规模测试删除
# 先删除几个最老的、SA 不存在的 Secret
kubectl -n cattle-impersonation-system delete secret <很老的 secret 名>
观察 Rancher 功能是否正常。
步骤 3:批量清理策略(谨慎!)
筛选条件:
- 创建时间早于 N 天
- 对应 ServiceAccount 不存在
- 在测试环境验证过
示例脚本(先 dry-run):
# 找出 30 天前创建的、无 owner 的 Secret
kubectl -n cattle-impersonation-system get secret -o json | \
jq -r '.items[]
| select(.metadata.creationTimestamp < "'$(date -d "30 days ago" --iso-8601=seconds)'")
| select(.metadata.ownerReferences == null)
| .metadata.name' | head
# 确认无误后再删除
# ... | xargs kubectl -n cattle-impersonation-system delete secret
三、源头治理
3.1 升级 Rancher 版本
- 查询 Rancher issue 是否有 impersonation Secret 泄漏的已知 bug
- 升级到已修复版本
3.2 清理后效果对比
清理前后对比:
# 清理前
kubectl get secrets -A | wc -l
# 29222
# 清理后
kubectl get secrets -A | wc -l
# ~3000
# heap profile 对比
go tool pprof http://127.0.0.1/debug/pprof/heap
(pprof) top 20
# Secret.Unmarshal 从 214MB 降到 ~180MB
# 进程 RSS
ps aux | grep kube-apiserver
# 从 2.1GB 降到 ~1.5GB
关键知识点总结
一、pprof 使用要点
| 命令 | 用途 |
|---|---|
go tool pprof http://.../heap |
抓取内存快照 |
(pprof) top 20 |
显示内存占用 Top 20 |
(pprof) top -cum |
按累积占用排序 |
curl .../goroutine?debug=2 |
导出 goroutine 堆栈 |
二、内存与 Secret 的关系
- Secret 数量多 ≠ 内存高(静态)
- Secret 数量多 + 频繁 List/Watch = 内存高(动态)
- pprof 中
Secret.Unmarshal占用高是关键信号
三、Goroutine 与资源的关系
- 10k goroutine 不直接导致 OOM
- 但大量 cacher goroutine 说明资源访问压力大
- 需要结合 heap profile 和资源数量综合判断
四、排查思路
现象:内存高、重启频繁
↓
pprof heap 分析 → Secret.Unmarshal 占用高
↓
统计资源数量 → Rancher namespace 有 2.6万 Secret
↓
goroutine 分析 → cacher goroutine 过多
↓
结论:Secret 泄漏 + 频繁访问导致内存压力
↓
解决:清理历史 Secret + 升级 Rancher
常见问题 FAQ
Q1:为什么删除 Secret 后内存还是 180MB?
A:
- heap profile 是瞬时快照,不是峰值
- Go runtime 不会立刻把内存还给 OS
- apiserver 自身有常驻缓存
- 需要看 RSS 变化趋势,而非单次快照
Q2:top -cum Secret.Unmarshal 报错怎么办?
A:这是 pprof 里的命令
top -cum:按累积占用排序Secret.Unmarshal:过滤表达式(精确匹配)
如果报错 Focus expression matched no samples,说明:
- 当前 profile 没采样到该函数
- 使用
top -cum Secret(更宽松的匹配)
Q3:如何判断 Secret 是否可删除?
A:三层判断
- 对应的 ServiceAccount 是否还在使用
- 是否有 Pod/客户端正在使用该 token
- 建议先在测试环境验证
Q4:为什么只有生产环境有 2 万 Secret?
A:符合 "使用频率" 规律
- 生产:用户多、操作频繁、长期运行 → Secret 累积
- 测试:用户少、经常重建 → Secret 少
结语
本次问题的排查过程展示了性能分析的核心思路:
- 从现象入手:内存高、goroutine 多
- 工具定位:使用 pprof 精确分析热点
- 数据验证:统计资源数量分布
- 根因分析:Rancher Secret 泄漏 + 频繁访问
- 解决与验证:清理后对比效果
关键启示:
- 不要被表面现象误导(10k goroutine 不是直接原因)
- 用数据说话(pprof + 资源统计)
- 从源头解决问题(清理 + 升级)
参考资料
问题讨论:如果你在排查过程中遇到类似问题,欢迎交流讨论。
文章发布日期:2026-02-10