分布式系统常见设计

博客:cbb777.fun

全平台账号:安妮的心动录

github: github.com/anneheartre…

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

分布式核心要素

通常来说设计分布式系统的时候最需要考虑的核心要素有五个image.png

  • Capacity 容量(能力) 指的是分布式系统里的CPU 内存 硬盘 网络 文件描述符 socket连接数等等硬性的指标
  • Perfomant 性能 指的是IOPS TPS QPS Latency Jitter之类的性能指标要求,性能受限于容量,性能同时又影响了可靠性以及可用性
  • Availablility 可用性 指的是产品或者服务在随机事件内调用时处于可服务状态的概率 也就是正常运行的时间/总时间 之前说的异地多活也是为了保证可用性而出现的
  • Reliability 可靠性 一般是不出故障的概率 通常企业级产品是5个9打底的 可以简单的和可用性划上约等号
  • Scalability 可伸缩性 指的是处理集群能否动态缩扩容 使得处理能力越来越多和越来越少的某种能力 系统的可伸缩性决定了该系统能不能伸缩

一个分布式系统通常会面临以下几个个难题:故障传播性、业务拆分与聚合、以及分布式事务

为了解决故障传播性的难题,我们可以采用"隔板" "熔断" "降级" "限流" "容错"以及"资源管控"等方式

微服务服务治理几大模式

隔板模式

场景: 在分布式系统中通常将进程容器化 以进行资源隔离,然后在同一个进程中的所有业务都共享线程池,对外提供服务,但是这就导致了会经常遇到这样的问题:

  1. 业务A负载较高,抢占了线程池里的大部分线程资源,从而导致其他业务的服务质量下降
  2. 同一个进程内新加一个业务,这个业务会抢占其他业务的资源,可能会造成系统的不稳定,比如业务性能抖动
  3. 难以调试,多个业务共享一个线程池,当出现故障的时候很难通过简单的日志判断是哪个业务出了问题

隔板模式:在分布式系统里进行资源的隔离,资源隔离通常按照业务粒度分为进程级别和线程级别

进程隔离:通常使用的是容器化进行隔离,比如通过docker实现业务进程之间的资源隔离,底层就是通过namespace实现的操作系统级别的隔离,比如隔离进程、网络、通信等等。cgroup实现的硬件层面的隔离,比如CPU、内存等等。具体实现笔者之前的博客有提到

线程隔离:指给每个跑在进程里的业务按照业务类型创建一个线程池,从而实现线程级别粒度的资源隔离

优势:

  • 提高业务可靠性,减少业务受其他业务影响的程度,当一个业务耗尽自身资源后也不会影响到其他业务的服务质量
  • 降低新加入的业务给系统带来的风险,减少新加业务导致其他业务可能出现的性能抖动
  • 利于调试,通过线程池可以很方便的定位是哪个服务出了故障,并且可以通过监控线程池的请求失败次数、超时次数、拒绝请求次数等可以实时反应当前的业务质量

劣势:

粒度更细,很容易就能想到劣势是引入了额外的开销,具体开销的点如下

  1. 对象分配 创建多个线程对象
  2. 并发 可能会有一些竞态问题 为了避免竞态问题 则必须进行并发控制
  3. 线程的切换开销 操作系统层面的开销

这些开销对于整个系统或者业务来说,一般开销不会特别大,在一些要求不苛刻的场景可以忽略

微服务限流三大件:熔断、降级、限流

熔断模式

场景:

1.系统负载突然过高,比如突发的访问量、过多的请求以及IO压力过载都可能会造成某个节点故障,比如节点A,然后节点A挂了,又把负载传给节点B,节点B负载过高之后又挂了,这样一连串的挂过去就会把请求从单点故障转化成为系统级别的级联故障

2.我们希望在一个服务出现故障的时候,能够在一段时间内恢复,在请求被拒绝一段时间后再自动的去探测服务的可服务性

熔断模式:也称为断路器模式,当系统里的响应时间或者异常比率或者单位异常数超过某个阈值的时候,比如超时次数或者重试次数超过某个阈值,就会触发熔断,接着所有的调用都快速失败,从而保证下游系统的负载安全。

在断开一段时间之后,熔断器又试着让部分请求负载通过,如果这些请求成功,那么断路器就恢复正常,如果继续失败,那么就关闭服务,立刻返回失败,接着继续这个过程直到重试的次数超过一定的阈值,从而触发更加严重的"降级模式" image.png 具体过程如下

  • 熔断器开始处于闭合状态,如果达到触发条件,那么熔断器就会打开
  • 接着熔断器处于打开状态,所有走到这个路径里的请求会走快速失败通道,从而避免负载下行,给下游的服务造成压力,过一个时间周期之后会自动切换到半打开状态
  • 半打开状态:认为之前的错误可能已经被修复了,因此允许通过部分请求试着看看能不能处理成功,如果这些请求处理成功,那么就认为之前导致失败的错误已经被修复,此事熔断器就切换到闭合状态,并且将计数器重置。如果这些试着发送的请求还是失败,则认为之前的问题没有解决,熔断器切回到打开模式,然后开始重置计数器给系统一定的时间来修复错误
  • 接着重复以上过程,直到半打开状态重复的次数达到一定的阈值发现错误还没被修复,从而触发"降级"状态

降级模式

场景:

1.某些时候系统会遇到负载过高的问题,当系统外来的或者内部的负载过高,超过预先定义的阈值,为了保证更加重要的业务的服务质量,希望将一些非核心的业务降低服务质量,从而释放一些额外的资源给紧急业务使用。 比如一个分布式系统里的读、写、数据校验、空间回收都比较消耗资源,在业务高峰期为了保证读和写的服务治理,可以把数据校验的服务通过限流或者减少线程数之类的方式,使得该服务能够调用的资源配额减少,从而释放部分资源给读和写使用,保证读写的服务质量。

同样,在读和写业务不繁忙的时候,降低业务的资源配额,从而释放资源给空间回收使用。通过这种方式动态调整局部业务的服务质量从而保证关键业务的服务治理,提升用户体验。

2.在云服务中"可用性"是一个很重要的指标,所以我们希望分布式的系统尽可能稳定,不管出现怎么样的故障,都能过保持基本的可用性

降级模式

可以从故障处理和系统服务质量两个角度理解降级模式

从故障处理角度来说,服务降级就是这一功能或者服务直接不可用。

而在动态调整系统整体的服务质量的时候,降级是降低某些当前非重要或者非核心业务的资源,从而释放部分资源给重要的或紧急的业务使用

故障处理:是比熔断更加严重的故障处理方式,最后拿来兜底用的。

比如某个功能出故障,"熔断"是还有希望将这个功能救活。而"降级"是发现救了几次没活之后,直接砍掉这个服务,保证服务整体不出问题

系统服务质量:分为读功能降级、写功能降级、级联组件降级,还有自动降级或者人工降级。比如在云服务里,为了保证高可用性,在出现系统级的故障后,可以把写功能降级,就是这个服务状态变为只能读、只能查询而不能写。

因此在设计的比较好的云服务里,按时间的纬度来度量可用性已经没有了太大的意义,因为不管怎样服务都是可用的,系统都是活着的,起码部分服务可用,因此在云服务里更合理的新的衡量可用性的指标是请求失败比率,即哪些服务不能对外提供能力,占比具体为多少

降级设计思路

触发策略

  • 超时降级:在超时重试的次数达到一个阈值后就触发降级
  • 失败比率重试:当某个服务的失败比率达到一定比率后开始降级
  • 系统故障降级:比如网络故障,硬盘故障,电源故障,服务器故障,数据中心故障等等
  • 限流降级:某些访问量太大的场景会触发限流,当达到限流的阈值后,请求也会被降级
  • 重要业务救急:比如为了保证读或者查询的功能,降低写、数据校验的资源配额

降级处理

  • 资源配额调度,调度不紧急的业务支援紧急的重要的业务
  • 抛出异常,直接抛出异常,打印出错误日志,然后就不管了,请求会丢失,这在需要保证幂等性的请求里不太合适
  • 直接返回,直接返回拒绝服务,这里请求也会丢失,这在需要保证幂等性的请求里不太合适
  • 调用回退方法,调用出现服务降级时对应的业务处理逻辑,不同场景降级的逻辑不一样,比如可以把请求挂在等待队列里继续重试之类,这里需要根据业务场景合理设计回退方法

服务降级策略

可以把降级的等级分为几个层次,比如PO P1 P2 P3等等,等级越高表示问题越严重

1.重要业务救急降级可以定义为P0级,只是调度次要的资源去救急,不会出现故障

2.限流降级可以定义为P1 ,只是为了保证服务质量,而且如果不限流可能会出现系统负载过高从而出现故障

3.超时/失败比率降级以及失败比率可以定义为P2 出现小范围故障 而不蔓延不传播

4.系统故障级别可以定义为P3级别 此事可以只保证最低资源的读请求服务

image.png 通常来说,分布式系统中每个服务的配置信息会保存在一个配置中心里,这个配置中心里可以有有每个服务的开关信息以及一些重要的资源配置信息。通过动态调整服务的配置信息,比如降级触发策略、降级处理措施、降级分级策略等来实现服务降级功能。

