高并发场景

WIP 介绍高并发场景下的 design 和 troubleshotting。

如果将普通的程序比作是经典力学,那么研究高并发系统有点类似于研究热力学。当成千上万个过程彼此交互、竞争、等待,在有限的集群资源中将会产生不一样的场景。

Tools

这些工具主要是:

  1. O11y

Trace 任意函数的执行时间

该方案整理自某同事的 idea。

考虑下面的场景,我们需要查看某动态链接库 /path/to/libtiflash_proxy.sohandle_pending_applies 函数每次调用的耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
perf probe --del 'probe_libtiflash_proxy:*'
BIN=/path/to/libtiflash_proxy.so
TOKEN=handle_pending_applies
ITER=0
objdump $BIN --syms | grep $TOKEN | awk '{print $6}' | while read -r tok ; do
ITER=$(expr $ITER + 1)
NAME=$TOKEN\_$ITER
echo $NAME, $TOKEN, $ITER, $TOKEN\_$ITER
perf probe -x $BIN --no-demangle $NAME=$tok
perf probe -x $BIN --no-demangle $NAME=$tok%return
done
perf record -e probe_libtiflash_proxy:\* -aR sleep 10
perf script -s perf-script.py

附上 perf-script.py

分析某段时刻的调用栈

生成火焰图

Intuition

阻塞

有一些典型的特征:

  1. 时间偏移
    比如某些周期性的时间,如日志等,突然失去周期性,而在一瞬间打印了很多。

注意,阻塞的原因有很多:

  1. 不能正确解耦逻辑和 IO
  2. 队列积压,工作线程数偏少,导致对于每个任务而言,自己的等待时间会越来越大,似乎自己正在被阻塞

解耦 IO

如下所示,一瞬间打印了很多发送消息的日志,实际上这是由于没有正确解耦消息发送逻辑和 IO 逻辑。导致 IO 阻塞了同一个线程,从而积攒了大量的消息。特别地,如果因此产生了消息延迟,可能会雪崩。
在这个场景下,虽然我们使用了 ClickHouse 的线程池来处理异步的 IO,但由于线程池的队列大小过小,并且也没有指定等待超时时间,所以我们以为的异步实际上变成了同步。

在解耦 IO 时,常常会将一些消息或者写入缓存到 Queue 或者 WriteBatch 以攒批 IO。对于这样的情况,在判断时首先需要检查对象是否已经被真的发送或者写入,因为这会导致后续完全不同的调查方向。

雪崩

可能发生在基于消息传输进行同步的系统中。不妨以 Raft 为例,如果因为一些阻塞的原因,一些发给 Follower 的 Append 消息没有被及时处理,很可能该 Follower 就会认为 Leader 挂了,从而发起选举。而选举会产生更多的消息,从而导致消息进一步积压,甚至会扩散到其他正常的 Region 中。

概率

在某个接口被高频调用时,

退出

当一个程序异常退出,但看不到异常日志时,考虑:

  1. 日志服务是否未初始化,或者该段异常日志被直接打印到标准错误
  2. 该程序是否由于异常信号/OOM退出
    可以从 return code 或者 dmesg 或者 coredump 或者 stderr 等信息来看。
  3. 该程序是否主动退出
    在一些程序中,会针对一些异常情况直接调用 exit 退出程序。此时可以用 gdb 去 hook _exit 函数来查看退出时的堆栈。

调度

饥饿

一些程序会基于时间片的算法来进行调度。一些实现会从任务队列中取出所有等待的任务,执行这些任务,再检查是否超出时间片。如果执行这些任务本身的时间就比较长,甚至可能占用多个时间片,这就会导致调度算法接近于失效。

缓存

刷盘

一般写入的阶段可以分为:

  1. write batch
  2. WAL
    有的系统能容许一定的数据丢失,或者有其他手段起到 WAL 的作用,则可以去掉。
  3. memtable
  4. disk

Reference