1. 如何设计计算高可用架构
计算高可用的主要设计目标是当出现部分硬件损坏时,计算任务能够继续正常运行。因此计算高可用的本质是通过冗余来规避部分故障的风险,单台服务器是无论如何都达不到这个目标的。所以计算高可用的设计思想很简单:通过增加更多服务器来达到计算高可用。
计算高可用架构的设计复杂度主要体现在任务管理方面,即当任务在某台服务器上执行失败后,如何将任务重新分配到新的服务器进行执行。因此,计算高可用架构设计的关键点有下面两点。
哪些服务器可以执行任务
第一种方式和计算高性能中的集群类似,每个服务器都可以执行任务。
例如,常见的访问网站的某个页面。
第二种方式和存储高可用中的集群类似,只有特定服务器(通常叫“主机”)可以执行任务。当执行任务的服务器故障后,系统需要挑选新的服务器来执行任务。
例如,ZooKeeper的Leader才能处理写操作请求。
任务如何重新执行
第一种策略是对于已经分配的任务即使执行失败也不做任何处理,系统只需要保证新的任务能够分配到其他非故障服务器上执行即可。
第二种策略是设计一个任务管理器来管理需要执行的计算任务,服务器执行完任务后,需要向任务管理器反馈任务执行结果,任务管理器根据任务执行结果来决定是否需要将任务重新分配到另外的服务器上执行。
需要注意的是:“任务分配器”是一个逻辑的概念,并不一定要求系统存在一个独立的任务分配器模块。例如:
Nginx将页面请求发送给Web服务器,而CSS/JS等静态文件直接读取本地缓存。这里的Nginx角色是反向代理系统,但是承担了任务分配器的职责,而不需要Nginx做反向代理,后面再来一个任务分配器。
对于一些后台批量运算的任务,可以设计一个独立的任务分配系统来管理这些批处理任务的执行和分配。
ZooKeeper中的Follower节点,当接收到写请求时会将请求转发给Leader节点处理,当接收到读请求时就自己处理,这里的Follower就相当于一个逻辑上的任务分配器。
2. 常见计算高可用架构
2.1. 主备
主备架构是计算高可用最简单的架构,和存储高可用的主备复制架构类似,但是要更简单一些,因为计算高可用的主备架构无须数据复制

主备方案的详细设计:
主机执行所有计算任务。例如,读写数据、执行操作等。
当主机故障(例如,主机宕机)时,任务分配器不会自动将计算任务发送给备机,此时系统处于不可用状态。
如果主机能够恢复(不管是人工恢复还是自动恢复
坏,短时间内无法恢复),则需要人工操作,将备机
. 冷备
备机上的程序包和配置文件都准备好,但备机上的业务系统没有启动(注意:备机的服务器是启动的),主机故障后,需要人工手工将备机的业务系统启动,并将任务分配器的任务请求切换发送给备机。
2.1.2. 温备
备机上的业务系统已经启动,只是不对外提供服务,主机故障后,人工只需要将任务分配器的任务请求切换发送到备机即可。冷备可以节省一定的能源,但温备能够大大减少手工操作时间,因此一般情况下推荐用温备的方式。
主备架构的优点就是简单,主备机之间不需要进行交互,状态判断和切换操作由人工执行,系统实现很简单。而缺点正好也体现在“人工操作”这点上,因为人工操作的时间不可控,可能系统已经发生问题了,但维护人员还没发现,等了1个小时才发现。发现后人工切换的操作效率也比较低,可能需要半个小时才完成切换操作,而且手工操作过程中容易出错。例如,修改配置文件改错了、启动了错误的程序等。
和存储高可用中的主备复制架构类似,计算高可用的主备架构也比较适合与内部管理系统、后台管理系统这类使用人数不多、使用频率不高的业务,不太适合在线的业务。
2.2. 主从
和存储高可用中的主从复制架构类似,计算高可用的主从架构中的从机也是要执行任务的。任务分配器需要将任务进行分类,确定哪些任务可以发送给主机执行,哪些任务可以发送给备机执行

主从方案详细设计:
正常情况下,主机执行部分计算任务(如图中的“计算任务A”),备机执行部分计算任务(如图中的“计算任务B”)。
当主机故障(例如,主机宕机)时,任务分配器不会自动将原本发送给主机的任务发送给从机,而是继续发送给主机,不管这些任务执行是否成功。
如果主机能够恢复(不管是人工恢复还是自动恢复),任务分配器继续按照原有的设计策略分配任务,即计算任务A发送给主机,计算任务B发送给从机。
如果主机不能够恢复(例如,机器硬盘损坏,短时间内无法恢复),则需要人工操作,将原来的从机升级为主机(一般只是修改配置即可),增加新的机器作为从机,新的从机准备就绪后,任务分配器继续按照原有的设计策略分配任务。
主从架构与主备架构相比,优缺点有:
优点:主从架构的从机也执行任务,发挥了从机的硬件性能。
缺点:主从架构需要将任务分类,任务分配器会复杂一些。
2.3. 集群
主备架构和主从架构通过冗余一台服务器来提升可用性,且需要人工来切换主备或者主从。这样的架构虽然简单,但存在一个主要的问题:人工操作效率低、容易出错、不能及时处理故障。因此在可用性要求更加严格的场景中,我们需要系统能够自动完成切换操作,这就是高可用集群方案。
高可用计算的集群方案根据集群中服务器节点角色的不同,可以分为两类:一类是对称集群,即集群中每个服务器的角色都是一样的,都可以执行所有任务;另一类是非对称集群,集群中的服务器分为多个不同的角色,不同的角色执行不同的任务,例如最常见的Master-Slave角色。
需要注意的是,计算高可用集群包含2台服务器的集群,这点和存储高可用集群不太一样。存储高可用集群把双机架构和集群架构进行了区分;而在计算高可用集群架构中,2台服务器的集群和多台服务器的集群,在设计上没有本质区别,因此不需要进行区分。
2.3.1. 对称集群
对称集群更通俗的叫法是负载均衡集群

