postgres-x2是一个基于pgsql、面向otlp的分布式数据库,采用了shared-nothing的架构,目标是针对oltp\olap应用能做到可扩展的系统。源码在github上:https://github.com/postgres-x2/postgres-x2 。
最近在针对 postgres-x2做压力测试。测试是在一台dell r510上进行的,该服务器上有4颗x5650和64g内存,另外是两块老式的ssd,型号不详,最大写入速度100m左右。
测试的版本是直接从github上拉下来的,直接编译安装。在这台服务器上安装了gtm、coordinator、datanode各一个,gtm和coordinator的数据目录都在一个ssd上,datanode的数据目录在另外的一个ssd盘上。以最大化利用磁盘资源。之所以在一台服务器上进行测试,主要是想模拟一个网络带宽不受限的环境,也是为了简化问题。
coordinator的几个重要配置如下:
conf代码
1. max_connections = 1024
2. shared_buffers = 2048mb # shared_buffer没有必要设置过大
3. checkpoint_segments = 64
4. checkpoint_timeout = 20min # 让checkpoint间隔尽量大
5. logging_collector = on
6. autovacuum = off
7. min_pool_size = 100 # 连接池初始大小
8. max_pool_size = 1024
datanode的配置如下
1. max_connections = 1024
2. shared_buffers = 20480mb
3. vacuum_cost_delay = 10
4. vacuum_cost_limit = 10000
5. checkpoint_segments = 256
6. checkpoint_timeout = 10min
7. logging_collector = on
8. autovacuum_vacuum_cost_delay = 5ms
测试的主要方法是使用pgbench生成scale为1000的数据集合,大概有16g,主要的测试方法就是先执行checkpoint,将数据块刷回磁盘,以减小checkpoint的影响,然后执行下面的命令:
bash代码
1. pgbench -c n -j n -t 60
n是连接数,这个操作会模拟并发的n个客户连续访问数据库60秒,每个客户需要在60秒内不停执行一个有着5条sql的事务:
1. begin;
2. update pgbench_tellers set tbalance = tbalance + $1 where tid = $2;
3. update pgbench_branches set bbalance = bbalance + $1 where bid = $2;
4. select abalance from pgbench_accounts where aid = $1;
5. insert into pgbench_history (tid, bid, aid, delta, mtime) values ($1, $2, $3, $4, current_timestamp);
6. update pgbench_accounts set abalance = abalance + $1 where aid = $2;
7. end;
测试的时候,n从32增大到768,取tps。结果如下:

随着连接数的增加,tps几乎是线性降低。datanode数据目录所在的磁盘使用率也基本上从最高的60%降低到了20%,而整体的cpu使用率不怎么飙升,只是gtm看起来还比较忙。
感觉gtm比较有问题,于是祭出了gprof来做性能分析,结果啥都没有,google一下,发现是gtm屏蔽了sigprof信号,打上我的这个patch就ok了:
c代码
1. diff --git a/src/gtm/libpq/pqsignal.c b/src/gtm/libpq/pqsignal.c
2. index e3f482c..594540f 100644
3. --- a/src/gtm/libpq/pqsignal.c
4. +++ b/src/gtm/libpq/pqsignal.c
5. @@ -119,6 +119,10 @@ pqinitmask(void)
6. sigdelset(&blocksig, sigcont);
7. sigdelset(&authblocksig, sigcont);
8. #endif
9. +#ifdef sigprof
10. + sigdelset(&blocksig, sigprof);
11. + sigdelset(&authblocksig, sigprof);
12. +#endif
记得使用--enable-profiling选项来重新生成makefile 或是直接编辑makefile加上 -pg选项,然后重新编译一下gtm。拿pgbench运行一段时间之后,停掉gtm,在gtm的目录下会有一个gmon.out,执行:
1. gprof -b /usr/local/pgx2/bin/gtm gmon.out > gtm.out
在生成的gtm.out中,可以看到有两个函数几乎占用了cpu的大部分时间:
gmon代码
1. each sample counts as 0.01 seconds.
2. % cumulative self self total
3. time seconds seconds calls ms/call ms/call name
4. 31.41 49.90 49.90 8256962 0.01 0.01 gtm_gxidtohandle
5. 15.00 73.72 23.82 3791614 0.01 0.01 gtm_gettransactionsnapshot
6. 6.33 83.77 10.05 4305476 0.00 0.00 gtm_list_delete
7. 5.07 91.83 8.06 790 10.20 181.29 gtm_threadmain
8. 3.09 96.75 4.92 25006706 0.00 0.00 allocsetalloc
9. 2.06 100.02 3.27 752039341 0.00 0.00 globaltransactionidprecedes
10. 1.78 102.85 2.83 30641196 0.00 0.00 elog_start
11. 1.66 105.49 2.64 11157165 0.00 0.00 pq_recvbuf
12. 1.62 108.07 2.58 13648719 0.00 0.00 internal_flush
在花了半天看了这两个耗时的函数,大概有点眉目:
1,当coordinator上启动一个事务时,回去gtm申请一个一个事务id (xid) 和存放事务相关信息的gtm_transactioninfo数据结构,并把这个数据结构的指针放入一个全局的链表gtmtransactions.gt_open_transactions中,gtm将事务id返回给coordinator
2,事务执行时,会将xid发给gtm去获取快照。gtm会首先调用gtm_gxidtohandle函数去获得对应的gtm_transactioninfo数据结构的指针,gtm_gxidtohandle函数会遍历全局链表gtmtransactions.gt_open_transactions来获取。然后将该gtm_transactioninfo数据结构的指针传给gtm_gettransactionsnapshot函数来获得快照:遍历gtmtransactions.gt_open_transactions中的每个元素,获取全局最小的xmin,和活跃的事务id(小于最近提交事务的最大id),放入快照并返回给coordinator。
3,事务结束时,coordinator将xid返回给gtm,gtm根据xid查找对应的gtm_transactioninfo数据结构,将其回收,并删除gtmtransactions.gt_open_transactions中的对应item。
简单看来,这两个耗时的函数都是o(n)级别的复杂度,n 是gtmtransactions.gt_open_transactions的长度,也就是当前正在进行的事务数。因此,随着连接数的增加,coordinator和datanode内部锁竞争的加剧,会导致事务逐渐的积压起来,让gtmtransactions.gt_open_transactions长度变得越来越长,因此堵住了很多获取事务快照的事务。最终就是刚才描述的情形:事务等待gtm,磁盘使用率急剧降低。
所以,从直觉出发,其实可以直接用开放式hash表来优化gtm_gxidtohandle函数,key是事务id,value是对应的gtm_transactioninfo的指针,将这个的函数的操作复杂度降低到o(1)。但是,发现效果并不好:因为gtm_gettransactionsnapshot函数依然还是要去遍历所有的当前事务获取最小事务xmin和快照。
在经过一个礼拜的调整之后,直接采用教科书的方法:使用一个avl树来存储gtm_transactioninfo的指针,比较大小的方法就是对比其中的gxid;另外一个使用avl树来存最小的xmin。简单来说就是分别以xmin和事务id为主键建立两个类似btree的索引,以满足查找需求。最终去掉了gtmtransactions.gt_open_transactions,将这两个耗时的函数的复杂度降低到了o(log(n)),n是当前系统内的事务。
采用前面的测试方法,来获取tps。和前面的对比图如下:
可以看到优化之后的gtm响应时间基本维持在o(log(n)),从而使得tps在连接数增大时,tps没有像当前版本下降得这么剧烈(678个连接时,tps是当前版本的几乎5倍),datanode的数据目录所在磁盘利用利用率机会基本维持在65%~60%之间。这样看来,基本上能改善gtm的扩展能力。
当然,这个优化并非是完美的,因为在执行简单事务并且连接数不多的情况下,tps和原有的版本几乎相同,但是,一旦事务变得复杂,在gtm中停留的时间增大,或是连接数增大后同时执行的事务数量多了之后,优化之后的gtm相对现有的gtm会稳定很多。
最后,为了验证工作,选了三台相同配置的r510,配置和前面所述相同。一台上装gtm,其他两台上配置了coordinator和datanode各一个(配置也和前面相同)。
生成了scale为1000的数据,32g,使用前面的测试方法,但是分别执行在一张大表上的简单主键查询:
1. \setrandom aid 1 :1000
2. select abalance from pgbench_accounts where aid = :aid;
和基于主键的更新
sql代码
1. \setrandom aid 1 10000000
2. \setrandom delta -5000 5000
3. update pgbench_accounts set abalance = abalance + :delta where aid = :aid;
在两台coordinator同时上执行。
最终, 简单select操作的总qps最大值是45000(每个coordinator上的qps是22500),简单update操作的总 tps最大值是 35000(每个coordinator上最大的qps是17500)。碰巧的是获得最大值时,每台coordinator上执行pgbench的连接数都是64。而随着连接数增大到一定程度,优化之后的gtm会比当前的gtm 结果高50%以上。
从测试看来,在64个连接数的情况,增大一个服务器(部署上datanode和coordinator),可以增加17500的tps,网络流量增加20mb/s。因此如果真的想达到10w的tps,预计需要10台左右这样的服务器用来部署coordinator和datanode。
但是我们需要解决两个问题:一方面,我们需要子网的交换机来提供200mb/s以上的带宽连接各个服务器,这是首先必须解决的问题,当然也是比较容易解决的问题;另外一方面,对于gtm来说,我们必须采用类似的优化来增大gtm的可扩展能力,因为如果每个coordinator上使用64个连接,那么对于10台的集群来说,系统内操作的连接数就是640了,如果还采用目前的gtm,tps qps会急剧下降,这是根本没法做到的。
<b>本文来自云栖社区合作伙伴“dbgeek”</b>