查看原文
其他

Go 适合 IO 密集型?并不准确!

GoCN 2022-09-09

The following article is from 奇伢云存储 Author 奇伢



Go 程序适合什么样的场景?


你想过这个问题吗?Go 程序到底是哪种场景最适合。可能你已经听说过答案了:IO 密集型的场景。

什么是 CPU 密集型、IO 密集型?


计算机程序场景会分成 cpu 密集型 和 IO 密集型。CPU 密集型说的是一直在运算 CPU 指令,这种常见于算法运行喽。IO 密集型说的是经常下发 IO ,比如网卡,磁盘,或者其他外设。


Go 适合 IO 密集型,并不准确?


你肯定记住了答案:Go 适合 IO 密集型的场景。但其实这里并不准确。更准确的是 Go 适合的是网络 IO 密集型的场景,而非磁盘 IO 密集型。甚至可以说,Go 对于磁盘 IO 密集型并不友好


Go 对于 网络 IO 和磁盘 IO为什么会有差别?


根本原因:在于网络 socket 句柄和文件句柄的不同。网络 IO 能够用异步化的事件驱动的方式来管理,磁盘 IO 则不行。这个在我之前 Linux 句柄系列也详细提过这个。

socket 句柄可读可写事件都有意义,socket buffer 里有数据,说明对端网络发数据过来了,即满足可读事件。有 buffer 可以写,那么说明还能发送数据,满足可写事件。

所以 socket 的句柄实现了 .poll 方法,可以用 epoll 池来管理。文件句柄可读可写事件则没有意义,因为文件句柄理论上是永远都是可读可写的,不会阻塞调用。

所以文件的 .poll 一般是不实现的,所以自然也用不了 epoll 池来管理。而能否用 epoll 池来管理 fd 则决定了能否在 Go 里用 epoll 池 IO 复用的形式来实现 IO 并发。

socket 句柄可以设置为 noblocking (非阻塞的方式),这样当网络 IO 还未就绪的时候就可以在 Go 代码里把调度权切走,去执行其他协程,这样就实现了网络 IO 的并发

但是磁盘 IO 则不行,文件 IO 的 read/write 都是同步的 IO ,没有实现 .poll 所以也用不了 epoll 池来监控读写事件。所以磁盘 IO 的完成只能同步等待。

然而磁盘 IO 的等待则会带来 Go 最不能容忍的事情:卡线程接下来就来看看磁盘 IO 的 read/write 等系统调用的原理。


为什么 Go 不能容忍卡线程?


Go 的代码执行者是系统线程,也就是 G-M-P 模型的 M ,M 不断的从队列 P 中取 G(协程任务)出来执行。当 G 出现等待事件的时候(比如网络 IO),那么立马切走,取下一个执行。这样让 M 一直不停的满载,就能保证 Go 协程任务的高吞吐。

那么问题来了,如果某个 G 卡线程了,就相当于这个 M 被废了,吞吐能力就下降。如果 M 全卡住了那相当于整个程序卡死了。这个是 Go 绝对无法容忍的。

然而对于类似系统调用这种卡线程却是无法人为控制的。Go runtime 为了解决这个问题,就只能创建更多的线程来保证一直有可运行的 M 。

所以,你经常会发现,当系统调用很慢的时候,M 的数量会变多,甚至会暴涨。曾经,奇伢就遇到过,磁盘大量随机读,并且压力过载的情况,Go 程序线程数持续上涨,最终超过 1 万个被 panic 了。下面来看一下 Go 怎么处理这种系统调用的?


Go 的 read/write 系统调用


当文件句柄 read/write 的时候,走系统调用回上下包装两个函数:entersyscall,exitsyscall :

   entersyscall
   // 系统调用 read/write 
   exitssyscall

其中,这两个调用都是为了和 Go runtime 的调度逻辑做交互,协商解决。

entersyscall 的作用:

把当前 M 的 P 设置为 _Psyscall 状态,打上标识 解绑 P -> M 的绑定,但 M 还保留 P 的指针。

existsyscall 的作用:

由于 M 到 P 的指向还在,那么优先还是用原来的 P 如果原来的 P 被处理掉了,那么就去用一个新的 P ,如果还没有,那就把只能挂到全局队列了。

所以,你会发现,这里最重要的就是一个状态的标记。Go 的 sysmon(内部监控线程)发现有这种卡了超过 10 ms 的 M ,那么就会把 P 剥离出来,给到其他的 M 去处理执行,M 数量不够就会新创建。

系统调用的逻辑是属于 Go 程序外部代码,Go 用 entersyscall 和 exitsyscall 来包装一下,主要是和调度交互。

思考一下,cgo 好像也是外部代码,它又是怎么解决阻塞可能导致的问题呢?

也是用的 entersyscall 和 exitssyscall 来配合哦。


总结


  • Go 准确的说是适合网络 IO 密集型的场景;

  • 磁盘 IO 密集型可能会导致系统线程增多,甚至暴涨,超过 1 万个被 panic 也是可能的哦;

  • 系统调用的逻辑是属于 Go 程序外部代码逻辑,Go 用 entersyscall 和 exitsyscall 来包装一下,主要是和调度交互。



END



-猜你想看-

Cilium开源Tetragon – 基于eBPF的安全可观测性&运行时增强

或许,书应该一起读


想要了解Go更多内容,欢迎扫描下方👇 关注 公众号,回复关键词 [实战群]  ,就有机会进群和我们进行交流~

分享、在看与点赞,至少我要拥有一个叭~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存