Postgres XL实践与经验小结

本文是基于Postgres-XL 9.5r1.3版本的实践小结,应用于公司新手游“monster”的后台数据库服务。

准备主机环境

操作系统:ubuntu-14.04

# 格式化数据盘
fdisk /dev/xvdb
mkfs -t ext4 /dev/xvdb1
mkdir /mfpdata
echo /dev/xvdb1 /mfpdata ext4 defaults,discard,nobarrier,noatime 0 0 >> /etc/fstab
# 格式化日志盘
fdisk /dev/xvdc
mkfs -t ext4 /dev/xvdc1
mkdir /mfplog
echo /dev/xvdc1 /mfplog ext4 defaults,discard,nobarrier,noatime 0 0 >> /etc/fstab
# 安装依赖
apt-get update
apt-get install build-essential libreadline6 libreadline6-dev zlib1g zlib1g.dev flex bison jade -y --force-yes
# 增加用户postgres
useradd -m postgres
# 修改用户postgres默认sh为bash,SSH远程执行找不到命令或跳转登录后不是bash时需要设置这个
chsh -s /bin/bash postgres
# 设置用户postgres的最大文件打开数和最大进程数
echo '
postgres         -       nofile          655360
postgres         -       nproc          65536
' > /etc/security/limits.d/postgres-limits.conf
# 设置信号量kernel.sem
echo 4096 32000 1024 1024 > /proc/sys/kernel/sem
# 创建数据和日志根目录
mkdir -p /mfpdata/pgxl_data
mkdir -p /mfplog/pgxl_log
chown postgres:postgres /mfpdata/pgxl_data
chown postgres:postgres /mfplog/pgxl_log
# 编译安装pgxl
tar zxvf postgres-xl-9.5r1.3.tar.gz
cd postgres-xl-9.5r1.3/
./configure --prefix=/usr/local/pgxl/
make
make install
# 设置hosts映射关系
grep "172.31.32.10    gtm-master" /etc/hosts || echo -e "\n172.31.32.10    gtm-master" >> /etc/hosts
grep "172.31.32.11    gtm-slave" /etc/hosts  || echo "172.31.32.11    gtm-slave" >> /etc/hosts
grep "172.31.32.21    gtm-pxy-1" /etc/hosts || echo "\n172.31.32.21    gtm-pxy-1" >> /etc/hosts
grep "172.31.32.22    gtm-pxy-2" /etc/hosts || echo "172.31.32.22    gtm-pxy-2" >> /etc/hosts
grep "172.31.32.23    gtm-pxy-3" /etc/hosts || echo "172.31.32.23    gtm-pxy-3" >> /etc/hosts
grep "172.31.32.24    gtm-pxy-4" /etc/hosts || echo "172.31.32.24    gtm-pxy-4" >> /etc/hosts
grep "172.31.32.21    coord-1" /etc/hosts || echo "\n172.31.32.21    coord-1" >> /etc/hosts
grep "172.31.32.22    coord-2" /etc/hosts || echo "172.31.32.22    coord-2" >> /etc/hosts
grep "172.31.32.23    coord-3" /etc/hosts || echo "172.31.32.23    coord-3" >> /etc/hosts
grep "172.31.32.24    coord-4" /etc/hosts || echo "172.31.32.24    coord-4" >> /etc/hosts
grep "172.31.32.21    pgxl-dn-1" /etc/hosts || echo "\n172.31.32.21    pgxl-dn-1" >> /etc/hosts
grep "172.31.32.22    pgxl-dn-s-1" /etc/hosts || echo "\n172.31.32.22    pgxl-dn-s-1" >> /etc/hosts
grep "172.31.32.23    pgxl-dn-2" /etc/hosts || echo "172.31.32.23    pgxl-dn-2" >> /etc/hosts
grep "172.31.32.24    pgxl-dn-s-2" /etc/hosts || echo "172.31.32.24    pgxl-dn-s-2" >> /etc/hosts
# 节点免密SSH
ssh-keygen -t rsa
echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuxZTGqwHfBJenHld5S5+sfsVjSakziZA5PMY4cBpHDzBrSwiP5fYNQbqAkKK9RuTEKfJkSjkap1B3scIFIf4MPAkbQalLNeuRnEGXyu4dmQxZ/v+FqdEiifhlW+z18cT5zYGus3IDeSWnGWqZnf2VS6F2o2AwnRjVYSd3Kw16GlIv/IAeCCGXLr7N6ThPjS42pl/dmT5qZVlSX0kQH5MCuhArphEgPatQHkyWJxR131HSXiukuMPlzL4X7bC0tx4FE2ts+hD9edcZTk+VJGPafbnrg16YjhgwBF/Zvs/aPpity7dUhcf8jclxZ1WOcSJ3PGnsjlOxqAYy2rHY9pZ3 postgres@monsters1' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
echo '#免yes登陆
StrictHostKeyChecking no
#实时更新known_hosts
#UserKnownHostsFile /dev/null' > ~/.ssh/config
su - postgres
# 将/usr/local/pgxl/bin添加到用户postgres的运行环境
sed -i '1i\PATH=$PATH:/usr/local/pgxl/bin' ~/.bashrc

