网站高可用架构设计——服务高可用

一、服务高可用基础

    接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了。例如,业务响应缓慢、大量访问超时、大量访问出现异常(给用户弹出提示“无法连接数据库”),这类问题的主要原因在于系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。例如,最常见的数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会访问抛出异常,一会访问又是正常结果。导致接口级故障的原因一般有下面几种:

  • 内部原因:程序 bug 导致死循环,某个接口导致数据库慢查询,程序逻辑不完善导致耗尽内存等。

  • 外部原因:黑客攻击、促销或者抢购引入了超出平时几倍甚至几十倍的用户,第三方系统大量请求,第三方系统响应缓慢等。

1.分级管理

    运维将服务器进行分级管理,核心应用和服务优先使用更好的硬件,在运维响应速度上也格外迅速。显然用户及时付款购物比能不能评价商品更重要,所以订单、只服务福比评价服务有更高的优先权。

    同时在服务部署上进行必要的隔离,避免故障的连锁反应。低优先级的服务通过启动不同的县城或者部署在不同的虚拟机进行隔离,而高优先级的服务则需要部署在不同的物理机上,核心服务和数据甚至需要部署在不同地域的数据中心。

2.超时

    针对服务调用都要设置一个超时时间,以避免依赖的服务迟迟没有返回调用结果,把服务消费者拖死。这其中,超时时间的设定也是有讲究的,不是越短越好,因为太短可能会导致有些服务调用还没有来得及执行完就被丢弃了;当然时间也不能太长,太长有可能导致服务消费者被拖垮。找到比较合适的超时时间需要根据正常情况下,服务提供者的服务水平来决定。具体来说,就是按照服务提供者线上真实的服务水平,取 P999 或者 P9999 的值,也就是以 99.9% 或者 99.99% 的调用都在多少毫秒内返回为准。

3.排队与异步调用

    排队指的是,当并发请求很多的时候系统根据请求的先后将请求放到一个队列中进行排队。由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用 Kafka 这类消息队列来缓存用户请求。

下面是 1 号店的“双 11”秒杀排队系统架构

图片

其基本实现摘录如下:

【排队模块】

负责接收用户的抢购请求,将请求以先入先出的方式保存下来。每一个参加秒杀活动的商品保存一个队列,队列的大小可以根据参与秒杀的商品数量(或加点余量)自行定义。

【调度模块】

负责排队模块到服务模块的动态调度,不断检查服务模块,一旦处理能力有空闲,就从排队队列头上把用户访问请求调入服务模块,并负责向服务模块分发请求。这里调度模块扮演一个中介的角色,但不只是传递请求而已,它还担负着调节系统处理能力的重任,可以根据服务模块的实际处理能力,动态调节排队系统拉取请求能力。

【服务模块】

负责调用真正业务来处理服务,并返回处理结果,调用排队模块的接口回写业务处理结果。

4.跨公网调用第三方高可用

调用失败后直接返回异常还是重试?

当有一个接口跨公网第三方调用超时时,可能导致所有接口都不可用,即使大部分接口不依赖于跨公网第三方调用。

内部服务对业务方提供的N个接口,会共用服务容器内的工作线程(假设有100个工作线程)。假设这N个接口的某个接口跨公网依赖于第三方的接口,发生了网络抖动,或者接口超时(不妨设超时时间为5秒)。潜台词是,这个工作线程会被占用5秒钟,然后超时返回业务调用方。假设这个请求的吞吐量为20qps,言下之意,很短的时间内,所有的100个工作线程都会被卡在这个第三方超时等待上,而其他N-1个原本没有问题的接口,也得不到工作线程处理。

(1)异步代理法

业务场景:通过OpenID实时获取微信用户基本信息。

解决方案:增加一个代理,向服务屏蔽究竟是“本地实时”还是“异步远程”去获取返回结果。

图片

本地实时流程如上图1-5:

  • 业务调用方调用内部service;

  • 内部service调用异步代理service;

  • 异步代理service通过OpenID在本地拿取数据;

  • 异步代理service将数据返回内部service;

  • 内部service返回结果给业务调用方;

远程异步流程如上图6-8粗箭头的部分:

  • 异步代理service定期跨公网调用微信服务;

  • 微信服务返回数据;

  • 刷新本地数据;

优点:公网抖动,第三方接口超时,不影响内部接口调用。

缺点:本地返回的不是最新数据(很多业务可以接受数据延时)。

(2)第三方接口备份与切换

业务场景:调用第三方短信网关,或者电子合同等。

解决方案:同时使用(或者备份)多个第三方服务。

图片

  • 业务调用方调用内部service;

  • 内部service调用第一个三方接口;

  • 超时后,调用第二个备份服务,未来都直接调用备份服务,直到超时的服务恢复;

  • 内部service返回结果给业务调用方;

优点:公网抖动,第三方接口超时,不影响内部接口调用(初期少数几个请求会超时)。

缺点:不是所有公网调用都能够像短息网关,电子合同服务一样有备份接口的,像微信、支付宝等就只此一家。

(3)异步调用法

业务场景:本地结果,同步第三方服务,例如用户在58到家平台下单,58到家平台需要通知平台商家为用户提供服务。

解决方案:本地调用成功就返回成功,异步调用第三方接口同步数据(和异步代理有微小差别)。

图片

本地流程如上图1-3:

  • 业务调用方调用内部service;

  • 内部service写本地数据;

  • 内部service返回结果给业务调用方成功;

异步流程如上图4-5粗箭头的部分:

  • 异步service定期将本地数据取出(或者通知也行,实时性好);

  • 异步调用第三方接口同步数据;

优点:公网抖动,第三方接口超时,不影响内部接口调用。

缺点:不是所有业务场景都可以异步同步数据。

二、补偿机制——宁可慢,不可错。

    应用系统在分布式的情况下,在通信时会有着一个显著的问题,即一个业务流程往往需要组合一组服务,且单单一次通信可能会经过 DNS 服务,网卡、交换机、路由器、负载均衡等设备,而这些服务于设备都不一定是一直稳定的,在数据传输的整个过程中,只要任意一个环节出错,都会导致问题的产生。

    这样的事情在微服务下就更为明显了,因为业务需要在一致性上的保证。也就是说,如果一个步骤失败了,要么不断重试保证所有的步骤都成功,要么回滚到以前的服务调用。因此我们可以对业务补偿的过程进行一个定义,即当某个操作发生了异常时,如何通过内部机制将这个异常产生的「不一致」状态消除掉。

  • 回滚(事务补偿),逆向操作,回滚业务流程,意味着放弃,当前操作必然会失败;

  • 重试,正向操作,努力地把一个业务流程执行完成,代表着还有成功的机会。

1.重试

(1)重试简介

Retry 机制非常适合服务短时间不可用,或某个服务节点异常 这类场景。

虽然设置超时时间可以起到及时止损的效果,但是服务调用的结果毕竟是失败了,而大部分情况下,调用失败都是因为偶发的网络问题或者个别服务提供者节点有问题导致的,如果能换个节点再次访问说不定就能成功。而且从概率论的角度来讲,假如一次服务调用失败的概率为 1%,那么连续两次服务调用失败的概率就是 0.01%,失败率降低到原来的 1%。

所以,在实际服务调用时,经常还要设置一个服务调用超时后的重试次数。假如某个服务调用的超时时间设置为 100ms,重试次数设置为 1,那么当服务调用超过 100ms 后,服务消费者就会立即发起第二次服务调用,而不会再等待第一次调用返回的结果了。

在应用程序和客户端添加重试逻辑需保持谨慎,因为大量的重试会让事情变得更糟,甚至会阻止应用程序的恢复。在分布式系统中,微服务系统重试会触发多个其他的请求或重试,引起一个级联效应。为了尽量减少重试带来的影响,你应该最大限度限制它们的发生次数,并使用指数补偿算法来持续增加重试之间的延迟。重试由客户端(浏览器,其他微服务等)发起,客户端不知道这个操作是在处理请求之前失败还是之后失败的,你应该准备好应用程序来处理幂等性(idempotency)。例如,当操作重试购买时,不应该对用户进行重复扣费。对于每个事务,使用唯一的 幂等令牌(idempotency-key ),可以帮助处理重试。

设计重试时,我们需要定义出什么情况下需要重试,例如,调用超时、被调用端返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)。而对于一些别的错误,则最好不要重试,比如:业务级的错误(如没有权限、或是非法数据等错误),技术上的错误(如:HTTP 的 503 等,这种原因可能是触发了代码的 bug,重试下去没有意义)。

(2)重试的策略

    关于重试的设计,一般来说,都需要有个重试的最大值,经过一段时间不断的重试后,就没有必要再重试了,应该报故障了。在重试过程中,每一次重试失败时都应该休息一会儿再重试,这样可以避免因为重试过快而导致网络上的负担加重。 在重试的设计中,一般都会引入,Exponential Backoff 的策略,也就是所谓的 " 指数级退避 "。在这种情况下,每一次重试所需要的休息时间都会成倍增加。这种机制主要是用来让被调用方能够有更多的时间来从容处理我们的请求。

public static long getWaitTimeExp(int retryCount) {long waitTime = ((long) Math.pow(2, retryCount) );
return waitTime;
}

图片

(3)Spring 的重试策略

Spring系列spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于简单注释的。

图片

配置 @Retryable 注解,只对 SQLException 的异常进行重试,重试两次,每次延时5000ms。相关的细节可以看相应的文档。