分布式配置中心:管理各个服务的各种配置信息,包括但不限于以下内容

  1. 应用程序的基本配置参数,如数据库连接信息、日志级别、缓存配置等等
  2. 服务之间的调用配置,如远程服务的地址、超时设置、负载均衡策略等等
  3. 业务规则的配置,如业务策略、规则、权限等等
  4. 动态特性的配置,如开关、AB测试等等
  5. 系统的降级策略配置,如降级的规则,降级处理的方式等等

具体的实现可以如下,在配置中心定义一个降级策略的配置项,然后在系统中读取该配置项并根据其值进行对应的降级处理。

参照上文,可以定义一个DegradeStrategy配置项,值为NULL P0 P1 P2 P3几个常量中的一个,在程序代码中读取加载该配置,分为以下几种情况进行请求的处理

  • NULL 不进行降级处理
  • P0 走业务备用逻辑
  • P2 采用模拟的数据 或者缓存的数据进行响应
  • P3 直接返回错误

限流

动机:

可靠性:每个系统都有自己的容量限制,也就是说能够处理的业务请求能力是有限的,如果不控制这些输入的请求数,突发输入过多的请求量会造成过度的资源竞争从而引发系统故障降低系统的可靠性

可用性:限流有利于控制系统资源的消耗速率,有利于过载保护保证业务资源不被耗尽

流量监管:对输入的请求流量进行细粒度的控制,通过监管输入的请求量速率对超出的部分进行惩罚,比如直接丢弃,使得进入系统的请求量被限制在一个系统所能承受的合理的范围之内,流量监管比较适合对延时要求较高的业务

流量整形:控制最大输出请求速率,以确保请求量符合系统容量配置的最大传输速率规定。请求的流量被整形进行平滑处理,以使它符合下游服务的速率需求,流量整形比较适合可靠性要求较高的业务

流这个词在不同上下文的含义是不一样的

  • 网络限流:带宽、流量
  • IO限流:TPS QPS
  • 并发限流:并发请求数
  • 线程限流:线程数

限流处理策略

  • 直接拒绝:当请求量超过阈值之后,新的请求就会被直接拒绝,方式为直接返回或者抛出异常。这种方式比较适合对分布式系统的负载容量已知的情况下,比如通过全链路压测已经确定了准确的系统处理能力以及系统容量
  • 冷启:当分布式系统长期处于低负载的情况下,请求量突发的时候,会把请求负载很快拉到很高的水准,这样可能瞬间就把系统击垮。通过"冷启"的方式,让输入的请求量缓慢增加,慢慢增加到阈值附近,对应的是令牌桶算法
  • 匀速排队:系统流量均匀,对应漏桶算法

纵向限流

两窗算法+两桶算法(固定窗口、滑动窗口)+(令牌桶、漏桶) 按照工作原理又可以划分为 保险丝模式和变压器模式

保险丝与两窗算法

保险丝: 电路中的保险丝主要是起电流过载保护作用,当电路中的电流过载的时候,保险丝自身就会烧坏从而切断电流,保护后续电路的安全运行。

这与限流算法中的窗口算法原理类似,在拒绝请求之后,需要重新设置计数,因此我们定义它们为限流保险丝模式

固定窗口: 按照时间线划分成一个个固定大小的时间窗口,并且每个窗口都有一个计数器来统计这一时间窗口内的访问次数,如果访问的次数超过了一个预先定义的阈值,则拒绝接下来的请求。直到下一个时间窗口,开始重新计数,当计数器又超过则继续拒绝,再在下一个时间窗口重新设置计数器继续计数,依次类推...... image.png 优点: 实现简单 一个计数器就可以实现

缺点有边界场景和跨窗口场景两个点,前者导致流量不均,可能有时候无法处理某些请求;后者导致流量可能超过阈值而带来风险

边界场景:

在第一个[0,5]的时间窗口内,第一秒就把计数器打到超过500,则后续的四秒将无法服务,得等到下一个[5,10]的时间窗口内计数器才被重置为0,才可以对外提供服务

跨窗口场景:

当第一个时间窗口的[4,5]的计数器为300,没有超过阈值,然后第二个时间窗口的[5,6]计数器为320,也没超过阈值,但是在[4,6]的时间窗口内计数为620 超过阈值,可能带来风险

滑动窗口:

也类似于固定窗口的计数器,不过将窗口按照时间线做了进一步的划分,每次往后移动一个细分单元,再每次都对一个小窗口进行计数统计实现流量控制。比如刚刚上图,把窗口的大小从5S缩小到1S,且会自动按照时间线进行移动

能很好的规避掉跨窗口场景 但是对边界场景还是会不太平滑 不过也比固定窗口好很多了

变压器与两桶算法

