Postgres XL压力测试与性能调优

实验设备

用于部署postgres-xl的设备

主机:8台
CPU:8核Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz
内存:32G
磁盘:SSD
系统:Linux 3.10.0-229.el7.x86_64 CentOS 7.1

运行环境

集群:使用pgxl-9.5,每台主机一个datanode、一个coordinator和一个gtm_proxy。

准备工作:

  • 关闭透明大页。
  • 增加运行用户的最大进程数。
  • 修改/proc/sys/kernel/sem。

cat /proc/sys/kernel/sem可以看到四个数字,分别是SEMMSL、SEMMNS、SEMOPM、SEMMNI,主要调整第二、四个核心参数,否则在启动datanode时可能会报错,原因是配置的max_connections超过了SEMMNI或者SEMMNS的大小。

echo 4096 32000 1024 1024 > /proc/sys/kernel/sem
echo "kernel.sem = 4096 32000 1024 1024" >> /etc/sysctl.conf
  • 初始主要配置如下(此配置是从现有资源和经验角度做出的判断,后面会增加更多配置)。

Coordinator节点postgresql.conf:

max_connections = 200          # 允许的最大并发连接数
max_prepared_transactions = 200       # 允许的最大预提交事务数,与更新操作相关
shared_buffers = 8GB            # 用于缓存数据的内存(推荐内存的1/4)
temp_buffers = 80MB
work_mem = 100MB                # 用于内部排序和一些复杂的查询的内存
dynamic_shared_memory_type = posix
max_files_per_process = 2000
effective_io_concurrency = 8
max_worker_processes = 64
effective_cache_size = 8GB
max_pool_size = 1600            # Coordinator与Datanode之间的连接池的最大连接数
pool_conn_keepalive = 600

Datanode节点postgresql.conf:

max_connections = 1600
shared_buffers = 8GB
temp_buffers = 80MB
dynamic_shared_memory_type = posix
effective_io_concurrency = 8
max_worker_processes = 32
random_page_cost = 1.5
effective_cache_size = 24GB

GtmProxy节点gtm_pxy.conf

worker_threads = 2
keepalives_idle = 120
keepalives_interval = 30
keepalives_count = 2

Gtm节点gtm.conf

keepalives_idle = 120
keepalives_interval = 30
keepalives_count = 50

pgxl的常见conf文件中的配置项,如果存在相同name时,后面的值会覆盖前面的值,所以为测试方便可以直接将新配置添加到conf文档最后。例如:

echo "max_prepared_transactions = 100" >> /mfpdata1/data/pgxc/nodes/coord/postgresql.conf
echo "max_prepared_transactions = 100" >> /mfpdata1/data/pgxc/nodes/dn_master/postgresql.conf

实验数据和方法

选择现有的mongodb的bson类型数据作为实验数据。

小文档数据:
data : 697.67MiB docs : 17013091 avg obj size :43B

大文档数据:
data : 64.33GiB docs : 22504206 avg obj size :3KiB

首先导出mongodb数据,每个collection对应一个metadata文件和一个bson数据文件,忽略metadata文件。

然后编写程序读取bson数据文件,创建对应的表,每条bson数据转换成只有主键id和jsonb类型data的格式,data字段存储转换后的bson文档,然后批量插入到Postgres XL,插入后的数据大小会发生变化(jsonb与bson的区别,此处忽略)。导入数据程序可以识别数据主键类型来自动创建表结构,也可事先通过SQL创建表结构。例如:

CREATE TABLE check_in ( _id int primary key, data jsonb not null default '{}' ) distribute by hash(_id);

施压程序用java语言编写,使用jdbc驱动和c3p0连接池,支持操作如下:

  1. find_first:根据主键id查询数据。
  2. find_in:根据一批主键批量查询数据,批量大小为40。
  3. find:根据主键id大于随机值的一批数据,限制返回100到10100条。
  4. find_update:根据键id查询数据,并保持原状执行更新(即包含一次查询和一次更新)。
  5. find_insert:根据键id查询数据,并插入到同结构的copy表(即包含一次查询和一次插入)。
  6. find_delete:根据键id查询数据,并根据id从copy表删除数据(即包含一次查询和一次删除)。

实验用例

用例一

  • 仅向一台主机的coordinator施压,根据实际情况调整线程数为接近最优(即再增加施压主机或增大线程数,每秒并发完成数增加不明显甚至造成每次完成耗时增大)。
【操作】 【数据】 【线程数】 【每秒并发完成数】 【每次耗时(毫秒)】 【入网流量(MBit/s)】 【出网流量(MBit/s)】 【负载】
find_first 80 7600+ 11 340+ 360+ 36+
find_update 80 1500+ 55 200+ 500+ 26+
find_first 80 9200+ 9 160+ 150+ 15+
find_update 80 2300+ 34 150+ 200+ 16+
find 15 40+ 360+ 520+ 240+ 12+

初步结论:显然离最优配置相去甚远,没能发挥出PGXL吹嘘的高性能。接下来寻找瓶颈,继续调优。

首先通过ps aux | grep postgres可以看到postgresql是多进程的方式处理并发请求。无论Coordinator还是Datanode在接到并发请求时都产生很多进程,并且可以看到每个进程的操作状态(select、update、bind、idle等)。为了减少进程数、便于分析,客户端使用一个线程发送请求。

然后通过strace工具跟踪Coordinator进程,采集系统调用栈样本做分析。

# 将11437进程的系统调用栈输出到11437.strace文件,Ctrl+C停止
strace -o 11437.strace -Tttyi -s 400 -p 11437

截取其中一个请求,如下图:

pgxl_strace_head_001

pgxl_strace_head_002

pgxl_strace_head_003

一个简单的查询请求竟然有这么多次sendto调用,看来可优化空间很大~

过程分析:

  1. poll轮询等待请求。
  2. 收到客户端发来的预编译(PreparedStatement)后的查询语句,recvfrom(9,…)。
  3. 向GTM同步数据最新版本时间戳,sendto(3,…),recvfrom(3,…)
  4. 携带时间戳依次向八个datanode转发查询语句,sendto(18,…),recvfrom(18,…)等。
  5. 收到各datanode响应结果后,又依次向八个datanode发起重置Session权限和重置事务隔离等级。
  6. 最后将查询结果发送给客户端,sendto(9,…)。
  7. 再次poll轮询等待请求。

最后通过pstack工具多次跟踪Coordinator进程,采集进程栈样本结合源码做分析。

# 将11437进程的当前时刻的进程栈输出到11437.pstack文件
pstack 11437
# 反复多次
for ((i=0;i<=1000;i++)); do pstack 11437 >> 11437.pstack; sleep 0.01; done
# 根据方法名搜索,找到源码文件
grep -r PostmasterMain postgres-xl-9.5r1.1/src/
grep -r PortalRun postgres-xl-9.5r1.1/src/

过程分析:

程序启动后进入主循环PostgresMain,等待请求进入,收到请求后解析input_message,根据firstchar进入不同的处理分支,见backend/tcop/postgres.c。以下是几个分支的不完全分析:

  1. Q:完成一次简单查询(exec_simple_query)。首先为查询生成计划(pg_plan_queries –> PlannedStmt),然后完成执行(CreatePortal –> PortalDefineQuery –> PortalStart –> PortalRun –> PortalDrop)。
  2. P:分析传入的信息(exec_parse_message),创建CachedPlanSource(CreateCachedPlan –> parse_analyze_varparams –> CompleteCachedPlan)。其中parse_analyze_varparams会对数据是否仅来自一个datanode做分析,从源头决定后面的查询命令是单发还是群发。见backend/parser/analyze.c。
  3. B:绑定CachedPlan和Portal(exec_bind_message),首先获取CachedPlanSource,如果前面有“P”过程就会在这里得到stmt_name进而FetchPreparedStatement;然后通过CachedPlanSource获取CachedPlan(GetCachedPlan –> BuildCachedPlan –> pg_plan_queries –> PlannedStmt);最后绑定Portal(CreatePortal –> PortalDefineQuery –> PortalStart)。见backend/tcop/postgres.c和backend/utils/cache/plancache.c。
  4. E:Portal执行(GetPortalByName –> PortalRun)。