①重试策略

  • NeverRetryPolicy:只允许调用 RetryCallback 一次,不允许重试。

  • AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环。

  • SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为 3 次,RetryTemplate默认使用的策略。

  • TimeoutRetryPolicy:超时时间重试策略,默认超时时间为 1 秒,在指定的超时时间内允许重试。

  • CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置 3 个参数openTimeout、resetTimeout 和 delegate;

  • CompositeRetryPolicy:组合重试策略。有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即不可以。但不管哪种组合方式,组合中的每一个策略都会执行。

关于 Backoff 的策略如下。

  • NoBackOffPolicy:无退避算法策略,即当重试时是立即重试;

  • FixedBackOffPolicy:固定时间的退避策略,需设置参数 sleeper 和backOffPeriod,sleeper 指定等待策略,默认是 Thread.sleep,即线程休眠,backOffPeriod 指定休眠时间,默认 1 秒。UniformRandomBackOffPolicy:随机时间退避策略,需设置 sleeper、minBackOffPeriod 和 maxBackOffPeriod。该策略在 [minBackOffPeriod,maxBackOffPeriod] 之间取一个随机休眠时间,minBackOffPeriod 默认为 500 毫秒,maxBackOffPeriod 默认为 1500 毫秒。ExponentialBackOffPolicy:指数退避策略,需设置参数 sleeper、initialInterval、maxInterval 和 multiplier。initialInterval 指定初始休眠时间,默认为 100 毫秒。maxInterval 指定最大休眠时间,默认为 30 秒。multiplier 指定乘数,即下一次休眠时间为当前休眠时间 *multiplier。ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致 DDos,使用随机休眠时间来避免这种情况。

(4)重试设计的重点

  • 要确定什么样的错误下需要重试;

  • 重试的时间和重试的次数。这种在不同的情况下要有不同的考量。有时候,而对一些不是很重要的问题时,我们应该更快失败而不是重试一段时间若干次。比如一个前端的交互需要用到后端的服务。这种情况下,在面对错误的时候,应该快速失败报错(比如:网络错误请重试)。而面对其它的一些错误,比如流控,那么应该使用指数退避的方式,以避免造成更多的流量。

  • 如果超过重试次数,或是一段时间,那么重试就没有意义了。这个时候,说明这个错误不是一个短暂的错误,那么我们对于新来的请求,就没有必要再进行重试了,这个时候对新的请求直接返回错误就好了。但是,这样一来,如果后端恢复了,我们怎么知道呢,此时需要使用我们的熔断设计了。

  • 重试还需要考虑被调用方是否有幂等的设计。如果没有,那么重试是不安全的,可能会导致一个相同的操作被执行多次。

  • 重试的代码比较简单也比较通用,完全可以不用侵入到业务代码中。这里有两个模式。一个是代码级的,像 Java 那样可以使用 Annotation 的方式(在 Spring 中你可以用到这样的注解),如果没有注解也可以包装在底层库或是 SDK 库中不需要让上层业务感知到。另外一种是走 Service Mesh 的方式 对于有事务相关的操作。我们可能会希望能重试成功,而不至于走业务补偿那样的复杂的回退流程。对此,我们可能需要一个比较长的时间来做重试,但是我们需要保存请求的上下文,这可能对程序的运行有比较大的开销,因此,有一些设计会先把这样的上下文暂存在本机或是数据库中,然后腾出资源来做别的事,过一会再回来把之前的请求从存储中捞出来重试。

2.双发

假如一次调用不成功的概率为 1%,那么连续两次调用都不成功的概率就是 0.01%,根据这个推论,一个简单的提高服务调用成功率的办法就是每次服务消费者要发起服务调用的时候,都同时发起两次服务调用,一方面可以提高调用的成功率,另一方面两次服务调用哪个先返回就采用哪次的返回结果,平均响应时间也要比一次调用更快,这就是双发。

但是这样的话,一次调用会给后端服务两倍的压力,所要消耗的资源也是加倍的,,还有一种双发的形式即“备份请求”(Backup Requests),它的大致思想是服务消费者发起一次服务调用后,在给定的时间内如果没有返回请求结果,那么服务消费者就立刻发起另一次服务调用。这里需要注意的是,这个设定的时间通常要比超时时间短得多,比如超时时间取的是 P999,那么备份请求时间取的可能是 P99 或者 P90,这是因为如果在 P99 或者 P90 的时间内调用还没有返回结果,那么大概率可以认为这次请求属于慢请求了,再次发起调用理论上返回要更快一些。

在实际线上服务运行时,P999 由于长尾请求时间较长的缘故,可能要远远大于 P99 和P90。在我经历的一个项目中,一个服务的 P999 是 1s,而 P99 只有 200ms、P90 只有50ms,这样的话,如果备份请求时间取的是 P90,那么第二次请求等待的时间只有50ms。不过这里需要注意的是,备份请求要设置一个最大重试比例,以避免在服务端出现问题的时,大部分请求响应时间都会超过 P90 的值,导致请求量几乎翻倍,给服务提供者造成更大的压力。我的经验是这个最大重试比例可以设置成 15%,一方面能尽量体现备份请求的优势,另一方面不会给服务提供者额外增加太大的压力。

3.回滚

  • 显式回滚;调用逆向接口,进行上一次操作的反操作,或者取消上一次还没有完成的操作(须锁定资源);

    • 首先要确定失败的步骤和状态,从而确定需要回滚的范围。一个业务的流程,往往在设计之初就制定好了,所以确定回滚的范围比较容易。但这里唯一需要注意的一点就是:如果在一个业务处理中涉及到的服务并不是都提供了「回滚接口」,那么在编排服务时应该把提供「回滚接口」的服务放在前面,这样当后面的工作服务错误时还有机会「回滚」。

    • 其次要能提供「回滚」操作使用到的业务数据。「回滚」时提供的数据越多,越有益于程序的健壮性。因为程序可以在收到「回滚」操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等。

  • 隐式回滚:隐式回滚意味着这个回滚动作你不需要进行额外处理,往往是由下游提供了失败处理机制的。

对于跨库的事务,比较常见的解决方案有:两阶段提交、三阶段提交(ACID)但是这 2 种方式,在高可用的架构中一般都不 可取,因为跨库锁表会消耗很大的性能。

高可用的架构中一般不会要求强一致性,只要达到最终的一致性就可以了。可以考虑:事务表、消息队列、补偿机制、TCC 模式(占位 / 确认或取消)、Sagas模式(拆分事务 + 补偿机制)来实现最终的一致性。

三、熔断

1.熔断简介

假如服务提供者出现故障,短时间内无法恢复时,无论是超时重试还是双发不但不能提高服务调用的成功率,反而会因为重试给服务提供者带来更大的压力,从而加剧故障。针对这种情况,就需要服务消费者能够探测到服务提供者发生故障,并短时间内停止请求,给服务提供者故障恢复的时间,待服务提供者恢复后,再继续请求。这就好比一条电路,电流负载过高的话,保险丝就会熔断,以防止火灾的发生,所以这种手段就被叫作“熔断”。

简单来讲,熔断就是把客户端的每一次服务调用用断路器封装起来,通过断路器来监控每一次服务调用。如果某一段时间内,服务调用失败的次数达到一定阈值,那么断路器就会被触发,后续的服务调用就直接返回,也就不会再向服务提供者发起请求了。得熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够 记录最近调用发生错误的次数,然后决定允许操作继续,或者立即返回错误。

2.熔断状态

熔断之后,一旦服务提供者恢复之后,服务调用如何恢复呢?这就牵扯到熔断中断路器的几种状态。

  • Closed 状态:我们需要一个调用失败的计数器,如果调用失败,则使失败次数 加 1。如果最近失败次数超过了在给定时间内允许失败的阈值,则切换到断开(Open) 状态。此时开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(HalfOpen)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误,以回到正常工作的状态。在 Closed 状态下,错误计数器是基于时间的。在特定的时间间隔内会自动重置。这能够防止由于某次的偶然错误导致熔断器进入断开状态。也可以基于连续失败的次数。

  • Open 状态:当服务调用失败次数达到一定阈值时,断路器就会处于开启状态,后续的服务调用就直接返回,不会向服务提供者发起请求。

  • Half Open 状态:当断路器开启后,每隔一段时间,会进入半打开状态,这时候会向服务提供者发起探测调用,以确定服务提供者是否恢复正常。如果调用成功了,断路器就关闭;如果没有成功,断路器就继续保持开启状态,并等待下一个周期重新进入半打开状态。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮

图片

3.Hystrix实现

关于断路器的实现,最经典也是使用最广泛的莫过于 Netflix 开源的 Hystrix 了。Hystrix 的断路器也包含三种状态:关闭、打开、半打开。Hystrix 会把每一次服务调用都用 HystrixCommand 封装起来,它会实时记录每一次服务调用的状态,包括成功、失败、超时还是被线程拒绝。当一段时间内服务调用的失败率高于设定的阈值后,Hystrix 的断路器就会进入进入打开状态,新的服务调用就会直接返回,不会向服务提供者发起调用。再等待设定的时间间隔后,Hystrix 的断路器又会进入半打开状态,新的服务调用又可以重新发给服务提供者了;如果一段时间内服务调用的失败率依然高于设定的阈值的话,断路器会重新进入打开状态,否则的话,断路器会被重置为关闭状态。​​​​​​​

//其中决定断路器是否打开的失败率阈值可以通过下面这个参数来设定:HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()//而决定断路器何时进入半打开的状态的时间间隔可以通过下面这个参数来设定:HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()

断路器实现的关键就在于如何计算一段时间内服务调用的失败率,那么 Hystrix 是如何做的呢?

图片

    Hystrix 通过滑动窗口来对数据进行统计,默认情况下,滑动窗口包含 10 个桶,每个桶时间宽度为 1 秒,每个桶内记录了这 1 秒内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。当新的 1 秒到来时,滑动窗口就会往前滑动,丢弃掉最旧的 1 个桶,把最新 1 个桶包含进来

    任意时刻,Hystrix 都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据,这 10 个桶内记录的所有失败的、超时的、被线程拒绝的调用次数之和除以总的调用次数就是滑动窗口内所有服务的调用的失败率。

    应该使用断路器的方式实现舱壁模式,舱壁被用来将一艘船划分成多个部分,这样就可以在船体破裂的情况下对部分封闭。隔离壁的概念可以应用于软件开发中,做到资源隔离。通过采用舱壁模式,可以保护有限的资源不被耗尽。例如,如果有两种操作,它们与相同的数据库实例交互,我们的连接数量有限,那么可以使用两个连接池,而不是共享连接池。由于此客户端资源分离,当发生超时或者过度使用连接池的操作,不会导致所有其他操作的关闭。

4.熔断设计的重点

  • 错误的类型。需要注意的是请求失败的原因会有很多种。你需要根据不同的错误情况来调整相应的策略。所以,熔断和重试一样,需要对返回的错误进行识别。一些错误先走重试的策略(比如限流,或是超时),重试几次后再打开熔断。一些错误是远程服务挂掉,恢复时间比较长;这种错误不必走重试,就可以直接打开熔断策略。

  • 日志监控。熔断器应该能够记录所有失败的请求,以及一些可能会尝试成功的请求,使得管理员能够监控使用熔断器保护服务的执行情况。

  • 测试服务是否可用。在断开状态下,熔断器可以采用定期地 ping 一下远程服务的健康检查接口,来判断服务是否恢复,而不是使用计时器来自动切换到半开状态。这样做的一个好处是,在服务恢复的情况下,不需要真实的用户流量就可以把状态从半开状态切回关闭状态。否则在半开状态下,即便服务已恢复了,也需要用户真实的请求来恢复,这会影响用户的真实请求。

  • 手动重置。在系统中对于失败操作的恢复时间是很难确定的,提供一个手动重置功能能够使得管理员可以手动地强制将熔断器切换到闭合状态。同样的,如果受熔断器保护的服务暂时不可用的话,管理员能够强制将熔断器设置为断开状态。并发问题。相同的熔断器有可能被大量并发请求同时访问。熔断器的实现不应该阻塞并发的请求或者增加每次请求调用的负担。尤其是其中对调用结果的统计,一般来说会成为一个共享的数据结构,它会导致有锁的情况。在这种情况下,最好使用一些无锁的数据结构,或是 atomic 的原子操作。这样会带来更好的性能。

  • 资源分区。有时候,我们会把资源分布在不同的分区上。比如,数据库的分库分表,某个分区可能出现问题,而其它分区还可用。在这种情况下,单一的熔断器会把所有的分区访问给混为一谈,从而,一旦开始熔断,那么所有的分区都会受到熔断影响。或是出现一会儿熔断一会儿又好,来来回回的情况。所以,熔断器需要考虑这样的问题,只对有问题的分区进行熔断,而不是整体。

  • 重试错误的请求。有时候,错误和请求的数据和参数有关系,所以,记录下出错的请求,在半开状态下重试能够准确地知道服务是否真的恢复。当然,这需要被调用端支持幂等调用,否则会出现一个操作被执行多次的副作用。

四、服务降级

降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能,降级的核心思想就是优先保证核心业务。例如,论坛可以降级为只能看帖子,不能发帖子;也可以降级为只能看帖子和评论,不能发评论;而 App 的日志上传接口,可以完全停掉一段时间,这段时间内 App 都不能上传日志。

1.降级可能会牺牲

(1)降低一致性

从强一致性变成最终一致性。

把强一致性变成最终一致性的做法可以有效地释放资源,并且让系统运行得更快,从而可以扛住更大的流量。一般来说,会有两种做法,一种是简化流程的一致性,一种是降低数据的一致性。

降低数据的一致性一般来说会使用缓存的方式,或是直接就去掉数据。比如,在页面上不显示库存的具体数字,只显示有还是没有库存这两种状态。对于缓存来说,可以有效地降低数据库的压力,把数据库的资源交给更重要的业务,这样就能让系统更快速地运行。

(2)停止次要功能。

停止访问不重要的功能,从而释放出更多的资源。

最好不要停止次要的功能,首先可以限制次要的功能的流量,或是把次要的功能退化成简单的功能,最后如果量太大了,我们才会进入停止功能的状态。

(3)简化功能。

把一些功能简化掉,比如,简化业务流程,或是不再返回全量数据,只返回部分数据。

2.实现降级的方式

(1)系统后门降级

简单来说,就是系统预留了后门用于降级操作。例如,系统提供一个降级 URL,当访问这个 URL 时,就相当于执行降级指令,具体的降级指令通过 URL 的参数传入即可。这种方案有一定的安全隐患,所以也会在 URL 中加入密码这类安全措施。

系统后门降级的方式实现成本低,但主要缺点是如果服务器数量多,需要一台一台去操作,效率比较低,这在故障处理争分夺秒的场景下是比较浪费时间的。

(2)独立降级系统

为了解决系统后门降级方式的缺点,将降级操作独立到一个单独的系统中,可以实现复杂的权限管理、批量操作等功能。其基本架构如下:

图片

3.降级种类

(1)兜底数据

这方面有很多例子,比如某些页面挂了会返回寻亲子网。可以对一些关键数据设置一些兜底数据,例如设置默认值、静态值、设置缓存等。

  • 默认值: 设置安全的默认值,不会引起数据问题的值,比如库存为0

  • 静态值:请求的页面或api无法返回数据,提供一套静态数据展示,比如加载失败提示重试,或者寻亲子网,或者跳到默认菜单,给用户一个稍微好一点的体验。

  • 缓存: 缓存无法更新便使用旧的缓存

(2)超时降级

对调用的数据设置超时时间,当调用失败时,对服务降级,举个例子,当访问数据已经超时了,且这个业务不是核心业务,可以在超时之后进行降级,比如商品详情页上有推荐内容或者评价,但是可以降级显示评价暂时不显示,这对主要的用户功能——购物,不产生影响,如果是远程调用,则可以商量一个双方都可以接受的最大响应时间,超时则自动降级。

(3)故障降级

如果远程调用的服务器挂了(网络故障、DNS故障、HTTP服务返回错误),则可以进行降级, 例如返回默认值或者兜底数据或者静态页面,也可以返回之前的缓存数据。

(4)读降级

简而言之,在一个请求内,多级缓存架构下,后端缓存或db不可用,可以使用前端缓存或兜底数据让用户体验好一点。

对于读服务降级一般采用的策略有:暂时切换读: 降级到读缓存、降级到走静态化暂时屏蔽读: 屏蔽读入口、屏蔽某个读服务

通常读的流程为: 接入层缓存→应用层本地缓存→分布式缓存→RPC服务/DB

会在接入层、应用层设置开关,当分布式缓存、RPC服务/DB有问题时自动降级为不调用。当然这种情况适用于对读一致性要求不高的场景。

页面降级、页面片段降级、页面异步请求降级都是读服务降级,目的是丢卒保帅,保护核心线程,或者因数据问题暂时屏蔽。还有一种是页面静态化场景。

  • 动态化降级为静态化:比如,平时网站可以走动态化渲染商品详情页,但是,到了大促来临之际可以将其切换为静态化来减少对核心资源的占用,而且可以提升性能。其他还有如列表页、首页、频道页都可以这么处理。可以通过一个程序定期推送静态页到缓存或者生成到磁盘,出问题时直接切过去。

  • 静态化降级为动态化:比如,当使用静态化来实现商品详情页架构时,平时使用静态化来提供服务,但是,因为特殊原因静态化页面有问题了,需要暂时切换回动态化来保证服务正确性。以上都保证了出问题时有预案,用户可以继续使用网站,不影响用户购物体验。

(5)写降级

硬盘性能比不上内存性能,如果访问量很高的话,数据库频繁读写可能撑不住,那么怎么办呢,可以让内存(假如是Redis)库来暂时满足写任务,同时将执行的指令记录下来,然后将这个信息发送到数据库,也就是不在追求内存与数据库数据的强一致性,只要数据库数据与Redis数据库中的信息满足最终话一致性即可。

也就是说,正常情况下可以同步扣减库存,在性能扛不住时,降级为异步。另外,如果是秒杀场景可以直接降级为异步,从而保护系统。还有,如下单操作可以在大促时暂时降级,将下单数据写入Redis,然后等峰值过去了再同步回DB,当然也有更好的解决方案,但是更复杂。

还有如用户评价,如果评价量太大,那么也可以把评价从同步写降级为异步写。当然也可以对评价按钮进行按比例开放(比如,一些人看不到评价操作按钮)。比如,评价成功后会发一些奖励,在必要的时候降级同步到异步。

在cap原理和BASE理论中写操作存在于数据一致性这个环节,降级的目的是为了提供高可用性,在多数的互联网架构中,可用性是大于数据一致性的。所以丧失写入数据同步,通过上面的理论,我们也能勉强接受数据最终一致性。高并发场景下,写入操作无法及时到达或抗压,可以异步消费数据/cache更新/log等方式

