Fixing a kubelet memory leak in Kubernetes 1.36
AI 深度解读
背景
一位工程师在将单节点测试集群升级到 Kubernetes v1.36 后,遭遇了内存泄漏问题。该集群运行在 DigitalOcean 的托管 DOKS 服务上,节点配置仅为 2 GiB RAM。正是由于内存资源紧张,这一问题被迅速暴露——在内存充裕的生产环境中,泄漏可能需要更长时间才会显现。
核心内容
问题发现
工程师首先收到集群告警,发现 Pod 被频繁重启。但通过 kubectl top pods 检查时,并未发现任何异常占用内存的 Pod,节点上运行的应用也没有内存增长,远未达到内存限制。
通过在节点上执行 htop 并查看内存占用,最终定位到 kubelet 进程本身在持续增长。执行 systemctl restart kubelet 可暂时缓解问题,但泄漏会很快复发。
排查过程
由于 Kubernetes 使用 Go 语言编写,工程师利用 Go 的 pprof 包捕获了 kubelet 的堆内存 profile:
kubectl get --raw "/api/v1/nodes/${NODE}/proxy/debug/pprof/heap?debug=0" > "kubelet_pprof_heap.pb.gz"
随后使用 go tool pprof -top 分析内存占用情况。
按对象数量分析显示,近 100 万个 context 对象占据了主要内存,其中 context.(*cancelCtx).propagateCancel 占 45.52%,context.withCancel 占 26.93%,context.(*cancelCtx).Done 占 19.57%。
按堆内存使用分析显示,context.(*cancelCtx).propagateCancel 占用 86.33MB(51.28%),context.(*cancelCtx).Done 占用 29.50MB(17.52%),context.withCancel 占用 29MB(17.23%)。
调用链追踪到 k8s.io/kubernetes/pkg/kubelet/volumemanager.(*volumeManager).WaitForAttachAndMount 和 k8s.io/kubernetes/pkg/kubelet.(*Kubelet).SyncPod 等核心路径。
根因定位
工程师使用 Codex 辅助分析代码,迅速定位到问题根源:Kubernetes 1.36 在 2026-02-19 引入的一处代码变更。
原代码:
// initialize a context for the worker if one does not exist
if status.ctx == nil || status.ctx.Err() == context.Canceled {
status.ctx, status.cancelFn = context.WithCancel(context.Background())
}
ctx = status.ctx
新代码:
ctx, status.cancelFn = context.WithCancel(parentCtx)
这段代码在每次 startPodSync(每个 Pod 的核心协调循环)时都会执行。问题在于:当 status.cancelFn 已指向之前的取消函数时,新赋值会覆盖它。如果旧的取消函数未被调用(在典型成功场景下不会调用),旧的子 context 仍保持与父 context 的连接。Go 的 context 文档明确指出:调用 CancelFunc 会移除父级对子级的引用,若不调用则会泄漏子 context,直到父 context 被取消。
由于每个 Pod 的协调循环都会触发此逻辑,几天内就累积了近 100 万个泄漏的 context,在繁忙的集群上会更加严重。
修复过程
工程师首次向 Kubernetes 项目提交贡献,发现其流程高效,能快速分流问题并支持补丁合并。
初次补丁通过了本地测试,但在 Kubernetes CI 环境的 E2E 集成测试中失败,暴露出另一个问题:处理就绪性和存活探测的 prober workers 在使用 context 时也存在不当之处。为解决内存泄漏,团队决定先回退直接问题,将更广泛的 context 修复留待后续处理。
工程师在代码中留下注释提醒后续维护者:
// Be careful not to leak contexts (see #139823).
// Be careful that long-lived goroutines (such as prober workers) outlive
// the lifetime of a single startPodSync cancellation context.
时间线
- 2026-06-17:内存泄漏问题被报告
- 2026-06-18:定位到回归代码
关键要点
- 资源受限环境有助于暴露问题:2 GiB 内存的小节点因内存压力快速显现了泄漏,而内存充裕的生产环境可能延迟发现
- kubelet 自身可能成为内存泄漏源:排查时不应仅关注用户 Pod,Kubernetes 核心组件本身也可能出现问题
- Go context 泄漏是常见陷阱:未正确调用
CancelFunc会导致子 context 持续累积,这是 Go 并发编程中的典型错误 - AI 编程工具可加速陌生代码库分析:Codex 帮助工程师快速定位到正确的回归代码,无需深入理解 kubelet 实现细节
- 修复需考虑全局影响:初次补丁虽解决直接问题,但 E2E 测试暴露出 prober workers 的 context 使用问题,说明局部修复可能引发其他隐患
意义与影响
此次事件揭示了 Kubernetes 核心组件在版本升级中可能引入的隐蔽回归问题。对于运行 Kubernetes 1.36 的用户,尤其是资源受限的环境,需要关注 kubelet 内存使用情况。
从工程实践角度看,这提醒我们:
- context 生命周期管理至关重要:在长期运行的协调循环中,必须确保每个创建的 context 都有对应的取消机制
- 测试覆盖的局限性:本地测试通过但 E2E 测试失败,说明集成测试在发现并发和资源泄漏问题上的不可替代性
- 小集群的测试价值:低成本测试集群在验证新版本稳定性方面具有独特优势,能够快速暴露资源相关问题
- AI 辅助调试的实用性:在大型复杂代码库中,AI 工具可显著缩短问题定位时间,尤其适合不熟悉代码库的开发者
