PG多节点(quorum based), 0丢失 HA(failover,switchover)方案
背景
PostgreSQL 10加入了quorum based的同步复制功能,用户可以配置若干standby节点,并配置需要将WAL发送多少份才返回给客户端事务结束的消息。
ANY num_sync ( standby_name [, ...] )
原理详见
https://www.postgresql.org/docs/10/static/runtime-config-replication.html#runtime-config-replication-master
https://www.postgresql.org/docs/10/static/warm-standby.html#synchronous-replication
例子
s1,s2,s3为standby recovery.conf配置的application_name, 即standby的唯一标示
下面配置表示WAL需要复制到s1,s2,s3中的任意2个副本。
synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
下面配置表示WAL需要复制到任意standby的任意2个副本。
synchronous_standby_names = 'ANY 2 (*)'
PostgreSQL的quorum base配置比较灵活,用户可以根据地域、延迟、保护级别等需求来配置synchronous_standby_names。
例如
master有4个standby分别是s1,s2,s3,s4,s1在同机房, s2,s3在某个同城45公里以内的机房, s4在其他城市。
那么可以这么配置
下面配置表示WAL至少在一个其他机房有一份拷贝,防止整个机房的故障。
synchronous_standby_names = 'ANY 2 (s1,s2,s4)'
未来甚至有更灵活的配置(畅想)
下面配置表示s2,s3只算一次,但是它们任意一个feedback都算数。不失可靠性的情况下,提高可用性。
synchronous_standby_names = 'ANY 2 (s1,[s2,s3],s4)'
但是问题来了,当master出现故障时,如何failover,如何switchover呢?
架构
1、每个数据库实例对应一个静态IP,PostgreSQL master - slave搭建好时就固定下来。
2、每个数据库实例对应一个角色,master或slave。
3、每个角色对应一个域名,当实例为master角色时,对应的域名为master,当角色为slave时,对应slave的域名。
4、集群初次创建好之后,将IP和域名的对应关系写入DNS。
5、HA管理软件,使用master域名从DNS得到静态IP,连接到静态IP,并探测master是否正常。发生异常时,进入failover流程。(后面讲failover流程)
6、最终应用、PROXY,通过域名连接数据库。
例如,这是tom lane所在的crunchydata公司开源的一个PostgreSQL proxy,用golang写的,不做SQL解析,仅仅通过SQL HINT做简单的路由,够用。效率比较高。
https://github.com/CrunchyData/crunchy-proxy
如果使用客户端连接多实例的话,可以参考如下文章
《PostgreSQL 10.0 preview 功能增强 - libpq支持多主机连接(failover,LB)让数据库HA和应用配合更紧密》
7、failover结束后,master角色更替,ha管理软件通知DNS修改解析信息。
8、即使应用程序、PROXY因为DNS缓存,有可能在DNS TTL失效前,短暂的连接到错误的MASTER,也没有关系,因为master配置了quorum based sync replication,所以failover结束后,(即使old master突然好了)写请求下去是不会响应的。
读请求则可能受到影响,可能读到old master的data。(短暂影响)
配置
1、初始配置
初始配置,将master, slave配置好,以前面的图为例,master配置复制到3个副本。
slave配置,连接master域名,application_name配置为master ID+slave ID。
例子(关键配置)
DNS
N1 : 192.168.1.100 : master
N2 : 192.168.1.101 : slave1
N3 : 192.168.1.102 : slave2
N4 : 192.168.1.103 : slave3
N5 : 192.168.1.104 : slave4
N6 : 192.168.1.105 : slave5
master(N1) postgresql.conf
synchronous_standby_names = 'ANY 3 (N1_N2, N1_N3, N1_N4, N1_N5, N1_N6)'
slave recovery.conf
N2:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N1_N2'
N3:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N1_N3'
N4:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N1_N4'
N5:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N1_N5'
N6:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N1_N6'
2、DNS配置
TTL设置(尽量缩短客户端的dns cache时间, 例如10秒),域名映射配置。
192.168.1.100 : master
192.168.1.101 : slave1
192.168.1.102 : slave2
192.168.1.103 : slave3
192.168.1.104 : slave4
192.168.1.105 : slave5
3、ha管理软件配置
可以使用一个单独的数据库来存储,或者使用文件配置。
3.1 IP,数据库监听端口和ID的关系(端口必须固定、相等)
N1 : 192.168.1.100 , 1921
N2 : 192.168.1.101 , 1921
N3 : 192.168.1.102 , 1921
N4 : 192.168.1.103 , 1921
N5 : 192.168.1.104 , 1921
N6 : 192.168.1.105 , 1921
3.2 角色与域名的关系
master : master
slave : slave1
slave : slave2
slave : slave3
slave : slave4
slave : slave5
3.3 数据库用户密码
user : xx
pwd : xx
3.4 重试间隔,重试次数。
3.5 quorum数 = 3 # (取自master postgresql.conf的配置synchronous_standby_names = ‘ANY 3 (N1_N2, N1_N3, N1_N4, N1_N5, N1_N6)’)。
3.6 总节点数(包括master) = 6
failover流程
1、请求解析,从master角色的域名”master”,得到IP。
2、探测IP,数据库监听端口连通性。(异常时,注意重试次数、超时)
异常时,重试若干次(设置好重试间隔、重试次数),重试若干次均不可用,则进入failover流程。一旦重启期间可用,则退出failover。
3、探测数据库是否可以正常登录。(异常时,注意重试次数、超时)
异常时,重试若干次(设置好重试间隔、重试次数),重试若干次均不可用,则进入failover流程。一旦重启期间可用,则退出failover。
4、登录数据库,探测数据库可用性,(数据库alive检测,封装成数据库函数,返回true or false表示数据库是否可用)。(异常时,注意重试次数、超时)
返回false,或者返回异常,则数据库不可用。
异常时,重试若干次(设置好重试间隔、重试次数),重试若干次均不可用,则进入failover流程。一旦重启期间可用,则退出failover。
孤立slave
孤立slave的意思是让slave进入孤立状态,能接收读请求,但是wal receiver进程不工作,也不发feedback给master,和master脱离关系。
孤立必须具备持久性,例如,重启后依旧处于孤立状态。
实例操作方法
failover过程中,会涉及实例的操作,可以通过多种方式实现
1、通过数据库UDF实现数据库实例的文件、脚本等操作。
2、通过在数据库主机部署agent软件实现数据库实例的文件、脚本等操作。
推荐使用agent。
failover
1、使用配置,获取slave角色对应的域名,获取域名对应的IP。后面连接SLAVE的操作,均使用IP。
ip - node name 已有映射关系。
2、孤立若干个slave,若干是如何计算的呢?
公式
若干 = 总节点数(包括master) - quorum数 = 6-3 = 3
3、孤立若干个slave后,如果还有未孤立的slave,则继续孤立。
第2步的slave即使孤立不成功,也继续。(假设N2也异常)
4、从已孤立的SLAVE中,选择一个LSN最大的slave(我们这里假设为N3),作为new master,修改postgresql.conf。
synchronous_standby_names = 'ANY 3 (N3_N1, N3_N2, N3_N4, N3_N5, N3_N6)'
5、重命名new master的recovery.conf
rename recovery.conf to recovery.done
6、修改其他已孤立的slave - recovery.conf
N4:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N3_N4'
N5:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N3_N5'
N6:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N3_N6'
7、通知dns修改域名映射
-- 对调old master和new master
192.168.1.102 : master
192.168.1.101 : slave1
192.168.1.102 : slave2
192.168.1.100 : slave3
192.168.1.104 : slave4
192.168.1.105 : slave5
8、重启孤立实例。
9、解除孤立。
10、激活new master。
11、结束failover流程。
注意事项
任何步骤失败,解除孤立,从头再来。
修复异常节点
实例回退修复
通常不需要修复,因为不可能出现数据丢失的情况,对于用户视角已结束的事务,new master的wal内容和old master一定是一样的。
https://www.postgresql.org/docs/10/static/app-pgrewind.html
但是一些未结束事务(或者用户视角没有收到结束ACK的事务,虽然这些事务可能在OLD MASTER本地已持久化),可能会在old master,导致old master有更多的WAL(从业务视角来看,并无差异,因为事务ACK没有返回给客户),所以old master可能需要rewind。参考pg_rewind的方法。
1、配置recovery.conf
old master:
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N3_N1'
N2(异常slave):
primary_conninfo = 'host=master port=xx user=xx password=xx application_name=N3_N2'
2、配置old master postgresql.conf
注释
# synchronous_standby_names = ..........
重启或启动修复实例
如果不使用仲裁节点,而是让数据库自行选举的话可以考虑Raft协议
参考
什么是正确的新主?
在被锁定(不接收主库的WAL)的超过半数的节点里面,WAL最大的。
锁定的目的是防止选举过程中的波动,确保被选出的WAL一定是最大的。
锁定多数节点的目的是保证客户端视角的0事务丢失(因为对于主库来说,必须要等待WAL已拷贝到多数副本才返回客户端事务提交成功的ACK,客户端未收到ACK前,客户端视角的事务状态为UNKNOWN)。
如何选出新主?
1、一个备库(Follower)在随机超时(150~300ms)内没有收到其他任何节点的投票或心跳包,然后转换为候选人(Candidate),然后将自己进入WAL稳定态(即不接收任何来源的WAL),然后current TermID ++,然后投票给自己,向所有人发出选举请求(并携带WAL的最大位置信息,log Term ID, current Term ID),然后重置随机超时时间。
2、其他的备库,在收到选举投票的请求后,将自己进入WAL稳定态(即不接收任何来源的WAL),然后判断如果“(请求者log Term ID > 自己的log Term ID) 或 (请求者current TermID > 自己的current TermID 并且 请求者WAL >= 自己的WAL 并且 请求者log Term ID = 自己的log Term ID)”投给他(否则不投)。投票后将自己的current Term ID设置为请求者的current Term ID。投票后重置自己的选举超时时间。(不投则继续等待之前的随机超时)
3、当获得了多数人的投票(包括你自己)时,你就转换为新主,将log Term ID改为current Term ID,将log leader修改为自己的ID。重置随机超时时间,发送消息宣告消息包(内容current Term ID)给所有角色通知他们持久化log Term ID,超时则重新选举。
如果在4的所有节点完成调整之前,当选者挂了,不会影响继续选举。
4、收到宣告心跳包之前,Follower还可以给其他人投票,只要2的满足条件。
收到宣告(心跳)的节点,判断(收到的心跳包中的current Term ID == 自己的current Term ID)是否成立,如果成立,则将log Term ID修改为新主宣告的current Term ID,将log leader修改为新主ID,然后联系新主,执行pg_rewind,配置recovery.conf,作为新主的备库角色启动数据库。
注意这里的pg_rewind必须是原子操作,要么就变成和NEW LEADER一样,要么就是原样,不能有中间状态。
这样才能满足Raft协议不丢数据的需求。
5、新主每隔heart timeout后,发送统治心跳。
6、Follower收到统治心跳后,重置随机选举超时时间。
7、以上循环往复。
说明:
1、current Term ID,每次发起选举都会自增。
2、log Term ID,日志中持久化的TermID,用于选主的判定。
3、log leader,日志中持久化的主ID。
4、成员ID,每个成员都有一个唯一ID,每个成员ID对应了各自的属性(角色,IP,端口,用户名等连接串信息)。
5、pg_rewind,PostgreSQL的块级回溯命令,依据主进行回溯,可以达到一致状态。
6、heart timeout,选主成功后,维持统治的心跳用的。
7、随机选举超时时间,150~300毫秒。这个时间段内没有收到心跳或者选举请求,就开始选举。否则在心跳或投票后重置这个时间。
复活节点如何跟随统治
复活的节点,也是根据以上规则来跟随新的主库。
为什么不需要担心多写(多Leader)
因为一个Follower只有一个主,在发生网络脑裂,出现多LEADER,出现多写时,一定只有一个Leader能获得多数WAL副本,正常的提供写服务。另一个LEADER即使用户提交事务,也不能成功,会HANG住,不会有ACK返回给客户端。
根据选主的规则,恢复后,老的LEADER在没有跟上NEW LEADER之前,也不可能当选成为NEW LEADER。
UNKNOWN 事务的处理
unknown事务,就是客户端没有收到commit ACK的事务。不知道是成功还是失败。
多节点与单节点都可能出现UNKNOWN事务,效果、形态一致。
如何处理unknown事务呢?
unknown事务分为以下几种情况.
rollback, commit , rollback ps, commit ps 几种情况的unknown处理方法:
1、两阶段解决unknown状态问题
prepare 阶段unknown, 切换leader后,客户端检查prepare状态,如果没有ps则整个事务重新发起,因为数据库有事务原子性。如果ps存在则直接提交或回滚ps。
commit or rollback ps阶段unknown, 切换后检查ps状态,存在则重试commit or rollback ps.不存在则说明已经提交成功。无须处理。
2、非两阶段事务,rollback unknown无须处理,因为不管怎样都会回滚掉。要么完全没发生,要么成功rollback,要么数据库处理事务的异常rollback.这是数据库原子性保障的。
3、非两阶段事务的unknown commit处理,极度严谨的场景,可以设计事务状态可回溯,例如记录事务号或唯一流水号,事务号在数据库中是一个唯一的流水,可以根据事务号查询它的状态,比如postgresql.但是并不是所有数据库都有这种接口,比如非物理流式复制的数据库,则可以在事务中增加全局唯一流水号来查看事务是否提交。这里利用了事务的原子特性,既要么全成功要么全失败。可以举个使用例子。
为什么需要pg_rewind
虽然我们选出的是多数派中WAL最大的节点作为NEW LEADER,但是由于WAL是先产生和复制,后返回给客户端ACK的,因此死掉或被隔离的节点,WAL可能比NEW LEADER还多(只是这些事务没有返回客户端ACK而已,产生依旧还是会产生的,可以认为是无用功,或者认为是UNKNOWN 事务)。
因此需要通过pg_rewind,修正块级一致跟随NEW主库。
小结
1、建议使用虚拟IP,否则管理软件还需要维护一套IP变更的方法。
2、当节点数不为6,或者quorum数不等于3时,都可以支持,使用前面提到的孤立公式即可。
3、主库增加冻结功能(当与超过 sync standbys - ANY N + 1 个 sync standbys 断开连接,连续时间超过N秒时,冻结主库的写操作,变为只读节点)。
4、proxy只读请求的负载均衡策略,识别VIP当前所在主机,以主机为单位进行负载均衡,而不是VIP。 这样不会导致当多个VIP跑在同一个HOST上时(例如有的HOST挂了,会导致多个VIP可能跑在一个HOST上),出现负载不均衡的情况。
参考
https://www.postgresql.org/docs/10/static/runtime-config-replication.html#runtime-config-replication-master
https://www.postgresql.org/docs/10/static/app-pgrewind.html
http://dalibo.github.io/PAF/administration.html
https://github.com/digoal/PostgreSQL_HA_with_primary_standby_2vip