6

流控和 OOM

 1 year ago
source link: https://www.zenlife.tk/flow-control-and-oom.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

流控和 OOM

2022-04-26

OOM 是这边一个老大难的问题了。刚开始我觉得解这个问题,必须做成手动内存管理。看了更多的 case 之后,发现并不仅仅是手动内存管理这么简单。

带垃圾回收的语言,托管内存无法精细化控制释放时机,并不是当一块内存不被使用之后,立刻就释放的。内存的释放需要等待 GC 的时机,所以这个过程很不可控。 另外,由于内存的延迟释放,进程占用的比实际使用的会多一些,像 Go 由 GOGC 参数控制,分配的内存到实际使用的两倍的时候,才触发回收。这意味着,32G 的机器上面,一个 Go 进程实际使用 16G 都有可能出现 OOM,这个浪费还是比较大的。小内存机器上面 OOM 问题变得更加严重。

让进程 OOM 的可以分为常驻内存和非常驻内存。 常驻内存像比如 tidb 统计信息,可能大几个G 的统计信息,这个必须加载到内存中,如果在 4G 的云环境,那这种情况可能就无解了。 非常驻内存的 OOM 比如大查询,一个大的 join 操作之类的。这块之前的处理方式是落盘,尽量不要 OOM 掉。

还有一种 OOM 是内存泄漏导致的,比如 goroutine 泄漏了一直占用着内存,或者是大块内存的分配由于有引用而一直无法释放的情况。这种一般都还好解决,尤其是 goroutine 的,只要查出来是哪儿泄漏了,修复掉就行。

后面遇到的一些 case 更复杂一些,举个例子,我们限制了一个查询使用的总的内存量,当触发到 memory quota 的时候,就触发 tidb 停止像 tikv 读取数据,让上层算子先处理掉缓存中的数据 – 但还是 OOM 了。为什么?因为上层有可能有 join 这一类的算子,它会使输入膨胀,从而消耗更多的内存。 即使下层暂停住,上层算子的膨胀还是引起 OOM 了。 又或者再举一个例子,即使一个大查询,触发到了落盘,也有可能,由于落盘的速度较慢,而从 tikv 读取数据的速度较快,数据积压到缓冲区,之后仍然 OOM 掉了。

所以我发现这个场景是一个流控问题!

抽象出来,就是生产者与消费者的速率不匹配,生产者生产得较快,而消费者消费得慢,于是数据屯积在缓存区,缓冲区耗内存越来越多,最后 OOM 的。

流控问题立马联想到限流,限流最常见的漏桶和令牌桶算法。如果用漏桶,对生产者设置一个缓存区的大小上限,当缓冲区写满了,就停止生产者,这样可以容忍一定的突发流量。但是缓冲区的大小不太好定。 还有就是对 distsql 请求的场景,实际上我们是并发发送请求的,也就是生产者并发度还会有影响,并发会导致很多还在“途中”的请求,这些请求返回的数据也是占很大块内存的。

如果用令牌桶就是每隔一定的周期,生成一定数量的令牌。生产者需要拿到令牌才能执行,从而限制生产者的速率。但是这个速率也是不好确定的,需要根据消费者的消费速率来定。消费者的消费速度其实是不好量化的,每秒消费多少内存?

所以实际上需要一种消费者能反馈生产者的限流模式。我又刻又想到了 tcp 协议。

tcp 协议的滑动窗口,实际上就是一个让生产者消费者速率同步的机制。 生产者和消费者(发送方/接收方)都有自己的缓存区队列,接受方 ack 之后,发送方就可以继续传数据,否则如果滑动窗口满了,就得停下来。 但是滑动窗口大小应该设置为多少呢? tcp 协议其实是通过 "乘法减少" "加法增大" 来沟通协议的,当发现窗口增加的太大,出现拥塞丢包的时候,就将窗口减半,通过这样的方式来探测出合适的窗口大小。

tcp 的思想很好,但是直接用到我们的场景还是有点困难,暂时我还没想明白。但是能确认的是,需要某种"背压"机制,让消费者反馈生产者慢下来,才能够有效地流控,避免 OOM。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK