8.凤凰架构:构建可靠的大型分布式系统 --- 流量治理
Posted enlyhua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8.凤凰架构:构建可靠的大型分布式系统 --- 流量治理相关的知识,希望对你有一定的参考价值。
第8章 流量治理
容错性设计是微服务的另一个核心原则。随着拆分出的服务越来越多,随之而来会面临以下2个问题:
1.由于某一个服务崩溃,导致所有用到这个服务的其他服务都无法正常工作,一个点的错误经过层层传递,最终波及调用链上与此有关的所有服务,这便是
雪崩效应。如何防止雪崩效应便是微服务架构容错性设计的具体实践,否则服务化程度越高,整个系统反而越不稳定。
2.服务虽然没有崩溃,但由于处理能力有限,面临超过预期的突发的请求时,大部分请求直到超时都无法完成处理。类似于交通阻塞,如果一开始没有得到及时的
处理,后面就需要很长时间此案使得全部服务都恢复正常。
8.1 服务容错
微服务的九大核心特征:
1.服务组件化
2.按业务组织团队
3.做"产品"的态度
4.智能端点与哑管道
5.去中心化治理
6.去中心化管理数据
7.基础设施自动化
8.容错设计
9.演进式设计
8.1.1 容错策略
容错策略是指:面对故障,我们该做些什么。
1.故障转移(Failover)
高可用的服务集群中,多数的服务 --- 尤其是那些经常被其他服务所依赖的关键路径上的服务,均会部署多个副本。这些副本可能部署在不同的
节点(避免节点宕机)、网络交换机(避免网络分区) 甚至可用区(避免整个地区发生灾害或电力、骨干网故障)中。故障转移是指如果调用的服务器出现故障,
系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试通过其他副本返回成功调用的结果,从而保证整体的高可用性。
故障转移的容错策略应该有一定的调用次数限制,譬如允许最多重试3个服务,如果3个服务都发生报错,那还是返回调用失败。原因不仅是因为重试
是有执行成本的,更是因为过度的重试反而可能让系统处于更加不利的状况。譬如有如下调用链:
Server A => Server B => Server C
假设A的超时阈值是100ms,而B调用C花费了60ms,如果不幸失败了。此时做故障转移的意义其实不大,因为即使下一次调用能够返回正确结果,也很
可能同样需要花费60ms时间,时间总和已经触及A服务的超时阈值,所以在这种情况下故障转移反而对系统是不利的。
2.快速失败(Failfast)
还有另外一些业务场景是不允许做故障转移的,因为故障转移策略能够实施的前提是服务具备幂等性。对于非幂等的服务,重复调用就可能产生脏数据,
而脏数据带来的麻烦远大于单纯的某次服务调用失败,此时就应该以快速失败作为首选的容错策略。譬如,在支付场景中,需要调用银行的扣款接口,如果
该接口返回的结果是网络异常,程序很难判断到底是扣款指令发送给银行时出现的网络异常,还是银行扣款后返回结果给服务时的网络异常。为了避免重复
扣款,此时最恰当可行的方案就是尽快让服务报错,坚决避免重试,尽快抛出异常,由调用者自行处理。
3.安全失败(Failsafe)
在一个调用链中的服务通常也有主路和旁路之分,换句话说,并不是每个服务都是不可或缺的,有部分服务失败了也不影响核心业务的正确性。譬如开发
基于Spring 管理的应用程序时,通过扩展点、事件或者AOP注入的逻辑往往就属于旁路逻辑,典型的有审计、日志、调试信息,等等。属于旁路逻辑的另外
一个显著特征是后续处理不会依赖其返回值,或者它的返回值是什么都不会影响后续处理的结果,譬如只是将返回值记录到数据库,而不使用它参与最终结果
的运算。对之类逻辑,一种理想的容错策略是即使旁路逻辑实际调用失败了,也当做正确的来返回,如果需要返回值的话,系统就自动返回一个符合要求的
数据类型对应的零值,然后自动记录一条服务调用出错的日志备查即可,这种策略被称为 安全失败策略。
4.沉默失败(Failsilent)
如果大量的请求需要等到超时或者长时间处理后才宣告失败,很容易由于某个远程服务的请求堆积而消耗大量的线程、内存、网络等资源,进而影响到
整个系统的稳定。面对这种情况,一种合理的失败策略是当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,将
错误隔离开来,避免对系统其它部分产生影响,此即为沉默失败策略。
5.故障恢复(Failback)
故障恢复一般不单独存在,而是作为其他容错策略的补充措施。一般在微服务管理框架中,如果设置容错策略为故障恢复的话,通常默认会采用快速
失败加上故障恢复的策略组合。故障恢复是指当服务调用失败后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。
故障恢复策略一方面可以尽力促使失败的调用最终能够被正确执行,另一方面也可以为服务注册中心和负载均衡器及时提供服务恢复的通知消息。
故障恢复显然也是要求服务必须是幂等的,由于它的重试是在后台异步执行的,即使最后调用成功了,原来的请求也早已响应完毕,所以故障恢复策略一般
用于对实时性要求不高的主路逻辑,同时也适合处理那些不需要返回值的旁路逻辑。为了避免内存中异步调用任务堆积,故障恢复与故障转移一样,应该有
最大的重试次数限制。
上面5种以"Fail"开头的策略是针对调用失败时如何进行弥补的,以下两种策略则是在调用之前就开始考虑如何获得最大的成功概率:
1.并行调用(Forking)
即双重保险或多重保险的策略,它是指一开始就同时向多个服务副本发起调用,只要其中有任何一个返回成功,那调用便成功,这是一种在关键场景
中使用更高的执行成本换取执行时间和成功概率的策略。
2.广播调用(Broadcast)
广播调用与并行调用是相对应的,都是同时发起多个调用,但并行调用是任何一个调用成功便宣告成功,广播调用则要求所有的请求全部成功,这次
调用才算成功,任何一个服务提供者出现异常都算调用失败。广播调用通常用于实现"刷新分布式缓存"这类的操作。
8.1.2 容错设计模式
容错设计模式是指:要实现某种容错策略,我们该如何去做。
容错设计模式,如微服务中常见的 断路器模式、舱壁隔离模式、重试模式,等等;以及流量控制模式,如滑动时间窗口、漏桶模式、令牌桶模式,等等。
1.断路器模式
断路器的基本思路很简单,就是通过代理(断路器对象)来一对一(一个远程服务对应一个断路器对象)的接管服务调用者的远程请求。断路器会持续的监控并
统计服务返回的成功、失败、超时、拒绝等各种结果,当出现故障时(失败、超时、拒绝)的次数达到断路器的阈值时,它的状态就自动变成"OPEN",后续此
断路器代理的远程访问都将直接返回调用失败,而不会发出真正的远程服务请求。通过断路器对远程服务的熔断,避免因持续的失败或拒绝而消耗资源,以及因
持续的超时而堆积请求,最终达到避免雪崩的效应的目的。由此可见,断路器本质是一种快速失败策略的实现。
从调用序列来看,断路器就是一种有限状态机。断路器模式就是根据自身状态变化自动调整代理请求策略的过程,一般要设置下面三种状态:
1.closed
表示断路器关闭,此时的远程请求会真正发送给服务提供者。断路器刚刚建立时默认处于这个状态,此后将持续监控远程请求的数量和执行结果,
决定是否进入open状态。
2.open
表示断路器开启,此时不会进行远程请求,直接向服务调用者返回调用失败的信息,以实现快速失败的策略。
3.half open
这是一种中间状态。断路器必须带有自动的故障恢复能力,当进入open之后,将"自动"(一般是下一次请求触发而不是计时器触发的)切换到
half open状态。在该状态下,断路器会放行一次远程调用,然后根据这次调用的结果,转换为closed或者open状态,以实现断路器的弹性恢复。
closed 和 open 状态的含义是十分清晰的,值得讨论的是这两者的转换条件是什么?现实中比较可行的是,同时满足下面2个条件时,将断路器的状态
自动转换为open:
1.一段时间内(如10s内)请求数量到达一定的阈值(譬如20个)。这个条件的意思是如果请求本身就很少,就用不着断路器介入;
2.一段时间内(如10s内)请求的故障率(发生失败、超时、拒绝的统计比例)到达一定的阈值(譬如50%)。这个条件的意思是如果请求本身都能正确返回,也
用不着断路器介入。
当同时满足以上2个条件时,断路器就会进入open状态。
服务熔断和服务降级之间的联系与差别。断路器的作用是自动进行服务熔断的,这是一种快速失败的容错策略的实现方法。在快速失败策略明确反馈了故障
信息给上游服务后,上游服务必须能够主动处理调用失败的后果,而不是坐视故障扩散。这里的"处理"指的是一种典型的服务降级逻辑,降级逻辑可以包括,但
不应该仅限于把异常信息抛到用户界面,而应该尽力通过其他路径解决问题,譬如把原本要处理的业务记录下来,留待后续重新处理是最低限度的通用降级逻辑。
服务降级不一定是在出现错误后才被动执行的,在许多场景中,人们所讨论的降级更可能是指需要主动迫使服务进入降级逻辑的情况。譬如,出现可预见的
峰值流量,或者系统检修等,要关闭系统部分功能或者部分旁路功能,这时候就可能主动迫使这些服务降级。当然,此时服务降级就不一定处于服务容错的目的了,
更可能是流量控制的范畴。
2.舱壁隔离模式
调用外部服务的故障大致可以分为"失败"、"拒绝"以及"超时",其中超时引起的故障更容易给调用者带来全局性的风险。这是由于目前主流的网络访问大多
基于 TPR 并发模型(Thread per Request)来实现的,只要请求一直不结束,就要一直占用某个线程不能释放。而线程是典型的整个系统的全局性资源,尤其
在Java这类将线程映射为操作系统内核线程来实现的语言环境中,为了让某一个远程服务的局部失败演变成全局失败,就必须设置某种止损方案,这便是服务隔离
的意义。
但是局部线程池有一个显著的弱点,它额外增加了cpu的开销,因为每个独立的线程池都要进行排队、调度和上下文切换工作。根据Netflix官方给出的数据,
一旦启用Hystrix线程池来进行服务隔离,大概会为每次服务调用增加3~10ms的延时,如果调用链中有20次远程服务调用,那每次请求大概会增加60~200ms的
代价来换取服务隔离的安全保障。
为了应对这个情况,还有一种更轻量级的控制服务最大连接数的办法:信号量机制。如果不考虑清理线程池、客户端主动中断线程这些额外的功能,仅仅是为了
控制一个服务并发调用的最大次数,可以只为每个远程服务维护一个线程安全的计数器,并不需要建立局部线程池。具体的做法是,当服务开始调用时计数器加1,
服务返回结果后计数器减1,一旦计数器超过设置的阈值就开始限流,在回落到阈值范围之前都不再允许请求。由于不需要承担线程的排队、调度、切换工作,所以
单纯维护一个计数器的信号量的性能损失,相对于局部线程池来说可以忽略不计。
上面介绍的是从微观、服务调用的角度来应用舱壁隔离设计模式,舱壁隔离设计模式还可以在更高层、更宏观的场景中使用。不是按调用线程,而是按照功能、
按子系统,按用户类型等条件来隔离资源。譬如,根据用户等级、用户是否为VIP,用户来访的地域等各种因素,将请求分流到独立的服务中去,这样即使某一个实例
完全崩溃了,也只是影响到其中一部分用户,以尽可能控制波及范围。一般来说,我们会选择在服务调用端或者边车代理上实现服务层的隔离,在dns或者网关处实现
系统层面的隔离。
3.重试模式
上面介绍了如何断路器模式实现快速失败策略,使用舱壁隔离模式实现沉默失败策略,在断路器中举例的主动对非关键的旁路服务进行降级,亦可算是对安全失败
策略的一种体现。下面,以重试模式来介绍故障转移和故障恢复这两种容错策略的主流实现。
故障转移和故障恢复策略都需要对服务进行重复调用,差别是这些重复调用可能是同步的、也可能是后台异步进行;可能会重复调用同一个服务,也可能会调用到
服务的其他副本。无论具体是通过怎样的调用、调用的服务实例是否相同,都可以归结为重试设计模式的应用范畴。重试模式适合解决系统中的瞬时故障,简单的说
就是有可能自己恢复(自愈,也叫作 回弹性)的临时性失灵,如网络抖动。服务的临时过载(如典型的503)这些都属于瞬时故障。在实践中,重试面临的风险反而
大多数来源于太过简单而导致的滥用。我们判断是否应该且是否能够对一个服务进行重试时,应同时满足以下几个前提条件:
1.仅在主路逻辑的关键服务上进行同步的重试,而非关键的服务,一般不把重试首选的容错方案,尤其不该进行同步的重试。
2.仅对瞬时故障导致的失败进行重试。尽管很难精确判定一个故障是否属于可自愈的瞬时故障,但从http的状态码上至少可以获得一些初步的判断。譬如,收到
401的响应,说明服务本身是可用的,只是没有权限,这时候再重试没有任何意义。功能完善的服务治理工具会提供具体的重试策略配置(如Envoy 的Retry
Policy),可以根据包括http响应码在内的各种具体条件来设置不同的重试参数。
3.仅对幂等的服务进行重试。
4.重试必须有明确的终止条件,常用的终止条件有两种。
a) 超时终止
并不限于重试,所有调用远程服务都应该有超时机制以避免无限期的等待。这里只是强调重试模式更加应该配合超时模式来使用,否则对系统是有害的。
b) 次数终止
重试必须有一定的限度,不能无限制的进行下去,通常最多只重试2~5次。重试不仅会给调用者带来负担,对于服务提供者也同样是负担,所以应该
避免重试次数设置过大。此外,如果服务提供者返回的响应头中带有 Retry-After ,即使它没有强制的约束力,我们也应该充分尊重服务端的要求,做
个有礼貌的调用者。
8.2 流量控制
任何一个系统的运算、存储、网络资源都不是无限的,当系统资源不足以支撑超过预期的突发流量时,便应该有所取舍,建立面对超额流量自我保护的机制,这个机制就是
"限流"。
最大处理能力为80tps的系统遇到100tps的请求时,应该能完成其中的80tps,也即有20tps的请求失败或者被拒绝才对,这是理想情况,也是我们追求的目标。但
事实上,如果不做任何处理,更可能出现的结果是这100个请求中的每一个请求都开始处理了,只是大部分请求完成了其中10次服务调用中的8次、或者9次,就会超时退出。
导致多数服务调用被白白浪费了,没有几个请求能完整的走完业务操作。为了避免这种情况出现,一个健壮的系统需要做到恰当的流量控制,更具体的说,需要解决下面3个问题:
1.依据什么限流
对于要不要控制流量,要控制哪些流量,控制力度有多大等操作,我们无法在系统设计阶段静态的给出确定的结论,必须根据系统此前一段时间的运行状况,甚至未来
一段时间的预测情况来动态决定。
2.具体如何限流
要解决系统具体是如何做到允许一部分请求通行,而另外一部分流量实行受控制的失败降级问题,就必须掌握常用的服务限流算法和设计模式。
3.超额流量如何处理
对于超额流量可以有不同的处理策略,例如可以直接返回失败(如429 Too Many Requests),或者迫使它们进入降级逻辑,这种策略被称为否决式限流;也可以
让请求排队等待,暂时阻塞一段时间后继续处理,这种被称为阻塞式限流。
8.2.1 流量统计指标
要做流量控制,首先要弄清楚哪些指标能反应系统的流量压力大小。相较而言,容错的统计指标是明确的,容错的触发条件基本上只取决于请求的故障率,发生失败、
决绝与超时都算作故障。但限流的统计指标就不那么明确了。如下3个定义:
1.每秒事务数(Transaction per Second,TPS)
tps 是衡量信息系统吞吐量的最终标准。事务,可以理解为一个逻辑上具备原子性的业务操作。
2.每秒请求数(Hit per Second,HPS)
HPS 是指每秒从客户端发向服务端的请求数(请将Hit理解为Request而不是Click)。如果只要一个请求能完成一笔业务,那HPS和TPS是等价的,但在一些
场景中,一笔业务可能需要多次请求才能完成。
3.每秒查询数(Query per Second,QPS)
QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那qps和hps是等价的。但在分布式系统中,一个请求的响应往往要由后端多个
服务节点共同协作完成的。
上面三个指标都是基于调用计数的指标,在整体目标上我们当然希望能够基于tps来限流的,因为信息系统最终是为人类用户提供服务的,用户并不关心业务到底是
由多少个请求、多少个后台查询共同协作完成的。但是,系统的业务五花八门,不同的业务操作给系统带来的压力往往差异巨大,不具备可比性。更关键的是,流量控制
是针对用户实际的操作场景来限流的,这不同于压力测试场景中无间隙的全自动化操作,真实业务操作的耗时无可避免的受限于用户交互带来的不确定性。此时,如果
按照业务开始时计数器加1,业务结束时计数器减1,通过限制最大的tps来限流的话,就不能准确的反映出系统所承受的压力,所以直接针对tps来限流实际上是很难的。
目前,主流系统大多倾向于使用hps作为首选的限流指标,它是相对容易观察统计的,而且能够在一定程度上反应系统当前以及接下来一段时间内的压力。但限流
指标并不存在任何必须遵守的权威,根据系统的实际需要,哪怕完全不选择基于调用计数的指标都是可能的。譬如下载、视频、直播等IO密集的系统,往往会把每次
请求和响应报文的大小,而不是调用次数作为限流指标,譬如只允许单位时间内通过100MB的流量。又譬如网络游戏等基于长连接的应用,可能会把登录用户数作为
限流指标,当用户数超过一定阈值就会让你登录前排队等候。
8.2.2 限流设计模式
1.流量计数器模式
做限流最容易想到的一种方法是设置一个计数器,根据当前时刻的流量计数结果是否超过阈值来决定是否限流。如前面的场景中,该系统能承受的最大持续流量
是80tps,那就可以通过控制任何一秒内的业务请求次数来限流,超过80次就直接拒绝掉超额的部分。这种做法很直观,但不严谨,如下可以证明:
a) 即使每一秒的统计流量都没有超过80tps,也不能说明系统没有遇到过大于80tps的流量压力。
可以想象如下场景,如果系统连续2s都收到60tps的访问请求,但这2个60tps请求分别是在前1s里面的后0.5s,以及后1s中的前面0.5s所发生的。这样
虽然每个周期的流量都不超过80tps,但系统确实曾经在1s内发生了阈值120tps的请求。
b) 即使连续若干秒的统计流量都超过了80tps,也不能说明流量压力就一定超过了系统的承受能力。
可以想象如下场景,如果在10s的时间片段中,前3stps平均值到了100,而后7s的平均值是30左右,此时系统是否能够处理完这些请求而不产生超时
失败呢?答案是可以的,因为条件中给出的超时时间是10s,而最慢的请求也能在8s左右处理完毕。如果只是基于固定时间周期来控制请求阈值为80tps,反而
会误杀一部分请求,导致部分请求出现原本不必要的失败。
流量计数器的缺陷根源在于它只是针对时间点进行离散的统计,为了弥补该缺陷,一种名为"滑动时间窗"的限流模式被设计出来,它可以实现基于时间片段的
统计。
2.滑动时间窗模式
如编译原理中的 窥孔优化、tcp协议的阻塞控制 等都用到滑动窗口算法。
当频率固定每秒一次的定时器被唤醒时,它应该完成下面几项工作,这也是滑动时间窗的工作过程:
a) 将数组最后一位的元素丢弃,并把所有元素都后移一位,然后在数组的第一位插入一个新的空元素。这个步骤即为"滑动窗口";
b) 将计数器中所有统计信息写入第一位的空元素中;
c) 对数组中所有元素进行统计,并复位清空计数器的数据以供下一个统计周期使用。
滑动时间窗口模式的限流完全弥补了流量计数器的缺陷,可以保证在任意时间片段内,只需要经过简单的调用计数比较,就能控制请求次数一定不会超过限流的
阈值,这在单机限流或者分布式服务单点网关中的限流很常用。不过,这种限流模式也有缺点,它通常只适用于否决式限流,超过阈值的流量就必须强制失败或者
降级,很难进行阻塞等待处理,也就很难在细粒度上对流量曲线进行调整,起不到削峰填谷的作用。下面介绍2种适用于阻塞式限流的限流模式。
3.漏桶模式
在计算机网络中,专门有一个术语 --- 流量整型(Traffic Shaping)来描述如何限制网络设备的流量突变,使得网络报文比较均匀的速度向外发送。流量
整型通常都需要用到缓冲区实现,当报文发送的速度比较快时,首先在缓冲区暂存,然后在控制算法的调节下均匀的发送这些被缓冲的报文。常用的控制算法有漏桶
算法和令牌桶算法两种,这两种算法的思路截然相反,但达到的效果却又是相似的。
把请求当做水,水来了都先放进池子里,水池同时又以额定的速度出水。这样,水池还能充当缓冲区,让出水口的速度不至于过快。不过,由于请求总是有
超时时间的,所以缓冲区大小也必须是有限度的。此时,从网络流量的整型角度来看,体现为不符数据包被丢弃,而从信息系统的角度来看,则体现为不符请求会
遭遇失败和降级。
漏桶在代码实现上看非常简单,它其实就是一个以请求对象作为元素的先入先出队列,队列长度就相当于漏桶的大小,当队列已满时变拒绝新请求的进入。
漏桶实现起来容易,难点在于如何确定漏桶的2个参数:桶的大小和水流出的速率。如果桶设置太大,那服务依然可能遭遇流量过大的冲击,不能完全发挥限流的
作用;如果设置的太小,那很可能误杀一部分正常的请求,这种情况与流量计数器模式中举过的例子是一样的。流出速率在漏桶算法中一般是个固定值,类似于本节
开头应用场景中那样固定拓扑结构的服务是很合适的,但同时你也应该明白那是经过最大限度简化的场景,现实中系统的处理速度往往受到其内部拓扑结构变化和
动态伸缩的影响,所以能够支持变动请求处理速率的令牌桶算法可能更受欢迎。
4.令牌桶模式
漏桶是水池,那令牌桶就是你去银行办事时摆在门口的那台排队机。它与漏桶一样都是基于缓冲区的限流算法,只是方向刚好相反,漏桶只是从水池里面
向系统发送请求,令牌桶则是系统往排队机中放入令牌。
假设我们要限制系统x秒内最大的请求次数不超过y,那每隔x/y时间往漏桶中放一个令牌,当有请求进来时,首先要从桶中取出一个准入的令牌,然后才能
进入系统进行处理。任何时候,一旦请求进入桶中发现没有令牌可取了,就应该马上宣告失败或者进入服务降级的逻辑。与漏桶类似,令牌桶同样具有最大容量,
这意味着当系统比较空闲时,桶中令牌积累到一定程度就不再无限增加了,此时预存在桶中的令牌便是请求最大的缓冲的余量。上面这段话转化为以下步骤来
指导程序编码。
a) 让系统以一个由限流目标决定的速率向桶中注入令牌,譬如要控制系统的访问不要超过100次,速率即设定为1/100=10ms;
b) 桶中最多可以存放n个令牌,n的具体数量由超时时间和服务处理能力共同决定。如果桶已经满了,第n+1个进入的令牌会被丢弃;
c) 请求到达时先从桶中取走1个令牌,如果桶已经空了就进入降级逻辑。
令牌桶模式的实现看起来复杂,每个固定时间就要放新的令牌桶中,但其实并不需要真的用一个专用线程或者定时器来做这件事情,只要在令牌中增加一个
时间戳记录,每次获取令牌桶前,比较一下时间戳与当前时间,就可以轻易计算出这段时间需要放多少令牌进入,然后依次放完全部令牌即可,所以真正编码不会
很复杂。
8.2.3 分布式限流
前面讨论的限流算法和模式全部是针对整个系统的限流,总是有意无意的假设或者默认系统只提供一种业务操作,或者所有业务操作的消耗都是等价的,并不涉及
不同业务请求进入系统的服务集群后,分别会调用哪些服务、每个服务节点处理能力有何差别等问题。那些限流算法,直接使用在单体架构的集群上是完全可行的,但到
了微服务架构下,它们最多只能应用于集群最入口处的网关上,对整个服务集群进行流量控制,而无法细粒度的管理流量在内部微服务节点中的流转情况。所以,我们把
前面介绍的限流算法都统称为单机限流,把能够精确控制分布式集群中每个服务消耗量的限流算法称为分布式限流。
这2种限流算法实现上的核心差别在于如何管理限流的统计指标,单机限流很好办,因为指标都存储在服务的内存中,而分布式限流的目的就是要让各个服务节点协同
限流,无论是将限流功能封装为专门的远程服务,抑或是在系统采用的分布式框架中提供专门的限流支持,都需要将原本在每个服务节点自己内存中统计的数据开放出来,
让全局的限流服务可以访问到才行。
一种常见的简单分布式限流方法是将所有服务的统计结果都存到集中式缓存中(如redis),以实现在集群内的共享,并可以通过分布式锁、信号量等机制,解决这些
数据读写访问时并发控制问题。在可以共享统计数据的前提下,原本用于单机的限流模式理论上也是可以应用于分布式环境中的,可是其代价是也显而易见的:每次服务
调用都必须额外增加一次网络开销,所以这种方法的效率肯定是不高的,流量压力大的时候,限流本身还会显著降低系统的处理能力。
只要集中统计信息,就不可避免的会产生网络开销,所以,为了缓解这里产生的性能损耗,一种可以考虑的办法是在令牌桶限流模式基础上进行"货币化改造",即
不把令牌看做只有准入和不准入的"通行证",而是看作数值形式的"货币额度"。当请求进入集群时,首先在API网关处领取一定数额的货币,为了体现不同等级的用户
重要性差别,他们的额度可以有所差异,譬如让vip用户的额度更高甚至是无限的。我们将用户A的额度表示为 Quanity(a)。任何一个服务在响应请求时都需要消耗
集群一定的资源,所以访问每个服务时都要求消耗一定量的货币,假设服务X要消耗的额度表示为 Cost(x),那当用户A访问了N个服务后,他剩余的额度为Limit(N)
表示为:
Limit(N) = Quanity(a) - (∑1到n)Const(x)
此时,我们可以把剩余额度 Limit(x)作为内部限流的质保,规定在任何时候,一旦剩余额度Limit(n)小于或者等于0,就不再允许访问其他服务了。此时必须
先发送一次网络请求,重新向令牌桶申请一次额度,成功后才能继续访问,不成功则进入降级逻辑。除此之外的任何时刻,即Limit(x)不为零时,都无需额外网络访问,
因为计算Limit(x)是完全可以在本地完成的。
基于额度的限流方案对限流的精准度有一定的影响,可能存在的业务操作已经进行了一部分服务调用,却无法从令牌桶中再获取新额度,最终因"资金链断裂"而
导致业务操作失败。这种失败的代价是比较昂贵的,它白白浪费了部分已经完成了的服务资源,但总体来说,它仍然是一种并发性能和限流效果都相对折中可行的分布式
限流方案。对于分布式系统来说,容错是必须要有、无法妥协的措施。
以上是关于8.凤凰架构:构建可靠的大型分布式系统 --- 流量治理的主要内容,如果未能解决你的问题,请参考以下文章
1.凤凰架构:构建可靠的大型分布式系统 --- 服务架构演进史
1.凤凰架构:构建可靠的大型分布式系统 --- 服务架构演进史