实验设备
用于部署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连接池,支持操作如下:
- find_first:根据主键id查询数据。
- find_in:根据一批主键批量查询数据,批量大小为40。
- find:根据主键id大于随机值的一批数据,限制返回100到10100条。
- find_update:根据键id查询数据,并保持原状执行更新(即包含一次查询和一次更新)。
- find_insert:根据键id查询数据,并插入到同结构的copy表(即包含一次查询和一次插入)。
- 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
截取其中一个请求,如下图:
一个简单的查询请求竟然有这么多次sendto调用,看来可优化空间很大~
过程分析:
- poll轮询等待请求。
- 收到客户端发来的预编译(PreparedStatement)后的查询语句,recvfrom(9,…)。
- 向GTM同步数据最新版本时间戳,sendto(3,…),recvfrom(3,…)
- 携带时间戳依次向八个datanode转发查询语句,sendto(18,…),recvfrom(18,…)等。
- 收到各datanode响应结果后,又依次向八个datanode发起重置Session权限和重置事务隔离等级。
- 最后将查询结果发送给客户端,sendto(9,…)。
- 再次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。以下是几个分支的不完全分析:
- Q:完成一次简单查询(exec_simple_query)。首先为查询生成计划(pg_plan_queries –> PlannedStmt),然后完成执行(CreatePortal –> PortalDefineQuery –> PortalStart –> PortalRun –> PortalDrop)。
- P:分析传入的信息(exec_parse_message),创建CachedPlanSource(CreateCachedPlan –> parse_analyze_varparams –> CompleteCachedPlan)。其中parse_analyze_varparams会对数据是否仅来自一个datanode做分析,从源头决定后面的查询命令是单发还是群发。见backend/parser/analyze.c。
- 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。
- 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>
初步结论:
- 在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;
结果如下图:
发现Coordinator仅把查询转发给了数据所在的datanode。观察客户端发来的数据恍然大悟,原来是PreparedStatement和Statement的区别。如果是PreparedStatement会被直接转给所有的datanode;如果是普通的SQL则首先经过“P”流程分析出数据应该来自一个datanode还是多个,如果是一个则仅发送到那一个datanode。
另外通过explain analyze也可以分析出查询被分发到哪些datanode,如下图:
观察select in进一步发现pgxl貌似并不会做更复杂的分析,转发SQL时要么一个要么全部。
调整客户端代码,以find_first小文档数据为例,每秒并发完成数由16000提高到26000。
【操作】 | 【数据】 | 【线程数】 | 【每秒并发完成数】 | 【每次耗时(毫秒)】 | 【入网流量(MBit/s)】 | 【出网流量(MBit/s)】 | 【负载】 |
---|---|---|---|---|---|---|---|
find_first | 小 | 80 | 26000+ | 3 | 130+ | 130+ | 24+ |
再次用strace跟踪Coordinator进程,对比后发现Coordinator仅向数据所有datanode转发查询请求。
初步结论:
- 要根据实际业务需要使用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>
初步结论:
- 如果实际业务操作允许从各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+ |
初步结论:
- Postgres XL的整体性能还是比较出色的。
- 一定要使用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/

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