一次 gRPC 死锁把服务拖垮:我如何用 pprof 在 5 分钟内定位 goroutine 泄漏

发布时间:2026/7/11 1:21:10

一次 gRPC 死锁把服务拖垮:我如何用 pprof 在 5 分钟内定位 goroutine 泄漏
一次 gRPC 死锁把服务拖垮我如何用 pprof 在 5 分钟内定位 goroutine 泄漏上周生产环境出了个怪事。一个 Go 微服务的 goroutine 数从正常的几百个飙到 12 万内存直接吃满然后 OOM 被 K8s 杀掉。重启之后过半小时又这样。看监控像 goroutine 泄漏但代码里明明用了context.WithTimeout也没看到哪里有死循环。折腾了半天最后用 pprof 五分钟定位到根因一个 gRPC 流的关闭顺序问题导致的死锁。今天把这个排查过程记下来免得下次又踩。现象服务莫名其妙被 OOM先说症状。我们的服务是一个 gRPC 网关负责把 HTTP 请求转成 gRPC 调下游服务。平时 goroutine 数稳定在 300-500 个P99 延迟 50ms 左右。上周上线一个新功能后运维同学开始收到告警Goroutine 数缓慢上涨每小时涨几千个内存跟着线性增长6 小时后达到 8G 限制K8s 触发 OOMKillPod 重启重启后半小时曲线又开始往上爬一开始以为是内存泄漏但 heap profile 看下来内存占用大的地方全是runtime.malggoroutine 的栈内存。也就是说不是堆内存泄漏是 goroutine 泄漏。第一步用 pprof 看 goroutine 栈Go 内置的 pprof 简直是排查这类问题的神器。我们的服务已经集成了net/http/pprof直接 curl 就能拿到数据curl-shttp://localhost:6060/debug/pprof/goroutine?debug2|head-200导出来的 goroutine 栈密密麻麻几万行。但仔细看大部分都卡在同一个地方goroutine 114514 [semacquire, 3 minutes]: sync.runtime_SemacquireMutex(0xc0001a2030, 0x0, 0x1) /usr/local/go/src/runtime/sema.go:71 0x25 sync.(*Mutex).Lock(...) /usr/local/go/src/sync/mutex.go:138 0x8c main.(*StreamManager).Close(0xc0001a2000) /app/stream.go:45 0x3a google.golang.org/grpc.(*clientStream).RecvMsg(...) /app/vendor/google.golang.org/grpc/stream.go:884 0x1b2thousands of goroutines 都在等同一个 mutex。这个StreamManager.Close方法看起来就是罪魁祸首。根因gRPC 流关闭时的死锁来看问题代码。我们有个StreamManager管理 gRPC 双向流大概长这样typeStreamManagerstruct{mu sync.Mutex stream pb.MyService_ConnectClient closedbool}func(sm*StreamManager)Close(){sm.mu.Lock()defersm.mu.Unlock()ifsm.closed{return}sm.closedtrue// 关闭 gRPC 流 - 这里会阻塞sm.stream.CloseSend()}看起来没问题对吧加锁、标记关闭、关闭流。但问题就藏在CloseSend()里。gRPC 的CloseSend()内部会等待流的接收端也关闭。而如果接收端的处理 goroutine 正好也在调用StreamManager.Close()就会形成经典的死锁Goroutine A业务逻辑调用Close()→ 拿到锁 → 调用CloseSend()→ 等待接收端关闭Goroutine B接收循环收到 EOF → 调用Close()→ 尝试拿锁 → 被阻塞A 拿着锁等 BB 等着 A 释放锁。更坑的是CloseSend()在网络异常时可能会永远等下去不会超时。所以这些 goroutine 就永远卡在那越积越多。修复方案把 IO 操作移出临界区修复其实很简单不要把可能阻塞的 IO 操作放在锁里面。func(sm*StreamManager)Close(){sm.mu.Lock()ifsm.closed{sm.mu.Unlock()return}sm.closedtruestream:sm.stream// 复制引用sm.mu.Unlock()// 提前释放锁// IO 操作在锁外执行即使阻塞也不会死锁ifstream!nil{ctx,cancel:context.WithTimeout(context.Background(),5*time.Second)defercancel()done:make(chanstruct{})gofunc(){stream.CloseSend()close(done)}()select{case-done:// 正常关闭case-ctx.Done():log.Println(CloseSend timeout, forcing cleanup)}}}核心改动就两点锁里只做状态变更不做 IO标记closed true之后就释放锁给 CloseSend 加超时即使网络卡了5 秒后也会强制返回不会永远 hang 住改完上线goroutine 数立刻稳定回 400 个左右内存也恢复正常。踩坑记录坑 1defer 释放锁的陷阱原来的代码用了defer sm.mu.Unlock()虽然简洁但如果CloseSend()阻塞锁就永远释放不了。在可能阻塞的 IO 操作前建议显式Unlock()。坑 2pprof 默认只采样部分 goroutineGo 的 goroutine profile 默认只采样前 50 个等待的 goroutine。如果问题不明显可能看不到全貌。可以加上?debug2看全部或者用runtime.SetMutexProfileFraction调整采样率。坑 3gRPC 流没有内置超时CloseSend()不像普通 RPC 调用那样有context.WithTimeout控制。如果不自己做超时处理网络抖动时很容易 hang 住。gRPC 流的优雅关闭建议都加上 goroutine select 的超时保护。坑 4本地复现不出来这个问题在本地开发环境完全复现不了因为本地网络太快了CloseSend()瞬间就能完成。只有在生产环境网络有延迟或丢包时才会触发。所以生产环境一定要加监控告警goroutine 数、内存、goroutine 泄漏都得看。写在最后这次排查最大的收获不是修了这个 bug而是意识到锁 阻塞 IO 定时炸弹。Go 的 mutex 很简单但用不好就是生产事故的源头。尤其是 gRPC、数据库连接、网络请求这种可能阻塞的 IO千万别放在锁里面。pprof 真的是 Go 程序员的救命工具。如果你还没在工程里接入net/http/pprof强烈建议现在就加上。几行代码的事关键时刻能省你几个小时。对了改完代码后我还加了一条监控规则goroutine 数超过 5000 就告警。毕竟谁也不想半夜被 PagerDuty 叫醒。

相关新闻

【Bug已解决】Claude Team Plan 购买席位报错 Payment failed 解决方案

【Bug已解决】Claude Team Plan 购买席位报错 Payment failed 解决方案

2026/7/8 21:58:08

【Bug已解决】Claude Team Plan 购买席位报错 Payment failed 解决方案 1. 问题描述 企业管理员在 Claude 控制台为团队计划(Team Plan)增购席位时,反复遇到支付失败的报错: Payment failed It looks like your payment method…

SpringBoot JUnit 教程

SpringBoot JUnit 教程

2026/7/11 16:54:37

1. 核心依赖 (Maven)<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope> </dependency>该 Starter 自动引入&#xff1a;JUnit 5 (Jupiter)Mock…

JQuery Tips(4)----一些关于提高JQuery性能的Tips

JQuery Tips(4)----一些关于提高JQuery性能的Tips

2026/7/11 15:18:18

在选择时&#xff0c;最好以ID选择符作为开头我想这个很好理解&#xff0c;因为JQuery内部使用document.getElementByID方法进行ID选择&#xff0c;这种方法比其他所有对DOM选择的方法更快&#xff0c;所以以$("#")开头是最好的,比如:<div id"a"><…

西安软件开发服务商分层调研报告:中型合规服务商迎来发展红利

西安软件开发服务商分层调研报告:中型合规服务商迎来发展红利

2026/7/12 1:13:44

第一章 行业热点深度解析&#xff1a;市场两极分化&#xff0c;中型合规服务商迎来发展红利7 月 11 日&#xff0c;行业媒体发布服务商分层调研报告&#xff0c;软件开发赛道形成十分清晰的三级格局&#xff0c;行业淘汰节奏加快&#xff0c;不同类型开发团队的生存空间出现明显…

Bellman-Ford 算法 C++ 实现:负权图最短路径与负权环检测 5 步详解

Bellman-Ford 算法 C++ 实现:负权图最短路径与负权环检测 5 步详解

2026/7/12 1:13:44

Bellman-Ford 算法 C 实现&#xff1a;负权图最短路径与负权环检测 5 步详解1. 算法核心思想与适用场景Bellman-Ford 算法是图论中解决单源最短路径问题的经典算法&#xff0c;由 Richard Bellman 和 Lester Ford 在 20 世纪 50 年代提出。与 Dijkstra 算法相比&#xff0c;它的…

NBM7100A与PIC18F4685在低功耗设备中的能量管理方案

NBM7100A与PIC18F4685在低功耗设备中的能量管理方案

2026/7/12 1:13:44

1. 项目背景与核心挑战在物联网和低功耗设备领域&#xff0c;如何最大化利用不可充电的初级电池&#xff08;如锂锰电池&#xff09;的能量一直是个棘手问题。这类电池在突发电流负载下表现尤为脆弱——电压骤降会导致设备意外重启&#xff0c;而电池内剩余的大量能量却无法被有…

GodotSteam游戏性能优化实战:10个技巧解决内存与网络瓶颈

GodotSteam游戏性能优化实战:10个技巧解决内存与网络瓶颈

2026/7/12 1:13:44

1. 项目概述&#xff1a;为什么GodotSteam项目需要性能优化&#xff1f;如果你正在用Godot引擎开发Steam平台的游戏&#xff0c;尤其是带有多人联机功能的项目&#xff0c;那么“性能”这个词&#xff0c;大概率已经让你头疼过不止一次了。我经历过不止一个项目&#xff0c;在开…

Node.js 核心模块实战:path 与 fs

Node.js 核心模块实战:path 与 fs

2026/7/12 1:13:44

从路径拼接到文件读写&#xff0c;掌握 Node.js 两大核心模块&#xff0c;并理解 JS 异步编程的完整进化链。&#x1f4cc; 本文结构 path 模块&#xff08;路径处理&#xff09; → fs 模块&#xff08;文件系统&#xff09;→ 同步 vs 异步 → 异步进化史&#xff08;回调 →…

基于Antigravity与Gemini构建智能跑步教练系统的实践指南

基于Antigravity与Gemini构建智能跑步教练系统的实践指南

2026/7/12 1:03:44

1. 先搞清楚这个项目到底解决什么实际问题这个项目标题看起来有点炫&#xff0c;但核心其实很直接&#xff1a;用 Antigravity 和 Gemini 这两个工具组合起来&#xff0c;做一个能指导跑步训练的 AI 教练。不是那种只会说“加油”的语音助手&#xff0c;而是能分析你的跑步数据…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/12 0:03:42

Unity游戏文本翻译架构深度解析&#xff1a;XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/12 0:03:42

openEuler Raspberry Pi Kernel设备驱动开发指南&#xff1a;为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/12 0:03:42

openEuler系统集成测试实战&#xff1a;基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载&#xff1a;…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/12 0:03:42

Unity游戏文本翻译架构深度解析&#xff1a;XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/12 0:03:42

openEuler Raspberry Pi Kernel设备驱动开发指南&#xff1a;为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/12 0:03:42

openEuler系统集成测试实战&#xff1a;基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载&#xff1a;…