(6)其它降级

  • 前端降级:当系统出现问题的时候,尽量将请求隔离在离用户最近的位置,避免无效链路访问, 在后端服务部分或完全不可用的时候,可以使用本地缓存或兜底数据,在一些特殊场景下,对数据一致性要求不高的时候,比如秒杀、抽奖等可以做假数据。

  • JS降级:在js中埋降级开关,在访问不到达,系统阈值的时候可以避免发送请求。主要控制页面功能的降级,在页面中,通过JS脚本部署功能降级开关,在适当时机开启/关闭开关。

  • 接入层降级:可以在接入层,在用户请求还没到达服务的时候,通过、Nginx + Lua、Haproxy + lua过滤无效请求达到服务降级的目的, 主要控制请求入口的降级,请求进入后,会首先进入接入层,在接入层可以配置功能降级开关,可以根据实际情况进行自动/人工降级。这个可以参考第17章,尤其在后端应用服务出问题时,通过接入层降级从而给应用服务有足够的时间恢复服务。

  • 应用层降级:主要控制业务的降级,在应用中配置相应的功能开关,根据实际业务情况进行自动/人工降级。SpringCloud中可以通过Hystrix配置中心可以进行人工降级

  • 片段降级:例如打开淘宝首页,这一瞬间需要加载很多数据,有静态的例如图片、CSS、JS等,也有很多其他商品等等,这么多数据中,如果一部分没有请求到,那么就可以片段降级,意思是就不加载这些数据了,用其他数据顶替,例如其他商品信息或者等等。

  • 提前预埋:每次双十一之前,淘宝总会提醒你下载更新,按道理来讲,活动还没开始,更新啥呢?做法是对于一部分静态数据可以提前更新到你手机上,当你双十一时就不用再远程连接服务器加载了,避免了消耗网络资源。

4.降级开关

在服务器提供支持期间, 如果监控到线上一些服务存在问题,这个时候需要暂时将这些服务去掉,有时候通过服务调用一些服务,但是服务依赖的数据库可能存在,网卡被打满了,数据库挂了,很多慢查询等等,此时要做的就是暂停相关的系统服务,也就是人工使用开关降级。开关可以放在某地,定期同步开关数据,通过判断开关值来决定是否做出降级。

开关降级还有一个作用,例如新的服务版本刚开发处在灰度测试阶段,不太确定里面的逻辑等等是否正确,如果有问题应该可以根据开关的值切回旧的版本。

在服务调用方设置一个flag,标记服务是否可用,另外key可以存储存储在在本地,也可以存储在第三方的配置文件中,例如数据库、redis、zookeeper中。

比如,如果数据库的 压力比较大,在降级的时候,可以考虑只读取缓存的数据,而不再读取数据库中的 数据;如果非核心接口出现问题,可以直接返回服务繁忙或者返回固定的降级数据。

开关一般用在两种地方,一种是新增的业务逻辑,因为新增的业务逻辑相对来说不成熟,往往具备一定的风险,所以需要加开关来控制新业务逻辑是否执行;另一种是依赖的服务或资源,因为依赖的服务或者资源不总是可靠的,所以最好是有开关能够控制是否对依赖服务或资源发起调用,来保证即使依赖出现问题,也能通过降级来避免影响。

5.降级设计要点

在实际业务应用的时候,降级要按照对业务的影响程度进行分级,一般分为三级:一级降级是对业务影响最小的降级,在故障的情况下,首先执行一级降级,所以一级降级也可以设置成自动降级,不需要人为干预;二级降级是对业务有一定影响的降级,在故障的情况下,如果一级降级起不到多大作用的时候,可以人为采取措施,执行二级降级;三级降级是对业务有较大影响的降级,这种降级要么是对商业收入有重大影响,要么是对用户体验有重大影响,所以操作起来要非常谨慎,不在最后时刻一般不予采用。

  • 在设计降级的时候,需要清楚地定义好降级的关键条件,比如,吞吐量过大、响应时间过慢、失败次数多过,有网络或是服务故障,等等,然后做好相应的应急预案。这些预案最好是写成代码可以快速地自动化或半自动化执行的。

  • 功能降级需要梳理业务的功能,哪些是 must-have 的功能,哪些是 nice-to-have 的功能;哪些是必须要死保的功能,哪些是可以牺牲的功能。而且需要在事前设计好可以简化的或是用来应急的业务流程。当系统出问题的时候,就需要走简化应急流程。

  • 降级的时候,需要牺牲掉一致性,或是一些业务流程:对于读操作来说,使用缓存来解决,对于写操作来说,需要异步调用来解决。并且,我们需要以流水账的方式记录下来,这样方便对账,以免漏掉或是和正常的流程混淆。

  • 降级的功能的开关可以是一个系统的配置开关。做成配置时,你需要在要降级的时候推送相应的配置。另一种方式是,在对外服务的 API 上有所区分(方法签名或是开关参数),这样可以由上游调用者来驱动。比如:一个网关在限流时,在协议头中加入了一个限流程度的参数,让后端服务能知道限流在发生中。当限流程度达到某个值时,或是限流时间超过某个值时,就自动开始降级,直到限流好转。

  • 对于数据方面的降级,需要前端程序的配合。一般来说,前端的程序可以根据后端传来的数据来决定展示哪些界面模块。比如,当前端收不到商品评论时,就不展示。为了区分本来就没有数据,还是因为降级了没有数据的两种情况,在协议头中也应该加上降级的标签。

五、限流

限流的目的是通过对并发访问进行限速,相关的策略一般是,一旦达到限制的速率,那么就会触发相应的限流行为。一般来说,触发的限流行为如下。

  • 拒绝服务。把多出来的请求拒绝掉。一般来说,好的限流系统在受到流量暴增时,会统计 当前哪个客户端来的请求最多,直接拒掉这个客户端,这种行为可以把一些不正常的或者是带有恶意的高并发访问挡在门外。

  • 服务降级。关闭或是把后端服务做降级处理。这样可以让服务有足够的资源来处理更多的请求。降级有很多方式,一种是把一些不重要的服务给停掉,把 CPU、内存或是数据的资源让给更重要的功能;一种是不再返回全量数据,只返回部分数据。因为全量数据需要做 SQL Join 操作,部分的数据则不需要,所以可以让 SQL 执行更快,还有最快的一种是直接返回预设的缓存,以牺牲一致性的方式来获得更大的性能吞吐。

  • 特权请求。所谓特权请求的意思是,资源不够了,我只能把有限的资源分给重要的用户,比如:分给权利更高的 VIP 用户。在多租户系统下,限流的时候应该保大客户的,所以大客户有特权可以优先处理,而其它的非特权用户就得让路了。

  • 延时处理。在这种情况下,一般会有一个队列来缓冲大量的请求,这个队列如果满了,那么就只能拒绝用户了,如果这个队列中的任务超时了,也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰刺请求。

  • 弹性伸缩。动用自动化运维的方式对相应的服务做自动化的伸缩。这个需要一个应用性能的监控系统,能够感知到目前最繁忙的 TOP 5 的服务是哪几个。然后去伸缩它们,还需要一个自动化的发布、部署和服务注册的运维系统,而且还要快,越快越好。否则,系统会被压死掉了。当然,如果是数据库的压力过大,弹性伸缩应用是没什么用的,这个时候还是应该限流。

1.限流方式

(1)基于请求限流

基于请求限流指从外部访问的请求角度考虑限流,常见的方式有:限制总量、限制时间量。

限制总量的方式是限制某个指标的累积上限,常见的是限制当前系统服务的用户总量,例如某个直播间限制总用户数上限为 100 万,超过 100 万后新的用户无法进入;某个抢购活动商品数量只有 100 个,限制参与抢购的用户上限为 1 万个,1 万以后的用户直接拒绝。限制时间量指限制一段时间内某个指标的上限,例如,1 分钟内只允许 10000 个用户访问,每秒请求峰值最高为 10 万。

无论是限制总量还是限制时间量,共同的特点都是实现简单,但在实践中面临的主要问题是比较难以找到合适的阈值,例如系统设定了 1 分钟 10000 个用户,但实际上 6000 个用户的时候系统就扛不住了;也可能达到 1 分钟 10000 用户后,其实系统压力还不大,但此时已经开始丢弃用户访问了。

即使找到了合适的阈值,基于请求限流还面临硬件相关的问题。例如一台 32 核的机器和 64 核的机器处理能力差别很大,阈值是不同的,可能有的技术人员以为简单根据硬件指标进行数学运算就可以得出来,实际上这样是不可行的,64 核的机器比 32 核的机器,业务处理性能并不是 2 倍的关系,可能是 1.5 倍,甚至可能是 1.1 倍。

为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化,即:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。

基于上述的分析,根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统,例如负载均衡系统、网关系统、抢购系统等。

(2)基于资源限流

基于请求限流是从系统外部考虑的,而基于资源限流是从系统内部考虑的,即:找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源有:连接数、文件句柄、线程数、请求队列等。

例如,采用 Netty 来实现服务器,每个进来的请求都先放入一个队列,业务线程再从队列读取请求进行处理,队列长度最大值为 10000,队列满了就拒绝后面的请求;也可以根据 CPU 的负载或者占用率进行限流,当 CPU 的占用率超过 80% 的时候就开始拒绝新的请求。

基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实践中设计也面临两个主要的难点:如何确定关键资源,如何确定关键资源的阈值。通常情况下,这也是一个逐步调优的过程,即:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。

2.流控类型

(1)静态流控

