【内存泄露】记一次内存泄露排查,罪魁祸首是HttpClient

  • 序言

很久很久以前,曾经的青葱少年,在二进制的海洋里冲浪时,曾经遇到过有关HttpClient内存溢出的恶龙,当时的少年,抽出锋利的宝剑,迅速解决了恶龙。

而时光荏苒,日月如梭,少年已经成为了大叔,再一次,又遇到了这头恶龙…
562f82b0514cd5b2590d2c63bde5e3dd.png

1. 突然重启

有时候,面对丰盈的理想,收获总是瘦骨嶙峋。

在长期的生存环境的压力下,每个人都不愿意把目光投向更遥远的璀璨太阳般的理想丰碑上,迅速的解决当下,然后转头在冰冷的海水里多摸几条鱼显得那么的现实而充满着多巴胺感觉。

是的,我们做的工业用程序,严格意义上来说,应该能持续运行好几年,然而每天重启是这个程序的标配。

话虽如此,真实形式并没有那么严峻。而我就这么倒霉的碰到了当年的恶龙——内存快速溢出导致程序重启。
acb2a64c3af667427a16b4a613265989.png

大家都很忙,显然这不是别人的锅。

只能停下自己手中的活,一个人独自啃骨头,还是没肉的那种。

现象: 程序内存每秒增加200k左右,直到达到1.5G左右,就崩溃重启了。
涉及dll: 至少有10个以上,其中有3个是我写的。
环境: win7,.net framework 4.X
崩溃日志: 没有

2. 分析Dump

当问题出现的时候,我们总是先怀疑有很多的因素与之有关,而茫茫大海,想要找到关键的因素,真如捞针一般。

面对这个问题,我也陷入了短暂的迷茫中。

由于影响因子众多,那么我假设以前的程序都是好的,就先review后面修改的代码,经过一番阅读,并没有找到和集合相关的一直增加的代码。

ca88ee958867c9e4844240176b360096.png
打开dotmemory,对抓取的dump进行分析。
155b1be15d98c174826ef190bfbd74ca.png
看起来.net的回收GC,并没有很大的问题,而仅有的大对象也是我们确认保存的对象,并没有暴涨的可能。
6ce4ee8597b0daad7decef751a3c292c.png
一般来说,如果.net GC没有太多问题的话,那很有可能有未托管对象的释放问题。

这可能是个寻找问题的方向。

3. 分析句柄和socket。

为什么刚开始没有分析句柄和socket呢?

究其核心原因,由于是windows,并没有linux那么方便的搜索句柄和socket的便利性,以及这个程序并没有高并发的需求,因此忽略了这些基本的分析。

回过头来,我们找找看。

句柄没有增加,但是Socket的Close_Wait有异常。

netstat -an|find "ESTABLISHED" /c
netstat -an|find "CLOSE_WAIT" /c

贡献这两个命令,搜索出来的Close_Wait达到惊人的3w个。
这是干啥了?有人没释放?

搜索代码,终于发现了恶龙的真面目,原来是你吗?HttpClient?

代码如下:

public static string GetAPIResp(string url, int waitTime = 3000)
        {
            string res = string.Empty;
            try
            {             
                var httpClient = new HttpClient();  
                HttpResponseMessage response = httpClient.GetAsync(url, HttpCompletionOption.ResponseContentRead).Result;
                if (response == null)
                {
                    ...
                }
                else if (response.IsSuccessStatusCode)
                {
                    Task<string> t = response.Content.ReadAsStringAsync();
                    if (!t.Wait(iWaitTime))
                    {          
                        return string.Empty;
                    }                   
                   
                    return t.Result;
                }                
            }
            catch (Exception ex)
            {
                Error(ex.ToString());
            }

            return res;
        }

4. HttpClient 的用法

以下是微软建议:
HttpClient可以复用,但在高吞吐的应用中,也应该复用,否则可能造成Socket资源紧张,因为Socket关闭后,根据TCP协议的安全指引,要‘冷处理’一会后,才能被重新使用,Windows下可以是2分钟/4分钟。

这是个使用建议:

HttpClient仅可在应用程序的整个生命周期中实例化一次并重复使用。以下情况可能会导致SocketException错误:

  • 为每个请求创建一个新的HttpClient实例。

  • 负载较重的服务器。

  • 为每个请求创建新的HttpClient实例可能会耗尽可用的套接字。

那么,既然找到了原因,更改就比较简单了,把在函数内的HttpClient实例拿到类里,变成静态成员即可。

如下:

private static HttpClient httpClient = new HttpClient();

重新加载测试,内存恢复正常,不再累计增加。

此次排查问题,超长的耗费了我2天时间,当年那个犀利的屠龙少年,终于老了。

871765fcd66d8fb4b1cdc9df96170a1d.png

后记

f1744f531feef42127bb5a2e32a5321d.png

遥想当年,风华正茂,曾经仗剑只身游孤海,曾经热血沸腾立宏图,怎奈柴米油盐酱醋茶,零落成泥随水流,风浪依然在,少年白了头。

可惜可叹,人生如此咦。

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值