生活中的弹力系统设计
艺术来源于生活,系统架构也是如此。
基于微服务的分布式系统架构所面临的挑战非常巨大,充满了各种不确定性。为了能够提高 SLA,我们需要让系统能够更有弹性,在系统部分出现故障的情况下,尽可能地减少损失。常见的弹性系统设计模式有:降级,限流,重试,补偿,异步,幂等,隔离,熔断等等。本文将结合生活中的一些例子来介绍这些模式是什么,以及什么时候应当考虑使用这些模式。
降级模式
所谓降级模式就是,当服务出现资源瓶颈,吞吐量跟不上的时候(注意是吞吐量),为了让系统能够正常运行,并承受常规情况下吞吐上限,牺牲掉一些相对次要的功能,保住关键业务的一种设计模式。比如在电商秒杀活动中,如果系统流量过大吞吐跟不上,就可以考虑牺牲掉一些 feature,比如用户评价啊,商品具体的详情啊等等,都不进行展示(都已经是秒杀了,谁还看那些东西)。
在生活中,当高速公路收费站排队太长的时候,路网系统会手动切换到免费放行模式。在节假日这种明显会产生大流量的情况下,系统还会自动切换到免费放行模式。其实这就是一种降级设计,通过牺牲收费这个相对次要的功能,保住了车辆通行顺畅这一关键业务。
限流模式
限流设计对关键业务的保护尤其重要,比如说用户中心就是一个关键业务,是一个不能挂的服务,而如果说现在有个程序员写了一个 bug,在某种 corner case 下会导致某个服务不断地去用户中心拉取大量数据,这种时候就很容易导致用户中心的请求队列里面堆积太多请求,原本正常的请求反而会被 delay,甚至得不到正确响应。
另一种常见的使用限流模式的场景出现在 open api 的设计中,由于你的 api 不再是内部系统调用的,而是暴露给第三方,你根本不知道别人会怎么用你的 api 啊,这时候限流就成为你必须要考虑的事情。
还是举一个生活中道路交通的例子,每次放假结束回上海的时候,都会遇到交警在高速公路崇明路段提前收窄道路的情况,人为降低道路通行能力。这在某种程度上就是为了缓解上海长江隧道的拥堵情况,让大家不要都堵在一个点,而是选择绕行,或者在服务区休息休息,或者看到道路拥堵就改个时间出行。。
重试模式
重试模式与 CAP 有关。分布式事务也是系统设计中的大坑,而在微服务的语境下,这个大坑往往很难避免。如果你知道 CAP 理论,你应该明白我们总是要在 C 和 A 之间做出一些牺牲。对于那些对一致性要求极高的系统(比如银行转账),有时候我们只能选择牺牲一定的可用性,但是对于更多的系统来说,往往我们可以考虑争取更高的可用性,而牺牲掉一些强一致性。重试模式就是牺牲强一致性而追求更高可用性的一种设计模式。
假设某个事务需要改变 A,B 两个系统的状态,但是当 A 的状态成功发生改变后,B 系统却迟迟不能响应,或者 B 系统干脆挂掉了。这时候如果对强一致性没有那么高的要求,你可以选择稍微等待片刻后,重新对系统 B 发起请求(B 系统的对应接口应当是幂等的)。如果你运气不错,可能重试个一两次,B 系统就活过来了,事务也就能成功完成了。
这很像我们在网上买东西,快递员给你送过来,结果你说你今天出差不在家,让他明天再来。于是快递员明天又来了一回,你拿到了你买的东西,交易事务也就成功了。
补偿模式
补偿模式则是处理牺牲强一致性而追求更高可用性的另一种设计模式,通常会和重试模式配合使用。还在上面的例子,A 系统状态更新成功了,但是在请求 B 系统的时候,B 系统却报了个错,表示这个交易无法完成(对方账户没了啊,商品库存不够了啊,商品已经下架了啊)。这时候无论你怎么进行重试,事务都不可能完成了。那怎么办呢?你需要去补偿 A 系统。你要告诉 A 系统:”有内鬼,交易终止”,然后让 A 系统补偿之前的状态更新操作(账号上把钱加回来啊啥的)。
当然,你一定会想到,如果补偿也失败了呢?而且是那种业务上完全无法完成的补偿,这时候咋办?理论上来说,我觉得这个问题是无解的。比如 A 让你把钱交给 B,但是在你给到 B 之前 B 被人杀了,你想把钱还给 A,结果发现 A 也被杀了,那么这件把 A 的钱转交给 B 的事务就用于没法被”做完”,或者被”没做”。对于这种问题,我们只能说减小它发生的概率(比如设计一些两阶段提交之类的东西,当然这显然增加了系统的复杂度,而我们讨厌复杂度),并且设计好兜底方案。如果说你的业务真的无法接受这件事情的发生的话,恐怕你只能让 A 自己直接把钱交给 B 了(不要拆分这两个系统)。
还是说快递小哥的故事,快递小哥连续来你家几天后,你告诉他你要出国两年,这时候他只能帮你退货了,然后再把钱退回到你的账户,于是你买东西这个事务就被 undo 了。
幂等模式
幂等设计其实很好理解,在前面重试那边我们也做了介绍。简单来说就是一个请求被执行一次和执行多次得到的效果应该是一样的,幂等设计是重试的一个基本保障。
生活中也有很多幂等的操作,比如有强迫症的同学锁车门的时候,按一下锁门不放心,多按两下,甚至走了两步还是不放心,又走回来重新锁了一次车门。锁车门这个动作就是幂等的,锁一次和锁十次的效果一样。
异步模式
异步模式能够很好的解决系统 delay 的问题,也能够提升系统的吞吐量。在面对一些需要较长处理时间的业务时,我们常常会使用异步模式,后端服务在接收到请求后立即返回给用户”请求已收到”的信息,如果有必要还可以返回一个 token 给用户。通常来说,处理用户请求的并非接收用户请求的服务,而是其他一些跑在后面的 worker 服务,而当这个请求对应的任务被做完后,再想办法通知用户就好了(一些 web 应用会选择 websocket 的方式,或者前端轮询,或者调用系统本来就设计好的消息功能)。比如你在微博上发视频,在你选择完视频后,微博后端就收到你的请求了,然后他默默的把视频传上去,进行一些编辑,把视频存储起来,然后微博会给你发一条私信,告诉你视频发好了,然后你的视频才会出现在各个 follower 的时间轴中。
异步模式在各种快餐店里广泛应用。比如你去麦当劳买汉堡,你在前台付钱,前台帮你生成订单并发到后厨,后厨按序处理订单并在处理完你的订单后通知你去拿汉堡。
隔离模式
隔离模式是我特别喜欢的一个模式。也是我认为在构建复杂业务系统的时候必须要考虑的一种设计模式。假设你现在在构建一个非常复杂的业务,你也有很多不同的客户,不同的客户有着各种不同的需求,不同的客户也付了不同的钱(百分之十的客户付了百分之九十的钱)。这时候,你最不希望发生的事情就是由于为了帮助一个不那么重要的客户上线一个不那么关键的功能,导致整个系统出现故障,然后你那些付了大钱的大客户都受到了影响。再举一个例子,假设你现在有一些很稳定的业务,同时又有一些很不稳定的业务,这些不稳定的业务可能今天把数据库连接打满,明天又把机器内存吃光,你一定不希望他们影响你稳定业务的工作。
隔离模式要解决的就是这种问题,你应当把你的关键业务和关键客户从你的普通业务和普通客户中隔离开,把你的稳定业务和你的不稳定业务隔离开。
这种模式和大型船只设计很像,船底部的船舱一般都被隔离成很多块,这样就是船底破了洞也不会导致整个船漏水。
熔断模式
熔断模式常用于处理服务提供者不可用的问题。当某个关键业务突然不可用的时候,其调用者的调用会不停失败,更为糟糕的是,更上层的调用者也会受到波及,就会出现服务雪崩。这时候系统处于一个非常危险的情况下,更糟糕的是,有时候你发现了问题并选择重启这个关键业务,但是它一上线就被巨大的流量打挂了。为了解决这个问题,我们可以采用熔断模式,在关键业务出现故障后将其置为熔断模式,此时其他服务在一段时间内将不被允许访问它,之后等服务重新上线并趋于稳定,再慢慢闭合熔断器,让流量逐渐恢复正常。
熔断器模式就像我们生活中的短路保护器,当电路中出现短路问题,导致电流突然升高,为了防止更大事故发生,通常会在电路中设置熔断器。当电流升高到一定程度后,熔断器自动熔断,断开电路。
如何使用这些弹力模式?
分布式系统中充满了各种各样的问题,而在面对不同的问题时,我们需要采取不同的模式。这些模式不过是些工具,并没有好坏之分,你必须先理解你面对的问题,才能知道你应该应用什么模式。下面是一些关于模式应用的 tips。
突发流量很大? 降级,限流
业务流量就是很大? 异步
操作非常耗费时间,导致 delay 过长? 异步
需要更好的架构解耦? 异步
需要更好的安全性? 隔离,熔断
暂时性的错误? 重试,幂等
非暂时性的错误? 熔断
分布式事务的问题? 补偿,重试,幂等