Go运行时的两个主要限制


Go 的并发运行时在云应用程序中可以很好地扩展。

大多数 Go 应用程序容器都会在 Kubernetes 集群上的某个地方发出 CPU 请求,消耗几个 vCPU 和一些 RAM,并且可以很好地进行扩展,与您在生产中合理创建 K8s pod 的大小一样大(例如 32vCPU 和 128GiB RAM) )。

然而,当在裸机上运行时,我们发现到目前为止 Go 运行时的两个关键限制:

  1. 大量内存会导致垃圾收集器大量占用CPU
  2. 序每秒执行数十万个请求可能会导致Go运行时的网络后端在系统调用上出现瓶颈。

使用大量 RAM 实现垃圾收集

  • 如果您有大量 RAM 空间,请设置GOGC=500您的环境并尝试再次运行您的程序,看看它是否性能更好。上下调整该数字(默认为 100),以找到 GC 使用的 CPU 和 RAM 之间的适当平衡
  • 您还可以设置GOMEMLIMIT=100GiB或任何对您的应用程序有意义的限制,以在 Go 运行时达到使用中时强制执行 GC内存限制。

当我们调整垃圾回收器时,我们看到硬件的性能提升了约 2 倍,但内存使用量却增加了约 3.2 倍(在本例中,内存使用量从约 2.5GB 增加到约 8GB),CPU 使用率显著下降,从内存缓存中读取数据等简单内存操作的 p99 延迟大幅减少。

Go 运行时并发限制 - EPoll 和套接字
Go 的网络后端使用基于EPoll的系统,称为Netpoll
Netpoll 代表您的 Go 程序处理与网络相关的系统调用,并进行了大量优化,以确保您的程序获得新的字节。

Netpoll 实现调用 EPoll 并一次性获取一大块“就绪”套接字。
在大多数情况下,对于较小的机器,几十个套接字可能会同时变得可用,因此 Netpoll 可以抓住它们并在单个系统调用的范围内使用它们,从而为 Go 运行时和您的应用程序授予更多的执行时间。

然而,在我们的大型系统上,我们每秒通过数千个 TCP 套接字向 ScyllaDB 集群发出数十万个请求,这些请求的时间不到 1 毫秒。因此,在给定的 EPoll 调用中,可能有一千个或更多 TCP 套接字可供我们的应用程序拾取。

不幸的是,Netpoll在一次 EPoll 调用中最多只能缓冲 128 个套接字,这意味着我们必须进行多次 EPoll 调用才能获取所有可用的套接字。

在 CPU 配置文件中,这表现为将近 65% 的 CPU 时间花费在 runtime.netpoll 中的 syscall.EpollWait 上!

事实证明,这成为扩展 I/O 密集型 Golang 应用程序的一个重要瓶颈,并导致在大规模垂直规模运行单个 Go 二进制文件时应用程序性能非常差。

解决Netpoll epoll困境
为了解决这个问题:我们需要在每个主机上运行更多数量的 Go 运行时,并将它们各自的网络 I/O 工作负载减少到 Go 运行时可以管理的范围。

  • 在每台主机上运行了更多的Go运行时,并将网络I/O负载分散到多个容器中,从而提高了性能和吞吐量。