一直以来我都相信熟练地使用各种小工具能大大提升生产力,把人们从无聊的机械工作中解脱出来。昨天我就在做一些无聊的机械工作,正好这些技能就派上了用场。我觉得可以把它分享出来,或许就会有年轻人被安利了。

无聊的机械工作

我们写了一个数据库,昨天我要做的事情是在不同的实验设定下运行标准的 TPC-C 测试。我们用的 TPC-C 测试程序是 py-tpcc,每次运行它会产生如下格式的结果:

==================================================================================================
Execution Results after 60 seconds
--------------------------------------------------------------------------------------------------
                          Executed                Time (µs)               Rate
  DELIVERY                722                     30254463.91105652       23.86 txn/s
  NEW_ORDER               8680                    21493624.687194824      403.84 txn/s
  ORDER_STATUS            765                     401196.0029602051       1906.80 txn/s
  PAYMENT                 8276                    5778122.663497925       1432.30 txn/s
  STOCK_LEVEL             774                     1124487.1616363525      688.31 txn/s
--------------------------------------------------------------------------------------------------
  TOTAL                   19217                   59051894.426345825      325.43 txn/s

我首先要做的就是跑实验,然后把上面 txn/s 前面的这6个数字粘贴到 Google Docs 上。同一个实验需要跑多次,这样我们才能给出一个误差的范围。再加上有若干组不同的实验设定,其实数据点数还蛮多的。

一开始我的做法就是跑实验,然后两个窗口不停切换复制粘贴。做了两三次之后觉得实在是太憋屈了,感觉整天的时间都要耗在这上面。大好时光竟然来做这个?还不如睡觉呢!于是我决定稍微花点时间让它自动化进行。

自动运行实验

第一步就是要让实验能够自己运行,然后把结果保存好。花5分钟时间写好这么一个小脚本,保存成 run-all.sh

run_mongodb() {
    clients=$1

    cd ~/tmp/apavlo-pytpcc/pytpcc
    ./tpcc.py --config=mongodb.config --duration 60 --clients $clients --warehouses 1 mongodb >> result-w1-mongodb-denorm-$clients.txt

    printf "use tpcc\n db.dropDatabase()\n" | mongo
    sleep 10
}

run() {
    run_mongodb "$@"
    run_mongodb "$@"
    run_mongodb "$@"
    run_mongodb "$@"
    run_mongodb "$@"
}

run 1
run 2
run 5
run 10

这里我定义了两个函数:run_mongodb 会运行一次实验,实验的参数实际上只有一个——客户端数量——通过 $1 传入。实验的结果每次会追加到一个文本文件里面,这个文本文件的文件名对于同样的实验参数是一样的,也就是说,同样的实验会把结果保存在同一个文件中。另一个函数 run 就很简单了,把同样参数的实验跑5遍。最后写上我们想要运行的实验参数,这里写了分别测试1个、2个、5个和10个客户的情况。

运行 bash run-all.sh,看一下没出什么问题,就可以去玩耍了。

提取实验数据

做了顿饭回来实验就跑好了,并且每组实验都保存在各自的文件中。下一步的工作就是把实验数据粘贴到 Google Docs 上面。这里的基本思路是,把数据提取出来,转换成 CSV 格式。这样一来就可以用表格处理软件打开 CSV 文件,复制,然后粘贴到 Google Docs 上面了。

观察一下 pytpcc 的输出,我们会发现要把其中的数据转换成 CSV 格式还是很简单的:

  1. 从文件一行一行读入
  2. 按照空白字符分割成若干个部分
  3. 如果第一个部分不是 DELIVERY 等6个关键词,就跳过
  4. 如果是的话,把分割后的第4个部分取出来,这就是我们需要的数据
  5. 特殊处理一下 TOTAL,在遇到这一行的时候我们在输出里面添加一个换行,表示一次实验结束了

很容易地就可以写出下面的 Python 代码:

import sys

with open(sys.argv[1]) as f:
    for line in f:
        split = line.strip().split()
        if not split:
            continue
        if split[0] in ['DELIVERY', 'NEW_ORDER', 'ORDER_STATUS', 'PAYMENT', 'STOCK_LEVEL']:
            sys.stdout.write(split[3])
            sys.stdout.write(',')
        elif split[0] == 'TOTAL':
            sys.stdout.write(split[3])
            sys.stdout.write('\n')

