脑裂(split-brain),指在一个高可用(HA)系统中,当联系着的两个节点断开联系时,本来为一个整体的系统,分裂为两个独立节点,这时两个节点开始争抢共享资源,结果会导致系统混乱,数据损坏。
对于无状态服务的HA,无所谓脑裂不脑裂;但对有状态服务(比如MySQL)的HA,必须要严格防止脑裂。(但有些生产环境下的系统按照无状态服务HA的那一套去配置有状态服务,结果可想而知...)
一般采用2个方法
1. 仲裁
当两个节点出现分歧时,由第3方的仲裁者决定听谁的。这个仲裁者,可能是一个锁服务,一个共享盘或者其它什么东西。
2. fencing
当不能确定某个节点的状态时,通过fencing把对方干掉,确保共享资源被完全释放,前提是必须要有可靠的fence设备。
理想的情况下,以上两者一个都不能少。
但是,如果节点没有使用共享资源,比如基于主从复制的数据库HA,我们也可以安全的省掉fence设备,只保留仲裁。而且很多时候我们的环境里也没有可用的fence设备,比如在云主机里。
那么可不可以省掉仲裁,只留fence设备呢?
不可以。因为,当两个节点互相失去联络时会同时fencing对方。如果fencing的方式是reboot,那么两台机器就会不停的重启。如果fencing的方式是power off,那么结局有可能是2个节点同归于尽,也有可能活下来一个。但是如果两个节点互相失去联络的原因是其中一个节点的网卡故障,而活下来的正好又是那个有故障的节点,那么结局一样是悲剧。
所以,单纯的双节点,无论如何也防止不了脑裂。
以PostgreSQL或MySQL的数据复制为例来说明这个问题。
在基于复制的场景下,主从节点没有共享资源,所以2个节点都活着本身没有问题。问题是客户端会不会访问到本该死掉的那个节点。这又牵扯到客户端路由的问题。
客户端路由有几种方式,基于VIP,基于Proxy,基于DNS或者干脆客户端维护一个服务端地址列表自己判断主从。不管采用哪种方式,主从切换的时候都要更新路由。
基于DNS的路由是不太靠谱的,因为DNS可能会被客户端缓存,很难清干净。
基于VIP的路由有一些变数,如果本该死掉的节点没有摘掉自己身上的VIP,那么它随时可能出来捣乱(即使新主已经通过arping更新了所有主机上的arp缓存,如果某个主机的arp过期,发一个arp查询,那么就会发生ip冲突)。所以可以认为VIP也是一种特殊的共享资源,必需把它从故障节点上摘掉。至于怎么摘,最简单的办法就是故障节点发现自己失联后自己摘,如果它还活着的话(如果它死了,也就不用摘了)。如果负责摘vip的进程无法工作怎么办?这时候就可以用上本来不太靠谱的软fence设备了(比如ssh)。
基于Proxy的路由是比较靠谱的,因为Proxy是唯一的服务入口,只要把Proxy一个地方更新了,就不会发生客户端误访问的问题了,但是也要考虑Proxy的高可用。
至于基于服务端地址列表的方法,客户端需要通过后台服务判断主从(比如PostgreSQL/MySQL会话是否处于只读模式)。这时,如果出现2个主,客户端就会错乱。为防止这个问题,原主节点发现自己失联后要自己把服务停掉,这和前面摘vip的道理是一样的。
因此,为了不让故障节点捣乱,故障节点应该在失联后自己释放资源,为了应对释放资源的进程本身出现故障,可以加上软fence。在这个前提下,可以认为没有可靠的物理fence设备也是安全的。
主从切换后数据会不会丢和脑裂可以认为是2个不同的问题。还以PostgreSQL或MySQL的数据复制为例来说明。
对PostgreSQL,如果配置成同步流复制,可以做到不管路由是否正确,都不会丢数据。因为路由到错误节点的客户端根本写不进任何数据,它会一直等待从节点的反馈,而它以为的从节点,现在已经是主子了,当然不会理它。当然如果老是这样也不好,但它给集群监视软件纠正路由错误提供了充足的时间。
对MySQL,即使配置成半同步复制,在超时发生后,它可能会自动降级为异步复制。为了防止MySQL的复制降级,可以设置一个超大的rpl_semi_sync_master_timeout,同时保持rpl_semi_sync_master_wait_no_slave为on(即默认值)。但是,这时如果从宕了,主也会hang住。这个问题的破解方法和PostgreSQL是一样的,或者配置成1主2从,只要不是2个从都宕机就没事,或者由外部的集群监视软件动态切换半同步和异步。
如果本来就是配置的异步复制,那就是说已经做好丢数据的准备了。这时候,主从切换时丢点数据也没啥大不了,但要控制自动切换的次数。比如控制已经被failover掉的原主不允许自动上线,否则如果因为网络抖动导致故障切换,那么主从就会不停的来回切,不停的丢数据,破坏数据的一致性。
你可以自己完全从头开始实现一套符合上述逻辑的脚本。但我更倾向于基于成熟的集群软件去搭建,比如Pacemaker+Corosync+合适的资源Agent。Keepalived我是极不推荐的,它就不适合用于有状态服务的HA,即使你把仲裁,fence那些东西都加到方案里,总觉得别扭。
使用Pacemaker+Corosync的方案也有一些注意事项
1)了解资源Agent的功能和原理
了解资源Agent的功能和原理,才能知道它适用的场景。比如pgsql的资源Agent是比较完善的,支持同步和异步流复制,并且可以在两者之前自动切换,并且可以保证同步复制下数据不会丢失。但目前MySQL的资源Agent就很弱了,没有使用GTID又没有日志补偿,很容易丢数据,还是不要用的好,继续用MHA吧(但是,部署MHA时务必要防范脑裂)。
2)确保法定票数(quorum)
quorum可以认为是Pacemkaer自带的仲裁机制,集群的所有节点中的多数选出一个协调者,集群的所有指令都由这个协调者发出,可以完美的杜绝脑裂问题。为了使这套机制有效运转,集群中至少有3个节点,并且把no-quorum-policy设置成stop,这也是默认值。(很多教程为了方便演示,都把no-quorum-policy设置成ignore,生产环境如果也这么搞,又没有其它仲裁机制,是很危险的!)
但是,如果只有2个节点该怎么办?
一是拉一个机子借用一下凑足3个节点,再设置location限制,不让资源分配到那个节点上。
二是把多个不满足quorum小集群拉到一起,组成一个大的集群,同样适用location限制控制资源的分配的位置。
但是如果你有很多双节点集群,找不到那么多用于凑数的节点,又不想把这些双节点集群拉到一起凑成一个大的集群(比如觉得不方便管理)。那么可以考虑第三种方法。
第三种方法是配置一个抢占资源,以及服务和这个抢占资源的colocation约束,谁抢到抢占资源谁提供服务。这个抢占资源可以是某个锁服务,比如基于zookeeper包装一个,或者干脆自己从头做一个,就像下面这个例子。
http://my.oschina.net/hanhanztj/blog/515065
(这个例子是基于http协议的短连接,更细致的做法是使用长连接心跳检测,这样服务端可以及时检出连接断开而释放锁)
但是,一定要同时确保这个抢占资源的高可用,可以把提供抢占资源的服务做成lingyig高可用的,也可以简单点,部署3个服务,双节点上个部署一个,第三个部署在另外一个专门的仲裁节点上,至少获取3个锁中的2个才视为取得了锁。这个仲裁节点可以为很多集群提供仲裁服务(因为一个机器只能部署一个Pacemaker实例,否则可以用部署了N个Pacemaker实例的仲裁节点做同样的事情。)。但是,如非迫不得已,尽量还是采用前面的方法,即满足Pacemaker法定票数,这种方法更简单,可靠。