负载均衡集群详细设计:
正常情况下,任务分配器采取某种策略(随机、轮询等)将计算任务分配给集群中的不同服务器。
当集群中的某台服务器故障后,任务分配器不再将任务分配给它,而是将任务分配给其他服务器执行。
当故障的服务器恢复后,任务分配器重新将任务分配给它执行。
负载均衡集群的设计关键点在于两点:
任务分配器需要选取分配策略。
任务分配器需要检测服务器状态。
任务分配策略比较简单,轮询和随机基本就够了。状态检测稍微复杂一些,既要检测服务器的状态,例如服务器是否宕机、网络是否正常等;同时还要检测任务的执行状态,例如任务是否卡死、是否执行时间过长等。常用的做法是任务分配器和服务器之间通过心跳来传递信息,包括服务器信息和任务信息,然后根据实际情况来确定状态判断条件。
例如,一个在线页面访问系统,正常情况下页面平均会在500毫秒内返回,那么状态判断条件可以设计为:1分钟内响应时间超过1秒(包括超时)的页面数量占了80%时,就认为服务器有故障。
例如,一个后台统计任务系统,正常情况下任务会在5分钟内执行完成,那么状态判断条件可以设计为:单个任务执行时间超过10分钟还没有结束,就认为服务器有故障。
通过上面两个案例可以看出,不同业务场景的状态判断条件差异很大,实际设计时要根据业务需求来进行设计和
责。以Master-Slave为例,部分任务是Master服务器才能执行,部分任务是Slave服务器才能执行。

非对称集群架构详细设计:
集群会通过某种方式来区分不同服务器的角色。例如,通过ZAB算法选举,或者简单地取当前存活服务器中节点ID最小的服务器作为Master服务器。
任务分配器将不同任务发送给不同服务器。例如,图中的计算任务A发送给Mas
务器故障后,需要将剩余的Slave服务器中的一个重新指定为Master服务器;如果是Slave服务器故障,则并不需要重新分配角色,只需要将故障服务器从集群剔除即可。
非对称集群相比负载均衡集群,设计复杂度主要体现在两个方面:
任务分配策略更加复杂:需要将任务划分为不同类型并分配给不同角色的集群节点。
角色分配策略实现比较复杂:例如,可能需要使用ZAB、Raft这类复杂的算法来实现
独立的任务分配器节点,每个Server都是任务分配器,Follower收到请求后会进行判断,如果是写请求就转发给Leader,如果是读请求就自己处理。角色指定:ZooKeeper通过ZAB算法来选举Leader,当Leader故障后,所有的Follower节点会暂停读写操作,开始进行选举,直到新的Leader选举出来后才继续对Client提供服务。
3. 异地多活架构
无论是高可用计算架构,还是高可用存储架构,其本质的设计目的都是为了解决部分服务器故障的场景下,如何保证系统能够继续提供服务。但在一些极端场景下,有可能所有服务器都出现故障。例如,典型的有机房断电、机房火灾、地震、水灾……这些极端情况会导致某个系统所有服务器都故障,或者业务整体瘫痪,而且即使有其他地区的备份,把备份业务系统全部恢复到能够正常提供业务,花费的时间也比较长,可能是半小时,也可能是12小时。因为备份系统平时不对外提供服务,可能会存在很多隐藏的问题没有发现。如果业务期望达到即使在此类灾难性故障的情况下,业务也不受影响,或者在几分钟内就能够很快恢复,那么就需要设计异地多活架构。
3.1. 应用场景
异地多活架构的关键点就是异地、多活,其中异地就是指地理位置上不同的地方,类似于“不要把鸡蛋都放在同一篮子里”;多活就是指不同地理位置上的系统都能够提供业务服务,这里的“活”是活动、活跃的意思。判断一个系统是否符合异地多活,需要满足两个标准:
正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。
某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。
与“活”对应的是字是“备”,备是备份,正常情况下对外是不提供服务的,如果需要提供服务,则需要大量的人工干预和操作,花费大量的时间才能让“备”变成“活
然,因为实现异地多活架构不是没有代价的,相反其代价很高,具体表现为:
系统复杂度会发生质的变化,需要设计复杂的异地多活架构。
成本会上升,毕竟要多在一个或者多个机房搭建独立的一套业务系统。
因此,异地多活虽然功能很强大,但也不是每个业务不管三七二十一都要上异地多活。例如,常见的新闻网站、企业内部的IT系统、游戏、博客站点等,如果无法承受异地多活带来的复杂度和成本,是可以不做异地多活的,只需要做异地备份即可。因为这类业务系统即使中断,对用户的影响并不会很大,例如,A新闻网站看不了,用户换个新闻网站即可。而共享单车、滴滴出行、支付宝、微信这类业务,就需要做异地多活了,这类业务系统中断后,对用户的影响很大。例如,支付宝用不了,就没法买东西了;滴滴用不了,用户就打不到车了。
当然,如果业务规模很大,能够做异地多活的情况下还是尽量。首先,这样能够在异常的场景下给用户提供更好的体验;其次,业务规模很大肯定会伴随衍生的收入,例如广告收入,异地多活能够减少异常场景带来的收入损失。同样以新闻网站为例,虽然从业务的角度来看,新闻类网站对用户影响不大,反正用户也可以从其他地方看到基本相同的新闻,甚至用户几个小时不看新闻也没什么问题。但是从网站本身来看,几个小时不可访问肯定会影响用户对网站的口碑;其次几个小时不可访问,网站上的广告收入损失也会很大。
3.2. 架构模式
根据地理位置上的距离来划分,异地多活架构可以分为同城异区、跨城异地、跨国异地。
3.2.1. 同城异区
同城异区指的是将业务部署在同一个城市不同区的多个机房。例如,在北京部署两个机房,一个机房在海淀区,一个在通州区,然后将两个机房用专用的高速网络连接在一起。
如果我们考虑一些极端场景(例如,美加大停电、新奥尔良水灾),同城异区似乎没什么作用,那为何我们还要设计同城异区这种架构呢?答案就在于“同城”。
同城的两个机房,距离上一般大约就是几十千米,通过搭建高速的网络,同城异区的两个机房能够实现和同一个机房内几乎一样的网络传输速度。这就意味着虽然是两个不同地理位置上的机房,但逻辑上我们可以将它们看作同一个机房,这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。
那如果采用了同城异区架构,一旦发生新奥尔良水灾这种灾难怎么办呢?很遗憾,答案是无能为力。但我们需要考虑的是,这种极端灾难发生概率是比较低的,可能几年或者十几年才发生一次。其次,除了这类灾难,机房火灾、机房停电、机房空调故障这类问题发生的概率更高,而且破坏力一样很大。而这
种问题,而两个城市离得太近又无法应对如美加大停电这种问题,跨城异地其实就是为了解决这两类问题的,因此需要在距离上比较远,才能有效应对这类极端灾难事件。
跨城异地虽然能够有效应对极端灾难事件,但“距离较远”这点并不只是一个距离数字上的变化,而是量变引起了质变,导致了跨城异地的架构复杂度大大上升。距离增加带来的最主要问题是两个机房的网络传输速度会降低,这不是以人的意志为转移的,而是物理定律决定的,即光速真空传播大约是每秒30万千米,在光纤中传输的速度大约是每秒20万千米,再加上传输中的各种网络设备的处理,实际还远远达不到理论上的速度。
除了距离上的限制,中间传输各种不可控的因素也非常多。例如,挖掘机把光纤挖断、中美海底电缆被拖船扯断、骨干网故障等,这些线路很多是第三方维护,针对故障我们根本无能为力也无法预知。例如,广州机房到北京机房,正常情况下RTT大约是50毫秒左右,遇到网络波动之类的情况,RTT可能飙升到500毫秒甚至1秒,更不用说经常发生的线
做到真正意义上的多活,业务系统需要考虑部署在不同地点的两个机房,在数据短时间不一致的情况下,还能够正常提供业务。这就引入了一个看似矛盾的地方:数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致。
如何解决这个问题呢?重点还是在“数据”上,即根据数据的特性来做不同的架构。如果是强一致性要求的数据,例如银行存款余额、支付宝余额等,这类数据实际上是无法做到跨城异地多活的。我们来看一个假设的例子,假如我们做一个互联网金融的业务,用户余额支持跨城异地多活,我们的系统分别部署在广州和北京,那么如果挖掘机挖断光缆后,会出现如下场景:
用户A余额有10000元钱,北京和广州机房都是这个数据。
用户A向用户B转了5000元钱,这个操作是在广州机房完成的,完成后用户A在广州机房的余额是5000元。
由于广州和北京机房网络被挖掘机挖断,广州机房无法将余额变动通知北京机房,此时北京机房用户A的余额还是10000元。
用户A到北京机房又发起转账,此时他看到自己的余额还有10000元,于是向用户C转账10000元,转账完成后用户A的余额变为0。
用户A到广州机房一看,余额怎么还有5000元?于是赶紧又发起转账,转账5000元给用户D;此时广州机房用户A的余额也变为0了。
最终,本来余额10000元的用户A,却转了20000元出去给其他用户。
对于以上这种假设场景,虽然普通用户很难这样自如地操作,但如果真的这么做,被黑客发现后,后果不堪设想。正因为如此,支付宝等金融相关的系统,对余额这类数据,一般不会做跨城异地的多活架构,而只能采用同城异区这种架构。
而对数据一致性要求不那么高,或者数据不怎么改变,或者即使数据丢失影响也不大的业务,跨城异地多活就能够派上用场了。例如,用户登录(数据不一致时用户重新登录即可)、新闻类网站(一天内的新闻数据变化较少)、微博类网站(丢失用户发布的微博或者评论影响不大),这些业务采用跨城异地多活,能够很好地应对极端灾难的场景。
3.2.3. 跨国异地
跨国异地指的是业务部署在不同国家的多个机房。相比跨城异地,跨国异地的距离就更远了,因此数据同步的延时会更长,正常情况下可能就有几秒钟了。这种程度的延迟已经无法满足异地多活标准的第一条:“正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务”。例如,假设有一个微博类网站,分别在中国的上海和美国的纽约都建了机房,用户A在上海机房发表了一篇微博,此时如果他的一个关注者B用户访问到美国的机房,很可能无法看到用户A刚刚发表的微博。虽然跨城异地也会有此类同步延时问题,但正常情况下几十毫秒的延时对用户来说基本无感知的;而延时达到几秒钟就感觉比较明显了。
因此,跨国异地的“多活”,和跨城异地的“多活”,实际的含义并不完全一致。跨国异地多活的主要应用场景一般有这几种情况:
为不同地区用户提供服务
例如,亚马逊中国是为中国用户服务的,而亚马逊美国是为美国用户服务的,亚马逊中国的用户如果访问美国亚马逊,是无法用亚马逊中国的账号登录美国亚马逊的。
只读类业务做多活
例如,谷歌的搜索业务,由于用户搜索资料时,这些资料都已经存在于谷歌的搜索引擎上面,无论是访问英国谷歌,还是访问美国谷歌,搜索结果基本相同,并且对用户来说,也不需要搜索到最新的实时资料,跨国异地的几秒钟网络延迟,对搜索结果是没有什么影响的。
3.3. 设计技巧
3.3.1. 保证核心业务的异地多活
“异地多活”是为了保证业务的高可用,但很多架构师在考虑这个“业务”时,会不自觉地陷入一个思维误区:我要保证所有业务都能“异地多活”!
假设我们需要做一个“用户子系统”,这个子系统负责“注册”“登录”“用户信息”三个业务。为了支持海量用户,我们设计了一个“用户分区”的架构,即正常情况下用户属于某个主分区,每个分区都有其他数据的备份,用户用邮箱或者手机号注册,路由层拿到邮箱或者手机号后,通过Hash计算属于哪个中心,然后请求对应的业务中心。基本的架构如下:

这样一个系统,如果3个业务要同时实现异地多活,会发现这些难以解决的问题:
注册问题
A中心注册了用户,数据还未同步到B中心,此时A中心宕机,为了支持注册业务多活,可以挑选B中心让用户去重新注册。看起来很容易就支持多活了,但仔细思考一下会发现这样做会有问题:一个手机号只能注册一个账号,A中心的数据没有同步过来,B中心无法判断这个手机号是否重复,如果B中心让用户注册,后来A中心恢复了,发现数据有冲突,怎么解决?实际上是无法解决的,因为同一个手机号注册的账号不能以后一次注册为准;而如果B中心不支持本来属于A中心的业务进行注册,注册业务的多活又成了空谈。
如果我们修改业务规则,允许一个手机号注册多个账号不就可以了吗?
这样做是不可行的,类似一个手机号只能注册一个账号这种规则,是核心业务规则,修改核心业务规则的代价非常大,几乎所有的业务都要重新设计,为了架构设计去改变业务规则(而且是这么核心的业务规则)是得不偿失的。
用户信息问题
用户信息的修改和注册有类似的问题,即A、B两个中心在异常的情况下都修改了用户信息,如何处理冲突?
由于用户信息并没有账号那么关键,一种简单的处理方式是按照时间合并,即最后修改的生效。业务逻辑上没问题,但实际操作也有一个很关键的“坑”:怎么保证多个中心所有机器时间绝对一致?在异地多中心的网络下,这个是无法保证的,即使有时间同步也无法完全保证,只要两个中心的时间误差超过1秒,数据就可能出现混乱,即先修改的反而生效。
还有一种方式是生成全局唯一递增ID,这个方案的成本很高,因为这个全局唯一递增ID的系统本身又要考虑异地多活,同样涉及数据一致性和冲突的问题。
综合上面的简单分析可以发现,如果“注册”“登录”“用户信息”全部都要支持异地多活,实际上是挺难的,有的问题甚至是无解的。那这种情况下我们应该如何考虑“异地多活”的架构设计呢?答案其实很简单:优先实现核心业务的异地多活架构!
对于这个模拟案例来说,“登录”才是最核心的业务,“注册”和“用户信息”虽然也是主要业务,但并不一定要实现异地多活,主要原因在于业务影响不同。对于一个日活1000万的业务来说,每天注册用户可能是几万,修改用户信息的可能还不到1万,但登录用户是1000万,很明显我们应该保证登录的异地多活。
对于新用户来说,注册不了的影响并不明显,因为他还没有真正开始使用业务。用户信息修改也类似,暂时修改不了用户信息,对于其业务不会有很大影响。而如果有几百万用户登录不了,就相当于几百万用户无法使用业务,对业务的影响就非常大了:公司的客服热线很快就被打爆,微博、微信上到处都在传业务宕机,论坛里面到处是抱怨的用户,那就是互联网大事件了!
而登录实现“异地多活”恰恰是最简单的,因为每个中心都有所有用户的账号和密码信息,用户在哪个中心都可以登录。用户在A中心登录,A中心宕机后,用户到B中心重新登录即可。
如果某个用户在A中心修改了密码,此时数据还没有同步到B中心,用户到B中心登录是无法登录的,这个怎么处理?
只保证绝大多数用户的异地多活
3.3.2. 保证核心数据最终一致性
异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活架构设计的核心。但大部分架构师在考虑数据同步方案时,会不知不觉地陷入完美主义误区:我要所有数据都实时同步!
数据冗余是要将数据从A地同步到B地,从业务的角度来看是越快越好,最好和本地机房一样的速度最好。但让人头疼的问题正在这里:异地多活理论上就不可能很快,因为这是物理定律决定的。
因此异地多活架构面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。
既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考:
尽量减少异地多活机房的距离,搭建高速网络
这和我上一期讲到的同城异区架构类似,但搭建跨城异地的高速网络成本远远超过同城异区的高速网络,成本巨大,一般只有巨头公司才能承担。
尽量减少数据同步,只同步核心业务相关的数据
简单来说就是不重要的数据不同步,同步后没用的数据不同步,只同步核心业务相关的数据。
以前面的“用户子系统”为例,用户登录所产生的token或者session信息,数据量很大,但其实并不需要同步到其他业务中心,因为这些数据丢失后重新登录就可以再次获取了。
这时你可能会想到:这些数据丢失后要求用户重新登录,影响用户体验!
确实如此,毕竟需要用户重新输入账户和密码信息,或者至少要弹出登录界面让用户点击一次,但相比为了同步所有数据带来的代价,这个影响完全可以接受。
只保证绝大多数用户的异地多活
保证最终一致性,不保证实时一致性
BASE理论告诉我们,即业务不依赖数据同步的实时性,只要数据最终能一致即可。例如,A机房注册了一个用户,业务上不要求能够在50毫秒内就同步到所有机房,正常情况下要求5分钟同步到所有机房即可,异常情况下甚至可以允许1小时或者1天后能够一致。
最终一致性在具体实现时,还需要根据不同的数据特征,进行差异化的处理,以满足业务需要。例如,对“账号”信息来说,如果在A机房新注册的用户5分钟内正好跑到B机房了,此时B机房还没有这个用户的信息,为了保证业务的正确,B机房就需要根据路由规则到A机房请求数据。
而对“用户信息”来说,5分钟后同步也没有问题,也不需要采取其他措施来弥补,但还是会影响用户体验,即用户看到了旧的用户信息
只保证绝大多数用户的异地多活
3.3.3. 采用多种手段同步数据
数据同步是异地多活架构设计的核心,幸运的是基本上存储系统本身都会有同步的功能。例如,MySQL的主备复制、Redis的Cluster功能、Elasticsearch的集群功能。这些系统本身的同步功能已经比较强大,能够直接拿来就用,但这也无形中将我们引入了一个思维误区:只使用存储系统的同步功能!
既然说存储系统本身就有同步功能,而且同步功能还很强大,为何说只使用存储系统是一个思维误区呢?因为虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。
以MySQL为例,MySQL 5.1版本的复制是单线程的复制,在网络抖动或者大量数据同步时,经常发生延迟较长的问题,短则延迟十几秒,长则可能达到十几分钟。而且即使我们通过监控的手段知道了MySQL同步时延较长,也难以采取什么措施,只能干等。
Redis又是另外一个问题,Redis 3.0之前没有Cluster功能,只有主从复制功能,而为了设计上的简单,Redis 2.8之前的版本,主从复制有一个比较大的隐患:从机宕机或者和主机断开连接都需要重新连接主机,重新连接主机都会触发全量的主从复制。这时主机会生成内存快照,主机依然可以对外提供服务,但是作为读的从机,就无法提供对外服务了,如果数据量大,恢复的时间会相当长。
综合上面的案例可以看出,存储系统本身自带的同步功能,在某些场景下是无法满足业务需要的。尤其是异地多机房这种部署,各种各样的异常情况都可能出现,当我们只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。
解决的方案就是拓开思路,避免只使用存储系统的同步功能,可以将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案。
还是以前面的“用户子系统”为例,我们可以采用如下几种方式同步数据:
消息队列方式
对于账号数据,由于账号只会创建,不会修改和删除(假设我们不提供删除功能),我们可以将账号数据通过消息队列同步到其他业务中心。
二次读取方式
某些情况下可能出现消息队列同步也延迟了,用户在A中心注册,然后访问B中心的业务,此时B中心本地拿不到用户的账号数据。为了解决这个问题,B中心在读取本地数据失败时,可以根据路由规则,再去A中心访问一次(这就是所谓的二次读取,第一次读取本地,本地失败后第二次读取对端),这样就能够解决异常情况下同步延迟的问题。
存储系统同步方式
对于密码数据,由于用户改密码频率较低,而且用户不可能在1秒内连续改多次密码,所以通过数据库的同步机制将数据复制到其他业务中心即可,用户信息数据和密码类似。
回源读取方式
对于登录的session数据,由于数据量很大,我们可以不同步数据;但当用户在A中心登录后,然后又在B中心登录,B中心拿到用户上传的session id后,根据路由判断session属于A中心,直接去A中心请求session数据即可;反之亦然,A中心也可以到B中心去获取session数据。
重新生成数据方式
对于“回源读取”场景,如果异常情况下,A中心宕机了,B中心请求session数据失败,此时就只能登录失败,让用户重新在B中心登录,生成新的session数据。
最后“用户子系统”同步方式整体如下

