背景介绍
在当今数字化时代,分布式系统已成为支持现代应用的关键基础架构。分布式系统通过将计算和存储分散在多个节点上,提供了高性能、高可扩展性的弹性解决方案。然而,随着应用复杂性和用户需求的增加,分布式系统面临日益严峻的挑战,其中显著的挑战之一是在强一致性与高可用性之间的权衡。
CAP定理指出,一个分布式系统无法同时保证强一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。这使得设计者必须在这三个要素之间进行权衡,并选择适合特定应用场景的方案。由于网络分区在分布式系统中无法避免,CAP理论演变成了在强一致性和高可用性之间的选择。
一些系统对一致性要求极高,如金融系统、财务账务系统等,都要求数据写入的强一致性。而一些系统对可用性要求极高,如电商网站、12306平台等,在双11、黑五、春运等高峰时期,系统的可用性会受到极大考验,每宕机1秒钟,就有可能影响数十万个请求,对公司的信誉也会造成不利影响。
eBay支付核心账务系统FAS(Financial Accounting System)作为电商网站中的账务系统,承担了一致性和可用性的双重高要求。本文将结合FAS系统在生产环境中遇到的真实问题,探讨强一致性分布式系统在实现高可用性方面所面临的挑战,并分享FAS系统的实践经验。通过深入分析这一议题,我们可以更好地理解如何平衡高可用性和强一致性之间的关系。
01 一致性和可用性的矛盾
在一个系统中,若只有一台机器和一个线程负责所有计算与存储,则该系统具备强一致性。因为数据只有一份拷贝,且仅有一个线程对数据进行操作,避免了竞态条件。然而,这类系统存在单点故障的风险,因此我们需采用多台计算机器和多个存储备份以提升计算与存储的可用性。
当系统拥有多台机器和多个存储备份时,我们又会面临一致性的挑战。若欲保证强一致性,则每个数据更新请求需同步到所有数据备份才可提交。然而,若某个数据存储宕机或发生网络故障,数据更新请求便无法到达某一个或多个数据存储节点。此时,我们面对抉择:放弃不可达的数据存储还是持续重试?若放弃,可能引发一致性问题;若持续重试,可能影响系统的可用性。
这即是一致性和可用性之间的矛盾,是分布式系统设计中必须权衡和解决的关键问题。
02 FAS系统强一致性
和基本的高可用实现
FAS是一个金融级的账务系统,其必备的首要特性是数据写入的强一致性。因此,我们的目标是在确保一致性的同时,尽可能提高可用性。为实现这一目标,FAS系统采用了Raft协议,确保了强一致性和基本的高可用性。
—Raft协议简介—
Raft是一种分布式系统中的一致性共识算法,旨在确保一致性、容错性和可用性。详细介绍请参考Raft论文[1]。其核心概念包括:
领导者选举:节点间通过投票机制选出领导者以处理客户端请求。
日志复制:领导者把日志同步给其他的跟随者,确保所有节点拥有相同的日志记录。
安全性:利用多数派原则保障系统在故障或网络分区时的安全性。Raft集群通常包含奇数个节点以维护多数派原则。
“
Raft协议的强一致性基于以下几点:首先,协议确保每个时间点(任期)只有一个领导者,领导者独自处理客户端请求,从而消除了竞态条件。其次,通过多数派原则,只有大多数节点提交了更改,请求才被认为成功执行,从而保证请求结果同步到大多数节点。此外,即使当前领导者宕机,下一任领导者也会从拥有最新数据的节点选举出来,保证数据不丢失。这些特性解释了Raft协议如何实现强一致性。
“
在FAS系统中,一个Raft集群部署了5个节点,分别部署在三个数据中心,如图一所示。
图一:FAS系统Raft集群部署
这种部署方式下,Raft协议只需3个节点正常运行,即可选举出领导者提供服务。5个节点分布在3个数据中心,采用2-2-1的部署方式,即使一个数据中心宕机,仍有3个节点存活,满足大多数节点的要求,继续对外提供服务。因此,这种部署方式还具备数据中心级别的容灾能力。
总体而言,FAS的Raft集群部署方式可容忍2个节点宕机,并且具备一个数据中心级别的容灾能力,满足基本的高可用性要求。
03 FAS系统面临的
高可用挑战和解决方案
—高可用挑战的分类—
我们对整个FAS系统包含的模块进行了梳理,如图二所示。从客户端发出请求,经过FAS软件系统(图中为PU Cluster)处理请求,再到底层的硬件和网络,任何环节都可能发生故障。故障发生后,人工恢复数据耗时且可能出错。我们对可能出现的故障进行了分类:
网络故障:
客户端与集群领导者网络不通
集群内部跟随者与领导者之间存在单向网络通信问题
Raft算法 - 领导者选举期间的集群不可用情况
公司平台安全性 - 密钥更新问题
硬件故障与数据恢复
图二:FAS系统涉及的模块
“
1.客户端与集群领导者网络不通
尽管FAS系统的Raft集群有5个节点,但只有领导者能够接收客户端的请求,因此领导者成为了单点(Single Point of Failure)。如果某个客户端与领导者的网络不通,即使该客户端与集群中其他4个跟随者节点相通,请求也无法成功执行,只能收到领导者超时的返回。如下图三所示。
图三:Raft集群领导者单点故障
解决方案:
跟随者转发请求
为了保证强一致性,只能由领导者来处理客户端的请求,这是无法改变的。因此,我们的解决方案是利用跟随者作为桥梁。当客户端无法与领导者通信时,可以将请求发送给跟随者节点。跟随者作为桥梁将请求转发给领导者,并将领导者处理后的响应原路返回给客户端。通过这种机制,解决了客户端与领导者单点故障的问题。每个跟随者都可以作为桥梁转发请求,从而大大提高了系统的可用性。如图四所示:
图四:跟随者转发请求给领导者
这个机制需要客户端和服务器端共同实现,客户端的逻辑我们实现到了客户端库中。客户只需引用这个库包,就会拥有这个功能,无需关注内部细节。
弊端:
跟随者转发请求的方案解决了客户端与集群领导者单点故障的问题,但在网络正常情况下,这个方案也存在一定的弊端:
增加了集群内部的网络流量开销。原本请求直接发送给领导者,但采用这个方案会使流量增多,增加了从跟随者到领导者的流量。尽管对当前线上请求的影响不明显,但仍增加了网络开销。
增加了请求处理的时延。由于多了一个转发环节,请求的处理时间会延长。
优化:
为了优化这些弊端,我们做了一些改进。首先,正常情况下客户端仍直接将请求发送到集群领导者。只有在客户端与集群领导者的连接不通时,才会启用跟随者转发机制。
然而,存在一个问题:何时切换回直接发送给领导者的状态?为了尽快恢复到直接发送给领导者的状态,但又避免频繁切换造成不必要的错误和延迟,我们引入了探针机制。
探针机制的工作原理是每隔1分钟尝试切换回直接发送给领导者的状态。如果切换成功,则下一个请求将直接发送给领导者,恢复到正常状态;如果切换失败,则说明客户端与领导者的网络仍未恢复,继续使用跟随者转发机制,直到下一个1分钟的周期。
总之,通过跟随者转发和探针机制的结合使用,我们可以在网络恢复后尽快切换回正常状态,避免不必要的流量开销和时延,同时又能避免频繁切换带来的不必要错误和更大的延迟。
2.集群内部
跟随者与领导者单向网络故障
上一小节我们介绍了客户端与集群领导者网络不通的情况造成请求不能被处理,其实在Raft集群内部也可能发生网络故障,导致集群不稳定,但这个网络故障比较特殊,是单向网络故障。
通常情况下,一个包含5个节点的Raft集群,如果有2个节点的网络彻底不通,集群仍能正常工作。然而,一旦发生单向网络,整个集群将无法正常运行。单向网络的情况如图五所示:领导者无法将数据同步给Follower4,但Follower4可以向其他所有节点发送请求。在Kubernetes生态中,单向网络通常发生在Pod刚创建时,DNS服务尚未将该Pod的信息注册,导致其他节点无法访问它,但该Pod仍能访问其他节点。
图五:单向网络导致不断选主
在示例中,由于Follower4长时间未收到领导者的数据或心跳,认为领导者已不存活,因此发起选主请求。其他节点收到选主请求后会更新任期,领导者下台,然后重新选出新的领导者。然而,Follower4仍不会收到新领导者的数据或心跳,因此再次发起选主请求。这样,集群不断进行选主过程,导致整个集群处于不可用状态,在选主期间的请求会受到影响。
图六展示了我们在生产环境中发生单向网络的OPS(Operation Per Second)监控情况。在4:46到4:56期间,单向网络发生,导致OPS从150以上骤降至50以下,这是由于不断的选主过程导致许多请求失败。
图六:单向网络造成TPS下降
解决方案:
PreVote机制
为了解决单向网络导致的集群反复选主过程频繁发生的问题,我们引入了PreVote(预选举)机制。PreVote的核心思想是在一个节点发起选举之前,先发送一条PreVote请求。其他节点收到PreVote请求时,只有同时满足以下两个条件才会同意选举:
当前没有领导者存活。
发起PreVote请求的节点的日志与自己的日志一样或更超前。
图七:PreVote避免不必要的选举
有了PreVote机制后,只有在大部分节点同意了PreVote选举后,该节点才能真正发起选举请求。如图七所示,当Follower4想要发起选举请求时,首先发送一个PreVote请求。其他4个节点发现当前已有一个领导者正常工作,因此会拒绝该PreVote请求。此外,由于Follower4的日志滞后,其他节点更不会同意PreVote请求。因此,Follower4会发现自己的PreVote请求都被拒绝,因此不会继续发起正式的选举请求。这样,原来的领导者也不会下台,不会影响任何请求的处理。PreVote机制有效地避免了在单向网络情况下不断发生选主过程,提高了系统的可用性和稳定性。
图八:PreVote上线后的效果
图八显示了我们FAS系统中各个节点Raft日志Offset的增长情况监控图。通常情况下,随着时间的推移和请求的处理,Raft日志会持续增长。然而,在第二个节点的红框时间区间内,Raft日志的曲线保持了平衡。通过后台的系统日志记录,我们发现在那段时间内发生了单向网络问题,导致领导者无法将自己的Raft日志复制给该节点,因此该节点不断发送PreVote请求。
我们当时已经实现了PreVote机制,在那段时间内由于领导者仍然存活,所有其他节点都拒绝了该节点发送的PreVote请求。因此,整个系统没有受到任何影响,保持了稳定运行,可以从其他节点的Raft日志的offset都在持续增长看出,请求被持续的成功执行。PreVote机制的成功应用有效地避免了单向网络问题对系统的影响。
弊端:
PreVote机制解决了单向网络情况下Raft集群不断选主的问题,增加了系统的可用性。然而,任何事物都有两面性,PreVote机制也存在一些明显的弊端,主要是增加了选主的时延。因为在每次选主之前都要进行一轮预选举。
不过,系统的选主并不会经常发生,以FAS系统为例,可能几天甚至几周才会发生一次选举。另外,PreVote增加的选主的时延通常在毫秒级别,对系统影响甚微。因此,尽管PreVote机制增加了选主的时延,其对系统整体性能的影响非常小。
单向网络故障测试
我们做了PreVote功能之后,需要测试效果。但模拟单向网络故障并不是一件容易的事,普通的程序员根本无法去操作网络,特别还要单向网络。这里我们利用了eBay内部提供的Chaos Engineering工具,基于开源框架Litmus实现的一套Chaos的平台,可以指定任意两个节点之间,任意方向的网络故障。详情可见“eBay支付核心账务系统之混沌不摧”。
“
Raft集群为了保证一致性,只能由领导者去处理客户端请求。因此,在集群处于领导者选举过程中时,整个集群是无法对外提供服务的,直到选出一个领导者才能对外提供服务。因此,我们的目标是尽量缩短领导者选举的时间,并通过客户端的重试机制,在有了领导者之后让客户端能够成功处理请求。然而,需要注意的是,领导者选举时间并不是能够无限缩短的,现在我们先来了解一下Raft领导者选举的机制。
Raft集群选举的机制
在Raft集群中,领导者通过心跳机制维持其领导者角色。领导者会定期向跟随者发送数据,即使没有实际数据需要发送,领导者也会发送一个空Payload的请求作为心跳。这样,跟随者持续收到领导者发来的消息,就会知道当前有一个领导者在工作。
然而,如果领导者宕机或者网络发生故障,导致领导者无法发送数据或心跳给跟随者,跟随者在一段时间内(称为Leader Election Timeout)没有收到领导者的消息后,就会认为领导者不再存活。于是,跟随者会将自己的角色变为候选者(Candidate),并向其他所有节点发起选举请求。其他节点则有可能选举该候选节点作为新的领导者。图九的右半部分描述了这个选举的过程。
图九:Raft集群选举机制和客户端重试机制
FAS客户端重试机制
作为FAS的客户端,我们还实现了重试机制。如图九的左半部分所示,如果客户端发现当前的领导者不可达,它会将请求发送给下一个节点。如果下一个节点恰好是新的领导者,新的领导者就会直接执行请求。但如果下一个节点不是领导者,它会告知客户端正确的领导者信息。然后客户端会重试将请求发送给正确的领导者。这样,客户端能够有效地处理领导者不可达的情况,确保请求最终被正确处理。
Leader Election Timeout
了解了领导者选举机制,我们意识到其中一个重要的配置参数就是Leader Election Timeout的时间。这个值的设定至关重要:
不能设得太长:如果Leader Election Timeout的时间设置得太长,那么在领导者宕机后,跟随者需要较长时间才能发现领导者宕机并重新选举。这会导致整个集群的不可用时间变长。
不能设得太短:领导者给跟随者发送数据也需要时间,因此Leader Election Timeout的时间不能小于领导者发送数据给跟随者所需的时间。否则,领导者还没来得及发完数据给跟随者,跟随者就发起了新一轮的选举,导致数据无法传输。
因此,在设置Leader Election Timeout的时间时,需要权衡考虑,确保既能够及时发现领导者宕机并进行重新选举,又不会因为过短的超时时间而导致选举频繁发生或数据传输失败。
图十:Leader Election Timeout的设置
根据图十中FAS系统生产环境的数据统计,我们发现99.999%的数据传输可以在200毫秒内完成。因此,我们将Leader Election Timeout的时间从以前的1秒至2秒改为了200毫秒至400毫秒之间的随机值。
通过这一优化,我们成功将领导者宕机重新选举时服务不可用的时长从1秒至2秒缩短至200毫秒到400毫秒之间。此外,所有在这段不可用时间内的请求可以通过我们提供的客户端库中的重试机制在新的领导者选举完成后快速得到处理,客户无需自行实现重试逻辑。
“
——密钥更新问题
出于安全考虑,我们要求所有写入磁盘的数据都必须经过加密,并且加密所需的密钥每年都会更新一次,这给我们的集群带来了一些挑战。在处理完请求后,领导者会将结果加密后写入本地磁盘的Raft日志中,然后将加密的Raf日志发送给各个跟随者。跟随者接收到领导者发送的数据后,会先将其写入本地磁盘,然后使用相同的密钥解密数据,并将解密后的数据应用到本地的状态机中。
密钥存储在第三方服务中,每年定期更新一次。FAS系统原本的实现是只有在节点启动时才会访问这个第三方服务,获取密钥并将其保存在本地内存中,以便在需要加密时使用。
然而,如果密钥从版本v1更新到版本v2,并且此时FAS集群发生了重启,可能会导致一些问题。例如,重启后的节点1成为了领导者,并使用了最新的密钥v2对Raft日志进行加密。但节点3、节点4和节点5还没有重启,仍然拥有着旧版本v1的密钥,因此无法解密领导者发送的使用v2加密的Raft日志,导致整个集群不可用。
为了解决这个问题,我们有两种解决方案。
保证所有跟随者先进行重启。
密钥服务返回的是整个密钥历史链。所有跟随者先重启会获取到v1和v2两个版本的密钥。领导者还没有重启,所以发送的Raft日志中使用旧版本v1加密。Raft日志的元数据中会包含密钥的版本信息,跟随者可以根据这个信息使用旧版本的密钥v1进行解密操作即可。然而,这种方式需要确保所有跟随者先进行重启,是比较耗时和易错的,需要一些复杂的人工方法来控制之前已经重启过的跟随者不会成为领导者。
动态获取密钥信息。
这种方式不需要考虑节点的重启顺序。即使领导者节点先进行了重启并获取了最新的密钥v2进行加密,跟随者节点在Raft日志的元数据中发现了密钥是v2,而本地内存中只有旧版本v1的密钥,它们只需再次访问密钥服务获取最新的密钥v2即可完成解密操作。
前两年我们采用的是第一种方案,在实施的过程中还发生过生产事故,原因是密钥更新了的时候我们却不知晓,没能让所有跟随者先重启。去年的时候我们做了优化采用了第二种方式,通过动态获取密钥信息,避免了需要考虑节点重启顺序的问题,在密钥更新时既提高了可用性,还减少了人工成本。
“
eBay的应用部署在基于Kubernetes构建的Tess平台之上。Kubernetes的一个优势是Pod失效时,平台能够自动创建一个新的Pod来接替原先的Pod工作。这种机制对于无状态的服务非常友好。
然而,我们的系统是有状态的服务,其状态存储在本地的RocksDB文件中。状态是通过应用Raft日志来维护的,每个时刻的状态都对应着Raft日志的某个offset。当FAS系统启动时,需要加载状态信息文件(RocksDB文件),并从该状态对应的offset开始应用后续的Raft日志,以便正确追赶到最新的状态。如果启动时状态信息文件不存在,系统将无法成功启动。而当硬件故障发生时,有时Pod会重新调度到其他物理节点,导致原状态文件不再可用,这时我们需要将状态文件从集群中的其他节点拷贝到新的Pod上。这个过程通常耗费2个小时,而且容易出错。
图十一:自动拷贝状态信息文件
为了解决这个问题,我们采取了自动化措施。我们编写了脚本来自动完成原本需要人工拷贝数据的过程。通过自动化,我们大大节省了人力成本,并且在系统遇到机器故障导致Pod重新部署时,能够自我修复。即使在非工作时间发生故障,我们的团队也不需要为此而担心,因为自动化措施能够确保系统的可用性。
04 可用性指标
可用性是指一个系统能够在一定时间内持续正常运行和提供服务的能力,可以用一段时间内能够提供服务的时间除以这段时间的总时间得到的比例来计算。但这里对“能够提供服务的时间”并没有一个明确的规定。我们可以从三个层次来考虑这个问题。
没有接到报警电话。
在这种情况下,系统的可用性指标是指系统在一段时间内是否能够持续运行而没有触发报警电话。这种衡量方式主要考虑系统是否能够处理严重问题,但对于轻微的影响或者客户端无法察觉的问题可能不敏感。
系统本身可以提供服务,不管客户端能不能调用。
这个层次下,系统的可用性指标是指系统本身是否能够正常工作,而不考虑客户端能否成功调用系统服务。这种衡量方式适用于系统内部运行正常,但外部因素导致客户端无法连接的情况。比如FAS系统,一个领导者和四个跟随者组成了Raft集群,也能正常对外提供服务。但由于单点网络故障,某些客户端就是连不上领导者,尽管FAS集群没有问题,但客户端的请求却不能被成功处理。
每个客户端的请求都能被成功处理。
这是最严格的层次,要求系统对外完全可用,每个客户端的请求都能被成功处理。这种衡量方式要求系统在各种情况下都能正常响应客户端请求,是对系统可用性要求最高的一种衡量方式。
对于FAS系统来说,采用了最严格的第三个层次的可用性指标,即要求每个客户端的请求都能够被成功处理。这种衡量方式可以通过记录一整年内所有客户端失败的请求数来计算。总的请求数减去失败的请求数,然后除以总请求数,得到系统的可用性。根据统计,2023年FAS系统的可用性达到了99.99997625%(6个9以上),如果换算成时间,也就是一年中完全不可用的时间在8秒以内。
05 总结
eBay的FAS系统是一个服务于电商网站的财务账务系统,对一致性和高可用性有着极高的要求。为了满足这些要求,系统采用了Raft协议来实现数据写入的强一致性和基本的高可用性。
在系统设计和优化过程中,我们结合了生产环境中遇到的真实案例,探讨了引起可用性问题的各种场景,包括单点网络故障、单向网络、密钥变更等等。针对这些问题,我们采取了一系列措施来提高系统的可用性:PreVote、跟随者转发请求、调整领导者选举超时时间和重试机制、动态获取密钥信息、自动化数据恢复等。
通过这些优化和改进,我们大大提高了系统的可用性,使得FAS系统能够更好地满足业务需求,为电商网站提供稳定可靠的财务账务服务。
06 展望
在过去的几年中,我们取得了显著的系统可用性改进,为eBay提供了一套稳定的财务账务系统。然而,维持系统的高可用性是一个不断演进的过程。即使今天我们实现了6个9的高可用性,明天也不一定能保持这种水平。这是因为我们的系统建立在公司基础架构平台之上,而这个平台本身也面临着技术升级、环境变迁等挑战。因此,我们必须不断升级和改进系统,以适应这些变化。我坚信,在未来,我们还将面临各种新问题和挑战,但我们有能力克服困难,积累经验,并打造一个无限接近100%高可用性的系统,以更好地满足业务需求。
[1] https://raft.github.io/