静态流控主要是针对客户端访问速率进行控制。传统静态流控设计采用安装预分配方案:在软件安装时,根据集群服务节点数和静态流控阈值,计算每个服务节点分摊的QPS(Query Per Second:每秒查询量)阈值,在系统云信时,各个服务节点按照已分配的阈值进行流控,超出流控阈值的请求拒绝访问。 缺点:

  • 静态分配方案的最大缺点就是忽略了服务实例的动态变化:

  • 云端服务的弹性伸缩特性,使得服务节点数处于动态变化中。预分配方案行不通。

  • 服务节点宕机,或者有新的服务节点加入,导致服务节点数发生变化,静态分配的QPS需要实时动态调整,否则会导致流控不准。

(2)动态配额分配制

图片

    由服务注册中心以流控周期T为单位,动态推送每个节点分配的流控阈值QPS。当服务节点发生变更时,会出发服务注册中心重新计算每个节点的配额,然后进行推送,这样无论是新增还是减少服务节点数,都能够再下一个流控周期内被识别和处理,这就解决了传统静态分配方案无法适应节点数动态变化的问题。分配策略:生产环境中,每台机器或者VM配置可能不同,采用平均分配可能不适合。

  • 一种解决方案:服务注册中心在做配额计算时,根据各个服务节点的性能KPI数据(比如:服务调用平均时延)做加权,进行合理分配,降低流控偏差。

  • 另一种解决方案:配额指标返还和重新申请,每个服务节点根绝自身分配的指标值、处理速率做越策,如果计算结果表明指标还有剩余,则把多余的返回给服务注册中心。对于配额已经使用完的服务节点,重新主动去服务注册中心申请配额,如果连续N次都申请不到新的配额指标,则对新接入的请求消息做流控。

最后一点就是:结合负载均衡做静态流控,才能实现更精确的调度和控制。消费者根据各服务节点的负载情况做加权路由,性能差的节点路由到的消息更少,由于配额计算也根据负载做了加权调整,最终分配给性能差的节点配额指标也比较少,这样即保证了系统的负载均衡,又实现了配额的更合理分配。

缺点:

  • 如果流控周期T比较大,服务节点负载变化比较快,服务节点的负载反馈到注册中心,由注册中心统一计算之后再配额均衡,误差较大。

  • 如果流控周期比较小,获取性能KPI数据会有一定的时延,会到之后流控滞后产生误差。

  • 如果采用配额返还的重新申请方式,则会增加交互次数。

  • 扩展性差。

(3)动态配额申请制

能够解决动态配额分配制的缺点。其工作原理如下:

  • 系统部署的时候,根据服务节点数和静态流控QPS阈值,拿出一定比例的配额做初始分配,剩余的配额放在配额资源池中。

  • 哪个服务节点使用完了配额,就主动向服务注册中心申请配额。配额申请策略:如果流控周期位T,则将T分成更小的周期T/N(N位经验值,默认为10),当前的服务节点数位M,则申请的配额为:(总QPS配额-已经分配的QPS配额)/M * T/N

  • 总的配额如果被申请完,则返回0配额给各个申请配额的服务节点,服务节点对新接入的请求消息进行流控。 优点:

  • 各个服务节点最清楚自己的负载情况,性鞥KPI数据再本地内存中计算获得,实时性高。

  • 由各个服务节点根据自身负载情况去申请配额,保证性能高的节点有更高的额度,性能差的自然配额就少,这样能够更合理的调配资源,实现流控的准确性。

3.动态流控

动态流控的最终目标是为了保命,并不是对流量或者访问速度做精准控制。触发动态流控的因子是:资源,资源又分为系统资源和应用资源两大类,根据不同的资源负载情况,动态流控又分为多个级别,每个级别流控系数都不同也就是被拒绝掉的消息比例不同。每个级别都有相应的流控阈值,这个阈值通常支持在线动态调整。

(1)动态流控因子

动态流控因子包括系统资源和应用资源两大类。 常用的系统资源包括:

  • 应用进程所在主机/VM的CPU使用率

  • 应用进程所在主机/VM的内存使用率

常用的应用资源包括:

  • JVM堆内存使用率

  • 消息队列积压率

  • 会话积压率

具体实现策略是:系统启动时拉起一个管理线程,定时采集应用资源的使用率,并刷新动态流控的应用资源阈值。

(2)分级流控

通常,动态流控是分级别的,不同级别有不同的流控阈值,系统上线后会提供默认的流控阈值,不同留空因子的流控阈值不同,业务上线之后通常会根据现场的实际情况做阈值调优,因此流控阈值需要支持在线修改和动态生效。 注意:为了防止系统波动导致的偶发性流控,无论是进入流控状态还是从流控状态恢复,都需要连续采集N次并计算平均值,如果连续N次平均值大于流控阈值,则进入流控状态;同理,只有连续N次资源使用率平均值低于流控阈值,才能脱离流控状态恢复正常。

4.其它控制

(1)并发控制

并发控制针对线程的并发执行数进行控制,它的本质是限制对某个服务或者服务方法过度消费,耗用过多资源而影响其他服务的正常运行。 并发控制有两种形式:

  • 针对服务提供者的全局控制

  • 针对服务消费者的局部控制

(2)连接控制

通常分布式服务框架服务提供者和消费者直接此案有长连接私有协议,为了防止因为消费者连接数过多导致服务端负载压力过大,系统需要支持针对连接数进行流控。

  • 服务端连接数流控

  • 消费者连接数流控

5.限流的算法

(1)计数器

比如某个服务最多只能每秒钟处理100个请求。可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。

图片

//服务访问次数,可以放在Redis中,实现分布式系统的访问计数Long counter = 0L;//使用LinkedList来记录滑动窗口的10个格子。LinkedList<Long> ll = new LinkedList<Long>();
public static void main(String[] args){    Counter counter = new Counter();
    counter.doCheck();}