3.3.4. 只保证绝大多数用户的异地多活
前面我在给出每个思维误区对应的解决方案时,留下了几个小尾巴:某些场景下我们无法保证100%的业务可用性,总是会有一定的损失。例如,密码不同步导致无法登录、用户信息不同步导致用户看到旧的信息等,这个问题怎么解决呢?
其实这个问题涉及异地多活架构设计中一个典型的思维误区:我要保证业务100%可用!但极端情况下就是会丢一部分数据,就是会有一部分数据不能同步,有没有什么巧妙能做到100%可用呢?
很遗憾,答案是没有!异地多活也无法保证100%的业务可用,这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法100%解决的。所以针对这个思维误区,我的答案是“忍”!也就是说我们要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的0.01%的用户的可用性,做一个完美方案,结果却发现99.99%的用户都保证不了了。
对于某些实时强一致性的业务,实际上受影响的用户会更多,甚至可能达到1/3的用户。以银行转账这个业务为例,假设小明在北京XX银行开了账号,如果小明要转账,一定要北京的银行业务中心才可用,否则就不允许小明自己转账。如果不这样的话,假设在北京和上海两个业务中心实现了实时转账的异地多活,某些异常情况下就可能出现小明只有1万元存款,他在北京转给了张三1万元,然后又到上海转给了李四1万元,两次转账都成功了。这种漏洞如果被人利用,后果不堪设想。
当然,针对银行转账这个业务,虽然无法做到“实时转账”的异地多活,但可以通过特殊的业务手段让转账业务也能实现异地多活。例如,转账业务除了“实时转账”外,还提供“转账申请”业务,即小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求,然后后台异步发起真正的转账操作,如果此时北京业务中心不可用,转账请求就可以继续等待重试;假设等待2个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。小明再登录上来就会看到转账申请失败,原因是“余额不足”。
不过需要注意的是“转账申请”的这种方式虽然有助于实现异地多活,但其实还是牺牲了用户体验的,对于小明来说,本来一次操作的事情,需要分为两次:一次提交转账申请,另外一次是要确认是否转账成功。
虽然我们无法做到100%可用性,但并不意味着我们什么都不能做,为了让用户心里更好受一些,我们可以采取一些措施进行安抚或者补偿,例如:
挂公告
说明现在有问题和基本的问题原因,如果不明确原因或者不方便说出原因,可以发布“技术哥哥正在紧急处理”这类比较轻松和有趣的公告。
事后对用户进行补偿
例如,送一些业务上可用的代金券、小礼包等,减少用户的抱怨。
补充体验
对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。以“转账申请”为例,为了让用户不用确认转账申请是否成功,我们可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果,这样用户就不用时不时地登录系统来确认转账是否成功了。
3.4. 异地多活设计步骤
3.4.1. 业务分级
按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。
常见的分级标准有下面几种:
访问量大的业务
以用户管理系统为例,业务包括登录、注册、用户信息管理,其中登录的访问量肯定是最大的。
核心业务
以QQ为例,QQ的主场景是聊天,QQ空间虽然也是重要业务,但和聊天相比,重要性就会低一些,如果要从聊天和QQ空间两个业务里面挑选一个做异地多活,那明显聊天要更重要(当然,此类公司如腾讯,应该是两个都实现了异地多活的)。
产生大量收入的业务
同样以QQ为例,聊天可能很难为腾讯带来收益,因为聊天没法插入广告;而QQ空间反而可能带来更多收益,因为QQ空间可以插入很多广告,因此如果从收入的角度来看,QQ空间做异地多活的优先级反而高于QQ聊天了。
以我们一直在举例的用户管理系统为例,“登录”业务符合“访问量大的业务”和“核心业务”这两条标准,因此我们将登录业务作为核心业务。
3.4.2. 数据分类
挑选出核心业务后,需要对核心业务相关的数据进一步分析,目的在于识别所有的数据及数据特征,这些数据特征会影响后面的方案设计。
常见的数据特征分析维度有:
数据量
这里的数据量包括总的数据量和新增、修改、删除的量。对异地多活架构来说,新增、修改、删除的数据就是可能要同步的数据,数据量越大,同步延迟的几率越高,同步方案需要考虑相应的解决方案。
唯一性
唯一性指数据是否要求多个异地机房产生的同类数据必须保证唯一。例如用户ID,如果两个机房的两个不同用户注册后生成了一样的用户ID,这样业务上就出错了。
数据的唯一性影响业务的多活设计,如果数据不需要唯一,那就说明两个地方都产生同类数据是可能的;如果数据要求必须唯一,要么只能一个中心点产生数据,要么需要设计一个数据唯一生成的算法。
实时性
实时性指如果在A机房修改了数据,要求多长时间必须同步到B机房,实时性要求越高,对同步的要求越高,方案越复杂。
可丢失性
可丢失性指数据是否可以丢失。例如,写入A机房的数据还没有同步到B机房,此时A机房机器宕机会导致数据丢失,那这部分丢失的数据是否对业务会产生重大影响。
例如,登录过程中产生的session数据就是可丢失的,因为用户只要重新登录就可以生成新的session;而用户ID数据是不可丢失的,丢失后用户就会失去所有和用户ID相关的数据,例如用户的好友、用户的钱等。
可恢复性
可恢复性指数据丢失后,是否可以通过某种手段进行恢复,如果数据可以恢复,至少说明对业务的影响不会那么大,这样可以相应地降低异地多活架构设计的复杂度。
例如,用户的微博丢失后,用户重新发一篇一模一样的微博,这个就是可恢复的;或者用户密码丢失,用户可以通过找回密码来重新设置一个新密码,这也算是可以恢复的;而用户账号如果丢失,用户无法登录系统,系统也无法通过其他途径来恢复这个账号,这就是不可恢复的数据。
我们同样以用户管理系统的登录业务为例,简单分析如下表所示。