变压器指的是将电路中某一等级的电压或电流转换成另外一种同频率的电压或电流的设备,有利于稳流稳压,在计算机中对应的是两桶算法,即漏桶和令牌桶

漏桶

image.png image.png 漏桶算法工作步骤

  • 请求被随意的输入,有突发较多的请求量也有较小的请求量,这些请求进入系统之后不是立马被处理,而是放在一个桶中
  • 当桶了缓冲的请求超过设置的水位时,输入的请求被拒绝进入,直接丢失
  • 桶以恒定的结果将输入的请求输出

优点: 有利于流量的削峰填谷,且输出总是按照恒定的速率输出,因此有利于流量整形,平滑了突发的请求量

缺点:

1.无法接收突发流量 如果有超过桶设置水位的突发流量会被抛弃 这在幂等性的场景中明显是不适用的 比如支付场景 可能导致支付请求的丢失

2.因为漏桶总是按照恒定速率输出请求(也不是每时每刻都以该速率输出 当某时刻小于到达的请求量设置的输出值的时候 则会比设置值小),这是在假设后续的服务能够承接这个速率的前提之下的,如果无法保证这些输出的请求稳定的在一个固定的时间内处理完,可能会导致后续的服务进行资源抢用,而导致引发更大的级联故障

令牌桶

image.png 工作步骤

  • 这个桶每段时间会生成N个令牌
  • 桶子的最大令牌数量有限制
  • 如果有请求到来,则必须先在桶子里面拿令牌,然后进行请求的处理,之后从这个桶子里把令牌删掉
  • 如果桶子里面没令牌,当前请求无法通过,之后重试

优点:

  • 当桶子里的令牌满了,是丢令牌而不是丢请求,这样可以在幂等性请求的场景使用
  • 可以支持突发的流量

缺点: 对输出的请求速率没有做限制,有可能会打崩整个系统

算法实践:

  • 两窗算法实现比较简单,性能好,但是超出限流阈值之后会直接拒绝请求,适用于非幂等的请求场景
  • 漏桶算法,平滑控制输出的请求速率,但是超出水位的请求会被丢弃,适用于非幂等的请求场景
  • 令牌算法,可以支持突发的请求量,不控制输出的请求速率;超出阈值之后只会丢失令牌但不丢失请求,可以结合在幂等性请求的场景使用

横向限流

纵向限流解决的是某一个服务,一条链路的流量过高的问题,但是并没有解决这几个服务路径之间流量是否均匀分配的问题 image.png 横向限流的作用

  • 解决限流不均匀问题,尽可能让每个服务之间的流量是均匀的
  • 更细粒度的用户限流问题 限制每个用户(租户)可以进入系统的请求个数,纵向限流只能限制整体的进入网关的请求数,因此需要一个计数中心用于登记每个用户的请求数,从而进行更细粒度的流量控制,控制每个用户的请求数

通常是通过一个类似配置中心的方式实现横向限流 image.png

  • 可以将集群限流服务中心实现在一个网关实例里,与网关一起提供服务,好处是不需要再独立部署一个限流实例,缺点是如果网关挂掉,那么限流服务会一起挂掉,而且无法对网关层面进行横向限流,只实现了各个网关底下的服务的横向限流
  • 也可以独立拉起一个集群限流服务中心实例,用于提供全局限流计数服务,好处是与业务解耦,缺点是在集群内增加了一个额外的服务实例,增加了系统复杂度

常见的横向限流算法有计数算法以及时间标签算法

计数算法 image.png 拉起一个独立的分布式配置中心,在里面实现限流算法,比如两窗、两桶算法用于全局计数,而且保证这个计数是全局唯一的,不管集群规模多大,保证每个服务所使用的计数器和计时器都是唯一的,服务拿到这个计数ID之后再进行限流调度

CP模式:采用独立的限流中心,每个用户进入系统的请求都需要去远程的限流服务中心取一个计数返回,多了一个远程读取限流计数值的过程,会比较影响请求的性能 AP模式:本地维护一个限流技术的缓存,起一个独立线程维护,每隔一段时间本地限流缓存和远程进行同步,这种方式牺牲了限流的可靠性,但是保证了请求的性能

时间标签算法

计数算法只是实现了限制用户的请求量的最大值,并不能提供最小值保证,于是基于时间标签的算法被提出

例如在云服务中,用户1和用户2付费不一样,因此提供的最大限流上线是不一样的,但是如果采用计数算法的话并不能保证付费多的用户就一定能得到更高的服务质量保证。因此需要一个可以预留资源的算法

image.png 思路为:先保证最低的预留值,再根据权重划分剩下的资源,并且保证不要超过最大值。

猜你喜欢

转载自juejin.im/post/7234057613776240697