去年有一个 “How Much Memory Do You Need to Run 1 Million Concurrent Tasks?” 的文章测试了各种语言在运行 1 个、1 万、10 万、100 万个异步并发任务下需要多少内存,不过当时测试的版本都很旧,代码里也多多少少有各种槽点,不能反映最新的情况。

这次把所有的语言版本都更新到最新,并且还加入了针对 GraalVM 、GraalVM native-image 和 .NET NativeAOT 的测试,然后修掉了之前被人指出代码中不对的地方,测了一个 2024 年版本的 “How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?”。

可以在这里看详细测试: https://hez2010.github.io/async-runtimes-benchmarks-2024 。测试环境和代码也在里面有说明。

这里简单贴一下最终的测试结果:

1 个任务,测各语言 runtime 本身的 footprint:

1

1 万个并发任务:

10K

10 万个并发任务:

100K

100 万个并发任务:

1M

Go 在最开始的时候内存占用很小,但是 Goroutine 的开销非常的大,随着并发任务的数量上升内存大量上涨,个人怀疑是 Go 的 GC 跟不上分配了,估计再接着增大并发数的话 Go 很可能会 OOM 。

Rust 则是发挥稳定,从始至终都表现着非常好的占用水平。

C# 的 NativeAOT 的表现则是直接把 Rust 比下去了,甚至随着并发数量的增大,到后期不做 NativeAOT 的 C# 内存占用都要比 Rust 更小了,可能是因为 tokio 和 async_std 在内存分配和调度这一块儿还有改进空间?

Java 的 GraalVM 表现要比 OpenJDK 差很多,而 GraalVM 的 native-image 表现就要好不少。另外就是忽略 GraalVM 的成绩的话,从结果来看 Java 的 Virtual Thread 要远比 Goroutine 更轻量。

举报· 1236 次点击
登录 注册 站外分享
11 条回复  
test0x01 小成 2 小时前
有意思
001kh 初学 1 小时前
1 秒 10 个块的 Rust ,25 q1 发布 via kaspa
lesismal 小成 1 小时前
1. 实际场景里也不可能全是 cpu 消耗的 task, 所以 sleep 类型的 task 是合理的 2. 并发度极大超过系统核心数量的情况下, go 全用标准库直接 go func()是不公平的, 因为协程数量太大了, 这种完全可以优化成 size 可控的协程池, sleep 可以 timer 回调 3. 如果 sleep 是为了模拟实际场景里的 io 消耗例如网络 io, go 标准库主要是提供同步 io 接口, 那么多数需求确实需要每个 conn 一个协程, 但是, 你可以选择不用标准库, 例如我的 nbio, 百万连接 websocket echo 1k payload 压测, server 部署在 4c 8g ubuntu vm 上, 实际内存占用可以控制到 1g 以内: https://github.com/lesismal/go-websocket-benchmark?tab=readme-ov-file#1m-connections-1k-payload-benchmark-for-nbiogreatws 请注意, 3 里说的百万链接占用 1g 内存, 比 OP 的这种简单 task 占用多, 不代表相同 task 测试优化后的方案就比其他语言多这么多. 另外, 不同语言的这个测试, OP 用的都是未经优化的方式, 方案并不是好的解决方案, 所以这个测试本身就是不合理的. 比如, 如果只是用这种 sleep task, go 直接用 for { time.AfterFunc() } 占用很低的. 但我相信这并不对应任何实际应用场景. 毫无意义的测试, 却顺便拉踩, 捧 java 踩 go, 实在看不下去了我才必须出来澄清下.
server 小成 1 小时前
go 可以试下潘少的 ants
hez2010 楼主 小成 1 小时前
@lesismal > 毫无意义的测试, 却顺便拉踩, 捧 java 踩 go, 实在看不下去了我才必须出来澄清下. 跟 Go 的 goroutine 同样是 green thread 方案的 Java virtual thread ,在并没有做任何池化的情况下,只是简单的 new Thread ,在 1M tasks 占用确实比 goroutine 小了很多,这难道不能说明 goroutine 的资源占用确实不佳吗? 况且文章前面也肯定了 Go 在轻量负载时的占用小、否定了 Java 在轻量负载时的占用大,怎么就能被理解成踩一捧一? 如果你认为 Go 就是天下第一,一切 Go 表现不好的测试都是因为测试不好,而 Go 没有任何问题的话那我也没话说。况且这测试也不是我设计的。
lesismal 小成 1 小时前
试了下这个: var wg sync.WaitGroup for i := 0; i < 1000000; i++ { wg.Add(1) time.AfterFunc(10*time.Second, func() { wg.Done() }) } wg.Wait() macos m3 占用 150M, 应该是有不少定时器到时然后并发创建的 goroutine 较多 如果改用时间轮或者堆的定时器, 数量可控的协程池, 这个占用会降更多. @server 可以看下这个帖子: /t/1089474 实际对比下其他的看看, 我和一些朋友测试, ants 跟其他实现方式相比没有优势, 甚至是劣势, 以及实现复杂可能存在的不确定性
grzhan 小成 1 小时前
如 @lesismal 老板所说,这里 golang 的开销应该是有栈协程的开销,一个 goroutine 栈默认最小是 2K ,1000,000 * 2K / 1024 / 1024 = 1.9G ,算上 sudog 等其他的结构加起来可能差不多吧。 实际 golang 处理异步任务的时候确实不会开这么多协程,更多是把协程池化然后基于 channel 通信来处理。 在实际 Golang 开发过程中确实会有每个 conn 对应一个 goroutine 大量连接导致内存过高的 case ,然后社区里确实有对应魔改 netpoll 以及 nbio 这样的方案来解决类似的问题。
grzhan 小成 半小时前
我觉得不一定是拉踩,针对 Golang 的这一情况可以添加更多的说明会好一些。
grzhan 小成 半小时前
@lesismal 所以一般定时要求不严格的话很多 Golang 开源项目会给定时 duration 加个 10% 左右的随机抖动吧 例如 VictoriaMetrics 的 timeutil.AddJitterToDuration - https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/timeutil/timeutil.go
12下一页
返回顶部