3.4.3. 数据同步
确定数据的特点后,我们可以根据不同的数据设计不同的同步方案。常见的数据同步方案有:
存储系统同步
这是最常用也是最简单的同步方式。例如,使用MySQL的数据主从数据同步、主主数据同步。
这类数据同步的优点是使用简单,因为几乎主流的存储系统都会有自己的同步方案;缺点是这类同步方案都是通用的,无法针对业务数据特点做定制化的控制。例如,无论需要同步的数据量有多大,MySQL都只有一个同步通道。因为要保证事务性,一旦数据量比较大,或者网络有延迟,则同步延迟就会比较严重。
消息队列同步
采用独立消息队列进行数据同步,常见的消息队列有Kafka、ActiveMQ、RocketMQ等。
消息队列同步适合无事务性或者无时序性要求的数据。例如,用户账号,两个用户先后注册了账号A和B,如果同步时先把B同步到异地机房,再同步A到异地机房,业务上是没有问题的。而如果是用户密码,用户先改了密码为m,然后改了密码为n,同步时必须先保证同步m到异地机房,再同步n到异地机房;如果反过来,同步后用户的密码就不对了。因此,对于新注册的用户账号,我们可以采用消息队列同步了;而对于用户密码,就不能采用消息队列同步了。
重复生成
数据不同步到异地机房,每个机房都可以生成数据,这个方案适合于可以重复生成的数据。例如,登录产生的cookie、session数据、缓存数据等。
我们同样以用户管理系统的登录业务为例,针对不同的数据特点设计不同的同步方案,如下表所示。

