流量治理

服务容错

一个大的服务集群中,程序可能崩溃、节点可能宕机、网络可能中断。

容错策略和容错设计模式,最终目的是为了避免服务集群中某个节点的故障导致整个系统发生雪崩效应

 

 

容错策略

“面对故障,我们该做些什么”

  • 故障转移

    服务具备幂等性!!

    如果调用的服务器出现故障,系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试其他副本能否返回成功调用的结果,从而保证了整体的高可用性。故障转移的容错策略应该有一定的调用次数限制。

  • 快速失败

    非幂等服务。尽快让服务报错,坚决避免重试,尽快抛出异常,由调用者自行处理。

  • 安全失败

    对于旁路逻辑:后续处理不会依赖其返回值,或者它的返回值是什么都不会影响后续处理的结果,譬如只是将返回值记录到数据库,并不使用它参与最终结果的运算。

    即使旁路逻辑调用实际失败了,也当作正确来返回,如果需要返回值的话,系统就自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可

  • 沉默失败

    当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,将错误隔离开来,避免对系统其他部分产生影响

  • 故障恢复

    服务具备幂等性。

    作为其他容错策略的补充措施,默认会采用快速失败加上故障恢复的策略组合。它是指当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。

    为服务注册中心和负载均衡器及时提供服务恢复的通知信息。

  • 并行调用

    一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功

  • 广播调用

    要求所有的请求全部都成功,这次调用才算是成功,任何一个服务提供者出现异常都算调用失败

 

 

 

 

容错设计模式

“要实现某种容错策略,我们该如何去做”

 

断路器模式 快速失败策略

当出现故障(失败、超时、拒绝)的次数达到断路器的阈值时,它状态就自动变为“OPEN”,后续此断路器代理的远程访问都将直接返回调用失败,而不会发出真正的远程服务请求。

 

避免因持续的失败或拒绝而消耗资源,因持续的超时而堆积请求,最终的目的就是避免雪崩效应的出现。

 

 

断路器模式就是根据自身状态变化自动调整代理请求策略的过程。断路器三个状态:

  • CLOSED:远程请求会真正发送给服务提供者,将持续监视远程请求的数量和执行结果,决定是否要进入 OPEN 状态。

  • OPEN:表示断路器开启,此时不会进行远程请求,直接给服务调用者返回调用失败的信息,以实现快速失败策略。

  • HALF OPEN:中间状态,故障自动恢复。当进入 OPEN 状态一段时间以后,将“自动”切换到 HALF OPEN 状态。该状态下,会放行一次远程调用,然后根据这次调用的结果成功与否,转换为 CLOSED 或者 OPEN 状态,以实现断路器的弹性恢复。

 

 

CLOSED—>OPEN,同时满足以下两个条件:

  • 一段时间(譬如 10 秒以内)内请求数量达到一定阈值(譬如 20 个请求)。

  • 一段时间(譬如 10 秒以内)内请求的故障率(发生失败、超时、拒绝的统计比例)到达一定阈值(譬如 50%)。

 

服务熔断

断路器进行的是服务熔断,是一种快速失败的容错策略的实现方法。

服务降级

上游服务根据故障信息主动处理调用失败的后果

不仅仅把异常信息抛到用户界面去,而应该尽力想办法通过其他路径解决问题,譬如把原本要处理的业务记录下来,留待以后重新处理是最低限度的通用降级逻辑。

降级更多的场景是主动迫使服务进入降级逻辑的情况,出于应对可预见的峰值流量,或者是系统检修等原因,要关闭系统部分功能或关闭部分旁路服务,此时不属于服务容错的目的,而是流量控制的范畴。

 

舱壁隔离模式 沉默失败策略

设计舰船时,要在每个区域设计独立的水密舱室,一旦某个舱室进水,也只是影响这个舱室中的货物,而不至于让整艘舰艇沉没。

隔离资源(线程、内存)

服务隔离,沉默失败策略。

目前主流的网络访问大多是基于 TPR 并发模型(Thread per Request)来实现,只要请求一直不结束,就要一直占用着某个线程不能释放,“超时”会带来全局性的风险。

 

解决方法:

  1. 局部线程池:为每个服务单独设立线程池,这些线程池默认不预置活动线程,只用来控制单个服务的最大连接数。例如为某个服务设置最大线程数为5的线程池。

     

    缺点:额外增加了 CPU 的开销,每个独立的线程池都要进行排队、调度和下文切换工作

  2. 针对CPU开销,采用更轻量的可以用来控制服务最大连接数的办法:信号量机制。

    只为每个远程服务维护一个线程安全的计数器即可,并不需要建立局部线程池。

    当服务开始调用时计数器加 1,服务返回结果后计数器减 1,一旦计数器超过设置的阈值就立即开始限流,在回落到阈值范围之前都不再允许请求了。

 

舱壁隔离模式还可以在更高层、更宏观的场景中使用,不是按调用线程,而是按功能、按子系统、按用户类型等条件来隔离资源都是可以的,譬如,根据用户等级、用户是否 VIP、用户来访的地域等各种因素,将请求分流到独立的服务实例去,这样即使某一个实例完全崩溃了,也只是影响到其中某一部分的用户,把波及范围尽可能控制住。

 

 

重试模式 故障转移 故障恢复

适合解决系统中的瞬时故障,即有可能自己恢复的临时性失灵,网络抖动、服务的临时过载(典型的如返回了 503 Bad Gateway 错误)。

 

