如何构建一个高可靠系统(下)

本篇博客参考:余春龙的《架构设计2.0》,本篇博客中的图片来自于互联网,并非原创,在此谢谢原创们,如果你是图片的原创,不希望被引用,请及时联系我。

上篇博客介绍了构建一个高可靠系统的三个要素:限流、超时、重试,本篇博客将继续介绍剩下的要素。

过载保护:限流与熔断

再说限流

上篇博客由于篇幅限制,没有介绍限流算法与限流保护对象的问题,本篇博客将继续介绍限流,完善这两部分内容。

限流算法

常见的限流算法有四种:固定时间窗口,滑动时间窗口,令牌桶、漏桶。

固定时间窗口限流算法

固定时间窗口限流算法会在时间轴上划分一个个固定的时间窗口,固定时间窗口内只允许放行一定数量的请求,超出的请求会被拦截,如下图所示: image.png

设置的限流阈值为5,在固定的时间窗口内:

  • 请求数<=5,请求被放行
  • 请求数>5,请求被拦截

如果我们的请求是均匀分布的,那这个算法没什么问题,但是在实际场景中,请求并非是均匀分布的,那就会产生“超限”的问题: image.png

设置的限流阈值为5,时间窗口大小为1s,在第一个时间窗口内发生了4次请求,在第二个时间窗口内发生了3次请求,都没有超过设置的限流阈值5,但是在两个时间窗口临界区超限了:1s之内请求了6次。

滑动时间窗口限流算法

为了解决固定时间窗口限流算法在两个时间窗口临界区产生的“超限”问题,滑动时间窗口限流算法诞生了,滑动时间窗口限流算法没有在时间轴划分固定的时间窗口,而是将当前请求发生的时间作为时间窗口的终点,时间窗口的起点需要将时间窗口的终点(请求发生的时间)向前推,然后将时间窗口内的发生的请求进行累加,判断是否需要拦截当前请求。

令牌桶限流算法

image.png

令牌桶限流算法三个环节
  • 产生令牌:以给定的速率源源不断的产生令牌,直到达到了令牌桶令牌的上限
  • 消耗令牌:请求到来,会从令牌桶取出给定的令牌数
  • 判断是否通过:如果请求可以取出足够的令牌,放行请求,无法取出,拦截请求
令牌桶限流算法三个核心参数
  • 令牌牌产生的速率
  • 令牌的上限
  • 消耗令牌的数量

消耗令牌的数量:对于简单的请求,消耗令牌的数量可以设置的少一点,对于复杂的请求,消耗令牌的数量可以设置的多一点。

令牌桶限流算法特点

令牌桶限流算法允许突发流量:可能一段时间内,请求比较少,甚至没有请求,令牌桶积累了很多令牌,然后突然在短时间内,有众多请求打过来,这些请求都可以成功拿到令牌,都可以被放行。

令牌桶限流算法保护对象

令牌桶限流算法更多的是保护本应用。

漏桶限流算法

image.png

从图中,可以得出:

  • 漏桶的容量是固定的
  • 请求流出漏桶的速度是固定的
  • 请求流入漏桶的速度是任意的
  • 如果漏桶是空的,则不需要流出
  • 如果漏桶是满的,请求无法流入漏桶,请求将被丢弃
漏桶限流算法特点

漏桶限流算法不允许突发流量,哪怕很长一段时间内,一个请求都没有,突然来了一些请求,也要按照固定的速度流出。

漏桶限流算法应用

Nginx中的限流模块采用的漏桶限流算法,我们经常使用的MQ,其实也可以看成是一个漏桶:不管上面的请求多么激烈,我就慢慢消费。

漏桶限流算法保护对象

漏桶限流算法更多的是保护下游应用。

好多人都认为限流是服务端对自己的保护措施,我觉得这是片面的,不同的限流算法,不同的观察角度,不同的应用场景,有不同的保护对象。

熔断

一个服务依赖于另外一个服务,如果依赖的服务出现故障:一直超时或者一直抛出异常,那在一段时间内就没有必要调用这个服务了,直接返回“失败”,这就是熔断。

如果没有熔断机制,每次请求,都要走一遍可能已经“凉”了的依赖服务,这是没有任何意义的,而且会导致:

  • 依赖服务越来越“凉”:依赖服务在一段时间内一直超时或者一直抛出异常,可能是由于依赖服务正在处理“诡异”的请求,导致占用了大量的CPU、内存、网络资源,而无法继续处理其他请求。等处理完这个“诡异”的请求,也许就恢复正常了,但是这个时候,继续向这个服务发出请求,可能会进一步加剧服务的压力。
  • 拖死调用方:依赖服务在一段时间内一直超时,调用方一直死死的等在那里,连接数会慢慢的被占满,无法处理其他请求。

