This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
<audio id="audio" title="41 | 弹力设计篇之“认识故障和弹力设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/51/d1/51be13faf4d139164df9f1e5b57831d1.mp3"></audio>
我前面写的《分布式系统架构的本质》系列文章,从分布式系统的业务层、中间件层、数据库层等各个层面介绍了高并发架构、异地多活架构、容器化架构、微服务架构、高可用架构、弹性化架构等,也就是所谓的“纲”。通过这个“纲”,你能够按图索骥,掌握分布式系统中每个部件的用途与总体架构思路。
为了让你更深入地了解分布式系统,在接下来的几期中,我想谈谈分布式系统中一些比较关键的设计模式,其中包括容错、性能、管理等几个方面。
<li>
**容错设计又叫弹力设计**,其中着眼于分布式系统的各种“容忍”能力,包括容错能力(服务隔离、异步调用、请求幂等性)、可伸缩性(有/无状态的服务)、一致性(补偿事务、重试)、应对大流量的能力(熔断、降级)。可以看到,在确保系统正确性的前提下,系统的可用性是弹力设计保障的重点。
</li>
<li>
**管理篇**会讲述一些管理分布式系统架构的一些设计模式比如网关方面的边车模式还有一些刚刚开始流行的如Service Mesh相关的设计模式。
</li>
<li>
**性能设计篇**会讲述一些缓存、CQRS、索引表、优先级队列、业务分片等相关的架构模式。
</li>
我相信,你在掌握了这些设计模式之后,无论是对于部署一个分布式系统,开发一个分布式的业务模块,还是研发一个新的分布式系统中间件,都会有所裨益。
今天分享的就是《分布式系统设计模式》系列文章中的第一篇《弹力设计篇之“认识故障和弹力设计”》。
# 系统可用性测量
对于分布式系统的容错设计在英文中又叫Resiliency弹力。意思是系统在不健康、不顺甚至出错的情况下有能力hold得住挺得住还有能在这种逆境下力挽狂澜的能力。
要做好一个设计,我们需要一个设计目标,或是一个基准线,通过这个基准线或目标来指导我们的设计,否则在没有明确基准线的指导下,设计会变得非常不明确,并且也不可预测,不可测量。可测试和可测量性是软件设计中非常重要的事情。
我们知道,容错主要是为了可用性,那么,我们是怎样计算一个系统的可用性的呢?下面是一个工业界里使用的一个公式:
$$Availability=\frac{MTTF}{MTTF +MTTR}$$
其中,
<li>
MTTF 是 Mean Time To Failure平均故障前的时间即系统平均能够正常运行多长时间才发生一次故障。系统的可靠性越高MTTF越长。注意从字面上来说看上去有Failure的字样但其实是正常运行的时间。
</li>
<li>
MTTR 是 Mean Time To Recovery平均修复时间即从故障出现到故障修复的这段时间这段时间越短越好。
</li>
这个公式就是计算系统可用性的也就是我们常说的多少个9如下表所示。
<img src="https://static001.geekbang.org/resource/image/c3/72/c3ac18852cc1067b3d7df4223a340372.png" alt="" />
根据上面的这个公式,为了提高可用性,我们要么提高系统的无故障时间,要么减少系统的故障恢复时间。
然而,我们要明白,我们运行的是一个分布式系统,对于一个分布式系统来说,要不出故障简直是太难了。
# 故障原因
老实说我们很难计算我们设计的系统有多少的可用性因为影响一个系统的因素实在是太多了除了软件设计还有硬件还有第三方服务如电信联通的宽带SLA当然包括“建筑施工队的挖掘机”。
所以正如SLA的定义这不只是一个技术指标而是一种服务提供商和用户之间的contract或契约。这种工业级的玩法就像飞机一样并不是把飞机造出来就好了还有大量的无比专业的配套设施、工具、流程、管理和运营。
简而言之SLA的几个9就是能持续提供可用服务的级别。不过工业界中会把服务不可用的因素分成两种一种是有计划的一种是无计划的。
无计划的宕机原因。下图来自Oracle的 [High Availability Concepts and Best Practices](https://docs.oracle.com/cd/A91202_01/901_doc/rac.901/a89867/pshavdtl.htm)。
<img src="https://static001.geekbang.org/resource/image/a8/0b/a879f083b84e956e3b3ab549fac18a0b.jpg" alt="" />
有计划的宕机原因。下图来自Oracle的[High Availability Concepts and Best Practices](https://docs.oracle.com/cd/A91202_01/901_doc/rac.901/a89867/pshavdtl.htm)。
<img src="https://static001.geekbang.org/resource/image/3b/ad/3b17a354d64de88e8a51c381b64401ad.jpg" alt="" /><br />
可以看到,宕机原因主要有以下这些。
**无计划的**
- 系统级故障,包括主机、操作系统、中间件、数据库、网络、电源以及外围设备。
- 数据和中介的故障,包括人员误操作、硬盘故障、数据乱了。
- 还有自然灾害、人为破坏,以及供电问题等。
**有计划的**
- 日常任务:备份,容量规划,用户和安全管理,后台批处理应用。
- 运维相关:数据库维护、应用维护、中间件维护、操作系统维护、网络维护。
- 升级相关:数据库、应用、中间件、操作系统、网络,包括硬件升级。
我们再给它们归个类。
1. **网络问题**。网络链接出现问题,网络带宽出现拥塞……
1. **性能问题**。数据库慢SQL、Java Full GC、硬盘IO过大、CPU飙高、内存不足……
1. **安全问题**。被网络攻击如DDoS等。
1. **运维问题**。系统总是在被更新和修改,架构也在不断地被调整,监控问题……
1. **管理问题**。没有梳理出关键服务以及服务的依赖关系,运行信息没有和控制系统同步……
1. **硬件问题**。硬盘损坏、网卡出问题、交换机出问题、机房掉电、挖掘机问题……
# 故障不可避免
如果你看过我写过的《分布式系统架构的本质》和《故障处理》这两个系列的文章,就会知道要管理好一个分布式系统是一件非常难的事。对于大规模的分布式系统,出现故障基本上就是常态,甚至还有些你根本就不知道会出问题的地方。
在今天来说,一个分布式系统的故障已经非常复杂了,因为故障是分布式的、多米诺骨牌式的。就像我在《分布式系统架构的本质》中展示过的这个图一样。
<img src="https://static001.geekbang.org/resource/image/bd/3e/bd48fbd74405e8380defdf708b6b3e3e.png" alt="" />
如果你在云平台上或者使用了“微服务”面对大量的IoT设备以及不受控制的用户流量那么系统故障会更为复杂和变态。因为上面这些因素增加了整个系统的复杂度。
所以,要充分地意识到下面两个事。
- **故障是正常的,而且是常见的**。
- **故障是不可预测突发的,而且相当难缠**。
所以亚马逊的AWS才会把Design for Failure做为其七大Design Principle的重点。这告诉我们不要尝试着去避免故障而是要把处理故障的代码当成正常的功能做在架构里写在代码里。
因为我们要干的事儿就是想尽一切手段来降低MTTR——故障的修复时间。
这就是为什么我们把这个设计叫做弹力Resiliency
<li>
一方面,在好的情况下,这个事对于我们的用户和内部运维来说是完全透明的,系统自动修复不需要人的干预。
</li>
<li>
另一方面,如果修复不了,系统能够做自我保护,而不让事态变糟糕。
</li>
这就是所谓的“弹力”——能上能下。这让我想到三国杀里赵云的技能——“能进能退乃真正法器”,哈哈。
# 小结
好了,今天的内容就到这里。相信通过今天的学习,你应该已经明白了弹力设计的真正目的,并对系统可用性的衡量指标和故障的各种原因有所了解。下一篇文章,我们将开始罗列一些相关的设计模式。
在文章的最后,很想听听大家在设计一个分布式系统时,设定了多高的可用性指标?实现的难点在哪里?踩过什么样的坑?你是如何应对的?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="42 | 弹力设计篇之“隔离设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/3b/7d0a29470858983b3a19545a23cd7a3b.mp3"></audio>
隔离设计对应的单词是Bulkheads中文翻译为隔板。但其实这个术语是用在造船上的也就是船舱里防漏水的隔板。一般的船无论大小都会有这个东西大一点的船都会把船舱隔成若干个空间。这样如果船舱漏水只会进到一个小空间里不会让整个船舱都进水而导致整艘船都沉了如下图所示。
<img src="https://static001.geekbang.org/resource/image/0d/c4/0d73168bd1af3e1266c24c2064d391c4.png" alt="" />
<img src="https://static001.geekbang.org/resource/image/53/97/53ae45b51f9e2678ec7e8661ef233297.png" alt="" />
我们的软件设计当然也“漏水”,所以为了不让“故障”蔓延开来,需要使用“隔板”技术,来将架构分隔成多个“船舱”来隔离故障。
多扯一句著名的泰坦尼克号也有Bulkheads设计然而其设计上有个缺陷。如下图所示当其撞上冰山漏水时因为船体倾斜导致水漫过了隔板从而下沉了。
<img src="https://static001.geekbang.org/resource/image/5b/bf/5bfda0399c45e1ae6a53860698740dbf.png" alt="" />
在分布式软件架构中,我们同样需要使用类似的技术来让我们的故障得到隔离。这就需要我们对系统进行分离。一般来说,对于系统的分离有两种方式,一种是以服务的种类来做分离,一种是以用户来做分离。下面具体说明一下这两种方式。
# 按服务的种类来做分离
下面这个图中,说明了按服务种类来做分离的情况。
<img src="https://static001.geekbang.org/resource/image/34/eb/34e3b94399f89a825a0046234607f9eb.png" alt="" />
上图中,我们将系统分成了用户、商品、社区三个板块。这三个块分别使用不同的域名、服务器和数据库,做到从接入层到应用层再到数据层三层完全隔离。这样一来,在物理上来说,一个板块的故障就不会影响到另一板块。
在亚马逊,每个服务都有自己的一个数据库,每个数据库中都保存着和这个业务相关的数据和相应的处理状态。而每个服务从一开始就准备好了对外暴露。同时,这也是微服务所推荐的架构方式。
然而任何架构都有其好和不好的地方,上面这种架构虽然在系统隔离上做得比较好,但是也存在以下一些问题。
<li>
如果我们需要同时获得多个板块的数据,那么就需要调用多个服务,这会降低性能。注意,这里性能降低指的是响应时间,而不是吞吐量(相反,在这种架构下,吞吐量可以得到提高)。
对于这样的问题,一般来说,我们需要小心地设计用户交互,最好不要让用户在一个页面上获得所有的数据。对于目前的手机端来说,因为手机屏幕尺寸比较小,所以,也不可能在一个屏幕页上展示太多的内容。
</li>
<li>
如果有大数据平台,就需要把这些数据都抽取到一个数据仓库中进行计算,这也增加了数据合并的复杂度。对于这个问题,我们需要一个框架或是一个中间件来对数据进行相应的抽取。
</li>
<li>
另外,如果我们的业务逻辑或是业务流程需要跨板块的话,那么一个板块的故障也会导致整个流程走不下去,同样会导致整体业务故障。
对于这个问题一方面我们需要保证这个业务流程中各个子系统的高可用性并且在业务流程上做成Step-by-Step的方式这样用户交互的每一步都可以保存以便故障恢复后可以继续执行而不是从头执行。
</li>
<li>
还有如果需要有跨板块的交互也会变得有点复杂。对此我们需要一个类似于Pub/Sub的高可用、且可以持久化的消息订阅通知中间件来打通各个板块的数据和信息交换。
</li>
<li>
最后还会有在多个板块中分布式事务的问题。对此我们需要“二阶段提交”这样的方案。在亚马逊中使用的是Plan Reserve Commit/Cancel 模式。
</li>
也就是说先做一个plan的API调用然后各个子系统reserve住相应的资源如果成功则Commit如果有一个失败则整体Cancel。这其实很像阿里的TCC try confirm/cancel。
可见,隔离了的系统在具体的业务场景中还是有很多问题的,是需要我们小心和处理的。对此,我们不可掉以轻心。根据我的经验,这样的系统通常会引入大量的异步处理模型。
# 按用户的请求来做分离
下图是一个按用户请求来做分离的图示。
<img src="https://static001.geekbang.org/resource/image/a7/5e/a7293c5fe813a7e8e2498aac34c4825e.png" alt="" />
在这个图中,可以看到,我们将用户分成不同的组,并把后端的同一个服务根据这些不同的组分成不同的实例。让同一个服务对于不同的用户进行冗余和隔离,这样一来,当服务实例挂掉时,只会影响其中一部分用户,而不会导致所有的用户无法访问。
这种分离和上面按功能的分离可以融合。说白了,这就是所谓的“多租户”模式。对于一些比较大的客户,我们可以为他们设置专门独立的服务实例,或是服务集群与其他客户隔离开来,对于一些比较小的用户来说,可以让他们共享一个服务实例,这样可以节省相关的资源。
对于“多租户”的架构来说,会引入一些系统设计的复杂度。一方面,如果完全隔离,资源使用上会比较浪费,如果共享,又会导致程序设计的一些复杂度。
通常来说多租户的做法有三种。
<li>
完全独立的设计。每个租户有自己完全独立的服务和数据。
</li>
<li>
独立的数据分区,共享的服务。多租户的服务是共享的,但数据是分开隔离的。
</li>
<li>
共享的服务,共享的数据分区。每个租户的数据和服务都是共享的。
</li>
这三种方案各有优缺点,如图所示。
<img src="https://static001.geekbang.org/resource/image/0c/9c/0c7cb0d25fb2c65a8db011ba61b8729c.png" alt="" />
通过上图,可以看到:
<li>
如果使用完全独立的方案,在开发实现上和资源隔离度方面会非常好,然而,成本会比较高,计算资源也会有一定的浪费。
</li>
<li>
如果使用完全共享的方案,在资源利用和成本上会非常好,然而,开发难度非常大,而且数据和资源隔离非常不好。
</li>
所以,一般来说,技术方案会使用折衷方案,也就是中间方案,服务是共享的,数据通过分区来隔离,而对于一些比较重要的租户(需要好的隔离性),则使用完全独立的方式。
然而在虚拟化技术非常成熟的今天我们完全可以使用“完全独立”完全隔离的方案通过底层的虚拟化技术Hypervisor的技术如KVM或是Linux Container的技术如Docker来实现物理资源的共享和成本的节约。
# 隔离设计的重点
要能做好隔离设计,我们需要有如下的一些设计考量。
<li>
我们需要定义好隔离业务的大小和粒度,过大和过小都不好。这需要认真地做业务上的需求和系统分析。
</li>
<li>
无论是做系统板块还是多租户的隔离,你都需要考虑系统的复杂度、成本、性能、资源使用的问题,找到一个合适的均衡方案,或是分布实施的方案尤其重要,这其中需要你定义好要什么和不要什么。因为,我们不可能做出一个什么都能满足的系统。
</li>
<li>
隔离模式需要配置一些高可用、重试、异步、消息中间件,流控、熔断等设计模式的方式配套使用。
</li>
<li>
不要忘记了分布式系统中的运维的复杂度的提升,要能驾驭得好的话,还需要很多自动化运维的工具,尤其是使用像容器或是虚拟机这样的虚拟化技术可以帮助我们更方便地管理,和对比资源更好地利用。否则做出来了也管理不好。
</li>
<li>
最后,你需要一个非常完整的能够看得到所有服务的监控系统,这点非常重要。
</li>
# 小结
好了,我们来总结一下今天分享的主要内容。首先,我从船体水密舱的设计,引出了分布式系统设计中的隔离设计。然后我介绍了常见的隔离有两种,一种是按服务种类隔离,另一种是按用户隔离(即多租户)。下篇文章中,我们讲述异步通讯设计。希望对你有帮助。
也欢迎你分享一下你是如何为分布式系统做隔离设计的。
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="43 | 弹力设计篇之“异步通讯设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/f9/7ae353e4ef2446b405b7fa4aa23604f9.mp3"></audio>
前面所说的隔离设计通常都需要对系统做解耦设计,而把一个单体系统解耦,不单单是把业务功能拆分出来,正如上面所说,拆分完后还会面对很多的问题。其中一个重要的问题就是这些系统间的通讯。
通讯一般来说分同步和异步两种。同步通讯就像打电话,需要实时响应,而异步通讯就像发邮件,不需要马上回复。各有千秋,我们很难说谁比谁好。但是在面对超高吐吞量的场景下,异步处理就比同步处理有比较大的优势了,这就好像一个人不可能同时接打很多电话,但是他可以同时接收很多的电子邮件一样。
同步调用虽然让系统间只耦合于接口,而且实时性也会比异步调用要高,但是我们也需要知道同步调用会带来如下几个问题。
<li>
同步调用需要被调用方的吞吐不低于调用方的吞吐。否则会导致被调用方因为性能不足而拖死调用方。换句话说,整个同步调用链的性能会由最慢的那个服务所决定。
</li>
<li>
同步调用会导致调用方一直在等待被调用方完成如果一层接一层地同步调用下去所有的参与方会有相同的等待时间。这会非常消耗调用方的资源。因为调用方需要保存现场Context等待远端返回所以对于并发比较高的场景来说这样的等待可能会极度消耗资源。
</li>
<li>
同步调用只能是一对一的,很难做到一对多。
</li>
<li>
同步调用最不好的是,如果被调用方有问题,那么其调用方就会跟着出问题,于是会出现多米诺骨牌效应,故障一下就蔓延开来。
</li>
所以,异步通讯相对于同步通讯来说,除了可以增加系统的吞吐量之外,最大的一个好处是其可以让服务间的解耦更为彻底,系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力。
异步通讯通常来说有三种方式。
# 异步通讯的三种方式
## 请求响应式
在这种情况下发送方sender会直接请求接收方receiver被请求方接收到请求后直接返回——收到请求正在处理。
对于返回结果有两种方法一种是发送方时不时地去轮询一下问一下干没干完。另一种方式是发送方注册一个回调方法也就是接收方处理完后回调请求方。这种架构模型在以前的网上支付中比较常见页面先从商家跳转到支付宝或银行商家会把回调的URL传给支付页面支付完后再跳转回商家的URL。
很明显,这种情况下还是有一定耦合的。是发送方依赖于接收方,并且要把自己的回调发送给接收方,处理完后回调。
## 通过订阅的方式
这种情况下接收方receiver会来订阅发送方sender的消息发送方会把相关的消息或数据放到接收方所订阅的队列中而接收方会从队列中获取数据。
这种方式下发送方并不关心订阅方的处理结果它只是告诉订阅方有事要干收完消息后给个ACK就好了你干成啥样我不关心。这个方式常用于像MVCModel-View-Control这样的设计模式下如下图所示。
<img src="https://static001.geekbang.org/resource/image/d8/37/d8d96ed4e4616626b9e079dc13637937.png" alt="" />
这就好像下订单的时候,一旦用户支付完成了,就需要把这个事件通知给订单处理以及物流,订单处理变更状态,物流服务需要从仓库服务分配相应的库存并准备配送,后续这些处理的结果无需告诉支付服务。
为什么要做成这样?好了,重点来了!前面那种请求响应的方式就像函数调用一样,这种方式有数据有状态的往来(也就是说需要有请求数据、返回数据,服务里面还可能需要保存调用的状态),所以服务是有状态的。如果我们把服务的状态给去掉(通过第三方的状态服务来保证),那么服务间的依赖就只有事件了。
你知道分布式系统的服务设计是需要向无状态服务Stateless努力的这其中有太多的好处无状态意味着你可以非常方便地运维。所以事件通讯成为了异步通讯中最重要的一个设计模式。
就上面支付的那个例子商家这边只需要订阅一个支付完成的事件这个事件带一个订单号而不需要让支付方知道自己的回调URL这样的异步是不是更干净一些
但是,在这种方式下,接收方需要向发送方订阅事件,所以是接收方依赖于发送方。这种方式还是有一定的耦合。
## 通过Broker的方式
所谓Broker就是一个中间人发送方sender和接收方receiver都互相看不到对方它们看得到的是一个Broker发送方向Broker发送消息接收方向Broker订阅消息。如下图所示。
<img src="https://static001.geekbang.org/resource/image/aa/17/aa1c6db18e706012e8028b4d1bddb917.png" alt="" />
这是完全的解耦。所有的服务都不需要相互依赖而是依赖于一个中间件Broker。这个Broker是一个像数据总线一样的东西所有的服务要接收数据和发送数据都发到这个总线上这个总线就像协议一样让服务间的通讯变得标准和可控。
在Broker这种模式下发送方的服务和接收方的服务最大程度地解耦。但是所有人都依赖于一个总线所以这个总线就需要有如下的特性
- 必须是高可用的,因为它成了整个系统的关键;
- 必须是高性能而且是可以水平扩展的;
- 必须是可以持久化不丢数据的。
要做到这三条还是比较难的。当然好在现在开源软件或云平台上Broker的软件是非常成熟的所以节省了我们很多的精力。
# 事件驱动设计
上述的第二种和第三种方式就是比较著名的事件驱动架构EDA Event Driven Architecture。正如前面所说事件驱动最好是使用Broker方式服务间通过交换消息来完成交流和整个流程的驱动。
如下图所示,这是一个订单处理流程。下单服务通知订单服务有订单要处理,而订单服务生成订单后发出通知,库存服务和支付服务得到通知后,一边是占住库存,另一边是让用户支付,等待用户支付完成后通知配送服务进行商品配送。
<img src="https://static001.geekbang.org/resource/image/aa/59/aa95556d053e22be38a8beb40cf28759.png" alt="" />
每个服务都是“自包含”的。所谓“自包含”也就是没有和别人产生依赖。而要把整个流程给串联起来我们需要一系列的“消息通道Channel”。各个服务做完自己的事后发出相应的事件而又有一些服务在订阅着某些事件来联动。
事件驱动方式的好处至少有五个。
<li>
服务间的依赖没有了,服务间是平等的,每个服务都是高度可重用并可被替换的。
</li>
<li>
服务的开发、测试、运维,以及故障处理都是高度隔离的。
</li>
<li>
服务间通过事件关联所以服务间是不会相互block的。
</li>
<li>
在服务间增加一些Adapter如日志、认证、版本、限流、降级、熔断等相当容易。
</li>
<li>
服务间的吞吐也被解开了,各个服务可以按照自己的处理速度处理。
</li>
我们知道任何设计都有好有不好的方式。事件驱动的架构也会有一些不好的地方。
<li>
业务流程不再那么明显和好管理。整个架构变得比较复杂。解决这个问题需要有一些可视化的工具来呈现整体业务流程。
</li>
<li>
事件可能会乱序。这会带来非常Bug的事。解决这个问题需要很好地管理一个状态机的控制。
</li>
<li>
事务处理变得复杂。需要使用两阶段提交来做强一致性,或是退缩到最终一致性。
</li>
# 异步通讯的设计重点
首先,我们需要知道,为什么要异步通讯。
<li>
异步通讯最重要的是解耦服务间的依赖。最佳解耦的方式是通过Broker的机制。
</li>
<li>
解耦的目的是让各个服务的隔离性更好,这样不会出现“一倒倒一片”的故障。
</li>
<li>
异步通讯的架构可以获得更大的吞吐量,而且各个服务间的性能不受干扰相对独立。
</li>
<li>
利用Broker或队列的方式还可以达到把抖动的吞吐量变成均匀的吞吐量这就是所谓的“削峰”这对后端系统是个不错的保护。
</li>
<li>
服务相对独立,在部署、扩容和运维上都可以做到独立不受其他服务的干扰。
</li>
但我们需要知道这样的方式带来的问题,所以在设计成异步通信的时候需要注意如下事宜。
<li>
用于异步通讯的中间件Broker成为了关键需要设计成高可用不丢消息的。另外因为是分布式的所以可能很难保证消息的顺序因此你的设计最好不依赖于消息的顺序。
</li>
<li>
异步通讯会导致业务处理流程不那么直观因为像接力一样所以在Broker上需要有相关的服务消息跟踪机制否则出现问题后不容易调试。
</li>
<li>
因为服务间只通过消息交互,所以业务状态最好由一个总控方来管理,这个总控方维护一个业务流程的状态变迁逻辑,以便系统发生故障后知道业务处理到了哪一步,从而可以在故障清除后继续处理。
这样的设计常见于银行的对账程序银行系统会有大量的外部系统通讯比如跨行的交易、跨企业的交易等等。所以为了保证整体数据的一致性或是避免漏处理及处理错的交易需要有对账系统这其实就是那个总控这也是为什么银行有的交易是T+1隔天结算就是因为要对个账确保数据是对的。
</li>
<li>
消息传递中可能有的业务逻辑会有像TCP协议那样的send和ACK机制。比如A服务发出一个消息之后开始等待处理方的ACK如果等不到的话就需要做重传。此时需要处理方有幂等的处理即同一件消息无论收到多少次都只处理一次。
</li>
# 小结
好了,我们来总结一下今天分享的主要内容。首先,同步调用有四个问题:影响吞吐量、消耗系统资源、只能一对一,以及有多米诺骨牌效应。于是,我们想用异步调用来避免该问题。
异步调用有三种方式:请求响应、直接订阅和中间人订阅。最后,我介绍了事件驱动设计的特点和异步通讯设计的重点。下篇文章中,我们讲述幂等性设计。希望对你有帮助。
也欢迎你分享一下你在分布式服务的设计中,哪些情况下使用异步通讯?是怎样设计的?又有哪些情况使用同步通讯?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,139 @@
<audio id="audio" title="44 | 弹力设计篇之“幂等性设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/35/6c92f8e9c1e3172542c4304d33280a35.mp3"></audio>
所谓幂等性设计就是说一次和多次请求某一个资源应该具有同样的副作用。用数学的语言来表达就是f(x) = f(f(x))。
比如求绝对值的函数abs(x) = abs(abs(x))。
为什么我们需要这样的操作说白了就是在我们把系统解耦隔离后服务间的调用可能会有三个状态一个是成功Success一个是失败Failed一个是超时Timeout。前两者都是明确的状态而超时则是完全不知道是什么状态。
比如,超时原因是网络传输丢包的问题,可能是请求时就没有请求到,也有可能是请求到了,返回结果时没有正常返回等等情况。于是我们完全不知道下游系统是否收到了请求,而收到了请求是否处理了,成功/失败的状态在返回时是否遇到了网络问题。总之,请求方完全不知道是怎么回事。
举几个例子:
<li>
订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
</li>
<li>
订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
</li>
<li>
当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?
</li>
因为系统超时,而调用户方重试一下,会给我们的系统带来不一致的副作用。
在这种情况下,一般有两种处理方式。
<li>
一种是需要下游系统提供相应的查询接口。上游系统在timeout后去查询一下。如果查到了就表明已经做了成功了就不用做了失败了就走失败流程。
</li>
<li>
另一种是通过幂等性的方式。也就是说,把这个查询操作交给下游系统,我上游系统只管重试,下游系统保证一次和多次的请求结果是一样的。
</li>
对于第一种方式,需要对方提供一个查询接口来做配合。而第二种方式则需要下游的系统提供支持幂等性的交易接口。
# 全局ID
要做到幂等性的交易接口需要有一个唯一的标识来标志交易是同一笔交易。而这个交易ID由谁来分配是一件比较头疼的事。因为这个标识要能做到全局唯一。
如果由一个中心系统来分配,那么每一次交易都需要找那个中心系统来。 这样增加了程序的性能开销。如果由上游系统来分配则可能会出现ID分配重复的问题。因为上游系统可能会是一个集群它们同时承担相同的工作。
为了解决分配冲突的问题我们需要使用一个不会冲突的算法比如使用UUID这样冲突非常小的算法。但UUID的问题是它的字符串占用的空间比较大索引的效率非常低生成的ID太过于随机完全不是人读的而且没有递增如果要按前后顺序排序的话基本不可能。
在全局唯一ID的算法中这里介绍一个Twitter 的开源项目 Snowflake。它是一个分布式ID的生成算法。其核心思想是产生一个long型的ID其中
- 41bits作为毫秒数。大概可以用69.7年。
- 10bits作为机器编号5bits是数据中心5bits的机器ID支持1024个实例。
- 12bits作为毫秒内的序列号。一毫秒可以生成4096个序号。
<img src="https://static001.geekbang.org/resource/image/2b/28/2bda719052cb3b584c0db93e89d32528.png" alt="" />
其他的像Redis或MongoDB的全局ID生成都和这个算法大同小异。我在这里就不多说了。你可以根据实际情况加上业务的编号。
# 处理流程
对于幂等性的处理流程来说,说白了就是要过滤一下已经收到的交易。要做到这个事,我们需要一个存储来记录收到的交易。
于是,当收到交易请求的时候,我们就会到这个存储中去查询。如果查找到了,那么就不再做查询了,并把上次做的结果返回。如果没有查到,那么我们就记录下来。
<img src="https://static001.geekbang.org/resource/image/4c/1b/4c078da5bf5833cebc08a57bfb332b1b.png" alt="" />
但是上面这个流程有个问题。因为绝大多数请求应该都不会是重新发过来的所以让100%的请求都到这个存储里去查一下,这会导致处理流程变得很慢。
所以最好是当这个存储出现冲突的时候会报错。也就是说我们收到交易请求后直接去存储里记录这个ID相对于数据的Insert操作如果出现ID冲突了的异常那么我们就知道这个之前已经有人发过来了所以就不用再做了。比如数据库中你可以使用 `insert into … values … on DUPLICATE KEY UPDATE …` 这样的操作。
对于更新的场景来说,如果只是状态更新,可以使用如下的方式。如果出错,要么是非法操作,要么是已被更新,要么是状态不对,总之多次调用是不会有副作用的。
`update table set status = “paid” where id = xxx and status = “unpaid”;`
当然网上还有MVCC通过使用版本号等其他方式我觉得这些都不标准我们希望我们有一个标准的方式来做这个事所以最好还是用一个ID。
因为我们的幂等性服务也是分布式的,所以,需要这个存储也是共享的。这样每个服务就变成没有状态的了。但是,这个存储就成了一个非常关键的依赖,其扩展性和可用性也成了非常关键的指标。
你可以使用关系型数据库或是key-value的NoSQL如MongoDB来构建这个存储系统。
# HTTP的幂等性
** HTTP GET方法用于获取资源不应有副作用所以是幂等的**。比如GET `http://www.bank.com/account/123456`不会改变资源的状态不论调用一次还是N次都没有副作用。请注意这里强调的是一次和N次具有相同的副作用而不是每次GET的结果相同。GET `http://www.news.com/latest-news`这个HTTP请求可能会每次得到不同的结果但它本身并没有产生任何副作用因而是满足幂等性的。
**HTTP HEAD 和GET本质是一样的区别在于HEAD不含有呈现数据而仅仅是HTTP头信息不应用有副作用也是幂等的**。有的人可能觉得这个方法没什么用其实不是这样的。想象一个业务情景欲判断某个资源是否存在我们通常使用GET但这里用HEAD则意义更加明确。也就是说HEAD方法可以用来做探活使用。
**HTTP OPTIONS 主要用于获取当前URL所支持的方法所以也是幂等的**。若请求成功则它会在HTTP头中包含一个名为“Allow”的头值是所支持的方法如“GET, POST”。
**HTTP DELETE方法用于删除资源有副作用但它应该满足幂等性**。比如DELETE `http://www.forum.com/article/4231`调用一次和N次对系统产生的副作用是相同的即删掉ID为4231的帖子。因此调用者可以多次调用或刷新页面而不必担心引起错误。
**HTTP POST方法用于创建资源所对应的URI并非创建的资源本身而是去执行创建动作的操作者有副作用不满足幂等性**。比如POST `http://www.forum.com/articles`的语义是在`http://www.forum.com/articles`下创建一篇帖子HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源它们具有不同的URI所以POST方法不具备幂等性。
**HTTP PUT方法用于创建或更新操作所对应的URI是要创建或更新的资源本身有副作用它应该满足幂等性**。比如PUT `http://www.forum/articles/4231`的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的因此PUT方法具有幂等性。
所以对于POST的方式很可能会出现多次提交的问题就好比我们在论坛中发贴时有时候因为网络有问题可能会对同一篇贴子出现多次提交的情况。对此的一般的幂等性的设计如下。
<li>
首先在表单中需要隐藏一个token这个token可以是前端生成的一个唯一的ID。用于防止用户多次点击了表单提交按钮而导致后端收到了多次请求却不能分辨是否是重复的提交。这个token是表单的唯一标识。这种情况其实是通过前端生成ID把POST变成了PUT。
</li>
<li>
然后当用户点击提交后后端会把用户提交的数据和这个token保存在数据库中。如果有重复提交那么数据库中的token会做排它限制从而做到幂等性。
</li>
<li>
当然更为稳妥的做法是后端成功后向前端返回302跳转把用户的前端页跳转到GET请求把刚刚POST的数据给展示出来。如果是Web上的最好还把之前的表单设置成过期这样用户不能通过浏览器后退按钮来重新提交。这个模式又叫做 [PRG模式](https://en.wikipedia.org/wiki/Post/Redirect/Get)Post/Redirect/Get
</li>
# 小结
好了,我们来总结一下今天分享的主要内容。首先,幂等性的含义是,一个调用被发送多次所产生的副作用和被发送一次所产生的副作用是一样的。而服务调用有三种结果:成功、失败和超时,其中超时是我们需要解决的问题。
解决手段可以是超时后查询调用结果也可以是在被调用的服务中实现幂等性。为了在分布式系统中实现幂等性我们需要实现全局ID。Twitter的Snowflake就是一个比较好用的全局ID实现。最后我给出了幂等性接口的处理流程。
下篇文章中,我们讲述服务的状态。希望对你有帮助。
也欢迎你分享一下你的分布式服务中所有交易接口是否都实现了幂等性你所使用的全局ID算法又是什么呢
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,121 @@
<audio id="audio" title="45 | 弹力设计篇之“服务的状态”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/36/4e26d19fe41c2007275862bbd0758236.mp3"></audio>
之前在我们讲的幂等设计中,为了过滤掉已经处理过的请求,其中需要保存处理过的状态,为了把服务做成无状态的,我们引入了第三方的存储。而这一篇中,我们来聊聊服务的状态这个话题。我认为,只有清楚地了解了状态这个事,我们才有可能设计出更好或是更有弹力的系统架构。
所谓“状态”就是为了保留程序的一些数据或是上下文。比如之前幂等性设计中所说的需要保留每一次请求的状态或是像用户登录时的Session我们需要这个Session来判断这个请求的合法性还有一个业务流程中需要让多个服务组合起来形成一个业务逻辑的运行上下文Context。这些都是所谓的状态。
我们的代码中基本上到处都是这样的状态。
# 无状态的服务 Stateless
一直以来无状态的服务都被当作分布式服务设计的最佳实践和铁律。因为无状态的服务对于扩展性和运维实在是太方便了。没有状态的服务可以随意地增加和减少节点同样可以随意地搬迁。而且无状态的服务可以大幅度降低代码的复杂度以及Bug数因为没有状态所以也没有明显的“副作用”。
基本上来说无状态的服务和“函数式编程”的思维方式如出一辙。在函数式编程中一个铁律是函数是无状态的。换句话说函数是immutable不变的所有的函数只描述其逻辑和算法根本不保存数据也不会修改输入的数据而是把计算好的结果返回出去哪怕要把输入的数据重新拷贝一份并只做少量的修改关于函数式编程可以参看我在CoolShell上的文章《[函数式编程](https://coolshell.cn/articles/10822.html)》)。
但是,现实世界是一定会有状态的。这些状态可能表现在如下的几个方面。
- 程序调用的结果。
- 服务组合下的上下文。
- 服务的配置。
为了做出无状态的服务我们通常需要把状态保存到其他的地方。比如不太重要的数据可以放到Redis中重要的数据可以放到MySQL中或是像ZooKeeper/Etcd这样的高可用的强一致性的存储中或是分布式文件系统中。
于是,我们为了做成无状态的服务,会导致这些服务需要耦合第三方有状态的存储服务。一方面是有依赖,另一方面也增加了网络开销,导致服务的响应时间也会变慢。
所以,第三方的这些存储服务也必须要做成高可用高扩展的方式。而且,为了减少网络开销,还需要在无状态的服务中增加缓存机制。然而,下次这个用户的请求并不一定会在同一台机器,所以,这个缓存会在所有的机器上都创建,也算是一种浪费吧。
这种“转移责任”的玩法也催生出了对分布式存储的强烈需求。正如之前在《分布式系统架构的本质》系列文章中谈到的关键技术之一的“[状态/数据调度](https://time.geekbang.org/column/article/1609)”所说的因为数据层的scheme众多所以很难做出一个放之四海皆准的分布式存储系统。
这也是为什么无状态的服务需要依赖于像ZooKeeper/Etcd这样的高可用的有强一致的服务或是依赖于底层的分布式文件系统像开源的Ceph和GlusterFS。而现在分布式数据库也开始将服务和存储分离也是为了让自己的系统更有弹力。
# 有状态的服务 Stateful
在今天看来,有状态的服务在今天看上去的确比较“反动”,但是,我们也需要比较一下它和无状态服务的优劣。
正如上面所说的无状态服务在程序Bug上和水平扩展上有非常优秀的表现但是其需要把状态存放在一个第三方存储上增加了网络开销而在服务内的缓存需要在所有的服务实例上都有因为每次请求不会都落在同一个服务实例上这是比较浪费资源的。
而有状态的服务有这些好处。
<li>
**数据本地化Data Locality**。一方面状态和数据是本机保存,这方面不但有更低的延时,而且对于数据密集型的应用来说,这会更快。
</li>
<li>
**更高的可用性和更强的一致性**。也就是CAP原理中的A和C。
</li>
为什么会这样呢因为对于有状态的服务我们需要对于客户端传来的请求都必需保证其落在同一个实例上这叫Sticky Session或是Sticky Connection。这样一来我们完全不需要考虑数据要被加载到不同的节点上去而且这样的模型更容易理解和实现。
可见最重要的区别就是无状态的服务需要我们把数据同步到不同的节点上而有状态的服务通过Sticky Session做数据分片当然同步有同步的问题分片也有分片的问题这两者没有谁比谁好都有trade-off
这种Sticky Session是怎么实现的呢
最简单的实现就是用持久化的长连接。就算是HTTP协议也要用长连接。或是通过一个简单的哈希hash算法比如通过uid 求模的方式,走一致性哈希的玩法,也可以方便地做水平扩展。
然而,这种方式也会带来问题,那就是,节点的负载和数据并不会很均匀。尤其是长连接的方式,连上了就不断了。所以,玩长连接的玩法一般都会有一种叫“反向压力(Back Pressure)”。也就是说如果服务端成为了热点那么就主动断连接这种玩法也比较危险需要客户端的配合否则容易出Bug。
如果要做到负载和数据均匀的话,我们需要有一个元数据索引来映射后端服务实例和请求的对应关系,还需要一个路由节点,这个路由节点会根据元数据索引来路由,而这个元数据索引表会根据后端服务的压力来重新组织相关的映射。
当然,我们可以把这个路由节点给去掉,让有状态的服务直接路由。要做到这点,一般来说,有两种方式。一种是直接使用配置,在节点启动时把其元数据读到内存中,但是这样一来增加或减少节点都需要更新这个配置,会导致其它节点也一同要重新读入。
另一种比较好的做法是使用到Gossip协议通过这个协议在各个节点之间互相散播消息来同步元数据这样新增或减少节点集群内部可以很容易重新分配听起来要实现好真的好复杂
在有状态的服务上做自动化伸缩的是有一些相关的真实案例的。比如Facebook的Scuba这是一个分布式的内存数据库它使用了静态的方式也就是上面的第一种方式。Uber的Ringpop是一个开源的Node.js的根据地理位置分片的路由请求的库开源地址为[https://github.com/uber-node/ringpop-node](https://github.com/uber-node/ringpop-node) )。
还有微软的OrleansHalo 4就是基于其开发的其使用了Gossip协议一致性哈希和DHT技术相结合的方式。用户通过其ID的一致性哈希算法映射到一个节点上而这个节点保存了这个用户对应的DHT再通过DHT定位到处理用户请求的位置这个项目也是开源的开源地址为 [https://github.com/dotnet/orleans](https://github.com/dotnet/orleans) )。
关于可扩展的有状态服务这里强烈推荐Twitter的美女工程师Caitie McCaffrey的演讲Youtube视频《Building Scalable Stateful Service》(演讲PPT)其文字版是在High Scalability上的这篇文章《Making the Case for Building Scalable Stateful Services in the Modern Era》
# 服务状态的容错设计
在容错设计中,服务状态是一件非常复杂的事。尤其对于运维来说,因为你要调度服务就需要调度服务的状态,迁移服务的状态就需要迁移服务的数据。在数据量比较大的情况下,这一点就变得更为困难了。
虽然上述有状态的服务的调度通过Sticky Session的方式是一种方式但我依然觉得理论上来说虽然可以这么干这实际在运维的过程中这么干还是件挺麻烦的事儿不是很好的玩法。
很多系统的高可用的设计都会采取数据在运行时就复制的方案比如ZooKeeper、Kafka、Redis或是ElasticSearch等等。在运行时进行数据复制就需要考虑一致性的问题所以强一致性的系统一般会使用两阶段提交。
这要求所有的节点都需要有一致的结果这是CAP里的CA系统。而也有的系统采用的是大多数人一致就可以了比如Paxos算法这是CP系统。
但我们需要知道,即使是这样,当一个节点挂掉了以后,在另外一个地方重新恢复这个节点时,这个节点需要把数据同步过来才能提供服务。然而,如果数据量过大,这个过程可能会很漫长,这也会影响我们系统的可用性。
所以,我们需要使用底层的分布式文件系统,对于有状态的数据不但在运行时进行多节点间的复制,同时为了避免挂掉,还需要把数据持久化在硬盘上,这个硬盘可以是挂载到本地硬盘的一个外部分布式的文件卷。
这样当节点挂掉以后,以另外一个宿主机上启动一个新的服务实例时,这个服务可以从远程把之前的文件系统挂载过来。然后,在启动的过程中就装载好了大多数的数据,从而可以从网络其它节点上同步少量的数据,因而可以快速地恢复和提供服务。
这一点,对于有状态的服务来说非常关键。所以,使用一个分布式文件系统是调度有状态服务的关键。
# 小结
好了,我们来总结一下今天分享的主要内容。首先,我讲了无状态的服务。无状态的服务就像一个函数一样,对于给定的输入,它会给出唯一确定的输出。它的好处是很容易运维和伸缩,但需要底层有分布式的数据库支持。
接着我讲了有状态的服务它们通过Sticky Session、一致性Hash和DHT等技术实现状态和请求的关联并将数据同步到分布式数据库中利用分布式文件系统还能在节点挂掉时快速启动新实例。下篇文章中我们讲述补偿事务。希望对你有帮助。
也欢迎你分享一下你所实现的分布式服务是无状态的,还是有状态的?用到了哪些技术?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,156 @@
<audio id="audio" title="46 | 弹力设计篇之“补偿事务”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/d6/2c10416326c5ec6952f40cf8ebe5f3d6.mp3"></audio>
前面,我们说过,分布式系统有一个比较明显的问题就是,一个业务流程需要组合一组服务。这样的事情在微服务下就更为明显了,因为这需要业务上一致性的保证。也就是说,如果一个步骤失败了,那么要么回滚到以前的服务调用,要么不断重试保证所有的步骤都成功。
这里如果需要强一致性那在业务层上就需要使用“两阶段提交”这样的方式。但是好在我们的很多情况下并不需要这么强的一致性而且强一致性的最佳保证基本都是在底层完成的或是像之前说的那样Stateful的Sticky Session那样在一台机器上完成。在我们接触到的大多数业务中其实只需要最终一致性就够了。
# ACID 和 BASE
谈到这里有必要先说一下ACID和BASE的差别。传统关系型数据库系统的事务都有ACID属性即原子性Atomicity、一致性Consistency、隔离性Isolation又称独立性、持久性Durability。下面我逐一做下解释
<li>
**原子性**整个事务中的所有操作要么全部完成要么全部失败不可能停滞在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。
</li>
<li>
**一致性**:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
</li>
<li>
**隔离性**:两个事务的执行是互不干扰的,一个事务不可能看到其他事务运行时中间某一时刻的数据。两个事务不会发生交互。
</li>
<li>
**持久性**:在事务完成以后,该事务对数据库所做的更改便持久地保存在数据库之中,并不会被回滚。
</li>
事务的ACID属性保证了数据库的一致性比如银行系统中转账就是一个事务从原账户扣除金额以及向目标账户添加金额这两个数据库操作的总和构成一个完整的逻辑过程是不可拆分的原子操作从而保证了整个系统中的总金额没有变化。
然而这对于我们的分布式系统来说尤其是微服务来说这样的方式是很难满足高性能要求的。我们都很熟悉CAP理论——在分布式的服务架构中一致性Consistency、可用性Availability、分区容忍性Partition Tolerance在现实中不能都满足最多只能满足其中两个。
所以为了提高性能出现了ACID的一个变种BASE。
<li>
**Basic Availability**:基本可用。这意味着,系统可以出现暂时不可用的状态,而后面会快速恢复。
</li>
<li>
**Soft-state**:软状态。它是我们前面的“有状态”和“无状态”的服务的一种中间状态。也就是说,为了提高性能,我们可以让服务暂时保存一些状态或数据,这些状态和数据不是强一致性的。
</li>
<li>
**Eventual Consistency**:最终一致性,系统在一个短暂的时间段内是不一致的,但最终整个系统看到的数据是一致的。
</li>
可以看到BASE系统是允许或是容忍系统出现暂时性问题的这样一来我们的系统就能更有弹力。因为我们知道在分布式系统的世界里故障是不可避免的我们能做的就是把故障处理当成功能写入代码中这就是Design for Failure。
BASE的系统倾向于设计出更加有弹力的的系统这种系统的设计特点是要保证在短时间内就算是有数据不同步的风险我们也应该允许新的交易可以发生而后面我们在业务上将可能出现问题的事务给处理掉以保证最终的一致性。
举个例子,网上卖书的场景。
ACID的玩法就是大家在买同一本书的过程中每个用户的购买请求都需要把库存锁住等减完库存后把锁释放出来后续的人才能进行购买。于是在ACID的玩法下我们在同一时间不可能有多个用户下单我们的订单流程需要有排队的情况这样一来我们就不可能做出性能比较高的系统来。
BASE的玩法是大家都可以同时下单这个时候不需要去真正地分配库存然后系统异步地处理订单而且是批量的处理。因为下单的时候没有真正去扣减库存所以有可能会有超卖的情况。而后台的系统会异步地处理订单时发现库存没有了于是才会告诉用户你没有购买成功。
BASE这种玩法其实就是亚马逊的玩法因为要根据用户的地址去不同的仓库查看库存这个操作非常耗时所以不想做成异步的都不行。
在亚马逊上买东西,你会收到一封邮件说,系统收到你的订单了,然后过一会儿你会收到你的订单被确认的邮件,这时候才是真正地分配了库存。所以,有某些时候,你会遇到你先收到了下单的邮件,过一会又收到了没有库存的致歉的邮件。
有趣的是ACID的意思是酸而BASE却是碱的意思因此这是一个对立的东西。其实从本质上来讲ACID强调的是一致性CAP中的C而碱BASE强调的是可用性CAP中的A
# 业务补偿
有了上面对ACID和BASE的分析我们知道在很多情况下我们是无法做到强一致的ACID的。特别是我们需要跨多个系统的时候而且这些系统还不是由一个公司所提供的。比如在我们的日常生活中我们经常会遇到这样的情况就是要找很多方协调很多事而且要保证我们每一件事都成功否则整件事就做不到。
比如,要出门旅游, 我们需要干这么几件事。第一,向公司请假,拿到相应的假期;第二,订飞机票或是火车票;第三,订酒店;第四,租车。这四件事中,前三件必需完全成功,我们才能出行,而第四件事只是一个锦上添花的事,但第四件事一旦确定,那么也会成为整个事务的一部分。这些事都是要向不同的组织或系统请求。我们可以并行地做这些事,而如果某个事有变化,其它的事都会跟着出现一些变化。
设想下面的几种情况。
<li>
我没有订到返程机票,那么我就去不了了。我需要把订到的去程机票,酒店、租到的车都给取消了,并且把请的假也取消了。
</li>
<li>
如果我假也请好了,机票,酒店也订好了,只是车没租到,那么并不影响我出行这个事,整个事还是可以继续的。
</li>
<li>
如果我的飞机因为天气原因取消或是晚点了,那么我被迫要去调整和修改我的酒店预订和租车的预订。
</li>
从人类的实际生活当中,我们可以看出,上述的这些情况都是天天在发生的事情。所以,我们的分布式系统也是一样的,也是需要处理这样的事情——就是当条件不满足,或是有变化的时候,需要从业务上做相应的整体事务的补偿。
一般来说,业务的事务补偿都是需要一个工作流引擎的。亚马逊是一个超级喜欢工作流引擎的公司,这个工作流引擎把各式各样的服务给串联在一起,并在工作流上做相应的业务补偿,整个过程设计成为最终一致性的。
对于业务补偿来说,首先需要将服务做成幂等性的,如果一个事务失败了或是超时了,我们需要不断地重试,努力地达到最终我们想要的状态。然后,如果我们不能达到这个我们想要的状态,我们需要把整个状态恢复到之前的状态。另外,如果有变化的请求,我们需要启动整个事务的业务更新机制。
所以,一个好的业务补偿机制需要做到下面这几点。
<li>
要能清楚地描述出要达到什么样的状态(比如:请假、机票、酒店这三个都必须成功,租车是可选的),以及如果其中的条件不满足,那么,我们要回退到哪一个状态。这就是所谓的整个业务的起始状态定义。
</li>
<li>
当整条业务跑起来的时候,我们可以串行或并行地做这些事。对于旅游订票是可以并行的,但是对于网购流程(下单、支付、送货)是不能并行的。总之,我们的系统需要努力地通过一系列的操作达到一个我们想要的状态。如果达不到,就需要通过补偿机制回滚到之前的状态。**这就是所谓的状态拟合**。
</li>
<li>
对于已经完成的事务进行整体修改,可以考虑成一个修改事务。
</li>
其实,在纯技术的世界里也有这样的事。比如,线上运维系统需要发布一个新的服务或是对一个已有的服务进行水平扩展,我们需要先找到相应的机器,然后初始化环境,再部署上应用,再做相应的健康检查,最后接入流量。这一系列的动作都要完全成功,所以,我们的部署系统就需要管理好整个过程和相关的运行状态。
# 业务补偿的设计重点
业务补偿主要做两件事。
1. 努力地把一个业务流程执行完成。
1. 如果执行不下去,需要启动补偿机制,回滚业务流程。
所以,下面是几个重点。
<li>
因为要把一个业务流程执行完成,需要这个流程中所涉及的服务方支持幂等性。并且在上游有重试机制。
</li>
<li>
我们需要小心维护和监控整个过程的状态,所以,千万不要把这些状态放到不同的组件中,最好是一个业务流程的控制方来做这个事,也就是一个工作流引擎。所以,这个工作流引擎是需要高可用和稳定的。这就好像旅行代理机构一样,我们把需求告诉它,它会帮我们搞定所有的事。如果有问题,也会帮我们回滚和补偿的。
</li>
<li>
补偿的业务逻辑和流程不一定非得是严格反向操作。有时候可以并行,有时候,可能会更简单。总之,设计业务正向流程的时候,也需要设计业务的反向补偿流程。
</li>
<li>
我们要清楚地知道,业务补偿的业务逻辑是强业务相关的,很难做成通用的。
</li>
<li>
下层的业务方最好提供短期的资源预留机制。就像电商中的把货品的库存预先占住等待用户在15分钟内支付。如果没有收到用户的支付则释放库存。然后回滚到之前的下单操作等待用户重新下单。
</li>
# 小结
好了我们来总结一下今天分享的主要内容。首先我介绍了ACID和BASE两种不同级别的一致性。在分布式系统中ACID有更强的一致性但可伸缩性非常差仅在必要时使用BASE的一致性较弱但有很好的可伸缩性还可以异步批量处理大多数分布式事务适合BASE。
要实现BASE事务需要实现补偿逻辑因为事务可能失败此时需要协调各方进行撤销。补偿的各个步骤可以根据具体业务来确定是串行还是并行。由于补偿事务是和业务强相关的所以必须实现在业务逻辑里。下篇文章中我们讲述重试设计。希望对你有帮助。
也欢迎你分享一下你的分布式服务用到了怎样的一致性?你是怎么实现补偿事务的?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,199 @@
<audio id="audio" title="47 | 弹力设计篇之“重试设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/b9/6263aa4f5afe1b6bfcd0f7b8b2f7b7b9.mp3"></audio>
关于重试,这个模式应该是一个很普遍的设计模式了。当我们把单体应用服务化,尤其是微服务化,本来在一个进程内的函数调用就成了远程调用,这样就会涉及到网络上的问题。
网络上有很多的各式各样的组件如DNS服务、网卡、交换机、路由器、负载均衡等设备这些设备都不一定是稳定的。在数据传输的整个过程中只要任何一个环节出了问题最后都会影响系统的稳定性。
# 重试的场景
所以,我们需要一个重试的机制。但是,我们需要明白的是,**“重试”的语义是我们认为这个故障是暂时的,而不是永久的,所以,我们会去重试**。
我认为,设计重试时,我们需要定义出什么情况下需要重试,例如,调用超时、被调用端返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)。
而对于一些别的错误则最好不要重试比如业务级的错误如没有权限、或是非法数据等错误技术上的错误HTTP的503等这种原因可能是触发了代码的bug重试下去没有意义
# 重试的策略
关于重试的设计,一般来说,都需要有个重试的最大值,经过一段时间不断的重试后,就没有必要再重试了,应该报故障了。在重试过程中,每一次重试失败时都应该休息一会儿再重试,这样可以避免因为重试过快而导致网络上的负担加重。
在重试的设计中我们一般都会引入Exponential Backoff的策略也就是所谓的&quot;指数级退避&quot;。在这种情况下每一次重试所需要的休息时间都会成倍增加。这种机制主要是用来让被调用方能够有更多的时间来从容处理我们的请求。这其实和TCP的拥塞控制有点像。
如果我们写成代码应该是下面这个样子。
首先我们定义一个调用返回的枚举类型其中包括了5种返回错误——成功SUCCESS、维护中NOT_READY、流控中TOO_BUSY、没有资源NO_RESOURCE、系统错误SERVER_ERROR。
```
public enum Results {
SUCCESS,
NOT_READY,
TOO_BUSY,
NO_RESOURCE,
SERVER_ERROR
}
```
接下来我们定义一个Exponential Backoff的函数其返回2的指数。这样每多一次重试就需要多等一段时间。如第一次等200ms第二次要400ms第三次要等800ms……
```
public static long getWaitTimeExp(int retryCount) {
long waitTime = ((long) Math.pow(2, retryCount) );
return waitTime;
}
```
下面是真正的重试逻辑。我们可以看到,在成功的情况下,以及不属于我们定义的错误下,我们是不需要重试的,而两次重试间需要等的时间是以指数上升的。
```
public static void doOperationAndWaitForResult() {
// Do some asynchronous operation.
long token = asyncOperation();
int retries = 0;
boolean retry = false;
do {
// Get the result of the asynchronous operation.
Results result = getAsyncOperationResult(token);
if (Results.SUCCESS == result) {
retry = false;
} else if ( (Results.NOT_READY == result) ||
(Results.TOO_BUSY == result) ||
(Results.NO_RESOURCE == result) ||
(Results.SERVER_ERROR == result) ) {
retry = true;
} else {
retry = false;
}
if (retry) {
long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);
// Wait for the next Retry.
Thread.sleep(waitTime);
}
} while (retry &amp;&amp; (retries++ &lt; MAX_RETRIES));
}
```
上面的代码是非常基本的重试代码没有什么新鲜的我们来看看Spring中所支持的一些重试策略。
# Spring的重试策略
[Spring Retry](https://github.com/spring-projects/spring-retry) 是一个单独实现重试功能的项目我们可以通过Annotation的方式使用。具体如下。
```
@Service
public interface MyService {
@Retryable(
value = { SQLException.class },
maxAttempts = 2,
backoff = @Backoff(delay = 5000))
void retryService(String sql) throws SQLException;
...
}
```
配置 @Retryable 注解,只对 SQLException 的异常进行重试重试两次每次延时5000ms。相关的细节可以看相应的文档。我在这里只想让你看一下Spring有哪些重试的策略。
<li>
NeverRetryPolicy只允许调用RetryCallback一次不允许重试。
</li>
<li>
AlwaysRetryPolicy允许无限重试直到成功此方式逻辑不当会导致死循环。
</li>
<li>
SimpleRetryPolicy固定次数重试策略默认重试最大次数为3次RetryTemplate默认使用的策略。
</li>
<li>
TimeoutRetryPolicy超时时间重试策略默认超时时间为1秒在指定的超时时间内允许重试。
</li>
<li>
CircuitBreakerRetryPolicy有熔断功能的重试策略需设置3个参数openTimeout、resetTimeout和delegate关于熔断会在后面描述。
</li>
<li>
CompositeRetryPolicy组合重试策略。有两种组合方式乐观组合重试策略是指只要有一个策略允许重试即可以悲观组合重试策略是指只要有一个策略不允许重试即不可以。但不管哪种组合方式组合中的每一个策略都会执行。
</li>
关于Backoff的策略如下。
<li>
NoBackOffPolicy无退避算法策略即当重试时是立即重试
</li>
<li>
FixedBackOffPolicy固定时间的退避策略需设置参数sleeper和backOffPeriodsleeper指定等待策略默认是Thread.sleep即线程休眠backOffPeriod指定休眠时间默认1秒。
</li>
<li>
UniformRandomBackOffPolicy随机时间退避策略需设置sleeper、minBackOffPeriod和maxBackOffPeriod。该策略在[minBackOffPeriod, maxBackOffPeriod]之间取一个随机休眠时间minBackOffPeriod默认为500毫秒maxBackOffPeriod默认为1500毫秒。
</li>
<li>
ExponentialBackOffPolicy指数退避策略需设置参数sleeper、initialInterval、maxInterval和multiplier。initialInterval指定初始休眠时间默认为100毫秒。maxInterval指定最大休眠时间默认为30秒。multiplier指定乘数即下一次休眠时间为当前休眠时间*multiplier。
</li>
<li>
ExponentialRandomBackOffPolicy随机指数退避策略引入随机乘数之前说过固定乘数可能会引起很多服务同时重试导致DDos使用随机休眠时间来避免这种情况。
</li>
# 重试设计的重点
重试的设计重点主要如下:
<li>
要确定什么样的错误下需要重试;
</li>
<li>
重试的时间和重试的次数。这种在不同的情况下要有不同的考量。有时候,而对一些不是很重要的问题时,我们应该更快失败而不是重试一段时间若干次。比如一个前端的交互需要用到后端的服务。这种情况下,在面对错误的时候,应该快速失败报错(比如:网络错误请重试)。而面对其它的一些错误,比如流控,那么应该使用指数退避的方式,以避免造成更多的流量。
</li>
<li>
如果超过重试次数,或是一段时间,那么重试就没有意义了。这个时候,说明这个错误不是一个短暂的错误,那么我们对于新来的请求,就没有必要再进行重试了,这个时候对新的请求直接返回错误就好了。但是,这样一来,如果后端恢复了,我们怎么知道呢,此时需要使用我们的熔断设计了。这个在后面会说。
</li>
<li>
重试还需要考虑被调用方是否有幂等的设计。如果没有,那么重试是不安全的,可能会导致一个相同的操作被执行多次。
</li>
<li>
重试的代码比较简单也比较通用完全可以不用侵入到业务代码中。这里有两个模式。一个是代码级的像Java那样可以使用Annotation的方式在Spring中你可以用到这样的注解如果没有注解也可以包装在底层库或是SDK库中不需要让上层业务感知到。另外一种是走Service Mesh的方式关于Service Mesh的方式我会在后面的文章中介绍
</li>
<li>
对于有事务相关的操作。我们可能会希望能重试成功,而不至于走业务补偿那样的复杂的回退流程。对此,我们可能需要一个比较长的时间来做重试,但是我们需要保存请求的上下文,这可能对程序的运行有比较大的开销,因此,有一些设计会先把这样的上下文暂存在本机或是数据库中,然后腾出资源来做别的事,过一会再回来把之前的请求从存储中捞出来重试。
</li>
# 小结
好了我们来总结一下今天分享的主要内容。首先我讲了重试的场景比如流控但并不是所有的失败场景都适合重试。接着我讲了重试的策略包括简单的指数退避策略和Spring实现的多种策略。
这些策略可以用Java的Annotation来实现或者用Service Mesh的方式从而不必写在业务逻辑里。最后我总结了重试设计的重点。下篇文章中我们讲述熔断设计。希望对你有帮助。
也欢迎你分享一下你实现过哪些场景下的重试?所采用的策略是什么?实现的过程中遇到过哪些坑?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,114 @@
<audio id="audio" title="48 | 弹力设计篇之“熔断设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/7e/7f1a6e682db7d76533587592b255a57e.mp3"></audio>
熔断机制这个词对你来说肯定不陌生,它的灵感来源于我们电闸上的“保险丝”,当电压有问题时(比如短路),自动跳闸,此时电路就会断开,我们的电器就会受到保护。不然,会导致电器被烧坏,如果人没在家或是人在熟睡中,还会导致火灾。所以,在电路世界通常都会有这样的自我保护装置。
同样,在我们的分布式系统设计中,也应该有这样的方式。前面说过重试机制,如果错误太多,或是在短时间内得不到修复,那么我们重试也没有意义了,此时应该开启我们的熔断操作,尤其是后端太忙的时候,使用熔断设计可以保护后端不会过载。
# 熔断设计
熔断器模式可以防止应用程序不断地尝试执行可能会失败的操作使得应用程序继续执行而不用等待修正错误或者浪费CPU时间去等待长时间的超时产生。熔断器模式也可以使应用程序能够诊断错误是否已经修正。如果已经修正应用程序会再次尝试调用操作。
换句话来说,我觉得熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定是继续操作,还是立即返回错误。
<img src="https://static001.geekbang.org/resource/image/85/93/850b613bae392236fdd097c70f7a5093.png" alt=""><br>
(本图来自 Martin Fowler 的 Circuit Breaker
熔断器可以使用状态机来实现,内部模拟以下几种状态。
<li>
**闭合Closed状态**我们需要一个调用失败的计数器如果调用失败则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值则切换到断开(Open)状态。此时开启了一个超时时钟当该时钟超过了该时间则切换到半断开Half-Open状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误以回到正常工作的状态。在Closed状态下错误计数器是基于时间的。在特定的时间间隔内会自动重置。这能够防止由于某次的偶然错误导致熔断器进入断开状态。也可以基于连续失败的次数。
</li>
<li>
**断开(Open)状态**在该状态下对应用程序的请求会立即返回错误响应而不调用后端的服务。这样也许比较粗暴有些时候我们可以cache住上次成功请求直接返回缓存当然这个缓存放在本地内存就好了如果没有缓存再返回错误缓存的机制最好用在全站一样的数据而不是用在不同的用户间不同的数据因为后者需要缓存的数据有可能会很多
</li>
<li>
**半开Half-Open状态**:允许应用程序一定数量的请求去调用服务。如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到闭合状态,同时将错误计数器重置。
如果这一定数量的请求有调用失败的情况,则认为导致之前调用失败的问题仍然存在,熔断器切回到断开状态,然后重置计时器来给系统一定的时间来修正错误。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。
</li>
<img src="https://static001.geekbang.org/resource/image/34/7f/34151c1a1caa1bd57a6fcdd3c92b7d7f.png" alt=""><br>
(本图来自 Martin Fowler 的 Circuit Breaker
实现熔断器模式使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响。它快速地拒绝那些有可能导致错误的服务调用,而不会去等待操作超时或者永远不返回结果来提高系统的响应时间。
如果熔断器设计模式在每次状态切换的时候会发出一个事件,这种信息可以用来监控服务的运行状态,能够通知管理员在熔断器切换到断开状态时进行处理。
下图是Netflix的开源项目[Hystrix](https://github.com/Netflix/Hystrix)中的熔断的实现逻辑([其出处在这里](https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker))。
<img src="https://static001.geekbang.org/resource/image/90/e4/908a291d5698db2dc4734119371241e4.png" alt="">
从这个流程图中,可以看到:
<li>
有请求来了首先allowRequest()函数判断是否在熔断中,如果不是则放行,如果是的话,还要看有没有到达一个熔断时间片,如果熔断时间片到了,也放行,否则直接返回出错。
</li>
<li>
每次调用都有两个函数markSuccess(duration)和markFailure(duration) 来统计一下在一定的duration内有多少调用是成功还是失败的。
</li>
<li>
判断是否熔断的条件 isOpen(),是计算一下 failure/(success+failure) 当前的错误率,如果高于一个阈值,那么打开熔断,否则关闭。
</li>
<li>
Hystrix会在内存中维护一个数组其中记录着每一个周期的请求结果的统计。超过时长长度的元素会被删除掉。
</li>
# 熔断设计的重点
在实现熔断器模式的时候,以下这些因素需可能需要考虑。
<li>
**错误的类型**。需要注意的是请求失败的原因会有很多种。你需要根据不同的错误情况来调整相应的策略。所以,熔断和重试一样,需要对返回的错误进行识别。一些错误先走重试的策略(比如限流,或是超时),重试几次后再打开熔断。一些错误是远程服务挂掉,恢复时间比较长;这种错误不必走重试,就可以直接打开熔断策略。
</li>
<li>
**日志监控**。熔断器应该能够记录所有失败的请求,以及一些可能会尝试成功的请求,使得管理员能够监控使用熔断器保护服务的执行情况。
</li>
<li>
**测试服务是否可用**。在断开状态下熔断器可以采用定期地ping一下远程服务的健康检查接口来判断服务是否恢复而不是使用计时器来自动切换到半开状态。这样做的一个好处是在服务恢复的情况下不需要真实的用户流量就可以把状态从半开状态切回关闭状态。否则在半开状态下即便服务已恢复了也需要用户真实的请求来恢复这会影响用户的真实请求。
</li>
<li>
**手动重置**。在系统中对于失败操作的恢复时间是很难确定的,提供一个手动重置功能能够使得管理员可以手动地强制将熔断器切换到闭合状态。同样的,如果受熔断器保护的服务暂时不可用的话,管理员能够强制将熔断器设置为断开状态。
</li>
<li>
**并发问题**。相同的熔断器有可能被大量并发请求同时访问。熔断器的实现不应该阻塞并发的请求或者增加每次请求调用的负担。尤其是其中对调用结果的统计一般来说会成为一个共享的数据结构它会导致有锁的情况。在这种情况下最好使用一些无锁的数据结构或是atomic的原子操作。这样会带来更好的性能。
</li>
<li>
**资源分区**。有时候,我们会把资源分布在不同的分区上。比如,数据库的分库分表,某个分区可能出现问题,而其它分区还可用。在这种情况下,单一的熔断器会把所有的分区访问给混为一谈,从而,一旦开始熔断,那么所有的分区都会受到熔断影响。或是出现一会儿熔断一会儿又好,来来回回的情况。所以,熔断器需要考虑这样的问题,只对有问题的分区进行熔断,而不是整体。
</li>
<li>
**重试错误的请求**。有时候,错误和请求的数据和参数有关系,所以,记录下出错的请求,在半开状态下重试能够准确地知道服务是否真的恢复。当然,这需要被调用端支持幂等调用,否则会出现一个操作被执行多次的副作用。
</li>
# 小结
好了我们来总结一下今天分享的主要内容。首先熔断设计是受了电路设计中保险丝的启发其需要实现三个状态闭合、断开和半开分别对应于正常、故障和故障后检测故障是否已被修复的场景并介绍了Netflix的Hystrix对熔断的实现。最后我总结了熔断设计的几个重点。下篇文章中我们讲述限流设计。希望对你有帮助。
也欢迎你分享一下你实现过的熔断使用了怎样的算法?实现的过程中遇到过什么坑?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,213 @@
<audio id="audio" title="49 | 弹力设计篇之“限流设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/77/7e299d5ee30e0eff87990fb72f9b1b77.mp3"></audio>
保护系统不会在过载的情况下出现问题,我们就需要限流。
我们在一些系统中都可以看到这样的设计比如我们的数据库访问的连接池还有我们的线程池还有Nginx下的用于限制瞬时并发连接数的limit_conn模块限制每秒平均速率的limit_req模块还有限制MQ的生产速等等。
# 限流的策略
限流的目的是通过对并发访问进行限速,相关的策略一般是,一旦达到限制的速率,那么就会触发相应的限流行为。一般来说,触发的限流行为如下。
<li>
**拒绝服务**。把多出来的请求拒绝掉。一般来说,好的限流系统在受到流量暴增时,会统计当前哪个客户端来的请求最多,直接拒掉这个客户端,这种行为可以把一些不正常的或者是带有恶意的高并发访问挡在门外。
</li>
<li>
**服务降级**。关闭或是把后端服务做降级处理。这样可以让服务有足够的资源来处理更多的请求。降级有很多方式一种是把一些不重要的服务给停掉把CPU、内存或是数据的资源让给更重要的功能一种是不再返回全量数据只返回部分数据。
因为全量数据需要做SQL Join操作部分的数据则不需要所以可以让SQL执行更快还有最快的一种是直接返回预设的缓存以牺牲一致性的方式来获得更大的性能吞吐。
</li>
<li>
**特权请求**。所谓特权请求的意思是资源不够了我只能把有限的资源分给重要的用户比如分给权利更高的VIP用户。在多租户系统下限流的时候应该保大客户的所以大客户有特权可以优先处理而其它的非特权用户就得让路了。
</li>
<li>
**延时处理**。在这种情况下,一般会有一个队列来缓冲大量的请求,这个队列如果满了,那么就只能拒绝用户了,如果这个队列中的任务超时了,也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰刺请求。
</li>
<li>
**弹性伸缩**。动用自动化运维的方式对相应的服务做自动化的伸缩。这个需要一个应用性能的监控系统能够感知到目前最繁忙的TOP 5的服务是哪几个。
然后去伸缩它们,还需要一个自动化的发布、部署和服务注册的运维系统,而且还要快,越快越好。否则,系统会被压死掉了。当然,如果是数据库的压力过大,弹性伸缩应用是没什么用的,这个时候还是应该限流。
</li>
# 限流的实现方式
## 计数器方式
最简单的限流算法就是维护一个计数器Counter当一个请求来时就做加一操作当一个请求处理完后就做减一操作。如果这个Counter大于某个数了我们设定的限流阈值那么就开始拒绝请求以保护系统的负载了。
这个算法足够的简单粗暴。
## 队列算法
在这个算法下请求的速度可以是波动的而处理的速度则是非常均速的。这个算法其实有点像一个FIFO的算法。
<img src="https://static001.geekbang.org/resource/image/c8/3d/c8f774f88ab8a4b72378971263c0393d.png" alt="" />
在上面这个FIFO的队列上我们可以扩展出一些别的玩法。
一个是有优先级的队列,处理时先处理高优先级的队列,然后再处理低优先级的队列。 如下图所示,只有高优先级的队列被处理完成后,才会处理低优先级的队列。
<img src="https://static001.geekbang.org/resource/image/de/80/de51d6fc68df3d8c808b84e4bc455580.png" alt="" />
有优先级的队列可能会导致低优先级队列长时间得不到处理。为了避免低优先级的队列被饿死,一般来说是分配不同比例的处理时间到不同的队列上,于是我们有了带权重的队列。
如下图所示。有三个队列的权重分布是3:2:1这意味着我们需要在权重为3的这个队列上处理3个请求后再去权重为2的队列上处理2个请求最后再去权重为1的队列上处理1个请求如此反复。
<img src="https://static001.geekbang.org/resource/image/c7/54/c775345e3b8f599e26a4d7f64941cd54.png" alt="" />
队列流控是以队列的的方式来处理请求。如果处理过慢,那么就会导致队列满,而开始触发限流。
但是这样的算法需要用队列长度来控制流量在配置上比较难操作。如果队列过长导致后端服务在队列没有满时就挂掉了。一般来说这样的模型不能做push而是pull方式会好一些。
## 漏斗算法 Leaky Bucket
漏斗算法可以参看Wikipedia的相关词条 [Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket)。
下图是一个[漏斗算法的示意图](https://www.slideshare.net/vimal25792/leaky-bucket-tocken-buckettraffic-shaping) 。
<img src="https://static001.geekbang.org/resource/image/95/00/95326ea1624d4206a26ff275b39efc00.png" alt="" />
我们可以看到,就像一个漏斗一样,进来的水量就好像访问流量一样,而出去的水量就像是我们的系统处理请求一样。当访问流量过大时这个漏斗中就会积水,如果水太多了就会溢出。
一般来说这个“漏斗”是用一个队列来实现的当请求过多时队列就会开始积压请求如果队列满了就会开拒绝请求。很多系统都有这样的设计比如TCP。当请求的数量过多时就会有一个sync backlog的队列来缓冲请求或是TCP的滑动窗口也是用于流控的队列。
<img src="https://static001.geekbang.org/resource/image/d4/a0/d4b8b6ceb8de4400dfc97f3ff0feeaa0.png" alt="" />
我们可以看到漏斗算法其实就是在队列请求中加上一个限流器来让Processor以一个均匀的速度处理请求。
## 令牌桶算法Token Bucket
关于令牌桶算法主要是有一个中间人。在一个桶内按照一定的速率放入一些token然后处理程序要处理请求时需要拿到token才能处理如果拿不到则不处理。
下面这个图很清楚地说明了这个算法。
<img src="https://static001.geekbang.org/resource/image/99/f0/996b8d60ed90c470ce839f8826e375f0.png" alt="" />
从理论上来说,令牌桶的算法和漏斗算法不一样的是,漏斗算法中,处理请求是以一个常量和恒定的速度处理的,而令牌桶算法则是在流量小的时候“攒钱”,流量大的时候,可以快速处理。
然而我们可能会问Processor的处理速度因为有队列的存在所以其总是能以最大处理能力来处理请求这也是我们所希望的方式。因此令牌桶和漏斗都是受制于Processor的最大处理能力。无论令牌桶里有多少令牌也无论队列中还有多少请求。总之Processor在大流量来临时总是按照自己最大的处理能力来处理的。
但是试想一下如果我们的Processor只是一个非常简单的任务分配器比如像Nginx这样的基本没有什么业务逻辑的网关那么它的处理速度一定很快不会有什么瓶颈而其用来把请求转发给后端服务那么在这种情况下这两个算法就有不一样的情况了。
漏斗算法会以一个稳定的速度转发,而令牌桶算法平时流量不大时在“攒钱”,流量大时,可以一次发出队列里有的请求,而后就受到令牌桶的流控限制。
另外,令牌桶还可能做成第三方的一个服务,这样可以在分布式的系统中对全局进行流控,这也是一个很好的方式。
## 基于响应时间的动态限流
上面的算法有个不好的地方,就是需要设置一个确定的限流值。这就要求我们每次发布服务时都做相应的性能测试,找到系统最大的性能值。
当然性能测试并不是很容易做的。有关性能测试的方法请参看我在CoolShell上的这篇文章《[性能测试应该怎么做](https://coolshell.cn/articles/17381.html)》。虽然性能测试比较不容易,但是还是应该要做的。
然而,在很多时候,我们却并不知道这个限流值,或是很难给出一个合适的值。其基本会有如下的一些因素:
<li>
实际情况下很多服务会依赖于数据库。所以不同的用户请求会对不同的数据集进行操作。就算是相同的请求可能数据集也不一样比如现在很多应用都会有一个时间线Feed流不同的用户关心的主题人人不一样数据也不一样。
而且数据库的数据是在不断变化的可能前两天性能还行因为数据量增加导致性能变差。在这种情况下我们很难给出一个确定的一成不变的值因为关系型数据库对于同一条SQL语句的执行时间其实是不可预测的NoSQL的就比RDBMS的可预测性要好
</li>
<li>
不同的API有不同的性能。我们要在线上为每一个API配置不同的限流值这点太难配置也很难管理。
</li>
<li>
而且,现在的服务都是能自动化伸缩的,不同大小的集群的性能也不一样,所以,在自动化伸缩的情况下,我们要动态地调整限流的阈值,这点太难做到了。
</li>
基于上述这些原因,我们限流的值是很难被静态地设置成恒定的一个值。
我们想使用一种动态限流的方式。这种方式,不再设定一个特定的流控值,而是能够动态地感知系统的压力来自动化地限流。
这方面设计的典范是TCP协议的拥塞控制的算法。TCP使用RTT - Round Trip Time 来探测网络的延时和性能,从而设定相应的“滑动窗口”的大小,以让发送的速率和网络的性能相匹配。这个算法是非常精妙的,我们完全可以借鉴在我们的流控技术中。
我们记录下每次调用后端请求的响应时间然后在一个时间区间内比如过去10秒的请求计算一个响应时间的P90或P99值也就是把过去10秒内的请求的响应时间排个序然后看90%或99%的位置是多少。
这样我们就知道有多少请求大于某个响应时间。如果这个P90或P99超过我们设定的阈值那么我们就自动限流。
这个设计中有几个要点。
<li>
你需要计算的一定时间内的P90或P99。在有大量请求的情况下这个非常地耗内存也非常地耗CPU因为需要对大量的数据进行排序。
解决方案有两种一种是不记录所有的请求采样就好了另一种是使用一个叫蓄水池的近似算法。关于这个算法这里我不就多说了《编程珠玑》里讲过这个算法你也可以自行Google英文叫 [Reservoir Sampling](https://en.wikipedia.org/wiki/Reservoir_sampling)。
</li>
<li>
这种动态流控需要像TCP那样你需要记录一个当前的QPS.如果发现后端的P90/P99响应太慢那么就可以把这个QPS减半然后像TCP一样走慢启动的方式直接到又开始变慢然后减去1/4的QPS再慢启动然后再减去1/8的QPS……
这个过程有点像个阻尼运行的过程,然后整个限流的流量会在一个值上下做小幅振动。这么做的目的是,如果后端扩容伸缩后性能变好,系统会自动适应后端的最大性能。
</li>
<li>
这种动态限流的方式实现起来并不容易。大家可以看一下TCP的算法。TCP相关的一些算法我写在了CoolShell上的《[TCP的那些事](https://coolshell.cn/articles/11609.html)》这篇文章中。你可以用来做参考来实现。
</li>
我在现在创业中的Ease Gateway的产品中实现了这个算法。
# 限流的设计要点
限流主要是有四个目的。
<li>
为了向用户承诺SLA。我们保证我们的系统在某个速度下的响应时间以及可用性。
</li>
<li>
同时,也可以用来阻止在多租户的情况下,某一用户把资源耗尽而让所有的用户都无法访问的问题。
</li>
<li>
为了应对突发的流量。
</li>
<li>
节约成本。我们不会为了一个不常见的尖峰来把我们的系统扩容到最大的尺寸。而是在有限的资源下能够承受比较高的流量。
</li>
在设计上,我们还要有以下的考量。
<li>
限流应该是在架构的早期考虑。当架构形成后,限流不是很容易加入。
</li>
<li>
限流模块性能必须好,而且对流量的变化也是非常灵敏的,否则太过迟钝的限流,系统早因为过载而挂掉了。
</li>
<li>
限流应该有个手动的开关,这样在应急的时候,可以手动操作。
</li>
<li>
当限流发生时,应该有个监控事件通知。让我们知道有限流事件发生,这样,运维人员可以及时跟进。而且还可以自动化触发扩容或降级,以缓解系统压力。
</li>
<li>
当限流发生时,对于拒掉的请求,我们应该返回一个特定的限流错误码。这样,可以和其它错误区分开来。而客户端看到限流,可以调整发送速度,或是走重试机制。
</li>
<li>
限流应该让后端的服务感知到。限流发生时我们应该在协议头中塞进一个标识比如HTTP Header中放入一个限流的级别告诉后端服务目前正在限流中。这样后端服务可以根据这个标识决定是否做降级。
</li>
# 小结
好了,我们来总结一下今天分享的主要内容。
首先,限流的目的是为了保护系统不在过载的情况下导致问题。接着讲了几种限流的策略。然后讲了,限流的算法,包括计数器、队列、漏斗和令牌桶。然后讨论了如何基于响应时间来限流。最后,我总结了限流设计的要点。下篇文章中,我们讲述降级设计。希望对你有帮助。
也欢迎你分享一下你实现过怎样的限流机制?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,118 @@
<audio id="audio" title="50 | 弹力设计篇之“降级设计”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8f/6b/8fb3b6371831dcfd5646088b578b2c6b.mp3"></audio>
所谓的降级设计Degradation本质是为了解决资源不足和访问量过大的问题。当资源和访问量出现矛盾的时候在有限的资源下为了能够扛住大量的请求我们就需要对系统进行降级操作。也就是说暂时牺牲掉一些东西以保障整个系统的平稳运行。
我记得我在伦敦参与诺丁山狂欢节的时候,以及看阿森纳英超足球比赛的时候,散场时因为人太多,所有的公交系统(公交车,地铁)完全免费,就是为了让人通行得更快。而且早在散场前,场外就备着一堆公交车和地铁了,这样就是为了在最短时间内把人疏散掉。
虽然亏掉了一些钱,但是相比因为人员拥塞造成道路交通拥塞以及还可能出现的一些意外情况所造成的社会成本的损失,公交免费策略真是很明智的做法。与此类似,我们的系统在应对一些突发情况的时候也需要这样的降级流程。
一般来说,我们的降级需要牺牲掉的东西有:
- **降低一致性**。从强一致性变成最终一致性。
- **停止次要功能**。停止访问不重要的功能,从而释放出更多的资源。
- **简化功能**。把一些功能简化掉,比如,简化业务流程,或是不再返回全量数据,只返回部分数据。
# 降低一致性
我们要清楚地认识到,这世界上大多数系统并不是都需要强一致性的。对于降低一致性,把强一致性变成最终一致性的做法可以有效地释放资源,并且让系统运行得更快,从而可以扛住更大的流量。一般来说,会有两种做法,一种是简化流程的一致性,一种是降低数据的一致性。
## 使用异步简化流程
举个例子,比如电商的下单交易系统,在强一致的情况下,需要结算账单,扣除库存,扣除账户上的余额(或发起支付),最后进行发货流程,这一系列的操作。
如果需要是强一致性的那么就会非常慢。尤其是支付环节可能会涉及银行方面的接口性能就像双11那样银行方面出问题会导致支付不成功而订单流程不能往下走。
在系统降级时,我们可以把这一系列的操作做成异步的,快速结算订单,不占库存,然后把在线支付降级成用户到付,这样就省去支付环节,然后批量处理用户的订单,向用户发货,用户货到付款。
<img src="https://static001.geekbang.org/resource/image/2c/43/2c8ea19d132f2efb333ea9e741ea8543.png" alt="" />
如上图所示,一开始需要的全同步的方式,降级成了全异步的方式,库存从单笔强一致性也变成了多笔最终一致性,如果库存不够了,就只能根据先来后到取消订单了。而支付也从最开始的下单请求时的强一致性,变成了用户到付的最终一致性。
一般来说,功能降级都有可能会损害用户的体验,所以,最好给出友好的用户提示。比如,“系统当前繁忙,您的订单已收到,我们正努力为您处理订单中,我们会尽快给您发送订单确认通知……还请见谅”诸如此类的提示信息。
## 降低数据的一致性
降低数据的一致性一般来说会使用缓存的方式,或是直接就去掉数据。比如,在页面上不显示库存的具体数字,只显示有还是没有库存这两种状态。
对于缓存来说,可以有效地降低数据库的压力,把数据库的资源交给更重要的业务,这样就能让系统更快速地运行。
对于降级后的系统不再通过数据库获取数据而是通过缓存获取数据。关于缓存的设计模式我在CoolShell中有一篇叫《缓存更新的套路》的文章中讲述过缓存的几种更新模式你有兴趣的话可以前往一读。在功能降级中我们一般使用Cache Aside模式或是Read Through模式。也就是下图所示的这个策略。
<img src="https://static001.geekbang.org/resource/image/bd/c4/bdf7522a231ec2c1136a70f07db0c5c4.png" alt="" />
- **失效**应用程序先从cache取数据如果没有得到则从数据库中取数据成功后放到缓存中。
- **命中**应用程序从cache中取数据取到后返回。
- **更新**:先把数据存到数据库中,成功后,再让缓存失效。
Read Through 模式就是在查询操作中更新缓存也就是说当缓存失效的时候过期或LRU换出Cache Aside是由调用方负责把数据加载到缓存而Read Through则用缓存服务自己来加载从而对应用方是透明的。
# 停止次要的功能
停止次要的功能也是一种非常有用的策略。把一些不重要的功能给暂时停止掉,让系统释放出更多的资源来。比如,电商中的搜索功能,用户的评论功能,等等。等待访问的峰值过去后,我们再把这些功能给恢复回来。
当然,最好不要停止次要的功能,首先可以限制次要的功能的流量,或是把次要的功能退化成简单的功能,最后如果量太大了,我们才会进入停止功能的状态。
停止功能对用户会带来一些用户体验的问题,尤其是要停掉一些可能对于用户来说是非常重要的功能。所以,如果可能,最好给用户一些补偿,比如把用户切换到一个送积分卡,或是红包抽奖的网页上,有限地补偿一下用户。
# 简化功能
关于功能的简化上上面的下单流程中已经提到过相应的例子了。而且从缓存中返回数据也是其中一个。这里再提一个就是一般来说一个API会有两个版本一个版本返回全量数据另一个版本只返回部分或最小的可用的数据。
举个例子对于一篇文章一个API会把商品详情页或文章的内容和所有的评论都返回到前端。那么在降级的情况下我们就只返回商品信息和文章内容而不返回用户评论了因为用户评论会涉及更多的数据库操作。
所以,这样可以释放更多的数据资源。而商品信息或文章信息可以放在缓存中,这样又能释放出更多的资源给交易系统这样的需要更多数据库资源的业务使用。
# 降级设计的要点
对于降级,一般来说是要牺牲业务功能或是流程,以及一致性的。所以,我们需要对业务做非常仔细的梳理和分析。我们很难通过不侵入业务的方式来做到功能降级。
在设计降级的时候,需要清楚地定义好降级的关键条件,比如,吞吐量过大、响应时间过慢、失败次数多过,有网络或是服务故障,等等,然后做好相应的应急预案。这些预案最好是写成代码可以快速地自动化或半自动化执行的。
功能降级需要梳理业务的功能哪些是must-have的功能哪些是nice-to-have的功能哪些是必须要死保的功能哪些是可以牺牲的功能。而且需要在事前设计好可以简化的或是用来应急的业务流程。当系统出问题的时候就需要走简化应急流程。
降级的时候,需要牺牲掉一致性,或是一些业务流程:对于读操作来说,使用缓存来解决,对于写操作来说,需要异步调用来解决。并且,我们需要以流水账的方式记录下来,这样方便对账,以免漏掉或是和正常的流程混淆。
降级的功能的开关可以是一个系统的配置开关。做成配置时你需要在要降级的时候推送相应的配置。另一种方式是在对外服务的API上有所区分方法签名或是开关参数这样可以由上游调用者来驱动。
比如:一个网关在限流时,在协议头中加入了一个限流程度的参数,让后端服务能知道限流在发生中。当限流程度达到某个值时,或是限流时间超过某个值时,就自动开始降级,直到限流好转。
对于数据方面的降级,需要前端程序的配合。一般来说,前端的程序可以根据后端传来的数据来决定展示哪些界面模块。比如,当前端收不到商品评论时,就不展示。为了区分本来就没有数据,还是因为降级了没有数据的两种情况,在协议头中也应该加上降级的标签。
因为降级的功能平时不会总是会发生属于应急的情况所以降级的这些业务流程和功能有可能长期不用而出现bug或问题对此需要在平时做一些演练。
# 小结
好了,我们来总结一下今天分享的主要内容。首先,降级设计本质上是为了解决资源不足和访问量过大的问题。降级的方法有降低一致性、停止次要功能和简化功能。最后,我总结了降级设计的要点。下篇文章中,我将总结整个弹力设计篇。希望对你有帮助。
也欢迎你分享一下你实现过怎样的降级机制?有没有和限流机制配合?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,135 @@
<audio id="audio" title="51 | 弹力设计篇之“弹力设计总结”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4f/96/4f17f1db90197cfc8d0d0f76e9175496.mp3"></audio>
我们前面讲了那么多的弹力设计的设计模式,这里做个总结。
# 弹力设计总图
首先,我们的服务不能是单点,所以,我们需要在架构中冗余服务,也就是说有多个服务的副本。这需要使用到的具体技术有:
- 负载均衡 + 服务健康检查可以使用像Nginx或HAProxy这样的技术
- 服务发现 + 动态路由 + 服务健康检查比如Consul或ZooKeeper
- 自动化运维Kubernetes 服务调度、伸缩和故障迁移。
然后,我们需要隔离我们的业务,要隔离我们的服务我们就需要对服务进行解耦和拆分,这需要使用到以前的相关技术。
<li>
bulkheads模式业务分片 、用户分片、数据库拆分。
</li>
<li>
自包含系统:所谓自包含的系统是从单体到微服务的中间状态,其把一组密切相关的微服务给拆分出来,只需要做到没有外部依赖就行。
</li>
<li>
异步通讯:服务发现、事件驱动、消息队列、业务工作流。
</li>
<li>
自动化运维:需要一个服务调用链和性能监控的监控系统。
</li>
然后,接下来,我们就要进行和能让整个架构接受失败的相关处理设计,也就是所谓的容错设计。这会用到下面的这些技术。
<li>
错误方面:调用重试 + 熔断 + 服务的幂等性设计。
</li>
<li>
一致性方面:强一致性使用两阶段提交、最终一致性使用异步通讯方式。
</li>
<li>
流控方面:使用限流 + 降级技术。
</li>
<li>
自动化运维方面:网关流量调度,服务监控。
</li>
我不敢保证有上面这些技术可以解决所有的问题,但是,只要我们设计得当,绝大多数的问题应该是可以扛得住的了。
下面我画一个图来表示一下。
<img src="https://static001.geekbang.org/resource/image/f9/2b/f9e6efa6202103a14d358ff6c80f0a2b.png" alt="" />
在上面这个图上,我们可以看到,有三大块的东西。
<li>
冗余服务。通过冗余服务的复本数可以消除单点故障。这需要服务发现,负载均衡,动态路由和健康检查四个功能或组件。
</li>
<li>
服务解耦。通过解耦可以做到把业务隔离开来不让服务间受影响这样就可以有更好的稳定性。在水平层面上需要把业务或用户分片分区业分做隔离用户做多租户。在垂直层面上需要异步通讯机制。因为应用被分解成了一个一个的服务所以在服务的编排和聚合上需要有工作流像Spring的Stream或Akka的flow或是AWS的Simple Workflow来把服务给串联起来。而一致性的问题又需要业务补偿机制来做反向交易。
</li>
<li>
服务容错。服务容错方面,需要有重试机制,重试机制会带来幂等操作,对于服务保护来说,熔断,限流,降级都是为了保护整个系统的稳定性,并在可用性和一致性方面在出错的情况下做一部分的妥协。
</li>
当然,除了这一切的架构设计外,你还需要一个或多个自动运维的工具,否则,如果是人肉运维的话,那么在故障发生的时候,不能及时地做出运维决定,也就空有这些弹力设计了。比如:监控到服务性能不够了,就自动或半自动地开始进行限流或降级。
# 弹力设计开发和运维
对于运维工具来说,你至少需要两个系统:
- 一个是像APM这样的服务监控
- 另一个是服务调度的系统Docker + Kubernetes。
此外如果你需要一个开发架构来让整个开发团队在同一个标准下开发上面的这些东西这里Spring Cloud就是不二之选了。
关于Spring Cloud和Kubernetes它们都是为了微服务而生但它们没有什么可比性因为前者偏开发后者偏运维。我们来看一下它们的差别。
<img src="https://static001.geekbang.org/resource/image/35/f4/35cd0722f99f91c904944ac1bbdd56f4.png" alt="" /><br />
图片来自Deploying Microservices: Spring Cloud vs Kubernetes
从上表我们可以得知:
<li>
Spring Cloud有一套丰富且集成良好的Java库作为应用栈的一部分解决所有运行时问题。因此微服务本身可以通过库和运行时代理解决客户端服务发现、负载均衡、配置更新、统计跟踪等。工作模式就像单实例服务集群。译者注集群中master节点工作当master挂掉后slave节点被选举顶替。并且一批工作也是在JVM中被管理。
</li>
<li>
Kubernetes不是针对语言的而是针对容器的所以它是以通用的方式为所有语言解决分布式计算问题。Kubernetes提供了配置管理、服务发现、负载均衡、跟踪、统计、单实例、平台级和应用栈之外的调度工作。该应用不需要任何客户端逻辑的库或代理程序可以用任何语言编写。
</li>
下图是微服务所需的关键技术以及这些技术中在Spring Cloud和Kubernetes的涵盖面。
<img src="https://static001.geekbang.org/resource/image/dc/af/dcab89f031d1a7083b4f0b3091873caf.png" alt="" /><br />
图片来自Deploying Microservices: Spring Cloud vs Kubernetes
两个平台依靠相似的第三方工具如ELK和EFK stacks, tracing libraries等。Hystrix和Spring Boot等库在两个环境中都表现良好。很多情况下Spring Cloud和Kubernetes可以形成互补组建出更强大的解决方案例如KubeFlix和Spring Cloud Kubernetes
下图是在Kubernetes上使用Spring Cloud可以表现出来的整体特性。要做出一个可运维的分布式系统除了在架构上的设计之外还需要一整套的用来支撑分布式系统的管控系统也就是所谓的运维系统。要做到这些不是靠几个人几天就可以完成的。这需要我们根据自己的业务特点来规划相关的实施路径。
<img src="https://static001.geekbang.org/resource/image/41/6a/41e9f7a084e6c81fcb3bb42d43b0076a.png" alt="" /><br />
图片来自Deploying Microservices: Spring Cloud vs Kubernetes
上面这张图中对于所有的特性都列举了一些相关的软件和一些设计的重点其中红色的是运维层面的和Spring Cloud和Kubernetes不相关的绿色的Spring Cloud提供的开发框架蓝色的是Kubernetes相关的重要功能。
从今天看下来微服务的最佳实践在未来有可能会成为SpringCloud和Kubernetes的天下了。这个让我们拭目以待。
我在本篇文章中总结了整个弹力设计,提供了一张总图,并介绍了开发运维的实践。希望对你有帮助。
也欢迎你分享一下你对弹力设计和弹力设计系列文章的感想。
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)