集群管控

在这之前有过一次简单的尝试,基于9.2版本,集群搭建和管理使用的是pgxc_ctl脚本,虽然存在一些不足,但核心功能都能比较顺利地实现。这次版本升级后pgxc_ctl出现一些新问题,最终导致放弃pgxc_ctl,转为用shell命令实现符合实际需要的管控脚本。源代码

pgxc_ctl的问题:

  1. 配置复杂,一些操作如增删节点、故障转移等,会追加内容到配置文件,导致配置文件越来越混乱难读。
  2. pgxc_ctl的init操作十分危险,会不加提示直接重置集群,导致数据丢失。(新版增加了force参数来区分是否强制重置)
  3. 新版pgxc_ctl的init在本应判断远程机上的datanode是否已存在的场景下,检查的是本地的文件目录。
  4. 新版pgxc_ctl在启动gtm master时会关闭文件描述符0,表现为gtm_proxy一连gtm就被断开,异常日志为could not receive data from client: Bad file descriptor LOCATION: pq_recvbuf, pqcomm.c:524,Expecting a startup message, but received <FF>。ls /proc/<gtm-pid>/fd/发现没有0 -> /dev/null,导致每次建立连接时都是创建0,所以闪断(参考

自定义管控脚本实现目标:

  1. 简明配置。
  2. 支持常用操作。
  3. 使集群易伸缩、高可用。

该管控脚本需要对集群中所有主机具有postgres用户的免密码SSH权限。

配置文件

配置文件分成集群配置文件、节点配置模板。

集群配置文件

集群配置文件定义了集群中的节点性质和之间的关系,如下:

# new node must be added at the end of array. old node must be removed from the end of array.
# keep the number of nodes stable

# GTM
GTM_MASTER_HOST=gtm-master
GTM_SLAVE_HOST=gtm-slave

# GTM PROXY
GTM_PROXY_NAMES[0]=gtm_pxy1
GTM_PROXY_HOSTS[0]=gtm-pxy-1

GTM_PROXY_NAMES[1]=gtm_pxy2
GTM_PROXY_HOSTS[1]=gtm-pxy-2

GTM_PROXY_NAMES[2]=gtm_pxy3
GTM_PROXY_HOSTS[2]=gtm-pxy-3

GTM_PROXY_NAMES[3]=gtm_pxy4
GTM_PROXY_HOSTS[3]=gtm-pxy-4

# DATANODE
DATANODE_NAMES[0]=datanode1
DATANODE_MASTER_HOSTS[0]=pgxl-dn-1
DATANODE_SLAVE_HOSTS[0]=pgxl-dn-s-1

DATANODE_NAMES[1]=datanode2
DATANODE_MASTER_HOSTS[1]=pgxl-dn-2
DATANODE_SLAVE_HOSTS[1]=pgxl-dn-s-2

# COORDINATOR
COORDINATOR_NAMES[0]=coord1
COORDINATOR_HOSTS[0]=coord-1

COORDINATOR_NAMES[1]=coord2
COORDINATOR_HOSTS[1]=coord-2

COORDINATOR_NAMES[2]=coord3
COORDINATOR_HOSTS[2]=coord-3

COORDINATOR_NAMES[3]=coord4
COORDINATOR_HOSTS[3]=coord-4

# DATABASE
DATABASES[0]=postgres
DATABASES[1]=mfsso
DATABASES[2]=mfpay
DATABASES[3]=monsters

该文件的内容是静态的,不会被管控监本修改,用作初始化和动态增加删除节点的依据。而集群节点的状态以及主从关系的变化都体现在另一个文件中(pgxl_runtime.conf),该文件为运行中自动生成,作用类似数据库。

节点配置模板

节点配置模板的大部分参数的值是约定不变的,少数需要随集群动态改变的参数由脚本自动修改。模板文件包括:

gtm.master.conf
gtm.slave.conf
gtm_proxy.conf
postgresql.coord.conf
postgresql.dn.master.conf
postgresql.dn.slave.conf
pg_hba.coord.conf
pg_hba.dn.conf
recovery.conf

访问权限

除了pg_hba.conf里需要对ip进行限制外,集群环境的PostgreSQL需要设置额外的数据库访问验证。

当使用用户密码的方式访问数据库时,需要为每个节点增加密码配置文件.pgpass。由于Coordinator彼此之间、Coordinator与Datanode之间、Datanode彼此之间都需要通讯,所以每个Coordinator和Datanode节点所在的主机都需要加,否则会报如下错误:

LOG:  failed to connect to node, connection string (...), connection error (fe_sendauth: no password supplied)

.pgpass文件在ownerUser的home下,每行格式为 hostname:port:database:username:password,密码与创建User时的一致。

echo '*:*:*:mfpay:pwd123456
*:*:*:mfsso:pwd123456
*:*:*:monsters:pwd123456' > ~/.pgpass
chmod 0600 ~/.pgpass
DROP ROLE IF EXISTS mfsso;
CREATE ROLE mfsso LOGIN PASSWORD 'pwd123456';

集群操作

一.初始化

初始化是依据集群配置文件,按照gtm->gtm_proxy->datanode->coordinator的先后顺序,依次在相应主机上创建相应节点目录和文件,而后复制相应的节点配置模板文件到节点目录下并修改部分参数值,最后在每个datanode和coordinator上注册所有节点信息的过程。其中有三点需要注意的地方:

  1. 初始化slave前需要先启动master。
  2. 注册节点信息前需要启动所有coordinator和datanode的master。
  3. 如果datanode的master配置了synchronous_standby_names=’xxxxx’,则必须当slave节点可以访问时才能做node的相关操作,如Create node,还包括DDL语句,相关操作要等待slave完成后才能提交。
initgtm -Z gtm -D $PGXL_DATA_HOME/$GTM_DIR_NAME
initgtm -Z gtm_proxy -D $PGXL_DATA_HOME/$GTM_PROXY_DIR_NAME
initdb --nodename $name -D $PGXL_DATA_HOME/$COORDINATOR_DIR_NAME

二.注册节点信息

pgxc_node表是提供节点信息的表,集群中的所有coordinator和datanode节点的pgxc_node表必须拥有一致的完整的节点信息。

postgres=# select * from pgxc_node;
 node_name | node_type | node_port |  node_host  | nodeis_primary | nodeis_preferred |   node_id   
-----------+-----------+-----------+-------------+----------------+------------------+-------------
 coord1    | C         |     21001 | coord-1     | f              | f                |  1885696643
 coord2    | C         |     21001 | coord-2     | f              | f                | -1197102633
 coord3    | C         |     21001 | coord-3     | f              | f                |  1638403545
 coord4    | C         |     21001 | coord-4     | f              | f                |  -890157977
 datanode1 | D         |     23001 | pgxl-dn-1   | t              | f                |   888802358
 datanode2 | D         |     23001 | pgxl-dn-2   | f              | f                |  -905831925

这里采用依据pgxl_runtime.conf生成完整的“Create and Alter”命令语句,然后在所有节点上无差别执行的方式来注册。

CREATE NODE coord3 WITH (TYPE='coordinator', HOST='coord-3', PORT=21001);
ALTER NODE coord3 WITH (TYPE='coordinator', HOST='coord-3', PORT=21001);
CREATE NODE datanode3 WITH (TYPE='datanode', HOST='pgxl-dn-3', PORT=23001, PRIMARY);
ALTER NODE datanode3 WITH (TYPE='datanode', HOST='pgxl-dn-3', PORT=23001, PRIMARY);
SELECT pgxc_pool_reload();

三.启动、停止、重启、杀掉节点进程

启动、停止和重启都是直接调用pg_ctl或gtm_ctl脚本方法,杀掉节点进程采用先找到进程号再kill的方式。另外重启存在一个小问题,每次重启后会额外增加一段-D /data_dir,所以使用停止加启动的方式代替。

gtm_ctl start -Z gtm -D $PGXL_DATA_HOME/$GTM_DIR_NAME
gtm_ctl stop -Z gtm_proxy -D $PGXL_DATA_HOME/$GTM_PROXY_DIR_NAME -m fast
pg_ctl restart -Z coordinator -D $PGXL_DATA_HOME/$COORDINATOR_DIR_NAME -m fast
pg_ctl start -Z datanode -D $PGXL_DATA_HOME/$DATANODE_DIR_NAME

四.探测节点状态

将节点状态分为运行中、已停止、宕机。通过ping和telnet命令来判断:

ping -c 1 -w 1 $host &>/dev/null && resp="Connected" || resp=""
echo -e "\n" | timeout 1 telnet $host $port 2>/dev/null | grep Connected

ping用于判断是否宕机,或者用telnet 22端口判断,但是每次都会在/var/log/auth.log里记录Bad protocol version identification日志,甚至会影响到SSH操作;telnet 23001端口判断datanode节点;telnet 21001端口判断coordinator节点。至于gtm和gtm_proxy不能通过telnet这种方式,它会占用连接不释放,一定量后数据节点将提示无法连接gtm,造成集群新连接的请求卡在鉴权阶段不可访问。所以需要通过SSH到目标机查找进程id的方式判断,但是相比telnet速度差很多。

五.重建gtm slave、gtm proxy、datanode slave

slave节点在某些情况下可能出现无法启动或数据损坏,报错如下:

too many KnownAssignedXids
terminating any other active server processes

这时需要rebuild该节点来恢复功能,类似删除后重新初始化指定节点。

六.故障转移gtm master、datanode master

当master节点不可用时,需要晋升可用的slave节点为新的master,以继续提供服务。无论gtm还是datanode,主节点失败后都将直接影响到集群能否继续提供服务。这里实现的failover与pgxc_ctl有所不同,不是弃主升从,而是使主从关系发生交换,旧的master节点再次可操作时需要重建成slave加入集群。

Datanode的master节点通过“wal sender process”进程实时将WAL日志(预写日志)发送给slave节点,slave节点启动子进程接收(可见“startup process recovering xxxxxx”),即所谓的流复制。即使slave节点停止master节点仍然可以正常工作,只是没有“wal sender process”进程。由于同步时使用的是rsync命令(archive_command参数配置的),并且主从节点在failover时会发生关系切换,所以需要将每一对主从关系的datanode节点配置为SSH互信。

故障转移gtm:

  • 修改gtm slave相关配置为master,重启gtm slave。
  • 修改gtm proxy配置连接到新的master,重启gtm proxy。

这里需要注意,gtm数据目录下的gtm.control文件中记录了next_xid、global_xmin、Sequence等重要信息,理论上需要master个slave做到实时同步里面的信息。然而gtm slave下的gtm.control只有重启时才与master同步,如果长时间不重启且数据更新很快会导致slave下的gtm.control严重滞后于master,failover时slave则不能胜任master,不知道是不是个bug。目前暂时通过外力实现该同步功能。

故障转移datanode:

  • 修改datanode slave相关配置为master,重启slave。
  • 在所有节点上修改节点注册信息。
  • 删除gtm master下的register.node文件,重启集群。此处需要优化,pgxc_ctl的做法是直接修改register.node文件。

这里需要注意register.node是动态生成的,可以防止各节点上注册的node名称冲突。当node启动时会注册到这个文件中,即表示当前集群中该节点已连接。如果出现名称冲突的node同时运行,则会在gtmlog中提示Node with the given ID number already exists。所以failover后,旧的master不能再次以master身份启动。

七.增加和删除coordinator、gtm proxy、datanode

实现伸缩性的功能。

增加新节点:gtm proxy比较容易,初始化后启动即可。coordinator和datanode的操作多一些,但步骤相似,以datanode为例:

  • 初始化datanode新节点。
  • 找到一个可用coordinator,执行pgxc_lock_for_backup()并且不要退出,以锁定DDL操作。视具体情况如果不会发生DDL操作则可忽略。
  • 找到一个原有的可用datanode,导出元数据和节点信息到xxx.sql文件。
  • 以restoremode模式启动新节点datanode,导入xxx.sql文件。
  • 以正常模式重启新节点datanode。
  • 如果是datanode,初始化并启动slave。
  • 在所有节点上增加新节点注册信息。
  • 如果前面锁定了DDL,退出那个session即解锁。
pg_dumpall -p 23001 -h $host -s --include-nodes --dump-nodes --file=dump.sql
pg_ctl start -Z restoremode -D $PGXL_DATA_HOME/$COORDINATOR_DIR_NAME -o -i
psql -p 23001 -h $host -f dump.sql

此处需要注意,与pgxc_ctl的一样都只是节点数量上的增加和元数据的复制,datanode不涉及业务表数据的转移。所以增加后通常要进行业务表数据的重新均衡操作。

删除节点:gtm proxy仍然很容易,停止即可。coordinator和datanode需要额外在其他节点上删除该节点信息。这里注意删除前要进行业务表数据的重新均衡操作,且Drop node是不可逆操作,重新Create时oid发生改变,引起关联错误

DROP NODE datanode3;
SELECT pgxc_pool_reload();

八.重新均衡数据

均衡数据就是按分表算法把所有数据重新分配一遍。均衡数据是以表作为最小维度进行的,当某个表在均衡数据时,该表将被锁住,数据量越大耗时越长。

\c monster monster
ALTER TABLE user_type_data ADD NODE (datanode2);
ALTER TABLE "User_type_data" DELETE NODE (datanode2);

这里需要注意当前版本存在一个问题,当表名含有大写字母时会执行失败,提示relation “public.user_type_data” does not exist,原因是在datanode上大写字母转为小写,导致找不到表,所以问题解决前表名一律用小写字母。

九.导出导入

这里直接使用原生的pg_dump等命令,并无包装。

pg_dumpall -h 172.28.2.10 -p 21001 --clean --if-exists --inserts --file=dumpall.sql
psql -h 172.28.2.10 -p 21001 -f dumpall.sql
psql -h 172.28.2.10 -p 21001 -f dumpall.sql 1>/dev/null 2>&1
pg_dumpall -p 21001 --clean --if-exists --inserts | gzip > dumpall.gz
gunzip -c dumpall.gz | psql -p 21001

注意,gz压缩导入时有的语句会报ERROR: syntax error at end of input,所以尽量解压后再导入。

十.HA监控

监控集群中各个节点和主机的状态,发生故障时尝试自动恢复服务。组合了以下几种策略:重启、故障转移、重建。

其他

  • 安装前需要注意时区和语言,尤其是语言,使用locale命令查看,server_encoding和client_encoding默认使用与环境相同的语言。
  • 当前版本下不支持从slave节点读取数据,会报FATAL: Was unable to obtain a snapshot from GTM.。设置global_snapshot_source = ‘coordinator’可以解决报错问题但仍然不能实现slave节点读数据。
  • 如果Coordinator或Datanode节点直接连接gtm,gtm会为每个连接产生一个gtm子线程,当总连接数超过gtm内部约定的上限1024后就会报 too many … 拒绝连接。而gtm_proxy会创建指定数量的工作线程来代理节点与gtm之间的连接。通过top -H p gtmpid可以看到,在高负载情况下,其子线程数量与所有gtm_proxy的worker_threads数量之和是一致的,合理配置则不会超过gtm的子进程上限。
  • 从不同Coordinator获取同一个Sequence有可能出现顺序混乱,即先用的值比后用的大。这个跟创建Sequence的cache大小有关,Coordinator使用时会缓存一批有序的值,理论上如果cache是0就不会有问题,但是这个cache不能为0。
  • 当前版本对临时表的支持仍然不足,使用临时表时需要清楚其在Coordinator和Datanode节点上的行为。通过create local temp table xxx()这种方式创建的临时表仅存在于当前的Coordinator,增删改查数据操作也只会发生在这个Coordinator上;通过create temp table xxx() distribute by yyy这种方式创建的临时表存在于当前Coordinator和所有datanode上,数据分布在datanode,与普通表类似。
  • 日志中出现大量以下信息表明可能需要手动清理这些临时表:
autovacuum: found orphan temp table "pg_temp_x"."xxxx" in database "monsters"
psql -p 21001
\c monsters
EXECUTE DIRECT ON (datanode1) 'drop table pg_temp_x.xxxx';
DROP TABLE pg_temp_y.yyyyy;

可以通过以下语句来查询需要手动清理的临时表:

select case when pgc.relname like '%_index' 
then 'drop index ' || pgns.nspname || '.' || pgc.relname || ';' 
else 'EXECUTE DIRECT ON (datanode1) ''drop table ' || pgns.nspname || '.' || pgc.relname || ''';' end as drop_query
from pg_class pgc
join pg_namespace pgns on pgc.relnamespace = pgns.oid
where pg_is_other_temp_schema(pgc.relnamespace) 
and pgc.relname not like '%toast%';

select b.nspname,relname,age(relfrozenxid) from pg_class a ,pg_namespace b where a.relnamespace=b.oid and b.nspname ~ 'temp' and relkind='r';
  • pg_xlog下的文件总大小受max_wal_size控制,如果超限会报如下错误。
LOG:  checkpoints are occurring too frequently (16 seconds apart)
HINT:  Consider increasing the configuration parameter "max_wal_size".
  • Datanode的master节点长期不与master节点联系,或者恢复过程较长且wal_keep_segments设置较小时,再次同步WAL日志可能会报如下错误。这是因为slave端启动时,会从上次记录的wal位置到master端的data/pg_xlog目录下查找文件,而master端的data/pg_xlog日志文件随时间推移已经被master端更新操作覆盖替换掉,已找不到00000001000000010000007C文件。
主   0ERROR:  requested WAL segment 00000001000000010000007C has already been removed
从   0FATAL:  could not receive data from WAL stream: ERROR:  requested WAL segment 00000001000000010000007C has already been removed
  • Coordinator之间也需要通信,同步数据,当某个Coordinator挂掉后将不能执行DDL操作(但是可以创建临时表,因为临时表仅存在于当前session),仅能执行DML操作。如果应用程序需要支持DDL操作,则需要将失败的Coordinator移除,待其恢复后在add回来。当某个Coordinator挂掉,从其他Coordinator创建表时会提示失败,并且这个Coordinator从此刻开始ping挂掉的Coordinator,从日志可以看到每隔30秒node (coord1:16384) down! Trying ping。
  • 创建表时会在每个节点都同步创建相同的表结构,但是oid有可能不同,所以使用某些根据oid找表的方法需要注意从不同的Coordinator查询得到的oid有可能不一致。以下是查询用户表及oid:
SELECT pg_class.oid, * FROM pg_class INNER JOIN pg_tables ON pg_class.relname = pg_tables.tablename WHERE pg_tables.schemaname = 'public';
  • postgres的一些参数可以通过不重启服务的方式重新加载,两种方式如下:
pg_ctl reload -D /data_dir
SELECT pg_reload_conf();
  • 查看postgres的相关控制信息,方式如下:
pg_controldata /data_dir
  • gtm崩溃或直接被杀掉进程有可能造成gtm.control文件损坏,集群无法访问,访问Coordinator或Datanode报错如下:
ERROR:  cache lookup failed for node xxxxx

查看gtm日志发现:

Failed to read file version
Restoring last GXID to 10000
Restoring global xmin to 10000

恢复方式为尝试修改gtm.control文件中的next_xid和global_xmin到足够大,或从实时备份中恢复。

Creative Commons License

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

发表评论