必须同时满足以下前提条件:

  • 仅在主路逻辑的关键服务上进行同步的重试,不是关键的服务,一般不把重试作为首选容错方案,尤其不该进行同步重试。

  • 仅对由瞬时故障导致的失败进行重试。通过HTTP状态码进行获得初步结论。

  • 仅对具备幂等性的服务进行重试。

    GET、HEAD、OPTIONS、TRACE 由于不会改变资源状态,这些请求应该被设计成幂等的;PUT 请求一般也是幂等的。

    POST请求是非幂等的。

  • 具有明确的终止条件,常用的终止条件有两种:

    • 超时终止:并不限于重试,所有调用远程服务都应该要有超时机制避免无限期的等待。

    • 次数终止:重试必须要有一定限度,不能无限制地做下去,通常最多就只重试 2 至 5 次。重试不仅会给调用者带来负担,对于服务提供者也是同样是负担。

 

流量控制

“限流”:面对超额流量自我保护的机制

 

流量统计指标
  • 每秒事务数(Transactions per Second,TPS):TPS 是衡量信息系统吞吐量的最终标准。

  • 每秒请求数(Hits per Second,HPS):HPS 是指每秒从客户端发向服务端的请求数。如果只要一个请求就能完成一笔业务,那 HPS 与 TPS 是等价的。

  • 每秒查询数(Queries per Second,QPS):QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那 QPS 和 HPS 是等价的。

 

目前,主流系统大多倾向使用 HPS 作为首选的限流指标

 

限流设计模式
  • 流量计数器模式
    • 即使每一秒的统计流量都没有超过 80 TPS,也不能说明系统没有遇到过大于 80 TPS 的流量压力。

    • 即使连续若干秒的统计流量都超过了 80 TPS,也不能说明流量压力就一定超过了系统的承受能力。

      只是针对时间点进行离散的统计

  • 滑动时间窗模式

    在不断向前流淌的时间轴上,漂浮着一个固定大小的窗口,窗口与时间一起平滑地向前滚动

    • 将数组最后一位的元素丢弃掉,并把所有元素都后移一位,然后在数组第一个插入一个新的空元素。这个步骤即为“滑动窗口”。

    • 将计数器中所有统计信息写入到第一位的空元素中。

    • 对数组中所有元素进行统计,并复位清空计数器数据供下一个统计周期使用。

    只适用于否决式限流,超过阈值的流量就必须强制失败或降级,很难进行阻塞等待处理,也就很难在细粒度上对流量曲线进行整形,起不到削峰填谷的作用。

 

流量整形:限制网络设备的流量突变,使得网络报文以比较均匀的速度向外发送。

通常都需要用到缓冲区来实现,当报文的发送速度过快时,首先在缓冲区中暂存,然后再在控制算法的调节下均匀地发送这些被缓冲的报文。

常用的控制算法有漏桶算法(Leaky Bucket Algorithm)和令牌桶算法(Token Bucket Algorithm)两种。

 

  • 漏桶模式

    水来了都先放进池子里,水池同时又以额定的速度出水,让请求进入系统中。

    注水过快的话,水池还能充当缓冲区,让出水口的速度不至于过快。当注水速度持续超过出水速度一段时间以后,水池终究会被灌满,从网络的流量整形的角度看是体现为部分数据包被丢弃,而在信息系统的角度看就体现为有部分请求会遭遇失败和降级

    • 一个以请求对象作为元素的先入先出队列(FIFO Queue),队列长度就相当于漏桶的大小,当队列已满时便拒绝新的请求进入。

    • 两个参数:桶的大小和水的流出速率。

  • 令牌桶模式

    系统每隔一段时间望桶内放入一定数量的令牌,当有请求进来时,首先要从桶中取得一个准入的令牌,然后才能进入系统处理。任何时候,一旦请求进入桶中却发现没有令牌可取了,就应该马上失败或进入服务降级逻辑。

    • 具体实现不需要真的用一个专用线程或者定时器来每间隔固定时间就要放新的令牌到桶中。

    • 只要在令牌中增加一个时间戳记录,每次获取令牌前,比较一下时间戳与当前时间,就可以轻易计算出这段时间需要放多少令牌进去,然后一次性放入即可。

 

以上所介绍的限流模式统称为单机限流,而能够精细控制分布式集群中每个服务消耗量的限流算法称为分布式限流。

 

核心差别在于如何管理限流的统计指标,单机限流很好办,指标都是存储在服务的内存当中,而分布式限流的目的就是要让各个服务节点的协同限流,无论是将限流功能封装为专门的远程服务,抑或是在系统采用的分布式框架中有专门的限流支持,都需要将原本在每个服务节点自己内存当中的统计数据给开放出来,让全局的限流服务可以访问到才行。

 

分布式限流

  • 存入集中式缓存(如 Redis),实现在集群内的共享,并通过分布式锁、信号量等机制,解决这些数据的读写访问时并发控制的问题。

    缺点:每次服务调用都必须要额外增加一次网络开销,所以这种方法的效率肯定是不高的,流量压力大时,限流本身反倒会显著降低系统的处理能力。

  • 在令牌桶限流模式基础上进行“货币化改造”,即不把令牌看作是只有准入和不准入的“通行证”,而看作数值形式的“货币额度”。

    当请求进入集群时,首先在 API 网关处领取到一定数额的“货币”,任何一个服务在响应请求时都需要消耗集群一定量的处理资源,所以访问每个服务时都要求消耗一定量的“货币”,剩余额度 LimitN作为内部限流的指标。

    LimitN = QuanityA - ∑NCostX

    LimitN不为零时,都无须额外的网络访问,只要一旦剩余额度 LimitN小于等于 0 时,就不再允许访问其他服务了。此时必须先发生一次网络请求,重新向令牌桶申请一次额度,成功后才能继续访问,不成功则进入降级逻辑。

posted @ 2022-06-18 18:28  roibin  阅读(187)  评论(0编辑  收藏  举报