TiFlash 性能测试的一个场景

我们需要测试在有大量活跃 Region 情况下 TiFlash 的性能,具体负载是对一个大表压 update where。因为原有的测试工具需要加载全量数据到内存,并且只能单线程运行,所以重新做了一个专门的压测工具。这个工具是从 N 条数据中 sample 出 K 个出来,并启动多个 worker 发送 SQL 命令。

本文讲述在压测过程中发现的几个现象,并讲述作为工程师如何快速定位集群中出现的这些。

第一个问题

第一个问题很好解决,原因是我用了 mysql 这个 crate,而它是阻塞的。这样我们开了很多线程,从而带来了很多的上下文切换。后面替换成 async-mysql 并且基于 tokio runtime,这样当任务需要等待的时候只会 yield,原线程还可以接着做其他任务。

第二个问题

现在 N = 500m,b = 10k,它们是尽量平均存放的。因为压测机器内存的限制,它只能选取 N 中的 K 个数据作为集合,然后在这个集合上 sample 构造请求。因为 cpu 的限制,一次只能 sample i 个作为请求发送。现在观察到下面的现象:

  1. 如果 k 较小,则这些请求分布的 region 很不均匀。反之,则分布更均匀。
    这里是因为如果 region 分布均匀,那么同样数量的请求会覆盖更多的 region,从而导致更多 region 被激活。而集群的某个指标和 region 被激活的数量有关。所以我们通过这个性质判断是否分布均匀。
  2. 请求的 QPS 是稳定的。

现在调查这个问题,首先是进行统计学的分析,k 的大小会不会影响对 k 进行二次抽样得到的 i 个数据的分布均匀度呢?不得而知,但可以考虑一个简单的问题,也就是从 N 中抽 K 个,那么能覆盖多少个 region?得到下面的期望 E(cover)

$$
E(cover) = b (1 - (\frac{b - 1}{b})^k)
$$

从这个期望看到,只要 k 达到 N 的 1%,那么就基本能够覆盖全部的 region 了。那对于有 N 和 i 的场景,就借助于模拟进行验证。通过模拟的验证,上面的期望是也是使用的。

如果既然直接 sample i 和在 k 中 sample i 是一样的,也就是不影响均匀程度,那么上面观察的结果是非预期的。这是因为对于同样的 i,k 不同,均匀程度不同,于是怀疑代码实现。特别地,如果数据处理算法没有问题,那么会首先考虑数据本身的问题。

我们的数据是从 n 个数据中抽样 k 个得到的,抽样算法是蓄水池算法。于是怀疑蓄水池算法的实现。果不其然,我发现 evict 的判定被我写反了,应该是满足 k / j 的情况下,会 evict,结果我按照不 evict 处理了。

综上,对于这种因为随机数写错了,导致的非 panic 的 bug。我们通过先统计学建模,再计算机模拟验证的方式,证明了数据生成过程是不符合预期的,进而找到了 bug。

第三个问题

在解决这个问题之后,请求确实均匀了。但并发还是上不去,只有 1K+ QPS,检查发现是 TiKV 磁盘满了,导致限流。

第四个问题

在解决上面三个问题后,压测的 SQL 可以达到 3K+ QPS。但进一步观察到另一个奇怪的现象。当 K 为 N 的 1% 的时候,是打到了 3K 并发,但是进一步增大 K,并发数反而降低了。调节压测程序的参数,发现压不上去,因此判定是集群的问题。

这里让人奇怪的点是,K 无论是 1% 还是 10% 的 N,它的 E(cover) 都已经能覆盖所有 region 了。那为什么 QPS 会不一样?

出于第三个问题的经验,首先查看了 TiKV 的写入指标。这里 15.15 对应的 10% 的情况,15.25 对应的是 1% 的情况。

可以看出两个情况下 SSD 都满了,那么为啥 QPS 还能分出个高下呢?同事说 procinfo::pid::io_task 测出来是 100%,但不意味着 SSD 到了瓶颈。

进一步查看写入,可以发现前台写入确实拉开了不小的差距。

总体的写入上来看,也看不出两个负载之间有什么特别大的区别。

比较 io,可以看到前半段 write iops 稍微高一点或者相等,但是 write bandwidth 明显低,考虑是有较多的小写入。难道是因为前面的写入更散导致的?

IOPS IO bandwidth

后面,大佬同事给出一个观察,他认为 QPS 的问题可能是因为读取慢而不是写入慢导致的。确实从下面的监控来说,后半段读取耗时少了很多。

他也发现了 running task 出现了堆积。也就是入得多出得少。并且 1 和 2 两台机器尤其吃紧。

进一步检查 qps,发现不同机器之间的写 qps 差别也比较大。读的 qps 差距不大,但这可能是因为 lb 的原因,例如如果有机器是 15kops,那么其他机器计算能到 30kops 也会被限制在 15kops 的。

关于写入的问题,检查 compaction,发现非 L0 到 L1 的 Major Compaction 很多。从而怀疑是否是因为第二个负载的池子比较有限,最多就涉及总共的 1% 的数据,而第一个池子的负载有总共池子的 10% 的数据。导致第二个池子的 hot key 实际上很少,compaction 不会到很下层。但因为第一个负载的 Compaction 没有明显多余第二个负载,所以这个怀疑应该也是不正确的。

这里大佬又观察到 Block Cache Hit 上第二个负载明显高于第一个负载。这能解释一部分问题了。因为第一个池子是 10% 的样本,写入更散,Block Cache 容易被打穿到磁盘,所以读就变慢了。而我们的 update where 又是需要读的。

这也解释了为什么要从 1Kops 跑一段时间才能到 3Kops,其实是在预热缓存。

Reference