private void doCheck(){    while (true)    {        ll.addLast(counter);                if (ll.size() > 10)        {            ll.removeFirst();        }                //比较最后一个和第一个,两者相差一秒        if ((ll.peekLast() - ll.peekFirst()) > 100)        {            //To limit rate        }                Thread.sleep(100);    }}

(2)漏桶算法

漏桶算法的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,则不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

在实现时一般会使用消息队列作为漏桶的实现,流量首先被放入到消息队列中排 队,由固定的几个队列处理程序来消费流量,如果消息队列中的流量溢出,那么后续的流量就会被拒绝。

(3)令牌桶算法

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。

  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。

  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。

  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

图片

令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的to network的速率。to network我们可以理解为消息的处理程序,执行某段业务或者调用某个RPC。

Guava和nginx都可实现

(4)漏桶和令牌桶的比较

令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。

整体而言,令牌桶算法更优,但是实现更为复杂一些。

6.Sentinel 

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。

  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。

①启动服务端的jar

https://github.com/alibaba/Sentinel/releases下载release的jar,然后启动即可。

这个jar是个标准的Springboot应用,可以通过

java -jar sentinel-dashboard-1.6.0.jar来启动,这样就是默认的设置,启动在8080端口。也可以加上一些自定义配置来启动

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar。具体配置的解释,可以到GitHub上看一下文档。

这里我们直接使用默认java -jar sentinel-dashboard-1.6.0.jar来启动,之后访问localhost:8080。可以看到界面:

图片

②pom​​​​​​​

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.0.5.RELEASE</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>        <groupId>com.maimeng.baobanq</groupId>    <artifactId>baobanserver</artifactId>    <version>0.0.1-SNAPSHOT</version>    <packaging>jar</packaging>    <name>baobanserver</name>    <description>Demo project for Spring Boot</description>     <properties>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>        <java.version>1.8</java.version>        <spring-cloud.version>Finchley.SR1</spring-cloud.version>    </properties>     <dependencies>               <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>               <!--sentinel-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>        </dependency>        <!--sentinel end-->               <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>    </dependencies>     <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.cloud</groupId>                <artifactId>spring-cloud-dependencies</artifactId>                <version>${spring-cloud.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>             <dependency>                <groupId>org.springframework.cloud</groupId>                <artifactId>spring-cloud-alibaba-dependencies</artifactId>                <version>0.2.2.RELEASE</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>     <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build> </project>

③配置文件​​​​​​​

spring: application:   name: baobanserver cloud:  sentinel:    transport:      dashboard: localhost:8080    #eager: true④controller@RestControllerpublic class TestController {     @GetMapping(value = "/hello")    public String hello() {        return "Hello Sentinel";    }}

⑤调用接口

调用一下hello接口。之后再次刷新server控制

图片

因为Sentinel采用延迟加载,只有在主动发起一次请求后,才会被拦截并发送给服务端。如果想关闭这个延迟,就在上面的yml里把eager的注释放掉。

⑥限制规则

在簇点链路里hello接口的流控那里设置限流规则,将单机阈值设为1.就代表一秒内 最多只能通过1次请求到达该hello接口

图片

7.基础和单机流控

(1)单机流控

单机流控就是流控的效果只针对服务的一个实例,比如你的服务部署了三个实例分别在三台机器上。请求访问到了A实例的时候,如果触发了流控,那么只会限制A实例后面的请求,不会影响其他实例上的请求。

图片

单机流控相对来说比较简单,不依赖中心化的存储。每个服务内部只需要记录自身的一些访问信息即可判断出是否需要流控操作。像Guava的RateLimiter就是典型的单机流控模式,将令牌数据全部存储在本地内存中,不需要有集中式的存储,不需要跟其他服务交互,自身就能完成流控功能。

(2)集群流控

集群流控就是流控的效果针对整个集群,也就是服务的所有的实例,比如你的服务部署了三个实例分别在三台机器上。总体限流QPS为100,请求访问到了A实例的时候,如果触发了流控,那么此时其他的请求到B实例的时候,也会触发流控。

图片

(3)使用场景对比

  • 保护层面对比:单机流控更适合作为兜底保护的一种方式,比如单机限流总的请求量为2000,如果超过2000开始限流,这样就能保证当前服务在可承受的范围内进行处理。如果我们用的是集群限流,假设当前集群内有10个节点,如果每个节点能承受2000的请求,那么加起来就是2万的请求。也就是说只要不超过2万个请求都不会触发限流。如果我们的负载均衡策略是轮询的话没什么问题,请求分布到各个节点上都比较均匀。但是如果负载均衡策略不是轮询,如果是随机的话,那么请求很有可能在某个节点上超过2000,这个时候其实这个节点是处理不了那么多请求的,最终会被拖垮,造成连锁反应。 

  • 准确度对比:比如需求是限制总的请求次数为2000,如果是单机流控,那么也就是每个节点超过200就开始限流。还是前面的问题,如果请求分配不均匀的话,其实整体总量还没达到2000,但是某一个节点超过了200,就开始限流了,对用户体验不是很好。所以集群限流适合用在有整体总量限制的场景,比如开放平台的API调用。

8.限流设计项

  • 限流应该是在架构的早期考虑。当架构形成后,限流不是很容易加入。

  • 限流模块性能必须好,而且对流量的变化也是非常灵敏的,否则太过迟钝的限流,系统早因为过载而挂掉了。

  • 限流应该有个手动的开关,这样在应急的时候,可以手动操作。

  • 当限流发生时,应该有个监控事件通知。让我们知道有限流事件发生,这样,运维人员可以及时跟进。而且还可以自动化触发扩容或降级,以缓解系统压力。

  • 当限流发生时,对于拒掉的请求,我们应该返回一个特定的限流错误码。这样,可以和其它错误区分开来。而客户端看到限流,可以调整发送速度,或是走重试机制。

  • 限流应该让后端的服务感知到。限流发生时,我们应该在协议头中塞进一个标识,比如HTTP Header 中,放入一个限流的级别,告诉后端服务目前正在限流中。这样,后端服务可以根据这个标识决定是否做降级

六、资源隔离

常见的资源,例如磁盘、网络、CPU等等,都会存在竞争的问题,在构建分布式架构时,可以将原本连接在一起的组件、模块、资源拆分开来,以便达到最大的利用效率或性能。资源隔离之后,当某一部分组件出现故障时,可以隔离故障,方便定位的同时,阻止传播,避免出现滚雪球以及雪崩效应。

1.隔离种类

(1)线程隔离

  • 资源一旦出现问题,虽然是隔离状态,想要让资源重新可用,很难做到不重启jvm。

  • 线程池内部线程如果出现OOM、FullGC、cpu耗尽等问题也是无法控制的

  • 线程隔离,只能保证在分配线程这个资源上进行隔离,并不能保证整体稳定性

(2)进程隔离

进程隔离这种思想其实并不陌生,Linux操作系统中,利用文件管理系统将各个进程的虚拟内存与实际的物理内存映射起来,这样做的好处是避免不同的进程之间相互影响,而在分布式系统中,线程隔离不能完全隔离故障避免雪崩,例如某个线程组耗尽内存导致OOM,那么其他线程组势必也会受影响,所以进程隔离的思想是,CPU、内存等等这些资源也通过不同的虚拟机来做隔离。

具体操作是,将业务逻辑进行拆分成多个子系统,实现物理隔离,当某一个子系统出现问题,不会影响到其他子系统。

(3)物理机隔离

图片

(4)机房隔离

机房隔离主要目的有两个,一方面是将不同区域的用户数据隔离到不同的地区,例如湖北的数据放在湖北的服务器,浙江的放在浙江服务器,等等,这样能解决数据容量大,计算密集,i/o(网络)密集度高的问题,相当于将流量分在了各个区域。

另一方面,机房隔离也是为了保证安全性,所有数据都放在一个地方,如果发生自然灾害或者爆炸等灾害时,数据将全都丢失,所以把服务建立整体副本(计算服务、数据存储),在多机房内做异地多活或冷备份、是微服务数据异构的放大版本。

如果机房层面出现问题的时候,可以通过智能dns、httpdns、负载均衡等技术快速切换,让区域用户尽量不收影响。

2.Hystrix

(1)Hystrix隔离策略

Hystrix的资源隔离策略分为两种:线程池和信号量。

  • 信号量隔离是设置一个并发处理的最大极值。当并发请求数超过极值时,通过fallback返回托底数据,保证服务完整性。请求并发大,耗时短(计算小,服务链段或访问缓存)时使用信号量隔离。因为这类服务的响应快,不会占用外部容器(如Tomcat)线程池太长时间,减少线程的切换,可以避免不必要的开销,提高服务处理效率。

  • 线程池隔离,将并发请求量大的部分服务使用独立的线程池处理,避免因个别服务并发过高导致整体应用宕机。请求并发大,耗时较长(一般都是计算大,服务链长或访问数据库)时使用线程池隔离。可以尽可能保证外部容器(如Tomcat)线程池可用,不会因为服务调用的原因导致请求阻塞等待。

隔离方式

是否支持超时

是否支持熔断

隔离原理

是否是异步调用

资源消耗

线程池隔离

支持,可直接返回

支持,当线程池到达maxSize后,再请求会触发fallback接口进行熔断

每个服务单独用线程池

可以是异步,也可以是同步。看调用的方法

大,大量线程的上下文切换,容易造成机器负载高

信号量隔离

不支持,如果阻塞,只能通过调用协议(如:socket超时才能返回)

支持,当信号量达到maxConcurrentRequests后。再请求会触发fallback

通过信号量的计数器

同步调用,不支持异步

小,只是个计数器

①信号量

图片

最重要的是,信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就导致了无法对访问做超时。

适用场景:公司内部服务、网关、高频高速调用

信号量隔离通过@HystrixCommand注解配置,常用注解属性有:

  • commandProperty - 配置信号量隔离具体数据。属性类型为HystrixProperty数组,常用配置内容如下:

  • execution.isolation.strategy - 设置隔离方式,默认为线程池隔离。可选值只有THREAD和SEMAPHORE。

  • execution.isolation.semaphore.maxConcurrentRequests - 最大信号量并发数,默认为10。

@Servicepublic class HystrixService {
    @Autowired    private LoadBalancerClient loadBalancerClient;
    /**     * 信号量隔离实现     * 不会使用Hystrix管理的线程池处理请求。使用容器(Tomcat)的线程处理请求逻辑。     * 不涉及线程切换,资源调度,上下文的转换等,相对效率高。     * 信号量隔离也会启动熔断机制。如果请求并发数超标,则触发熔断,返回fallback数据。     * commandProperties - 命令配置,HystrixPropertiesManager中的常量或字符串来配置。     *     execution.isolation.strategy - 隔离的种类,可选值只有THREAD(线程池隔离)和SEMAPHORE(信号量隔离)。     *      默认是THREAD线程池隔离。     *      设置信号量隔离后,线程池相关配置失效。     *  execution.isolation.semaphore.maxConcurrentRequests - 信号量最大并发数。默认值是10。常见配置500~1000。     *      如果并发请求超过配置,其他请求进入fallback逻辑。     */    @HystrixCommand(fallbackMethod="semaphoreQuarantineFallback",            commandProperties={              @HystrixProperty(                      name=HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,                       value="SEMAPHORE"), // 信号量隔离              @HystrixProperty(                      name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,                       value="100") // 信号量最大并发数    })    public List<Map<String, Object>> testSemaphoreQuarantine() {        System.out.println("testSemaphoreQuarantine method thread name : " + Thread.currentThread().getName());        ServiceInstance si =                 this.loadBalancerClient.choose("eureka-application-service");        StringBuilder sb = new StringBuilder();        sb.append("http://").append(si.getHost())            .append(":").append(si.getPort()).append("/test");        System.out.println("request application service URL : " + sb.toString());        RestTemplate rt = new RestTemplate();        ParameterizedTypeReference<List<Map<String, Object>>> type =                 new ParameterizedTypeReference<List<Map<String, Object>>>() {        };        ResponseEntity<List<Map<String, Object>>> response =                 rt.exchange(sb.toString(), HttpMethod.GET, null, type);        List<Map<String, Object>> result = response.getBody();        return result;    }        private List<Map<String, Object>> semaphoreQuarantineFallback(){        System.out.println("threadQuarantineFallback method thread name : " + Thread.currentThread().getName());        List<Map<String, Object>> result = new ArrayList<>();                Map<String, Object> data = new HashMap<>();        data.put("id", -1);        data.put("name", "thread quarantine fallback datas");        data.put("age", 0);                result.add(data);                return result;    }}

②线程池

图片

通过每次都开启一个单独线程运行。它的隔离是通过线程池,即每个隔离粒度都是个线程池,互相不干扰。线程池隔离方式,等于多了一层的保护措施,可以通过hytrix直接设置超时,超时后直接返回。

线程池隔离的实现方式是使用@HystrixCommand注解。相关注解配置属性如下:

  • groupKey - 分组命名,在application client中会为每个application service服务设置一个分组,同一个分组下的服务调用使用同一个线程池。默认值为this.getClass().getSimpleName();

  • commandKey - Hystrix中的命令命名,默认为当前方法的方法名。可省略。用于标记当前要触发的远程服务是什么。

  • threadPoolKey - 线程池命名。要求一个应用中全局唯一。多个方法使用同一个线程池命名,代表使用同一个线程池。默认值是groupKey数据。

  • threadPoolProperties - 用于为线程池设置的参数。其类型为HystrixProperty数组。常用线程池设置参数有:

  • coreSize - 线程池最大并发数,建议设置标准为:requests per second at peak when healthy * 99th percentile latency in second + some breathing room。即每秒最大支持请求数*(99%平均响应时间 + 一定量的缓冲时间(99%平均响应时间的10%-20%))。如:每秒可以处理请求数为1000,99%的响应时间为60ms,自定义提供缓冲时间为60*0.2=12ms,那么结果是 1000*(0.060+0.012) = 72。

  • maxQueueSize - BlockingQueue的最大长度,默认值为-1,即不限制。如果设置为正数,等待队列将从同步队列SynchronousQueue转换为阻塞队列LinkedBlockingQueue。

  • queueSizeRejectionThreshold - 设置拒绝请求的临界值。默认值为5。此属性是配合阻塞队列使用的,也就是不适用maxQueueSize=-1(为-1的时候此值无效)的情况。是用于设置阻塞队列限制的,如果超出限制,则拒绝请求。此参数的意义就是在服务启动后,可以通过Hystrix的API调用config API动态修改,而不用用重启服务,不常用。

  • keepAliveTimeMinutes - 线程存活时间,单位是分钟。默认值为1。

  • execution.isolation.thread.timeoutInMilliseconds - 超时时间,默认为1000ms。当请求超时自动中断,返回fallback,避免服务长期阻塞。

  • execution.isolation.thread.interruptOnTimeout - 是否开启超时中断。默认为TRUE。和上一个属性配合使用。

//启动器:/** * @EnableCircuitBreaker - 开启断路器。就是开启hystrix服务容错能力。 * 当应用启用Hystrix服务容错的时候,必须增加的一个注解。 */@EnableCircuitBreaker@EnableEurekaClient@SpringBootApplicationpublic class HystrixApplicationClientApplication {    public static void main(String[] args) {        SpringApplication.run(HystrixApplicationClientApplication.class, args);    }}

//实现类:@Servicepublic class HystrixService {
    @Autowired    private LoadBalancerClient loadBalancerClient;
    /**     * 如果使用了@HystrixCommand注解,则Hystrix自动创建独立的线程池。     * groupKey和threadPoolKey默认值是当前服务方法所在类型的simpleName     *      * 所有的fallback方法,都执行在一个HystrixTimer线程池上。     * 这个线程池是Hystrix提供的一个,专门处理fallback逻辑的线程池。     *      * 线程池隔离实现     * 线程池隔离,就是为某一些服务,独立划分线程池。让这些服务逻辑在独立的线程池中运行。     * 不使用tomcat提供的默认线程池。     * 线程池隔离也有熔断能力。如果线程池不能处理更多的请求的时候,会触发熔断,返回fallback数据。     * groupKey - 分组名称,就是为服务划分分组。如果不配置,默认使用threadPoolKey作为组名。     * commandKey - 命令名称,默认值就是当前业务方法的方法名。     * threadPoolKey - 线程池命名,真实线程池命名的一部分。Hystrix在创建线程池并命名的时候,会提供完整命名。默认使用gourpKey命名     *  如果多个方法使用的threadPoolKey是同名的,则使用同一个线程池。     * threadPoolProperties - 为Hystrix创建的线程池做配置。可以使用字符串或HystrixPropertiesManager中的常量指定。     *  常用线程池配置:     *      coreSize - 核心线程数。最大并发数。1000*(99%平均响应时间 + 适当的延迟时间)     *      maxQueueSize - 阻塞队列长度。如果是-1这是同步队列。如果是正数这是LinkedBlockingQueue。如果线程池最大并发数不足,     *          提供多少的阻塞等待。     *      keepAliveTimeMinutes - 心跳时间,超时时长。单位是分钟。     *      queueSizeRejectionThreshold - 拒绝临界值,当最大并发不足的时候,超过多少个阻塞请求,后续请求拒绝。     */    @HystrixCommand(groupKey="test-thread-quarantine",         commandKey = "testThreadQuarantine",        threadPoolKey="test-thread-quarantine",         threadPoolProperties = {            @HystrixProperty(name="coreSize", value="30"),            @HystrixProperty(name="maxQueueSize", value="100"),            @HystrixProperty(name="keepAliveTimeMinutes", value="2"),            @HystrixProperty(name="queueSizeRejectionThreshold", value="15")        },        fallbackMethod = "threadQuarantineFallback")    public List<Map<String, Object>> testThreadQuarantine() {        System.out.println("testQuarantine method thread name : " + Thread.currentThread().getName());        ServiceInstance si =                 this.loadBalancerClient.choose("eureka-application-service");        StringBuilder sb = new StringBuilder();        sb.append("http://").append(si.getHost())            .append(":").append(si.getPort()).append("/test");        System.out.println("request application service URL : " + sb.toString());        RestTemplate rt = new RestTemplate();        ParameterizedTypeReference<List<Map<String, Object>>> type =                 new ParameterizedTypeReference<List<Map<String, Object>>>() {        };        ResponseEntity<List<Map<String, Object>>> response =                 rt.exchange(sb.toString(), HttpMethod.GET, null, type);        List<Map<String, Object>> result = response.getBody();        return result;    }        private List<Map<String, Object>> threadQuarantineFallback(){        System.out.println("threadQuarantineFallback method thread name : " + Thread.currentThread().getName());        List<Map<String, Object>> result = new ArrayList<>();                Map<String, Object> data = new HashMap<>();        data.put("id", -1);        data.put("name", "thread quarantine fallback datas");        data.put("age", 0);                result.add(data);                return result;    }}
  • 对于所有请求,都交由tomcat容器的线程池处理,是一个以http-nio开头的的线程池;

  • 开启了线程池隔离后,tomcat容器默认的线程池会将请求转交给threadPoolKey定义名称的线程池,处理结束后,由定义的线程池进行返回,无需还回tomcat容器默认的线程池。线程池默认为当前方法名;

  • 所有的fallback都单独由Hystrix创建的一个线程池处理。

(2)断路器(熔断器)CircuitBreaker

Netflix开源了Hystrix组件,实现了断路器模式,SpringCloud对这一组件进行了整合。 在微服务架构中,一个请求需要调用多个服务是非常常见的。

图片

较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystric 是5秒20次) 断路器将会被打开。断路打开后,可以避免连锁故障,fallback方法可以直接返回一个固定值。

图片

①断路器机制

断路器很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.

②Fallback

Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存。

③资源隔离

在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.

七、稳定性保障

在各种不可预知的情况发生时仍然能够持续稳定的运行和提供服务”就是稳定性保障工作的目标。处理步骤为:“梳理异常情况->配置监控告警->评估影响面->预定解决方案。

图片

1.梳理异常情况

对异常情况进行类目细化。消息队列、数据库连接、FullGC等都是中间件导致的异常,而代码bug、数据不一致、资金金额算错这类异常属于开发写出的bug或者产品设计缺陷。综上,可将中间件这类异常定义为基础设施异常,bug和产品缺陷类的异常定义为业务功能异常。

再反向梳理一下,基础设施异常包括网络、容量、连接、磁盘、缓存、JVM等等中间件或者底层硬件设施产生的异常;这类异常日常一般不会发生,只可能发生在大而突然的流量变化时,基础设施扛不住过大的流量导致异常。因此多发生在大促期间。

业务功能异常包括代码逻辑异常和资金异常。与业务功能息息相关,因此这类异常一般出现在每次业务逻辑的变更后,与日常需求相对应。可在每次业务需求开发的同时进行梳理,也可根据异常梳理结果在代码中预置开关和订正工具,万一上线后异常情况出现,就可使用预置开关和订正工具进行止血和修复。

顺便提一下,根据异常的分类,平日里大家习惯性将稳定性保障工作分为日常稳定性保障和大促稳定性保障。

  • 日常稳定性保障主要针对业务功能异常。此类稳定性保障工作伴随着每次业务需求开发同步进行,发布要先于业务功能发布。日常的业务功能变更一般不会引起基础设施异常,因此日常稳定性保障较少涉及基础设施的保障。

  • 大促稳定性保障主要针对基础设施异常。因为大促会导致比日常高数十倍数百倍甚至数千倍的流量增长,此时基础设施将面对巨大的压力,就会出现各种异常。大促稳定性保障工作是在每次大促前需要完成的工作。

2.配置监控告警

监控告警分为三类,基础设施监控告警、业务功能监控告警、资金安全监控告警。

  • 基础设施监控告警一般都是在应用创建之初进行配置的,涵盖应用和所有的中间件、网络等。集团对基础监控告警的覆盖范围有明确规定。

  • 业务功能监控告警都是在日常业务功能开发时由开发人员配置的,用于监控特定的业务场景。

  • 资金安全监控告警主要面向与资金相关的应用,例如下单支付。如果没有就需要从0到1的创建,之后就随着每次业务功能开发进行增量式设置。

大促前夕一般会对所有的监控告警进行梳理并查漏补缺。

(1)数据流向图

监控告警配置其实是个省略句,其完整的表达应该是:监控告警数据源的准备、监控告警配置。

图片

监控告警的整体数据流向如上图所示。主要通过日志、消息、持久化数据作为数据源,将数据收集起来后用于监控告警的配置。

其中日志主要用于监控大盘展示,实时反应线上真是情况;消息主要用于实时核对的触发媒介,触发资金安全核对,资金安全核对通过采用旁路核对、资金一致性核对、两两核对等方式核对线上逻辑;持久化数据主要用于离线核对,通过两两核对的方式校验数据正确性。具体核对逻辑后文详述。

最终,监控核对都将告警信息汇总到告警系统,触发告警。同步到告警响应人进行处理。

(2)配置步骤

图片

值得一提的是,虽然先要有数据,才能配置监控。但是其实数据准备和监控配置这两者应该是并行的。根据监控项的大概规划准备监控数据,然后进行监控告警配置,有任何数据不满足配置的情况都要返回进行数据准备步骤。

个人认为一个监控的好坏评判标准主要通过三个指标:正确性、覆盖率、直观性。正确性保证监控的基本功能,能够正确的反应真实情况。没有正确性的监控毫无存在意义。覆盖率是衡量一个监控系统成功与否的关键指标,覆盖率越高,监控系统就越能够完美的体现系统的实际运行情况。直观的展示监控指标有助于快速发现异常、快速定位问题。

对于告警,个人认为告警的配置最重要的是:及时性、有效性、责任制。告警出现一般都是有异常情况,可能涉及到资损或者故障,发现越及时,止血越及时,损失就越小。告警最终都是人工处理,无效的告警会浪费人力成本,因此告警配置要注意过滤噪音,保障告警的有效性,保证报出的确实是问题。至于责任制则是强调告警必须要有人响应,每条告警最好分配到人。有响应的告警才是最终有效的。

(3)资金安全核对

资金安全核对的本质是检查是否有资损事件的发生,做到及时报警快速止血,最终达到资损防控的目的。资金逻辑相比一般的业务逻辑存在一些共性,因此我们能够针对这些共性思考出一些通用的资金安全核对方法和资损防控措施。

对于资金安全核对的方法论,根据前辈们的总结概括,资金安全问题主要是对资金关键要素的处理过程中出现异常导致的。资金关键要素的生命周期主要包含三个重要节点:生产、传递和消费。三个节点分别可能出现的错误为生产错误、漏传错传、消费错误。针对这些错误,前辈们提出三大核对方法,所谓核对,都是寻找有一个正确数据作为预期,将实际情况与预期数据进行对比核对。

图片

  • 基线核对是将历史数据作为预期进行对比核对,这种方式依赖历史数据的正确性,投入少,实效低,可发现大的资金问题。

  • 两两核对将上游作为预期,进行对比核对,精准度高,时效性高,成本比较高,难以覆盖全面。

  • 业务逻辑核对将业务专家的经验作为预期进行核对,需要大量人力投入,对经验的依赖度高,但是精准度高,时效性高。

面对资损防控,我们可以采用哪些具体的落地措施呢?资损防控的策略包括保存量、盘增量、控高危、盯特有。资损防控是一个长期的过程,需要时常维护,对存量布控不断优化保鲜;对新增变更进行资损评估,发布前卡点确认,保证有对应的核对规则;对于易资损场景和数据进行专项重保;对大促特有逻辑或者场景进行特别关注。

图片

资损防控,需要对全链路资金安全风险场景进行梳理,从失血类型、规则表达、规则类型、依赖因子等多方面分析资损场景,建立相应的失血模型。为保证快速、精确,可监听异步消息来进行实时核对,同时结合特定的错误日志告警,再加上小时级离线持久化数据核对兜底来保障资金安全。对于核对脚本,可通过组织review和攻防验证保障其正确性。

建立资损大盘,在大促高峰期值班期间安排专人盯盘,及时响应问题处理,同时对于高资损风险项预置必要的应急预案,在紧急情况发生时可以及时熔断止血以保障资金安全。

3.预估影响面

基础设施的异常分多种,例如程度较轻的仅仅是短期内的负载高,严重的某个中间件(例如MetaQ)不可用、机房断电,光缆被挖断。其异常的严重程度直接决定了影响面,可能错误率飙高、RT飙高、消息阻塞、FullGC频繁,影响到系统的持续稳定性,也可能系统瘫痪不可用、网络不可用、流量跌零。不过对于这种严重的异常一般不会发生。机房都是多机房部署,硬件容灾考虑有专门的团队去保障。中间件也要专门的团队运维保障。

稳定性保障工作对于基础设施异常一般只考虑大促流量激增引起的容量不足、系统压力大等问题。这类问题的原因明确,就是流量过大。直接现象就是错误率飙高、RT飙高、消息阻塞、FullGC频繁等,较严重的情况下会引起客诉和舆情。

业务功能异常就是错误,无论是逻辑异常还是资金计算错误,资金流转错误,本质上就是产品设计有缺、开发留下的bug或者某处配置有误。这类异常与具体的业务场景有关,小则仅影响某一个局部小功能,大则影响核心功能。可能引起客诉和舆情,资金异常可能导致资损。

4.预定解决方案

预定的解决方案大概有限流、压测、扩容、预案等措施,那么这些解决方案具体如何落地呢?他们之间是否存在一定的顺序?

解决方案与异常类型强相关。不同的类型有不同的解决方案。因此解决方案也分为业务功能异常的解决方案和基础设施异常的解决方案。

(1)业务异常的解决方案

稳定性保障对于业务异常主要是从“万一发生了怎么办”这个角度出发去思考解决方案的。因此需要提前准备锦囊,以备不时之需。

业务类异常的解决方案一般分为三类,止血解决方案、临时解决方案和长期解决方案。需要消耗的时间逐渐增多,对问题的解决程度逐渐增加。都是问题发生后的应对方案。

止血解决方案一般不需要走代码变更发布,通过预案、设置、开关等实现,通常也是需要提前有所计划,有所准备的。临时解决方案和长期解决方案一般需要走代码变更发布,耗时较长。因此遇到问题一般都先执行效率最高的止血解决方案,如果止血解决方案依旧承受较大的损失,就需要快速拿出临时方案来解决问题,临时方案虽然一定程度上解决了问题,但是可能存在一些小功能问题、性能问题或者优雅性方面的瑕疵。因此需要在问题得以缓解之后思考出一个稳定优雅的长期解决方案。当然,那已经是后话了,不属于稳定性保障的工作范畴。

对于业务功能异常,在日常开发时就需要提前打算,准备好能够多维度多程度降级的开关或者设置,留作异常发生时紧急止血使用。也就是预案。

预案需要预演进行验证,保证预案配置和执行的正确性。

图片

(2)预案

预案的本质是一个或者多个能够快速改变代码逻辑的设置。例如开关、diamond配置或者其他工具,将这些配置跳过繁琐的审批流程、实现快速执行就是预案,是用于大促前期关闭非核心功能、大促期间紧急问题及时止血保障主要功能而选择断尾某些功能的操作配置。

预案按照执行时间分为提前预案和应急预案。

  • 提前预案是大促前期自动执行用于关闭非核心功能以保障核心功能的预案,例如日志降级等;这类预案一般不会造成损失,风险可控,影响面是业务和消费者都可接受的。

  • 应急预案是大促期间发现线上问题,经过大促负责人审批同意后由相关测试或者开发人员手动执行,用于及时止血以保障主要功能的预案。这类预案类似于壁虎断尾的行为,舍弃小的损失,保留大的功能,因此一般都存在一定的损失。

预案项需要提前梳理清楚,对功能无影响但是对性能无影响的锦上添花的部分在大促期间是是可以作为提前预案降级;对于可能出现异常情况的代码逻辑,或者评估风险较高的逻辑都要不吝增加开关设置,实现多维度降级(业务身份维度、商品类目维度、商家维度等等),在大促前夕配置好紧急预案。

预案设置越多越好,但是预案执行须谨慎。预案越多,能够快速应对的异常场景就越多,就越能够快速止血,当然也要考虑维护成本。但是配置预案的时候就要全面细致的评估清楚其执行影响,这样到了执行的时候才能正确选择合适大小的创可贴应对止血伤口。执行预案时须谨慎,按照大促要求走相应的流程,保证有double check。

(3)预演

预演就是预先演练一遍。包括功能预演、活动预演、预案预演等。

  • 功能预演:提前将功能演示一遍,保证功能的正确性。注意核心功能的覆盖率,尤其是未参加过大促的新功能。

  • 活动预演:主要面向某些大型活动进行提前预演。例如预售。

  • 预案预演:对预案进行演练测试。每一个预案在创建后都会进行一次预案预演,以保证预案执行结果符合预期。

(4)基础设施异常的解决方案

稳定性保障对于基础设施异常主要是从“如何才能不发生”这个角度出发去思考解决方案的。因此需要提前修炼内功,增强自身实力。

对于基础设施异常,一旦发生,难以快速止血和修复。因此一般都是在大促前夕就要做足稳定性保障工作保证大促时不发生或者少发生这类异常,可通过压测、预演提前发现异常,提前提出解决方案进行修复处理。

如前文所述,对于基础设施异常,稳定性保障工作仅考虑流量过大导致的异常。因此这类异常的原因明确就是流量过大。其解决方案也就明确是解决流量问题,分为对外解决方案和对内解决方案。对外限流拒绝过多流量对自身进行保护,但限流需要基于容量预估,有考量有依据的设置。对内扩容增强自身实力,并提前预热做好应对准备。

内外解决方案通过压测相互协调配合,最终达到一种权衡利弊后的和谐。

图片

综上,先要进行容量预估,预估外部流量峰值,再根据预估容量进行限流设置,然后进行压测,评估内部容量是否能够支撑预估容量,如果无法支撑,就要考虑扩容,扩容后根据机器数量进行限流调整,然后再次压测,压测后能够支撑预估容量甚至游刃有余时可以考虑再次调整限流,承载更多的流量。最后可以考虑在大促前进行缓存预热防止流量峰值击穿缓存。

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值