PortalStart时会先选择策略(ChoosePortalStrategy),根据不同策略决定命令如何执行、发给哪些节点执行等。其中扩展了对分片数据的支持(PORTAL_DISTRIBUTED)。见backend/tcop/pquery.c和backend/utils/cache/plancache.c。

pg_plan_queries会创建PlannedStmt,其中扩展了对分片数据的支持(pgxc_direct_planner)。见backend/pgxc/plan/planner.c,backend/optimizer/plan/planner.c和backend/tcop/postgres.c。

用例二

  • 为Coordinator与Datanode之间的连接池开启persistent_datanode_connections = on,测试性能提升。

通过阅读postgresql.conf中的注释以及官方文档发现Coordinator与Datanode之间的连接池可以设置persistent_datanode_connections = on,意思是为Session打开的连接不会被放回到连接池中,节省取出、放回连接带来的损耗。

persistent_datanode_connections = on

调整后重启集群,以find_first小文档数据为例,每秒并发完成数由9200提高到16000。

【操作】 【数据】 【线程数】 【每秒并发完成数】 【每次耗时(毫秒)】 【入网流量(MBit/s)】 【出网流量(MBit/s)】 【负载】
find_first 80 16000+ 5 170+ 170+ 19+

再次用strace跟踪Coordinator进程,对比后发现不再发生下面这种调用:

15:14:13.271268 [00007fbc08e466cd] sendto(18, "Q\0\0\0GRESET ALL;RESET SESSION AUTHORIZATION;RESET transaction_isolation;\0", 72, 0, NULL, 0) = 72 <0.000063>
...
15:14:13.272245 [00007fbc08b62c20] poll([{fd=18<socket:[1544798]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=19<socket:[1544799]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=20<socket:[1544800]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=21<socket:[1544801]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=22<socket:[1544802]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=23<socket:[1544803]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=24<socket:[1544804]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}, {fd=25<socket:[1544805]>, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 8, 4294967295) = 7 ([{fd=18, revents=POLLIN|POLLRDNORM}, {fd=19, revents=POLLIN|POLLRDNORM}, {fd=20, revents=POLLIN|POLLRDNORM}, {fd=21, revents=POLLIN|POLLRDNORM}, {fd=22, revents=POLLIN|POLLRDNORM}, {fd=23, revents=POLLIN|POLLRDNORM}, {fd=24, revents=POLLIN|POLLRDNORM}]) <0.000045>
...
15:14:13.272422 [00007fbc08e4655d] recvfrom(18, "S\0\0\0\24is_superuser\0on\0S\0\0\0 session_authorization\0jelly\0C\0\0\0\21MultiCommand\0Z\0\0\0\5I", 16384, 0, NULL, NULL) = 78 <0.000045>

初步结论:

  1. 在Coordinator中打开persistent_datanode_connections = on是重要的调优参数。

用例三

  • 将jdbc客户端中使用的PreparedStatement修改为普通的Statement,测试性能提升。

从前面调用过程发现从客户端发到Coordinator的查询请求是按片键查询某一条数据,本该通过计算转给数据所在的datanode就可以完成,而实际却依次发给所有datanode(8个),其中7个返回无结果(这7个必然查询不到数据)。为什么Coordinator没有做出正确选择?登陆一个Coordinator执行一条SQL再strace跟踪试试。

# 登陆
psql  -U jelly -h 172.31.32.2 -p 31001 -d jelly_ios
# 执行查询SQL
select * from check_in where _id = 14902645;

结果如下图:

pgxl_strace_not_ps_001

发现Coordinator仅把查询转发给了数据所在的datanode。观察客户端发来的数据恍然大悟,原来是PreparedStatement和Statement的区别。如果是PreparedStatement会被直接转给所有的datanode;如果是普通的SQL则首先经过“P”流程分析出数据应该来自一个datanode还是多个,如果是一个则仅发送到那一个datanode。

另外通过explain analyze也可以分析出查询被分发到哪些datanode,如下图:

pgxl_explain_001

观察select in进一步发现pgxl貌似并不会做更复杂的分析,转发SQL时要么一个要么全部。

调整客户端代码,以find_first小文档数据为例,每秒并发完成数由16000提高到26000。

【操作】 【数据】 【线程数】 【每秒并发完成数】 【每次耗时(毫秒)】 【入网流量(MBit/s)】 【出网流量(MBit/s)】 【负载】
find_first 80 26000+ 3 130+ 130+ 24+

再次用strace跟踪Coordinator进程,对比后发现Coordinator仅向数据所有datanode转发查询请求。

初步结论:

  1. 要根据实际业务需要使用PreparedStatement和Statement,如果数据来自一个datanode则用Statement,反之则用PreparedStatement。

用例四

  • 增加Coordinator的global_snapshot_source = ‘coordinator’配置,测试性能提升。

调整后以find_first小文档数据为例,每秒并发完成数由26000提高到40000。

【操作】 【数据】 【线程数】 【每秒并发完成数】 【每次耗时(毫秒)】 【入网流量(MBit/s)】 【出网流量(MBit/s)】 【负载】
find_first 80 40000+ 1 130+ 130+ 45+

再用strace跟踪Coordinator进程,对比后发现不再发生下面这种调用:

15:14:13.251832 [00007fbc08e466cd] sendto(3, "C\0\0\0\20\0\0\0(\0\0\0\1\0\0\0\0", 17, 0, NULL, 0) = 17 <0.000075>
15:14:13.251982 [00007fbc08b62c20] poll([{fd=3<socket:[1545720]>, events=POLLIN|POLLERR}], 1, 20000) = 1 ([{fd=3, revents=POLLIN}]) <0.000062>
15:14:13.252164 [00007fbc08e4655d] recvfrom(3, "S\0\0\0\34\0\0\0\31\1\0\0\0\3\0\0\0\236\\S\1\236\\S\1\0\0\0\0", 16384, 0, NULL, NULL) = 29 <0.000047>

初步结论:

  1. 如果实际业务操作允许从各Datanode获取的数据不是同一版本时间戳,则可省略从GTM或GTM Proxy获取快照的步骤以提升性能。

用例五

  • 使用多个施压主机分别向四个Coordinator施压,所有主机均已完成上述调优。

以小文档数据为例,四台主机的表现如下,整体带来乘以4的每秒并发完成数。其中find_first没有对软中断进行调优,因为CPU8个核的idle都已经很低,调整后没有正面收效,反而每秒并发完成数有所下降。其他操作均echo fe > /sys/class/net/eth0/queues/rx-0/rps_cpus。

【操作】 【数据】 【线程数】 【每秒并发完成数】 【每次耗时(毫秒)】 【入网流量(MBit/s)】 【出网流量(MBit/s)】 【负载】
find_first 80 27000+ 2 90+ 100+ 55+
find_update_one 80 10000+ 7 60+ 70+ 30+
find_in 80 3800+ 15 270+ 420+ 77+

初步结论:

  1. Postgres XL的整体性能还是比较出色的。
  2. 一定要使用Gtm_Proxy。GTM的运行参数GTM_MAX_THREADS = 1024,严重制约高并发时的性能。如果所有Coordinator和Datanode都直连GTM,压力大时会在gtm.log中报Too many threads active,见gtm\main\gtm_thread.c。

参考

http://mysql.taobao.org/monthly/2016/02/07/
http://blog.163.com/digoal@126/blog/static/163877040201221382150858/
http://blog.163.com/czg_e/blog/static/46104561201141111445789/
http://blog.163.com/digoal@126/blog/static/16387704020121952051174/

Creative Commons License

本文基于署名-非商业性使用-相同方式共享 4.0许可协议发布,欢迎转载、使用、重新发布,但请保留文章署名wanghengbin(包含链接:https://wanghengbin.com),不得用于商业目的,基于本文修改后的作品请以相同的许可发布。

评论(1) “Postgres XL压力测试与性能调优

发表评论