3.4.4. 异常处理
无论数据同步方案如何设计,一旦出现极端异常的情况,总是会有部分数据出现异常的。例如,同步延迟、数据丢失、数据不一致等。异常处理就是假设在出现这些问题时,系统将采取什么措施来应对。异常处理主要有以下几个目的:
问题发生时,避免少量数据异常导致整体业务不可用。
问题恢复后,将异常的数据进行修正。
对用户进行安抚,弥补用户损失。
常见的异常处理措施有这几类
3.4.4.1. 多通道同步
多通道同步的含义是采取多种方式来进行数据同步,其中某条通道故障的情况下,系统可以通过其他方式来进行同步,这种方式可以应对同步通道处故障的情况。
以用户管理系统中的用户账号数据为例,我们的设计方案一开始挑选了消息队列的方式进行同步,考虑异常情况下,消息队列同步通道可能中断,也可能延迟很严重;为了保证新注册账号能够快速同步到异地机房,我们再增加一种MySQL同步这种方式作为备份。这样针对用户账号数据同步,系统就有两种同步方式:MySQL主从同步和消息队列同步。除非两个通道同时故障,否则用户账号数据在其中一个通道异常的情况下,能够通过另外一个通道继续同步到异地机房,如下图所示。

多通道同步设计的方案关键点有:
一般情况下,采取两通道即可,采取更多通道理论上能够降低风险,但付出的成本也会增加很多。
数据库同步通道和消息队列同步通道不能采用相同的网络连接,否则一旦网络故障,两个通道都同时故障;可以一个走公网连接,一个走内网连接。
需要数据是可以重复覆盖的,即无论哪个通道先到哪个通道后到,最终结果是一样的。例如,新建账号数据就符合这个标准,而密码数据则不符合这个标准。
3.4.4.2. 同步和访问结合
这里的访问指异地机房通过系统的接口来进行数据访问。例如业务部署在异地两个机房A和B,B机房的业务系统通过接口来访问A机房的系统获取账号信息,如下图所示。

同步和访问结合方案的设计关键点有:
接口访问通道和数据库同步通道不能采用相同的网络连接,不能让数据库同步和接口访问都走同一条网络通道,可以采用接口访问走公网连接,数据库同步走内网连接这种方式。
数据有路由规则,可以根据数据来推断应该访问哪个机房的接口来读取数据。例如,有3个机房A、B、C,B机房拿到一个不属于B机房的数据后,需要根据路由规则判断是访问A机房接口,还是访问C机房接口。
由于有同步通道,优先读取本地数据,本地数据无法读取到再通过接口去访问,这样可以大大降低跨机房的异地接口访问数量,适合于实时性要求非常高的数据。
3.4.4.3. 日志记录
日志记录主要用于用户故障恢复后对数据进行恢复,其主要方式是每个关键操作前后都记录相关一条日志,然后将日志保存在一个独立的地方,当故障恢复后,拿出日志跟数据进行对比,对数据进行修复。
为了应对不同级别的故障,日志保存的要求也不一样,常见的日志保存方式有:
服务器上保存日志,数据库中保存数据,这种方式可以应对单台数据库服务器故障或者宕机的情况。
本地独立系统保存日志,这种方式可以应对某业务服务器和数据库同时宕机的情况。例如,服务器和数据库部署在同一个机架,或者同一个电源线路上,就会出现服务器和数据库同时宕机的情况。
日志异地保存,这种方式可以应对机房宕机的情况。
上面不同的日志保存方式,应对的故障越严重,方案本身的复杂度和成本就会越高,实际选择时需要综合考虑成本和收益情况。
3.4.4.4. 用户补偿
无论采用什么样的异常处理措施,都只能最大限度地降低受到影响的范围和程度,无法完全做到没有任何影响。例如,双同步通道有可能同时出现故障、日志记录方案本身日志也可能丢失。因此,无论多么完美的方案,故障的场景下总是可能有一小部分用户业务上出问题,系统无法弥补这部分用户的损失。但我们可以采用人工的方式对用户进行补偿,弥补用户损失,培养用户的忠诚度。简单来说,系统的方案是为了保证99.99%的用户在故障的场景下业务不受影响,人工的补偿是为了弥补0.01%的用户的损失。
常见的补偿措施有送用户代金券、礼包、礼品、红包等,有时为了赢得用户口碑,付出的成本可能还会比较大,但综合最终的收益来看还是很值得的。例如暴雪《炉石传说》2017年回档故障,暴雪给每个用户大约价值人民币200元的补偿,结果玩家都求暴雪再来一次回档,形象地说明了玩家对暴雪补偿的充分认可。
4. 如何应对接口级故障
异地多活方案。它主要用来应对系统级的故障,例如机器宕机、机房故障和网络故障等问题。这些系统级的故障虽然影响很大,但发生概率较小。实际业务运行中,接口级的故障发生概率较高。
接口级故障的典型表现就是,系统并没有宕机、网络也没有中断,但业务却出现问题了,例如业务响应缓慢、大量访问超时和大量访问出现异常(给用户弹出提示“无法连接数据库”)。
这类问题的主要原因在于系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。最常见的情况就是,数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会儿访问抛出异常,一会儿访问又是正常结果。
导致接口级故障的原因可以分为两大类:
内部原因:包括程序bug导致死循环,某个接口导致数据库慢查询,程序逻辑不完善导致耗尽内存等。
外部原因:包括黑客攻击,促销或者抢购引入了超出平时几倍甚至几十倍的用户,第三方系统大量请求,第三方系统响应缓慢等。
解决接口级故障的核心思想和异地多活基本类似,都是优先保证核心业务和优先保证绝大部分用户。常见的应对方法有四种,降级、熔断、限流和排队。
4.1. 降级
降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。
例如,论坛可以降级为只能看帖子,不能发帖子;也可以降级为只能看帖子和评论,不能发评论;而App的日志上传接口,可以完全停掉一段时间,这段时间内App都不能上传日志。
降级的核心思想就是丢车保帅,优先保证核心业务。
例如,对于论坛来说,90%的流量是看帖子,那我们就优先保证看帖的功能;对于一个App来说,日志上传接口只是一个辅助的功能,故障时完全可以停掉。
常见的实现降级的方式有两种:
4.1.1. 系统后门降级
简单来说,就是系统预留了后门用于降级操作。例如,系统提供一个降级URL,当访问这个URL时,就相当于执行降级指令,具体的降级指令通过URL的参数传入即可。这种方案有一定的安全隐患,所以也会在URL中加入密码这类安全措施。
系统后门降级的方式实现成本低,但主要缺点是如果服务器数量多,需要一台一台去操作,效率比较低,这在故障处理争分夺秒的场景下是比较浪费时间的。
4.1.2. 独立降级系统
为了解决系统后门降级方式的缺点,我们可以将降级操作独立到一个单独的系统中,实现复杂的权限管理、批量操作等功能。
其基本架构如下:

