性能调优所使用的工具

使用 iostat 定位磁盘问题

在一个性能测试集群,我们选择了 AWS c3.4xlarge 机型,主要是为了在一台机器的两块盘上面分别跑 TiKV。在测试一段时间之后,我们发现有一台 TiKV 响应很慢,但是 RocksDB 并没有相关的 Stall 日志,而且慢查询也没有。

于是我登上 AWS 机器,使用 iostat -d -x -m 5 命令查看,得到如下输出:

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
xvda              0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00
xvdb              8.00 12898.00  543.00  579.00    31.66    70.15   185.84    51.93   54.39    7.03   98.79   0.60  66.80
xvdc              0.00     0.00  206.00 1190.00    10.58   148.62   233.56   106.67   70.90   13.83   80.78   0.56  78.40

上面发现,两个盘 xvdb 和 xvdc 在 wrqm/s 上面差距太大,当然后面一些指标也有明显的差距,这里就不在详细的解释 iostat 的输出。只是需要注意,大家通常将目光注意到 util 上面,但有时候光有 util 是反应不了问题的。

于是我继续用 fio 进行测试,(fio命令用于对磁盘进行压力测试)

fio -ioengine=libaio -bs=4k -direct=1 -thread -rw=write -size=10G -filename=test -name="PingCAP max throughput" -iodepth=4 -runtime=60

发现两个盘的写入有 2 倍的差距,xvdb 的写入竟然只有不到 70 MB,而 xvdc 有 150 MB,所以自然两个 TiKV 一个快,一个慢了。

对于磁盘来说,通常我们使用的就是 iostat 来进行排查,另外也可以考虑使用 pidstat,iotop 等工具。

使用 perf 定位性能问题

RC3 最重要的一个功能就是引入 gRPC,但这个对于 rust 来说难度太大。最开始,我们使用的是 rust-grpc 库,但这个库并没有经过生产环境的验证,我们还是胆大的引入了,只是事后证明,这个冒险的决定还是傻逼了,一些试用的用户跟我们反映 TiKV 时不时 coredump,所以我们立刻决定直接封装 c gRPC。因为现在大部分语言 gRPC 实现都是基于 c gRPC 的,所以我们完全不用担心这个库的稳定性。

在第一个版本的实现中,我们发现,rust 封装的 c gRPC 比 C Plus Plus 的版本差了几倍的性能,于是我用 perf stat 来分别跑 C Plus Plus 和 rust 的benchmark,得到类似如下的输出:

Performance counter stats for 'python2.7 tools/run_tests/run_performance_tests.py -r generic_async_streaming_ping_pong -l c++':

     216989.551636 task-clock (msec)         #    2.004 CPUs utilized
         3,659,896 context-switches          #    0.017 M/sec
             5,078 cpu-migrations            #    0.023 K/sec
         4,104,965 page-faults               #    0.019 M/sec
   729,530,805,665 cycles                    #    3.362 GHz
    stalled-cycles-frontend
    stalled-cycles-backend
   557,766,492,733 instructions              #    0.76  insns per cycle
   121,205,705,283 branches                  #  558.579 M/sec
     3,095,509,087 branch-misses             #    2.55% of all branches

     108.267282719 seconds time elapsed
 

上面是 C Plus Plus 的结果,然后在 rust 测试的时候,我们发现 context-switch 是 C Plus Plus 的 10 倍,也就是我们进行了太多次的线程切换。刚好我们第一个版本的实现是用 rust futures 的 park 和 unpark task 机制,不停的在 gRPC 自己的 Event Loop 线程和逻辑线程之前切换,但 C Plus Plus 则是直接在一个 Event Loop 线程处理的。于是我们立刻改成类似 C Plus Plus 架构,没有了 task 的开销,然后性能一下子跟 C Plus Plus 的不相伯仲了。

当然,perf 能做到的还远远不仅于此,我们通常会使用火焰图工具,关于火焰图,网上已经有太多的介绍,我们也通过它来发现了很多性能问题,这个后面可以专门来说一下。

使用 strace 动态追踪

因为我们有一个记录线程 CPU 的统计,通常在 Grafana 展示的时候都是按照线程名字来 group 的,并没有按照线程 ID。但我们也可以强制发送 SIGUSR1 信号给 TiKV 在 log 里面 dump 相关的统计信息。在测试 TiKV 的时候,我发现 pd worker 这个 thread 出现了很多不同线程 ID 的 label,也就是说,这个线程在不停的创建和删除。

要动态追踪到这个情况,使用 strace -f 是一个不错的方式,我同时观察 TiKV 自己的输出 log,发现当 TiKV 在处理分裂逻辑,给 PD worker 发送 message 的时候,就有一个新的线程创建出来。然后在查找对应的代码,发现我们每次在发消息的时候都创建了一个 tokio-timer,而这个每次都会新创建一个线程。

有时候,也可以使用 strace -c 来动态的追踪一段时间的系统调用。在第一版本的 rust gRPC 中,我们为了解决 future task 导致的频繁线程切换,使用 gRPC 自己的 alarm 来唤醒 Event Loop,但发现这种实现会产生大量的信号调用,因为 gRPC 的 alarm 会发送一个实时信号用来唤醒 epoll,后面通过火焰图也发现了 Event Loop 很多 CPU 消耗在 alarm 这边,所以也在开始改进。

这里需要注意,strace 对性能影响比较大,但对于内部性能测试影响还不大,不到万不得已,不建议长时间用于生产环境。

小结

上面仅仅是三个最近用工具发现的问题,当然还远远不止于此,后续也会慢慢补上。其实对于性能调优来说,工具只是一个辅助工具,最重要的是要有一颗对问题敏锐的心,不然即使工具发现了问题,因为不敏锐直接就忽略了。我之前就是不敏锐栽过太多的坑,所以现在为了刻意提升自己这块的能力,直接给自己下了死规定,就是怀疑一切能能怀疑的东西,认为所有东西都是有问题的。即使真的是正常的,也需要找到充足的理由去验证。

 

火焰图

在 前一篇 文章,我们简单提到了 perf,实际 perf 能做的事情远远不止这么少,这里就要好好介绍一下,我们在 TiKV 性能调优上面用的最多的工具 - 火焰图。

火焰图,也就是 FlameGraph,是超级大牛 Brendan Gregg 捣鼓出来的东西,主要就是将 profile 工具生成的数据进行可视化处理,方便开发人员查看。我第一次知道火焰图,应该是来自 OpenResty 的章亦春介绍,大家可以详细去看看这篇文章《动态追踪技术漫谈》。

之前,我的所有工作在很长一段时间几乎都是基于 Go 的,而 Go 原生提供了很多相关的 profile 工具,以及可视化方法,所以我没怎么用过火焰图。但开始用 Rust 开发 TiKV 之后,我就立刻傻眼了,Rust 可没有官方的工具来做这些事情,怎么搞?自然,我们就开始使用火焰图了。

使用火焰图非常的简单,我们仅仅需要将代码 clone 下来就可以了,我通常喜欢将相关脚本扔到 /opt/FlameGraph 下面,后面也会用这个目录举例说明。

一个简单安装的例子:

Copy

  wget https://github.com/brendangregg/FlameGraph/archive/master.zip
  unzip master.zip
  sudo mv FlameGraph-master/ /opt/FlameGraph

CPU

对于 TiKV 来说,性能问题最开始关注的就是 CPU,毕竟这个是一个非常直观的东西。

当我们发现 TiKV CPU 压力很大的时候,通常会对 TiKV 进行 perf,如下:

  perf record -F 99 -p tikv_pid -g -- sleep 60
  perf script > out.perf

上面,我们对一个 TiKV 使用 99 HZ 的频繁采样 60 s,然后生成对应的采样文件。然后我们生成火焰图:

/opt/FlameGraph/stackcollapse-perf.pl out.perf > out.folded
/opt/FlameGraph/flamegraph.pl out.folded > cpu.svg

上面就是生成的一个 TiKV 火焰图,我们会发现 gRPC 线程主要开销在 c gRPC core 上面,而这个也是现在 c gRPC core 大家普遍反映的一个问题,就是太费 CPU,但我相信凭借 Google gRPC team 的实力,这问题应该能够搞定。

另外,在 gRPC 线程上面,我们可以发现,protobuf 的编解码也占用了很多 CPU,这个也是现阶段 rust protobuf 库的一个问题,性能比较差,但幸好后面的办法有一个优化,我们准备马上采用。

另外,还需要注意,raftstore 线程主要的开销在于 RocksDB 的 Get 和 Write,对于 TiKV 来说,如果 raftstore 线程出现了瓶颈,那么整个 Raft 流程都会被拖慢,所以自然这个线程就是我们的重点优化对象。

可以看到,Get 的开销其实就是我们拿到 Raft 的 committed entries,然后扔给 apply Raft log 线程去异步 apply,所以自然这一步 Get 自然能扔到 apply worker 去处理。另外,对于 Write,鉴于 Raft log 的格式,我们可以非常方便的使用 RocksDB 一个 insert_with_hint 特性来优化,或者将 Write 也放到另一个线程去 async 处理。

可以看到,我们通过火焰图,能够非常方便的发现 CPU 大部分时间开销都消耗在哪里,也就知道如何优化了。

这里在说一下,大家通常喜欢将目光定在 CPU 消耗大头的地方,但有时候一些小的不起眼的地方,也需要引起大家的注意。这里并不是这些小地方会占用多少 CPU,而是要问为啥会在火焰图里面出现,因为按照正常逻辑是不可能的。我们通过观察 CPU 火焰图这些不起眼的小地方,至少发现了几处代码 bug。

Memory

通常大家用的最多的是 CPU 火焰图,毕竟这个最直观,但火焰图可不仅仅只有 CPU 的。我们还需要关注除了 CPU 之外的其他指标。有一段时间,我对 TiKV 的内存持续上涨问题一直很头疼,虽然 TiKV 有 OOM,但总没有很好的办法来定位到底是哪里出了问题。于是也就研究了一下 memory 火焰图。

要 profile TiKV 的 memory 火焰图,其实我们就需要监控 TiKV 的 malloc 分配,只要有 malloc,就表明这时候 TiKV 在进行内存分配。因为 TiKV 是自己内部使用了 jemalloc,并没有用系统的 malloc,所以我们不能直接用 perf 来探查系统的 malloc 函数。幸运的是,perf 能支持动态添加探针,我们将 TiKV 的 malloc 加入:

Copy

perf probe -x /deploy/bin/tikv-server -a malloc

然后采样生成火焰图:

Copy

perf record -e probe_tikv:malloc -F 99 -p tikv_pid -g -- sleep 10
perf script > out.perf
/opt/FlameGraph/stackcollapse-perf.pl out.perf > out.folded
/opt/FlameGraph/flamegraph.pl  --colors=mem out.folded > mem.svg

上面是生成的一个 malloc 火焰图,我们可以看到,大部分的内存开销仍然是在 RocksDB 上面。

通过 malloc 火焰图,我们曾发现过 RocksDB 的 ReadOption 在非常频繁的调用分配,后面准备考虑直接在 stack 上面分配,不过这个其实对性能到没啥太大影响 ? 。

除了 malloc,我们也可以 probe minor page fault 和 major page fault,因为用 pidstat 观察发现 TiKV 很少有 major page fault,所以我们只 probe 了 minor,如下:

Copy

perf record -e minor-faults -F 99 -p $1 -g -- sleep 10
perf script > out.perf
/opt/FlameGraph/stackcollapse-perf.pl out.perf > out.folded
/opt/FlameGraph/flamegraph.pl  --colors=mem out.folded > minflt.svg

Off CPU

有时候,我们还会面临一个问题。系统的性能上不去,但 CPU 也很闲,这种的很大可能都是在等 IO ,或者 lock 这些的了,所以我们需要看到底 CPU 等在什么地方。

对于 perf 来说,我们可以使用如下方式采样 off CPU。

Copy

perf record -e sched:sched_stat_sleep -e sched:sched_switch \
    -e sched:sched_process_exit -p tikv_pid -g -o perf.data.raw sleep 10
perf inject -v -s -i perf.data.raw -o perf.data

但不幸的是,上面的代码在 Ubuntu 或者 CentOS 上面通常都会失败,主要是现在最新的系统为了性能考虑,并没有支持 sched statistics。 对于 Ubuntu,貌似只能重新编译内核,而对于 CentOS,只需要安装 kernel debuginfo,然后在打开 sched statistics 就可以了,如下:

Copy

dnf install kernel-debug kernel-debug-devel kernel-debug-debuginfo
echo 1 | sudo tee /proc/sys/kernel/sched_schedstats

然后生成 off cpu 火焰图:

Copy

perf script -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace | awk '
    NF > 4 { exec = $1; period_ms = int($5 / 1000000) }
    NF > 1 && NF <= 4 && period_ms > 0 { print $2 }
    NF < 2 && period_ms > 0 { printf "%s\n%d\n\n", exec, period_ms }' | \
    /opt/FlameGraph/stackcollapse.pl | \
    /opt/FlameGraph/flamegraph.pl --countname=ms --title="Off-CPU Time Flame Graph" --colors=io > offcpu.svg

上面就是 TiKV 一次 off CPU 的火焰图,可以发现只要是 server event loop 和 time monitor 两个线程 off CPU 比较长,server event loop 是等待外部的网络请求,因为我在 perf 的时候并没有进行压力测试,所以 wait 是正常的。而 time monitor 则是 sleep 一段时间,然后检查时间是不是出现了 jump back,因为有长时间的 sleep,所以也是正常的。

上面我说到,对于 Ubuntu 用户,貌似只能重新编译内核,打开 sched statistics,如果不想折腾,我们可以通过 systemtap 来搞定。systemtap 是另外一种 profile 工具,其实应该算另一门语言了。

我们可以直接使用 OpenResty 的 systemtap 工具,来生成 off CPU 火焰图,如下:

Copy

wget https://raw.githubusercontent.com/openresty/openresty-systemtap-toolkit/master/sample-bt-off-cpu
chmod +x sample-bt-off-cpu

./sample-bt-off-cpu -t 10 -p 13491 -u > out.stap
/opt/FlameGraph/stackcollapse-stap.pl out.stap > out.folded
/opt/FlameGraph/flamegraph.pl --colors=io out.folded > offcpu.svg

可以看到,使用 systemptap 的方式跟 perf 没啥不一样,但 systemtap 更加复杂,毕竟它可以算是一门语言。而 FlameGraph 里面也自带了 systemaptap 相关的火焰图生成工具。

Diff 火焰图

除了通常的几种火焰图,我们其实还可以将两个火焰图进行 diff,生成一个 diff 火焰图,如下:

Copy

/opt/difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff2.svg

但现在我仅仅只会生成,还没有详细对其研究过,这里就不做过多说明了。

总结

上面简单介绍了我们在 TiKV 里面如何使用火焰图来排查问题,现阶段主要还是通过 CPU 火焰图发现了不少问题,但我相信对于其他火焰图的使用研究,后续还是会很有帮助的。

你可能感兴趣的:(分布式,数据库)