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

发布时间:2026/8/28 15:45:55

一次 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/8/27 0:30:01

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

SpringBoot JUnit 教程

SpringBoot JUnit 教程

2026/8/28 13:11:43

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/8/25 21:33:36

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

frontend-slides 完整指南:把 PPTX 转换成 HTML 演示文稿的零依赖方案

frontend-slides 完整指南:把 PPTX 转换成 HTML 演示文稿的零依赖方案

2026/8/28 15:39:14

frontend-slides 完整指南&#xff1a;把 PPTX 转换成 HTML 演示文稿的零依赖方案 【免费下载链接】ECC The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor…

3步Docker一键启动 Open WebUI:本地 AI 对话平台的完整部署路径

3步Docker一键启动 Open WebUI:本地 AI 对话平台的完整部署路径

2026/8/28 15:39:14

3步Docker一键启动 Open WebUI&#xff1a;本地 AI 对话平台的完整部署路径 【免费下载链接】open-webui User-friendly AI Interface (Supports Ollama, OpenAI API, ...) 项目地址: https://gitcode.com/GitHub_Trending/op/open-webui Open WebUI 是一个可完全离线运…

frontend-slides:零依赖生成动画HTML幻灯片,还能把PPT变成网页

frontend-slides:零依赖生成动画HTML幻灯片,还能把PPT变成网页

2026/8/28 15:39:14

frontend-slides&#xff1a;零依赖生成动画HTML幻灯片&#xff0c;还能把PPT变成网页 【免费下载链接】ECC The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cu…

铁路电子系统隔离DC-DC设计:从EN50155标准到实战选型

铁路电子系统隔离DC-DC设计:从EN50155标准到实战选型

2026/8/28 15:39:14

1. 铁路电子系统的供电难题&#xff1a;为什么必须用隔离DC-DC1.1 从一块板卡看整车供电环境做铁路电子的人都有过这种经历&#xff1a;拿一块在实验室调得好好的板卡&#xff0c;装到车上跑一趟试运行&#xff0c;回来就发现电源芯片烧了、通信偶尔断、MCU莫名其妙复位。问题不…

AI不是万能药:从工程角度看模型部署、RAG与Agent落地的真实边界

AI不是万能药:从工程角度看模型部署、RAG与Agent落地的真实边界

2026/8/28 15:39:14

先亮明结论&#xff1a;这句话不是反对 AI&#xff0c;而是反对“AI 是万能药”的营销话术。从工程视角看&#xff0c;AI 确实在改变一部分工作方式&#xff1a;代码补全、文档解析、知识库问答、批量内容生成、Agent 编排&#xff0c;这些都真实可用。但 AI 没有改变的东西同样…

C++函数模板在量化交易中的核心应用与性能优化实践

C++函数模板在量化交易中的核心应用与性能优化实践

2026/8/28 15:29:14

1. 从策略到代码&#xff1a;为什么量化交易离不开C模板如果你在量化交易这个行当里摸爬滚打过一阵子&#xff0c;尤其是在做高频、做市或者对延迟有极致要求的策略时&#xff0c;你一定会对C又爱又恨。爱的是它那无与伦比的性能和控制力&#xff0c;恨的是它那繁琐的语法和稍不…

[光学原理与应用-521]:对光的错误理解与纠偏

[光学原理与应用-521]:对光的错误理解与纠偏

2026/8/27 11:10:02

首先光是一种能量的载体和形态&#xff0c;宏观上观察到的光是由无数个微观的光量子组成的&#xff0c;每个光子在产生的瞬间&#xff0c;其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前&#xff0c;在微观层面&#xff0c;每个光量子的运动轨迹是以波函数所展现…

SIP通话转接原理与REFER方法实战解析

SIP通话转接原理与REFER方法实战解析

2026/8/27 7:25:23

1. 通话转接不是“挂断再拨号”&#xff0c;而是SIP会话的动态重定向你有没有遇到过这样的场景&#xff1a;客服坐席A正在和客户通电话&#xff0c;突然需要把这通对话无缝转给专家坐席B&#xff0c;客户完全感知不到中间的断连——既没听到忙音&#xff0c;也没被要求重新拨号…

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

2026/8/28 7:34:42

1. 为什么选择Kolla-ansible来部署单节点OpenStack&#xff1f;如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法&#xff0c;那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

基于Claude Code的开源AI求职框架:从职位搜索到Offer的全自动化闭环

基于Claude Code的开源AI求职框架:从职位搜索到Offer的全自动化闭环

2026/8/28 0:08:32

当AI助手能够独立完成从职位匹配、简历定制到面试准备的全链路求职流程时&#xff0c;求职不再是一场信息战&#xff0c;而是一场工程化战役。框架概述&#xff1a;本地运行的AI求职引擎这是一个构建在Claude Code之上的开源AI求职框架&#xff0c;核心理念是"在工作者的机…

Godot 4 仿 agar.io:相机缩放被 max_zoom 卡死,窗口越大球越小的根因与修复

Godot 4 仿 agar.io:相机缩放被 max_zoom 卡死,窗口越大球越小的根因与修复

2026/8/28 0:08:32

1. 问题现象 在 Godot 4 仿 agar.io 的 2D 项目中&#xff0c;相机缩放设计为「由球组整体尺寸决定」&#xff0c;世界可见高度恒定&#xff0c;窗口只作为视口裁剪。默认小窗口 1280x720 时相机高度正常&#xff1b;但窗口最大化到 2940x1912 后&#xff0c;视角被明显拉远、…

从软件测试大赛到实战:Java+Selenium自动化测试进阶指南

从软件测试大赛到实战:Java+Selenium自动化测试进阶指南

2026/8/28 0:08:32

1. 缘起&#xff1a;从校园到赛场&#xff0c;我的软件测试之路几年前&#xff0c;我还是一个在校园里对着Java课本和“Hello World”程序挠头的普通学生。软件测试对我来说&#xff0c;只是一个在开发流程末尾、用鼠标点点按钮的模糊概念。直到我偶然在学校的公告栏上看到了“…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/28 7:35:26

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具&#xff0c;覆盖选题构思、文献整理、内容生成、格式排版等核心场景&#xff0c;真正帮你高效搞定论文难题。 一、全流程王者&#xff1a;一站式搞定论文全链路&#xff08;一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/28 7:34:51

2026年真正好用的AI论文工具&#xff0c;核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测&#xff0c;千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队&#xff0c;覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/28 7:34:35

告别游戏崩溃&#xff1a;XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…