基于上述两个原因,我觉得熔断既是对自己的保护,也是对下游的保护。

一旦触发熔断,就不再调用依赖服务,那什么时候恢复呢?可以设置为经过一段时间内,暂时放行一些请求去依赖服务进行尝试,如果还是不行,继续熔断,如果恢复了,那就停止熔断。

熔断,直接抛出异常或者返回失败给人的感觉有点“暴力”,所以在实际开发中,熔断经常与降级一起使用。

降级

降级分为狭义上的降级和广义上的降级:

  • 狭义上的降级:调用方调用某个服务失败,而调用另外一个方法补救
  • 广义上的降级:秒杀的时候,关闭取消订单、修改收货地址、评论等不太重要的功能,只提供最核心的秒杀服务;产品详情页中有广告模块,但是广告服务出现异常了,可以不展示广告,或者展示固定的几个广告,而不影响产品详情的主要逻辑等等

是否有损

降级还分为有损降级或是无损降级,当然很多时候,是否有损降级是相对的,是需要看观察的角度的。

很多人提到“降级”都是愁眉苦脸的,认为一旦降级了,准没好事,怎么还会有无损降级,这还真不一定:

  • 我先前负责运单系统,会调用不同的快递公司接口查询快递信息,当调用快递公司接口多次失败,会降级,改为调用快递100或者快递鸟的接口。对于用户来说,这就是无损降级(用户可以正常查询快递信息),但是对于公司来说,就要分情况讨论了:如果调用快递100、快递鸟的接口收费更低,那对于公司来说,也是无损降级,如果调用快递100、快递鸟的接口收费更高,那对于公司来说,便是有损降级。
  • 创建订单,如果MySQL出现异常,会降级,把订单数据暂存到本地数据库,比如RocksDB中,MySQL恢复后,将RocksDB中暂存的订单提交到MySQL,这样不管对于用户来说,还是对于公司来说,这都是无损的:用户可以正常下单,订单不丢失。

隔离

隔离是指将系统或者资源分割,出现故障,不会出现滚雪球、雪崩效应,可以将故障的影响缩小到某个范围。 隔离的方式有很多,不同的场景下,有不同的隔离方式,很多时候,还会将不同的方式组合在一起使用。下面是几种常见的隔离方式:

数据隔离

将数据分成多个部分:

  • 在秒杀的时候,为了应对高并发,有时候会把同一个商品的库存打散在不同的库中
  • 在处理热key的时候,为了应对高并发,有时候会把同一份数据分散在不同的Redis实例,甚至不同的Redis集群中

这和“分库分表”是类似的,哪怕我们没有刻意这么做,我们在很大程度上也已经做到了数据隔离:用户数据在用户库、订单数据在订单库、商品数据在商品库,这也是数据隔离。

机器隔离

微服务划分的依据:

  • 业务
  • 稳定性
  • 性能
  • 重要性

我们将一个很大的单体应用根据业务、稳定性、性能、重要性划分出一个个微服务,其实在很大程度上,已经做到了机器隔离。

调用隔离

如果一个服务比较重要,它会较为频繁的调用依赖服务,那我们完全可以把依赖服务复制出来一份,调用方直接调用复制出来的依赖服务,如果采用的PRC框架比较成熟,依赖服务可以多部署几台机器,调用方直接指定调用依赖服务的哪几台机器。

线程池隔离

假设应用服务器设置的并发连接数是500,最多只能同时处理500个请求,服务依赖了其他服务,其中有一个依赖服务延迟突然变得很高,如果没有任何限制的话,那可能所有请求都被卡在这里,连接数被占满,整个服务就崩溃了。我们可以为每个依赖服务准备一个线程池(线程数和队列需要合理设置),需要调用服务,就往线程池中推送一个任务,如果线程池已满,就拒绝调用依赖服务,这样只会有部分请求卡在这里,连接数不会被占满,整个服务就不会崩溃。

在这个案例下,合理设置超时时间+线程池隔离 搭配使用更香哦。

线程池隔离从某种角度来说,也是在限流。

本篇博客介绍了构建一个高可靠系统的剩下的几个要素:熔断、降级、隔离,同时再一次分析了限流,计划是还要介绍监控、灰度、回滚、告警的,但是这几个东西和业务结合太紧密了,如果只是介绍下基本概念也没太多意思,所以暂时将这几个东西给“抛弃”了。

猜你喜欢

转载自juejin.im/post/7193972763378319421
今日推荐