非常幸运的是,昨天我写下这个代码之后,直接运行就得到了我想要的结果:

$ python3 read_result.py ~/tmp/apavlo-pytpcc/pytpcc/result-w1-mongodb-denorm-10.txt
10.15,52.02,162.75,103.98,65.60,62.64
10.14,52.74,158.00,105.61,64.93,63.34
10.11,52.25,159.85,104.15,64.26,62.80
10.32,52.55,152.49,105.50,65.46,63.55
10.20,52.62,161.14,105.56,65.41,63.47

下面的事情就是对所有的结果都运行一遍这个小脚本,用 find 就行了:

$ find . -name 'result-*.txt' -print -exec python3 read_result.py {} \;
./result-mongodb-tx-5.txt
6.56,87.02,67.13,56.97,43.89,48.88
6.86,89.11,62.66,40.82,44.81,44.41
6.51,88.26,58.73,57.49,44.01,48.43
3.08,90.08,66.50,58.72,45.89,35.54
6.48,88.07,63.08,58.27,43.21,49.31
./result-mongodb-tx-2.txt
8.06,97.98,71.89,69.76,53.31,58.84
8.30,101.53,55.65,69.67,53.62,60.42
8.01,99.31,77.18,68.49,52.79,58.75
7.97,100.58,75.25,69.49,52.39,59.36
3.99,99.87,57.47,51.13,56.04,41.89
...

值得注意的是,find 的输出是无序的,对我们来说要粘贴到 Google Docs 上面汇总当然是希望有序的比较好整理。第一反应没有想到特别简练的命令来解决这个问题,不过简单拼凑一下也是很容易:for x in `find . -name 'result-*.txt' -print | sort -V`; do echo $x; python3 read_result.py $x; done,也是一行就搞定。解释一下:find . -name 'result-*.txt' -print | sort -V 先执行 find 找出所有符合要求的文件,然后管道传给 sort 进行排序,其中 -V 可以让 result-10.txt 排到 result-2.txt 后面,这样我们就得到了有序的文件名列表了。外面是一个循环,依次遍历文件名列表,把当前文件名放在 $x 中,在循环体中先打印出文件名再执行 python 脚本。

把上面命令的输出保存成文件,并把后缀命名为 .csv,就可以愉快地用表格软件打开啦。

处理实验数据

把上面数据贴到 Google Docs 之后,我把其中 TOTAL 的那一列数据复制下来。现在我希望先把数据5个5个地分组(也就是同一个设定的多次试验)。说实在的,我虽然现在用习惯了 Sublime Text,但是我还真不知道怎么在里面实现这个效果。不过没关系,我可以回到老朋友 Vim,用宏可以轻松地解决:

  • qa 开始宏录制,保存成宏 a
    • $ 跳到行末
    • Ctrl + v 开始块选择
    • jjj 选择4行
    • Shift + a 在每行的行尾插入
    • , 我想插入逗号
    • ESC 回到普通模式
    • V 开始行选择
    • jjjj 向下选择5行
    • J 合并这5行
    • j 走到下一行(也就是下一组数据的起点)
  • q 退出宏录制
  • 34@a 重复执行34次宏 a

这样数据按照每行5个分好了。后面我就回到 Sublime Text 里面,把它处理成 Python 能处理的格式。同样,块选择依然是一个非常好用的功能。

最后,简单地调用一下 matplotlib 就可以把图画出来了。

#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import collections

DATA = collections.OrderedDict([
    ...
])


plt.style.use('ggplot')
plt.xlabel('The Number of Concurrent Clients')
plt.ylabel('Transactions per Second')

for i, (title, data) in enumerate(DATA.items()):
    xs = np.array([t[0] for t in data])
    ys = np.array([np.mean(t[1]) for t in data])
    es = np.array([np.std(t[1]) for t in data])
    bar = plt.errorbar(xs, ys, yerr=es, label=title)

plt.legend()
plt.tight_layout()
plt.savefig('tpcc.pdf')

总结

  1. 善用各种工具可以提升工作效率,甚至熟悉快捷键也很重要(当然,要到熟练掌握还是需要不断的练习的)
  2. 不需要要求自己精通每一个工具,只需要懂得常用的几个用法就好
  3. 把不同的工具组合在一起可以发挥巨大的功效