- 前情提要:
- 原帖省流:
有人在知乎再次发现了axum/hyper的内存泄漏问题。坛内大佬们找到了原始issue,基本的结论是更换成MiMalloc内存分配器会最大程度的降低泄漏可能性。
- 测试准备:
在VMware的虚拟机里测了一下,环境是ubuntu 24.04
桌面版,分配2.6Ghz8C8G。
rust是最新版本rustc 1.81.0-nightly (b5b13568f 2024-06-10)
,--release
模式。
所有终端都是ulimit -n 65536
开启最大文件数。
同环境下直接访问http。
服务端axum写了一个最基础的hello,分别测的是默认内存分配器,以及前一个帖子里提到的MiMalloc、JeMalloc、Dhat一共四种内存分配模式。
首先试了下性能,先用oha随便打了下默认模式,改改oha客户端并发参数,报告axum最大qps能到130000+。
计划客户端用reqwest直接秒内1000/10000qps并发打进axum里,用tokio的interval每秒打一次,连续打到一分钟以上。
代码用的就是官方范例,全都tokio异步化,不存在同步阻塞代码,报告打印皆是一秒一次,基本不影响并发性能。
此轮测试没有用脚本检测内存,就用自带的系统监视器看了一下,截了一些图,但论坛不好传图,就四舍五入分享一下数据。
- 测试结果:
启动的时候MiMalloc模式400+K内存,默认分配器、JeMelloc、Dhat都是不到300K内存。
客户端持续1000qps时,大概几秒后,MiMalloc的内存就会在30+M这样,其他三者在50~60M这样。
客户端持续10000qps时,大概几秒后,MiMalloc的内存会在300+M这样,其他三者在500~600M这样。
客户端压测结束时,所有内存分配模式的服务端,都会保持高内存占用,如果没有新请求,短时间内不会自动释放内存。
但此时低频率的发送请求(间歇性1~100qps),则会逐步释放内存。
- 经过连续1000qps和10000qps且又间歇性的低频请求后,某次测试过程的各阶段近似状态:
状态--------(0qps)----(1000qps)-(10000qps)--(10qps)
默认分配器: 300K -> 60M -> 600M -> 560M
dHat: 300K -> 60M -> 600M -> 560M
MiMalloc: 500K -> 35M -> 350M -> 9M
JeMalloc: 300K -> 56M -> 560M -> 30M
- 内存问题成因推测:
首先,必须得承认axum/hyper确实存在内存分配问题,在经过多轮请求后,并没有恢复到接近最初的内存状态。
在默认分配器的情况下,用reqwest以1000qps打了axum两个小时,72万次请求,内存还是在55~60M这样。
内存无法正确回收,疑似和请求并发数有关,和总请求数关系甚微。
Dhat和默认分配器表现基本一致,对Dhat不太熟,查看了大致文档,感觉还是以监控为主,对内存分配释放并没有什么优化。
MiMalloc和JeMalloc都比默认分配器更好的释放内存,尽管两者最终内存状态仍有区别,但基本都可以认为在承受范围之内。
测试结果和hyper的issues里讨论结果基本一致。
- 测试中遇到一些其他问题:
因为在同一个环境里,CPU爆表以及端口拥堵会同时影响到客户端和服务端。
虽然reqwest用了tokio的semaphore来控制并发,但如果瞬时并发数过大,CPU就会跑满,客户端这边就会产生大量send error,具体原因不明,猜测可能是TCP端口分配满了。
在这个环境下,reqwest大约能短时间连续秒内发28000不报错,但几秒后都会出现大量send error,会阻塞系统tcp通道,服务器即使没挂也无法提供服务。
所以最后决定只测最高10000qps的情况,基本能实现秒内吞吐,大约200~300ms就能完成所有请求的收发。
在CPU没爆表的情况下,各个内存分配器的服务端吞吐性能基本没有太大区别。
但即使是10000qps,跑了一段时间后,仍然有可能CPU爆表,挤压请求。
在连续10000qps的情况下,
其中默认分配器和Dhat大约十几秒后CPU开始跑满,开始出现send error的报错;
MiMalloc大约三十几秒后CPU开始跑满,开始出现send error的报错;
JeMalloc大约八十多秒后CPU开始跑满,开始出现send error的报错。
CPU跑满时,axum服务端会无法访问,在客户端停止发送请求后,服务端会在几秒内恢复正常。
- 结论:
根据这个测试过程,axum即使存在内存分配问题,但看起来内存并不会无限增加。
如果能够承受生产环境下最大qps所占用的内存,也可以选择不更换内存分配器。
如果需要内存预热,则需要用压测来进行预热。
MiMalloc和JeMalloc都是axum服务端内存分配优化的可选项,
MiMalloc在测试中对内存分配释放表现更好,
JeMalloc在兼顾内存释放的前提下,CPU占用率表现更好。
评论区
写评论我今天9:20开压,2000qps,开局262k,五分钟后9:25,内存稳定在77M-81M之间,现在9:45,还是79.5M左右。
计划开十个小时,下午再看看。
--
👇
zzl221000: 我记得这个问题就是每个新连接会占用内存,tcp复用就不会出问题 而且这个bug是内存会无限增加,只要你有足够多的连接 当时每天会校验100万个http代理,2000qps,几个小时程序就会oom 那会还在axum写了issue反馈,最后排查到是hyper的问题 https://github.com/tokio-rs/axum/issues/347#issuecomment-928090153 这里还有一份当时的 heaptrack ,可惜不会分析 heaptrack
我记得这个问题就是每个新连接会占用内存,tcp复用就不会出问题 而且这个bug是内存会无限增加,只要你有足够多的连接 当时每天会校验100万个http代理,2000qps,几个小时程序就会oom 那会还在axum写了issue反馈,最后排查到是hyper的问题 https://github.com/tokio-rs/axum/issues/347#issuecomment-928090153 这里还有一份当时的 heaptrack ,可惜不会分析 heaptrack
有时间可能监控排查一下客户端和tcp端口的分配问题,看看怎么单机持续发送几万的请求,更好的压测。
还有时间的话,可能测测actix和pingora的内存分配情况。
hyper的内存跟踪有点复杂,看issue里author搞了好几年都没跟踪出来我暂时就不费这个力了。
反正目前的解决办法就是在main.rs的空白处加两三行alloc声明,基本不费力。
文章翻译以后评论到相关issue里面了,希望hyper的团队能解决吧:
https://github.com/hyperium/hyper/issues/1790#issuecomment-2170644852
--
👇
JackySu: 牛逼 这测试的很详细 能不能加入memgrind看一下内存泄露呢 比如对比malloc demalloc次数之类的
牛逼 这测试的很详细 能不能加入memgrind看一下内存泄露呢 比如对比malloc demalloc次数之类的