kube-apiserver 内存问题排查实战:2万+ Secret 导致的性能优化

作者:小橙子🍊 发布时间: 2026-02-10 阅读量:5 评论数:0

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-system namespace
  • 这是 Rancher 为用户 impersonation 创建的 Secret

二、为什么 Secret 多会导致内存高?

2.1 业务视角 vs 技术视角的差异

业务视角:"只用最新的几个 Secret"
技术视角

  • 只要有组件做 List/Watch,就会处理全部 2.6 万条
  • 初始 Informer List 会把所有 Secret 从 etcd 拉出并反序列化
  • 每 Secret 都要经过 Secret.UnmarshalObjectMeta.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 的关系

  1. Secret 数量多 ≠ 内存高(静态)
  2. Secret 数量多 + 频繁 List/Watch = 内存高(动态)
  3. 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:三层判断

  1. 对应的 ServiceAccount 是否还在使用
  2. 是否有 Pod/客户端正在使用该 token
  3. 建议先在测试环境验证

Q4:为什么只有生产环境有 2 万 Secret?

A:符合 "使用频率" 规律

  • 生产:用户多、操作频繁、长期运行 → Secret 累积
  • 测试:用户少、经常重建 → Secret 少

结语

本次问题的排查过程展示了性能分析的核心思路:

  1. 从现象入手:内存高、goroutine 多
  2. 工具定位:使用 pprof 精确分析热点
  3. 数据验证:统计资源数量分布
  4. 根因分析:Rancher Secret 泄漏 + 频繁访问
  5. 解决与验证:清理后对比效果

关键启示

  • 不要被表面现象误导(10k goroutine 不是直接原因)
  • 用数据说话(pprof + 资源统计)
  • 从源头解决问题(清理 + 升级)

参考资料


问题讨论:如果你在排查过程中遇到类似问题,欢迎交流讨论。

文章发布日期:2026-02-10

评论