4.2. 熔断
熔断是指按照规则停掉外部接口的访问,防止某些外部接口故障导致自己的系统处理能力急剧下降或者出故障。

熔断和降级是两个比较容易混淆的概念,因为单纯从名字上看,好像都有禁止某个功能的意思。但它们的内涵是不同的,因为降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况。
假设一个这样的场景:A服务的X功能依赖B服务的某个接口,当B服务的接口响应很慢的时候,A服务的X功能响应肯定也会被拖慢,进一步导致A服务的线程都被卡在X功能处理上,于是A服务的其他功能都会被卡住或者响应非常慢。
这时就需要熔断机制了:A服务不再请求B服务的这个接口,A服务内部只要发现是请求B服务的这个接口就立即返回错误,从而避免A服务整个被拖慢甚至拖死。
实现熔断机制有两个关键点:
一是需要有一个统一的API调用层,由API调用层来进行采样或者统计。如果接口调用散落在代码各处,就没法进行统一处理了。
二是阈值的设计,例如1分钟内30%的请求响应时间超过1秒就熔断,这个策略中的“1分钟”“30%”“1秒”都对最终的熔断效果有影响。实践中,一般都是先根据分析确定阈值,然后上线观察效果,再进行调优。
4.3. 限流
降级是从系统功能优先级的角度考虑如何应对故障,而限流则是从用户访问压力的角度来考虑如何应对故障。限流指只允许系统能够承受的访问量进来,超出系统访问能力的请求将被丢弃。
虽然“丢弃”这个词听起来让人不太舒服,但保证一部分请求能够正常响应,总比全部请求都不能响应要好得多。
限流一般都是系统内实现的,常见的限流方式可以分为两类:基于请求限流和基于资源限流。
4.3.1. 基于请求限流
基于请求限流指从外部访问的请求角度考虑限流,常见的方式有两种。
第一种是限制总量,也就是限制某个指标的累积上限,常见的是限制当前系统服务的用户总量,例如:某个直播间限制总用户数上限为100万,超过100万后新的用户无法进入;某个抢购活动商品数量只有100个,限制参与抢购的用户上限为1万个,1万以后的用户直接拒绝。
第二种是限制时间量,也就是限制一段时间内某个指标的上限,例如1分钟内只允许10000个用户访问;每秒请求峰值最高为10万。
无论是限制总量还是限制时间量,共同的特点都是实现简单,但在实践中面临的主要问题是比较难以找到合适的阈值。例如系统设定了1分钟10000个用户,但实际上6000个用户的时候系统就扛不住了;或者达到1分钟10000用户后,其实系统压力还不大,但此时已经开始丢弃用户访问了。
即使找到了合适的阈值,基于请求限流还面临硬件相关的问题。例如一台32核的机器和64核的机器处理能力差别很大,阈值是不同的,可能有的技术人员以为简单根据硬件指标进行数学运算就可以得出来,实际上这样是不可行的,64核的机器比32核的机器,业务处理性能并不是2倍的关系,可能是1.5倍,甚至可能是1.1倍。
为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。
基于上述的分析,根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统,例如负载均衡系统、网关系统、抢购系统等。
4.3.2. 基于资源限流
基于请求限流是从系统外部考虑的,而基于资源限流是从系统内部考虑的,也就是找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源包括连接数、文件句柄、线程数和请求队列等。
例如,采用Netty来实现服务器,每个进来的请求都先放入一个队列,业务线程再从队列读取请求进行处理,队列长度最大值为10000,队列满了就拒绝后面的请求;也可以根据CPU的负载或者占用率进行限流,当CPU的占用率超过80%的时候就开始拒绝新的请求。
基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实际设计时也面临两个主要的难点:如何确定关键资源,以及如何确定关键资源的阈值。
通常情况下,这也是一个逐步调优的过程:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。
常用限流算法:
为了更好地实现前面描述的各种限流方式,通常情况下我们会基于限流算法来设计方案。常见的限流算法有两大类四小类,它们的实现原理和优缺点各不相同,在实际设计的时候需要根据业务场景来选择。
4.3.2.1.
第一大类是时间窗算法,它会限制一定时间窗口内的请求量或者资源消耗量,根据实现方式又可以细分为“固定时间窗”和“滑动时间窗”。
4.3.2.1.1. 固定时间窗
固定时间窗算法的实现原理是,统计固定时间周期内的请求量或者资源消耗量,超过限额就会启动限流,如下图所示:

它的优点是实现简单,缺点是存在临界点问题。例如上图中的红蓝两点只间隔了短短10秒,期间的请求数却已经达到200,超过了算法规定的限额(1分钟内处理100)。但是因为这些请求分别来自两个统计窗口,从单个窗口来看还没有超出限额,所以并不会启动限流,结果可能导致系统因为压力过大而挂掉。
4.3.2.1.2. 滑动时间窗
为了解决临界点问题,滑动时间窗算法应运而生,它的实现原理是,两个统计周期部分重叠,从而避免短时间内的两个统计点分属不同的时间窗的情况,如下图所示:

总体上来看,滑动时间窗的限流效果要比固定时间窗更好,但是实现也会稍微复杂一些。
4.3.2.2. 桶算法
第二大类是桶算法,用一个虚拟的“桶”来临时存储一些东西。根据桶里面放的东西,又可以细分为“漏桶”和“令牌桶”。
4.3.2.2.1. 漏桶
漏桶算法的实现原理是,将请求放入“桶”(消息队列等),业务处理单元(线程、进程和应用等)从桶里拿请求处理,桶满则丢弃新的请求,如下图所示:

我们可以看到漏桶算法的三个关键实现点:
流入速率不固定:可能瞬间流入非常多的请求,例如0点签到、整点秒杀。
匀速(极速)流出:这是理解漏桶算法的关键,也就是说即使大量请求进入了漏桶,但是从漏桶流出的速度是匀速的,速度的最大值就是系统的极限处理速度(对应图中的“极速”)。这样就保证了系统在收到海量请求的时候不被压垮,这是第一层的保护措施。需要注意的是:如果漏桶没有堆积,那么流出速度就等于流入速度,这个时候流出速度就不是匀速的。
桶满则丢弃请求:这是第二层保护措施,也就是说漏桶不是无限容量,而是有限容量,例如漏桶最多存储100万个请求,桶满了则直接丢弃后面的请求。
漏桶算法的技术本质是总量控制,桶大小是设计关键,具体的优缺点如下:
突发大量流量时丢弃的请求较少,因为漏桶本身有缓存请求的作用。
桶大小动态调整比较困难(例如 Java BlockingQueue),需要不断的尝试才能找到符合业务需求的最佳桶大小。
无法精确控制流出速度,也就是业务的处理速度。
漏桶算法主要适用于瞬时高并发流量的场景(例如刚才提到的0点签到、整点秒杀等)。在短短几分钟内涌入大量请求时,为了更好的业务效果和用户体验,即使处理慢一些,也要做到尽量不丢弃用户请求。
4.3.2.2.2. 令牌桶算法
令牌桶算法和漏桶算法的不同之处在于,桶中放入的不是请求,而是“令牌”,这个令牌就是业务处理前需要拿到的“许可证”。也就是说,当系统收到一个请求时,先要到令牌桶里面拿“令牌”,拿到令牌才能进一步处理,拿不到就要丢弃请求。
它的实现原理是如下图所示:

我们可以看到令牌桶算法的三个关键设计点:
有一个处理单元往桶里面放令牌,放的速率是可以控制的。
桶里面可以累积一定数量的令牌,当突发流量过来的时候,因为桶里面有累积的令牌,此时的业务处理速度会超过令牌放入的速度。
如果令牌不足,即使系统有能力处理,也会丢弃请求。
令牌桶算法的技术本质是速率控制,令牌产生的速率是设计关键,具体的优缺点如下:
可以动态调整处理速率,实现更加灵活。
突发大量流量的时候可能丢弃很多请求,因为令牌桶不能累积太多令牌。
实现相对复杂。
令牌桶算法主要适用于两种典型的场景,一种是需要控制访问第三方服务的速度,防止把下游压垮,例如支付宝需要控制访问银行接口的速率;另一种是需要控制自己的处理速度,防止过载,例如压测结果显示系统最大处理TPS是100,那么就可以用令牌桶来限制最大的处理速度。
漏桶算法可以应对瞬时高并发流量,令牌桶算法允许突发流量,两种说法好像差不多,到底谁更适合做秒杀呢?
其实,令牌桶的“允许突发”实际上只是“允许一定程度的突发”,比如系统处理能力是每秒100 TPS,突发到120 TPS是可以的,但如果突发到1000 TPS的话,系统大概率就被压垮了。所以处理秒杀时高并发流量,还是得用漏桶算法。
令牌桶的算法原本是用于网络设备控制传输速度的,而且它控制的目的是保证一段时间内的平均传输速度。之所以说令牌桶适合突发流量,是指在网络传输的时候,可以允许某段时间内(一般就几秒)超过平均传输速率,这在网络环境下常见的情况就是“网络抖动”。
但这个短时间的突发流量并不会导致雪崩效应,网络设备也能够处理得过来。对应到令牌桶应用到业务处理的场景,就要求即使有突发流量来了,系统自己或者下游系统要真的能够处理的过来,否则令牌桶允许突发流量进来,结果系统或者下游处理不了,那还是会被压垮。
因此,令牌桶在实际设计的时候,桶大小不能像漏桶那样设计很大,需要根据系统的处理能力来进行仔细的估算。例如,漏桶算法的桶容量可以设计为100万,但是一个每秒30 TPS的令牌桶,桶的容量可能只能设计成40左右。海外有的银行给移动钱包提供的接口TPS上限是30,压测到了40就真的挂了。
4.4. 排队
排队实际上是限流的一个变种,限流是直接拒绝用户,排队是让用户等待一段时间,全世界最有名的排队当属12306网站排队了。
排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。
由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用Kafka这类消息队列来缓存用户请求。
下图是1号店的“双11”秒杀排队系统架构:

(备注:图片参考刘霄晖《1 号店 11.11:秒杀排队系统设计理念》)
它的基本实现摘录如下:
【排队模块】 负责接收用户的抢购请求,将请求以先入先出的方式保存下来。每一个参加秒杀活动的商品保存一个队列,队列的大小可以根据参与秒杀的商品数量(或加点余量)自行定义。
【调度模块】 负责排队模块到服务模块的动态调度,不断检查服务模块,一旦处理能力有空闲,就从排队队列头上把用户访问请求调入服务模块,并负责向服务模块分发请求。这里调度模块扮演一个中介的角色,但不只是传递请求而已,它还担负着调节系统处理能力的重任。我们可以根据服务模块的实际处理能力,动态调节向排队系统拉取请求的速度。
【服务模块】 负责调用真正业务来处理服务,并返回处理结果,调用排队模块的接口回写业务处理结果。
4.5. 总结

评论区