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

发布时间:2026/7/12 8:33:36

一次 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/12 3:05:59

【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"><…

工业负载控制:TPD2015FN与PIC24EP512GU810的黄金组合

工业负载控制:TPD2015FN与PIC24EP512GU810的黄金组合

2026/7/12 8:24:25

1. 工业负载控制的核心挑战与选型逻辑 在工业自动化、机器人控制等高需求场景中&#xff0c;电感和电阻负载的精确控制一直是系统设计的难点。这类负载通常伴随着快速变化的电流、电压尖峰以及电磁干扰等问题。以工业机器人关节驱动为例&#xff0c;电机作为典型感性负载&#…

时序建模:自动驾驶的‘时间眼睛’与实战落地指南

时序建模:自动驾驶的‘时间眼睛’与实战落地指南

2026/7/12 8:24:25

1. 时序&#xff1a;自动驾驶理解交通的“时间眼睛”“时序是如何让自动驾驶更好理解交通的&#xff1f;”——这句话里藏着一个被大众忽略却决定生死的关键事实&#xff1a;交通不是一张张静止的照片&#xff0c;而是一条流动的时间河流。我在一线做感知算法优化的七年里&…

智能体手机:从被动工具到主动伙伴的AI交互变革

智能体手机:从被动工具到主动伙伴的AI交互变革

2026/7/12 8:24:25

上周和朋友聊起手机行业&#xff0c;他说现在各家都在喊“AI手机”&#xff0c;但真正能让人记住的、能解决实际问题的功能却不多。大多数所谓的AI功能&#xff0c;不过是语音助手换个名字&#xff0c;或者加几个滤镜效果。直到看到努比亚宣布要在2026年世界人工智能大会上推出…

动态分区分配算法 4 种策略深度解析:从内存碎片到算法选择决策树

动态分区分配算法 4 种策略深度解析:从内存碎片到算法选择决策树

2026/7/12 8:24:25

动态分区分配算法 4 种策略深度解析&#xff1a;从内存碎片到算法选择决策树 在操作系统内存管理的实践中&#xff0c;动态分区分配算法扮演着至关重要的角色。当多个进程需要共享有限的内存资源时&#xff0c;如何高效、合理地分配和回收内存分区&#xff0c;直接影响到系统的…

短视频脚本:AI 智能分镜脚本生成系统的鸿蒙实现

短视频脚本:AI 智能分镜脚本生成系统的鸿蒙实现

2026/7/12 8:24:25

短视频脚本&#xff1a;AI 智能分镜脚本生成系统的鸿蒙实现 一、引言 短视频已成为当今最主流的内容消费形式。然而&#xff0c;制作一条高质量的短视频并非易事——需要精心设计每一个分镜的画面、台词、字幕和时长&#xff0c;还要考虑不同平台的调性和不同风格的表达方式。对…

E-R图3大常见设计误区解析:对比5个真题案例中的实体与联系定义

E-R图3大常见设计误区解析:对比5个真题案例中的实体与联系定义

2026/7/12 8:14:25

E-R图设计中的三大认知陷阱&#xff1a;从真题案例看实体与联系的本质区别在数据库设计的初始阶段&#xff0c;概念模型构建的质量直接影响着后续整个系统的可靠性与扩展性。作为最常用的概念建模工具&#xff0c;E-R图&#xff08;实体-联系图&#xff09;看似简单直观&#x…

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;…