我把一个.NET项目的内存占用从4GB降到了400MB,花了三天

去年接手了一个.NET 6的Web API项目,部署在K8s上,内存限制设了4GB,结果频繁被OOM杀掉。监控一看,内存占用稳定在3.8GB左右,几乎触顶。

不是内存泄漏,是设计问题。花了三天优化,现在稳定运行在400MB左右。分享排查过程和优化思路。

第一天:定位问题,不是泄漏,是缓存滥用

先用dotnet-counters看GC指标,发现Gen2堆很大,但GC频率正常。说明不是泄漏,是对象长期存活,进了老年代。

排查代码,发现有个"聪明"的同事为了提升性能,把所有数据库查询结果都放进了静态字典缓存,永不过期。用户表、订单表、配置表,全量加载到内存。数据量不大,但对象引用关系复杂,GC压不下去。

第一天的教训:缓存要有过期策略,不是什么都值得缓存

改成Redis缓存,设置5分钟TTL。内存占用直接降到1.5GB。但Redis多了网络开销,接口延迟从20ms涨到35ms。这个trade-off我们认了,毕竟OOM被杀更致命。

第二天:发现大对象堆的坑

内存降到1.5GB后,发现还有优化空间。dotnet-gcdump分析发现,有很多85KB以上的对象,直接进了大对象堆(LOH)。LOH不压缩,容易产生内存碎片,导致占用虚高。

来源是日志系统,每次批量写入日志,拼接了一个大字符串。改成流式写入,或者限制单次批量大小,LOH对象明显减少。内存降到800MB。

第二天的教训:注意对象大小,超过85KB就进LOH,碎片问题很难排查

第三天:字符串驻留和闭包的意外开销

继续分析,发现有很多重复的字符串对象,比如状态码"SUCCESS"、"FAILED",每次API响应都new一个新的。改成常量或者string.Intern,减少重复实例。

还有一个隐蔽问题:LINQ的闭包捕获了外部变量,导致委托对象长期存活。改成显式的foreach循环,或者注意变量作用域,避免不必要的捕获。

这些细节优化做完,内存稳定在400MB左右。

三天的收获

不是学会了什么高级技巧,是养成了用工具分析的习惯。dotnet-counters看GC概况,dotnet-gcdump看堆内存分布,dotnet-trace看分配热点。以前凭感觉优化,现在用数据说话。

一个没解决的疑问

优化完后,我把过程写成文档分享给团队。结果有个同事说:"400MB和4GB在现在的服务器成本下差别不大,为什么要花三天优化?"

我当时的回答是:OOM被杀会导致服务不可用,这是稳定性问题,不是成本问题。但如果单纯从资源成本看,他说的也有道理。

你们怎么看?内存优化到什么地步算"够用了"?追求极致内存占用,还是保证不OOM就行?

posted @ 2026-06-11 17:04  freedangke  阅读(1)  评论(0)    收藏  举报