Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudflare接口服务中断故障复盘与思考 #37

Open
aCoder2013 opened this issue Dec 20, 2020 · 2 comments
Open

Cloudflare接口服务中断故障复盘与思考 #37

aCoder2013 opened this issue Dec 20, 2020 · 2 comments

Comments

@aCoder2013
Copy link
Owner

aCoder2013 commented Dec 20, 2020

前言

最近一段时间,各大厂商故障频发,就在上个月Cloudflare就出现了一次持续六个多小时的故障,接口的成功率下降至75%左右,并且管理后台已经几乎不可用,比平常慢了80多倍。Cloudflare也给出了一份详细的故障报告,
A Byzantine failure in the real world
, 简单来讲就是由于交换机异常,导致出现了网络丢包,etcd集群无法正常通讯导致无法正常对外提供服务,而上游业务强依赖etcd,导致服务出现异常。

故障原因

在一些需要强一致的场景下,Cloudflare大量使用了etcd来作为底层的存储层,这样即使其中少数节点挂掉,集群仍然能够正常对外提供服务,避免了单点问题。

首先在某一时间点,交换机出现异常(并且触发了内部的告警,无法通过ping连接到交换机),此时并没有完全挂掉,而只是出现部分节点网络丢包,因此没有触发自动切换机制(交换机有2个节点互备,非单点)。六分钟后,交换机在没有人为干预的情况下自动恢复了,但这几分钟的网络异常却导致了更严重的问题。异常交换机所在的机架上部署了etcd集群其中一个节点。而在交换机出现异常一分钟后,etcd集群节点之间通讯异常,导致无法选举出一个稳定的leader,集群无法正常对外提供读写服务:

4685_0

  • 节点1(部署在受影响机架上的节点)和节点3(当前leader)发生网络丢包
  • 节点1和节点2之前的网络通讯正常
  • 节点2和节点3之间的网络正常

由于节点1无法与当前leader节点3正常进行通讯,因此当选举超时后,在节点1的视角下,当前leader已经挂掉,因此会转到candidate,增加自己的term并尝试发起选举,节点2收到投票后,会更新自己的term,然后告诉节点3你已经不是leader了。但由于节点3无法和节点1进行正常通讯,因此超时后节点3会重复刚刚的动作,增加自己的term并尝试发起选举。整个集群选举无法出一个稳定的leader,导致无法正常对外提供读写服务。

这里只是举例其中一种可能发生的情况,也有可能节点1的日志落后于其他两个节点,因此节点2会拒绝节点1的选举请求,会投票给节点3,因此节点3仍然当选为leader,但是由于节点1无法与节点3(leader)正常通讯,因此超时后又会重复发起投票,而选举期间写入会阻塞,进而影响对外服务。

我们记得在Raft原始的论文中提到过:

[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.

也就是说即使发生了网络丢包等异常情况,只要大多数节点仍然能够正常通讯,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,从而避免了上述问题。

image

只要是分布式系统,就必然会遇到网络分区/丢消息的问题,例如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做一次转发,从而实现网络分区对上层无感知,当然性能还是有所损失。

image

在本次故障报告中,Cloudflare将其描述为拜占庭将军问题,其实不是太准确,说的直白一点,其实就是丢消息,并不是拜占庭将军问题,也不需要**BFT(byzantine fault tolerance)**协议才能解决,其实etcd本身已经支持了prevote,只是老版本默认是关闭的而已。

对业务的影响

Cloudflare的控制面板服务底层依赖的关系数据库部署在了同一个可用区的不同集群,每个集群都包含了一个主库、实时同步的备库以及一个或者多个异步备份节点。这样即使同一个数据中心的数据库挂掉,仍然能够切换到其他节点,而对于跨数据中心的冗余来说,Cloudflare将数据备份到了不同地理位置的多个数据中心,并利用etcd来进行集群成员的发现以及协调等。

在故障期间,etcd由于无法选举出稳定的leader,对外无法提供写入服务,因此两个集群之前的健康检测机制出现异常,无法正常交换对应集群内主库是否健康的消息,从而触发了自动的主从切换。

但是集群管理系统存在一个问题,当主从切换时,需要重建所有的备份,因此虽然主库已经恢复,但由于需要重建备库,此时备库集群是不可用的,具体恢复时间取决于主库的数据大小。其中一个数据库集群很快恢复了,因此没有造成太大的影响。但是另外一个集群,由于API的权限认证以及控制面板依赖,因此会又大量的读请求进来,Cloudflare通过多个备库来实现读写分离,降低主库的压力,但当发生主从切换时,需要重建所有备库,导致切换完成后备库全都不可用,所有的流量都打到了主库,造成主库负载非常高,故障主要是因为该问题引起

减轻主库的负载

由于所有的流量都打到了主库,因此此时主库负载非常高,首先是限流,其次上文中提到过,每个数据库集群在其他数据中心都有备份,但是自动切换机制仍然不支持,因此通过手动切换的方式,将流量切换到了其他数据中心,这一步极大的提升了接口的可用性。但是控制台由于涉及到用户登录等操作,创建session等流程需要写数据库以及redis集群,此时控制面板的体验变得更差了。

六小时后,备库重建完成,服务开始陆续恢复,并关闭对应的降级措施。

总结

当我们在做服务的稳定性时,总是会梳理系统有没有单点,比如数据库/Redis/第三方服务等,在这次故障中我们发现Cloudflare做的已经相当完善了,比如交换机是主从互备的,数据库采用了etcd,并且etcd基于Raft协议实现,只要大多数节点能够正常通讯,服务都是可用的,同时数据还备份到了不同的数据中心,故障过程中读流量支持切换到其他数据中心,而大多数其他小公司可能只部署到了一个数据中心,如果遇到同样的问题,由于所有的读流量都打到了主库,可能会导致主库挂掉等,问题会严重的多。在复盘的过程中,我们发现其实只是做到无单点/冗余,很多时候并不够,在本次故障中,大多数服务并没有完全挂掉,只是部分服务/流量异常,每个组件都处于降级状态,导致发生蝴蝶效应:

  • 交换机只是到部分节点的数据丢包,因此没有触发自动切换
  • etcd集群虽然是分布式的,但是由于发生了网络分区,导致集群无法选举出稳定的leader,无法对外提供写入服务
  • 网络恢复后,数据库需要重建备份,导致流量全部打到主库,主库负载太高,需要限流
  • 流量切换到其他数据中心后,接口开始恢复,但是由于无法写入,对于登录等涉及到写入的操作,情况反而恶化了

思考

  • 当我们梳理系统的依赖时,总是会去看是否为单点,比如Redis Sentinel/Redis Cluster,MySQL是否为主备,etcd是否为集群部署,但其实很多时候,挂掉并不可怕,可怕的是慢了,或者部分挂掉,比如etcd仍然能够提供读取服务,但是写入已经挂掉了,比如主从切换后由于需要重建所有备库,所有流量都打到主库造成负载过高。
  • 服务限流及隔离/依赖梳理,比如本次故障中,读取的流量可以切换到其他数据中心,降低主库的压力,而登录等涉及到写入的操作仍然在主库执行,而此时主库由于在进行重建备库等操作,性能会有所下降,因此上游服务仍然需要限流/降级措施,否则主库流量过大挂掉的话,问题会更严重
  • 故障演练的重要性,虽然数据库支持自动切换,但是没想到切换后重建备库花了六个多小时,之前AWS的S3也出过一次严重的故障,索引服务挂掉,重启时需要重建索引,但是由于数据量实在太大,重建花了很长时间。稳定性/高可用说起来很简单,很多时候系统没有出问题,不是因为系统稳定性做的有多完善,而是因为还没出问题。一旦下游出现了一丝抖动,可能会引起非常严重的故障,类似Netflix Facebook等公司的chaos monkey,将故障演练变为一种日常,例如随机关掉一台机器/随机网络丢包等。
  • 国外公司个人觉得做的很好的一点是,会非常大方的承认自己的过错,对外公布故障的影响面/前因后果以及后续的action等,而国内这方面还差的很远
  • 简单话/流程化/自动化,涉及到人工介入的,即使有一键降级等措施,从收到告警/登录VPN/定位/解决也需要几分钟
  • 对于底层的系统组件要了解其原理/协议,虽然etcd是分布式数据库,即使大多数节点挂掉仍然能够正常提供服务,但如果不了解相应的配置,仍然会吃大亏
@MarvinYu
Copy link

MarvinYu commented Mar 5, 2021

这样节点1发起选举时,节点2由于能够和当前leader节点1正常通讯,因此会拒绝节点1的预投票prevote,从而避免了上述问题。

这个描述需要修改为 【当前leader节点3】

@aCoder2013
Copy link
Owner Author

@MarvinYu 感谢!已修正

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants