最近一段时间,各大厂商故障频发,就在上个月Cloudflare就出现了一次持续六个多小时的故障,接口的成功率下降至75%左右,并且管理后台已经几乎不可用,比平常慢了80多倍。Cloudflare也给出了一份详细的故障报告,
A Byzantine failure in the real world, 简单来讲就是由于交换机异常,导致出现了网络丢包,etcd集群无法正常通讯导致无法正常对外提供服务,而上游业务强依赖etcd,导致服务出现异常。
[Consensus algorithms] are fully functional (available) as long as any majority of the servers are operational and can communicate with each other and with clients.
前言
最近一段时间,各大厂商故障频发,就在上个月Cloudflare就出现了一次持续六个多小时的故障,接口的成功率下降至75%左右,并且管理后台已经几乎不可用,比平常慢了80多倍。Cloudflare也给出了一份详细的故障报告, A Byzantine failure in the real world, 简单来讲就是由于交换机异常,导致出现了网络丢包,etcd集群无法正常通讯导致无法正常对外提供服务,而上游业务强依赖etcd,导致服务出现异常。
故障原因
首先在某一时间点,交换机出现异常(并且触发了内部的告警,无法通过ping连接到交换机),此时并没有完全挂掉,而只是出现部分节点网络丢包,因此没有触发自动切换机制(交换机有2个节点互备,非单点)。六分钟后,交换机在没有人为干预的情况下自动恢复了,但这几分钟的网络异常却导致了更严重的问题。异常交换机所在的机架上部署了etcd集群其中一个节点。而在交换机出现异常一分钟后,etcd集群节点之间通讯异常,导致无法选举出一个稳定的leader,集群无法正常对外提供读写服务:
由于节点1无法与当前leader节点3正常进行通讯,因此当选举超时后,在节点1的视角下,当前leader已经挂掉,因此会转到candidate,增加自己的term并尝试发起选举,节点2收到投票后,会更新自己的term,然后告诉节点3你已经不是leader了。但由于节点3无法和节点1进行正常通讯,因此超时后节点3会重复刚刚的动作,增加自己的term并尝试发起选举。整个集群选举无法出一个稳定的leader,导致无法正常对外提供读写服务。
我们记得在Raft原始的论文中提到过:
也就是说即使发生了网络丢包等异常情况,只要大多数节点仍然能够正常通讯,Raft协议仍然能够保证正常提供服务,但通过上面的描述,我们发现其实并不是如此,节点1和节点2/3发生了网络分区,导致整个集群都不可用。 那是不是说Raft无法应对这种情况呢,其实不是的。 通过上面的分析,我们可以看出,节点2和节点3(leader)是能够正常通讯的,但是节点1和leader由于网络丢包无法建立连接,因此发起了选举,节点2和节点1不停的发起去选举/投票/选主,导致集群无法正常提供服务,其实Diego Ongaro’s thesis中提到过解决方案,即PreVote,也就说当节点1作为候选者想要发起选举之前,首先需要发起一次PreVote预投票,如果得到了大多数节点的同意,此时才会增加自身的term并发起投票, 这里有一个比较关键的地方是,follower只有在当前leader超时后(在electionTimeout内仍然没有收到leader的心跳包),才会投票给候选者,这样节点1发起选举时,节点2由于能够和当前leader节点3正常通讯,因此会拒绝节点1的预投票prevote,从而避免了上述问题。
只要是分布式系统,就必然会遇到网络分区/丢消息的问题,例如Zookeeper/TiDB/HBase/MongoDB等,Raft协议提供了prevote的解决方案,但是相对来讲还是有一定的复杂性以及侵入性,并且需要上层应用打开相关配置,其实除了协议层的解决方案之外,还有另外一种更加通用的方式,在网络层解决,感兴趣的可以阅读这篇论文Toward a Generic Fault Tolerance Technique for Partial Network Partitioning, 简单来讲,比如节点1和节点4之前发生了网络分区无法正常通讯,但是从1 -> 2 -> 3 -> 4的网络是正常的,此时我们可以通过修改路由,做一次中转,将节点1到节点4的数据包通过节点2和节点3做一次转发,从而实现网络分区对上层无感知,当然性能还是有所损失。
对业务的影响
Cloudflare的控制面板服务底层依赖的关系数据库部署在了同一个可用区的不同集群,每个集群都包含了一个主库、实时同步的备库以及一个或者多个异步备份节点。这样即使同一个数据中心的数据库挂掉,仍然能够切换到其他节点,而对于跨数据中心的冗余来说,Cloudflare将数据备份到了不同地理位置的多个数据中心,并利用etcd来进行集群成员的发现以及协调等。
在故障期间,etcd由于无法选举出稳定的leader,对外无法提供写入服务,因此两个集群之前的健康检测机制出现异常,无法正常交换对应集群内主库是否健康的消息,从而触发了自动的主从切换。
但是集群管理系统存在一个问题,当主从切换时,需要重建所有的备份,因此虽然主库已经恢复,但由于需要重建备库,此时备库集群是不可用的,具体恢复时间取决于主库的数据大小。其中一个数据库集群很快恢复了,因此没有造成太大的影响。但是另外一个集群,由于API的权限认证以及控制面板依赖,因此会又大量的读请求进来,Cloudflare通过多个备库来实现读写分离,降低主库的压力,但当发生主从切换时,需要重建所有备库,导致切换完成后备库全都不可用,所有的流量都打到了主库,造成主库负载非常高,故障主要是因为该问题引起。
减轻主库的负载
由于所有的流量都打到了主库,因此此时主库负载非常高,首先是限流,其次上文中提到过,每个数据库集群在其他数据中心都有备份,但是自动切换机制仍然不支持,因此通过手动切换的方式,将流量切换到了其他数据中心,这一步极大的提升了接口的可用性。但是控制台由于涉及到用户登录等操作,创建session等流程需要写数据库以及redis集群,此时控制面板的体验变得更差了。
六小时后,备库重建完成,服务开始陆续恢复,并关闭对应的降级措施。
总结
当我们在做服务的稳定性时,总是会梳理系统有没有单点,比如数据库/Redis/第三方服务等,在这次故障中我们发现Cloudflare做的已经相当完善了,比如交换机是主从互备的,数据库采用了etcd,并且etcd基于Raft协议实现,只要大多数节点能够正常通讯,服务都是可用的,同时数据还备份到了不同的数据中心,故障过程中读流量支持切换到其他数据中心,而大多数其他小公司可能只部署到了一个数据中心,如果遇到同样的问题,由于所有的读流量都打到了主库,可能会导致主库挂掉等,问题会严重的多。在复盘的过程中,我们发现其实只是做到无单点/冗余,很多时候并不够,在本次故障中,大多数服务并没有完全挂掉,只是部分服务/流量异常,每个组件都处于降级状态,导致发生蝴蝶效应:
思考