你正在使用的Go版本是什么( go version
)?
$ go version
go version go1.12.4 linux/amd64
这个问题在最新版本中是否会重现?
Y
你正在使用什么操作系统和处理器架构( go env
)?
any
你做了什么?
当系统处理高峰负载时,它会创建很多goroutines,之后这些goroutine的垃圾回收会导致更多的CPU消耗。
可以通过以下方式复现:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func sayhello(wr http.ResponseWriter, r *http.Request) {}
func main() {
for i := 0; i < 1000000; i++ {
go func() {
time.Sleep(time.Second * 10)
}()
}
http.HandleFunc("/", sayhello)
err := http.ListenAndServe(":9090", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
在10秒后,inuse对象仍然保持不变。
你期望看到什么?
全局goroutines缩小到合适的大小。
你实际上看到了什么?
由malg创建的许多inuse对象
6条答案
按热度按时间tktrz96b1#
实际上,所有的东西都没有减少,这不利于稳定性,应该提供一种减少策略,比如sysmon监控发现超过一半的g都死了,然后释放它。
vulvrdjw2#
@aclements@mknyszek
ego6inou3#
你的观察是正确的。目前,运行时从未释放为goroutines创建的g对象,尽管它会重用它们。主要原因是调度器经常在没有写屏障的情况下操作g指针(许多调度器代码在没有P的情况下运行,因此不能有写屏障),这使得很难确定何时可以回收g。
一个可能的解决方案是使用一种类似于RCU的回收机制,该机制可以在Ms中理解每个M的调度器何时通过静止状态。然后我们可以在所有Ms都处于静止状态后的宽限期后安排未使用的gs被回收。不幸的是,我们不能简单地使用STW来检测这个宽限期,因为那些会停止所有的P,所以,就像写屏障一样,它们不会保护调度器示例在没有P的情况下操作gs。
@changkun,我不确定你的基准测试衡量的是什么。从
RunParallel
内部调用runtime.GC
是没有意义的。垃圾收集器已经是并发的,从runtime.GC
调用runtime.GC
并不会在第一个完成之前启动另一个垃圾收集。此外,如果有几个待处理的runtime.GC
调用,它们都会合并成一个单独的GC。如果意图只是测量GC需要多长时间,只需在不带RunParallel
的情况下调用runtime.GC
即可。vvppvyoh4#
在
RunParallel
中调用runtime.GC
不测量allglock
上的争用。GCs 由runtime.GC
本身串行化,因此它们不会争夺allglock
,并且它们由runtime.GC
合并,因此调用runtime.GC
N 次并发可能导致从 1 到 N 个 GCs,具体取决于调度的变数。抛开基准测试不谈,我认为我们都清楚
allgs
永远不会被收集,这会影响 GC 时间和堆大小。由于 gs 只是堆分配的,所以在 GC 期间收集它们是最有意义的,就像其他堆分配一样。问题是什么时候安全地将它们从
allgs
中解除链接并允许它们被收集,考虑到正常的 GC 可达性不变式不适用于 gs(同时,我们也不希望过于激进地将它们从allgs
中解除链接,因为我们希望分配池行为降低启动 goroutine 的成本)。这当然是可行的,但需要相当多的关注。oknwwptz5#
每次都延迟运行时垃圾回收?
lsmd5eda6#
@matteo-gz 在基准测试之间运行GC。有关提问,请参阅:
#general
频道是一个很好的起点。