高可用架构设计—降级、限流、资源隔离

服务降级

降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能,降级的核心思想就是优先保证核心业务。例如,论坛可以降级为只能看帖子,不能发帖子;也可以降级为只能看帖子和评论,不能发评论;而 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 上有所区分(方法签名或是开关参数),这样可以由上游调用者来驱动。比如:一个网关在限流时,在协议头中加入了一个限流程度的参数,让后端服务能知道限流在发生中。当限流程度达到某个值时,或是限流时间超过某个值时,就自动开始降级,直到限流好转。
  • 对于数据方面的降级,需要前端程序的配合。一般来说,前端的程序可以根据后端传来的数据来决定展示哪些界面模块。比如,当前端收不到商品评论时,就不展示。为了区分本来就没有数据,还是因为降级了没有数据的两种情况,在协议头中也应该加上降级的标签。

服务限流

一、限流基础与类型

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

  • 拒绝服务。把多出来的请求拒绝掉。好的限流系统在受到流量暴增时,会统计当前哪个客户端来的请求最多,直接拒掉这个客户端,这种行为可以把一些不正常的或者是带有恶意的高并发访问阻止掉。
  • 特权请求。所谓特权请求的意思是,资源不够了,我只能把有限的资源分给重要的用户,比如:分给权利更高的 VIP 用户。在多租户系统下,限流的时候应该保大客户的,所以大客户有特权可以优先处理,而其它的非特权用户就得让路了。
  • 延时处理。在这种情况下,一般会有一个队列来缓冲大量的请求,这个队列如果满了,那么就要拒绝用户,如果这个队列中的任务超时了,也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力,一般用于应对短暂的峰值请求。
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次资源使用率平均值低于流控阈值,才能脱离流控状态恢复正常。


 

二、限流应用

1.限流的算法
(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)漏桶和令牌桶的比较

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

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


 

2.Sentinel
  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。


 

3.基础和单机流控
(1)单机流控

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

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

(2)集群流控

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

(3)使用场景对比

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

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

4.限流设计项
  • 限流应该是在架构的早期考虑。当架构形成后,限流不是很容易加入。
  • 限流模块性能必须好,而且对流量的变化也是非常灵敏的,否则太过迟钝的限流,系统早因为过载而挂掉了。
  • 限流应该有个手动的开关,这样在应急的时候,可以手动操作。
  • 当限流发生时,应该有个监控事件通知。让我们知道有限流事件发生,这样,运维人员可以及时跟进。而且还可以自动化触发扩容或降级,以缓解系统压力。
  • 当限流发生时,对于拒掉的请求,我们应该返回一个特定的限流错误码。这样,可以和其它错误区分开来。而客户端看到限流,可以调整发送速度,或是走重试机制。
  • 限流应该让后端的服务感知到。限流发生时,我们应该在协议头中塞进一个标识,比如HTTP Header 中,放入一个限流的级别,告诉后端服务目前正在限流中。这样,后端服务可以根据这个标识决定是否做降级。

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

资源隔离与稳定性

1.隔离种类

(1)线程隔离
  • 资源一旦出现问题,虽然是隔离状态,想要让资源重新可用,很难做到不重启jvm。
  • 线程池内部线程如果出现OOM、FullGC、cpu耗尽等问题也是无法控制的
  • 线程隔离,只能保证在分配线程这个资源上进行隔离,并不能保证整体稳定性
(2)进程隔离

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

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

(3)物理机隔离

如果要保证当服务器宕机时不影响部署在上面运行的服务,需要采用分布式集群部署,而且要采用非亲和性安装:即服务实例需要部署到不同的物理机上,通常至少需要3台物理机,假如单台物理机的故障发生概率为0.1%,则3台同时发生故障的概率为0.001%,服务的可靠性将会达到 99.999%,完全可以满足大多数应用场景。

(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)来隔离资源.

### 如何在 Eclipse IDE 中创建 Maven 项目 #### 创建新 Maven 项目 为了在Eclipse中启动一个新的Maven项目,用户应当通过菜单栏选择`File > New > Project...`随后从新建项目向导里挑选`Maven > Maven Project`[^2]。 #### 设置项目结构和配置 一旦选择了Maven项目选项之后,可以选择是否要基于预设的模板来初始化项目。如果希望手动控制pom.xml的内,则应取消勾选“Create a simple project (skip archetype selection)”这一项以便能够浏览可用的archetype列表并从中做出选择。Archetypes是用于快速搭建特定类型的Maven项目的骨架代码生成器。 #### 编辑POM文件 对于每一个新的Maven项目而言,核心配置都保存于Project Object Model(POM)文件即`pom.xml`之中。此文件包含了关于项目及其依赖关系的信息。编辑该文件可定义诸如版本号、打包方式以及所需库等属性。例如: ```xml <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> <!-- 基本信息 --> <groupId>com.example</groupId> <artifactId>maven-demo-project</artifactId> <version>1.0-SNAPSHOT</version> <!-- 打包类型,默认为jar --> <packaging>jar</packaging> <!-- 属性定义 --> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <!-- 依赖管理 --> <dependencies> <!-- 添加所需的第三方库依赖 --> </dependencies> </project> ``` #### 构建与运行项目 完成上述步骤后即可利用内置工具或命令行执行mvn clean install指令来进行编译测试及安装操作。这一步骤将会依据所设定的目标平台自动处理源码转换成字节码的过程,并将产物放置至本地仓库供后续使用。 #### 更改JRE设置 有时默认关联的JRE可能不符合需求,这时可以通过右键点击项目名称->Properties->Java Build Path->Libraries节点下的JRE System Library条目上的Edit按钮来自由切换至其他已安装好的JDK版本[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值