WIP 介绍高并发场景下的 design 和 troubleshotting。
如果将普通的程序比作是经典力学,那么研究高并发系统有点类似于研究热力学。当成千上万个过程彼此交互、竞争、等待,在有限的集群资源中将会产生不一样的场景。
Tools
这些工具主要是:
- O11y
Trace 任意函数的执行时间
该方案整理自某同事的 idea。
考虑下面的场景,我们需要查看某动态链接库 /path/to/libtiflash_proxy.so
中 handle_pending_applies
函数每次调用的耗时。
1 | perf probe --del 'probe_libtiflash_proxy:*' |
分析某段时刻的调用栈
生成火焰图
Intuition
阻塞
有一些典型的特征:
- 时间偏移
比如某些周期性的时间,如日志等,突然失去周期性,而在一瞬间打印了很多。
注意,阻塞的原因有很多:
- 不能正确解耦逻辑和 IO
- 队列积压,工作线程数偏少,导致对于每个任务而言,自己的等待时间会越来越大,似乎自己正在被阻塞
解耦 IO
如下所示,一瞬间打印了很多发送消息的日志,实际上这是由于没有正确解耦消息发送逻辑和 IO 逻辑。导致 IO 阻塞了同一个线程,从而积攒了大量的消息。特别地,如果因此产生了消息延迟,可能会雪崩。
在这个场景下,虽然我们使用了 ClickHouse 的线程池来处理异步的 IO,但由于线程池的队列大小过小,并且也没有指定等待超时时间,所以我们以为的异步实际上变成了同步。
在解耦 IO 时,常常会将一些消息或者写入缓存到 Queue 或者 WriteBatch 以攒批 IO。对于这样的情况,在判断时首先需要检查对象是否已经被真的发送或者写入,因为这会导致后续完全不同的调查方向。
雪崩
可能发生在基于消息传输进行同步的系统中。不妨以 Raft 为例,如果因为一些阻塞的原因,一些发给 Follower 的 Append 消息没有被及时处理,很可能该 Follower 就会认为 Leader 挂了,从而发起选举。而选举会产生更多的消息,从而导致消息进一步积压,甚至会扩散到其他正常的 Region 中。
概率
在某个接口被高频调用时,
退出
当一个程序异常退出,但看不到异常日志时,考虑:
- 日志服务是否未初始化,或者该段异常日志被直接打印到标准错误
- 该程序是否由于异常信号/OOM退出
可以从 return code 或者 dmesg 或者 coredump 或者 stderr 等信息来看。 - 该程序是否主动退出
在一些程序中,会针对一些异常情况直接调用 exit 退出程序。此时可以用 gdb 去 hook_exit
函数来查看退出时的堆栈。
调度
饥饿
一些程序会基于时间片的算法来进行调度。一些实现会从任务队列中取出所有等待的任务,执行这些任务,再检查是否超出时间片。如果执行这些任务本身的时间就比较长,甚至可能占用多个时间片,这就会导致调度算法接近于失效。
缓存
刷盘
一般写入的阶段可以分为:
- write batch
- WAL
有的系统能容许一定的数据丢失,或者有其他手段起到 WAL 的作用,则可以去掉。 - memtable
- disk