==微服务拆分容易出现的问题==
1.雪崩问题
1.1 产生背景
在微服务远程调用的过程中,还存在几个问题需要解决。
①业务健壮性问题:
例如在之前的查询购物车列表业务中,购物车服务需要查询最新的商品信息,与购物车数据做对比,提醒用户。大家设想一下,如果商品服务查询时发生故障,查询购物车列表在调用商品服务时,是不是也会异常?从而导致购物车查询失败。
但从业务角度来说,为了提升用户体验,即便是商品查询失败,购物车列表也应该正确展示出来,哪怕是不包含最新的商品信息。
②级联失败问题:
还是查询购物车的业务,假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。
此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。
依次类推,整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。
==雪崩【级联失败】==:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用。
1.2 产生原因
微服务相互调用,服务提供者出现故障或阻塞。
服务调用者没有做好异常处理,导致自身故障。
调用链中的所有服务级联失败,导致整个集群故障
1.3 解决方案
尽量避免服务出现故障或阻塞。–请求限流和线程隔离
保证代码的健壮性;
保证网络畅通;
能应对较高的并发请求;
服务调用者做好远程调用异常的后备方案,避免故障扩散 –服务熔断
这些方案或多或少都会导致服务的体验上略有下降,比如请求限流,降低了并发上限;线程隔离,降低了可用资源数量;服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。因此这些方案都属于服务降级的方案。但通过这些方案,服务的健壮性得到了提升。
1.请求限流(降低访问流量)
==限制访问微服务的请求并发量,避免服务因流量激增出现故障==
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
2.线程隔离(降低独占资源数量)
==限制分给其他服务的线程数,保证不会因为一个服务挂了导致其他服务消耗完自己资源也挂了==
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。
举例子说明:
如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。
3.快速失败(fallback后备方案)
快速失败:给业务编写一个调用失败时的处理的逻辑,称为fallback。当调用出现故障(比如无线程可用)时,按照失败处理逻辑执行业务并返回,而不是直接抛出异常。
4.服务熔断(提前预测,不对劲就fallback)
==【相当于一个提前预判】设定一个断路器(开关),统计请求的异常比例和慢调用比例,超过阈值我就拒绝不让你用。熔断了去走服务的后备fallback逻辑(备份方案)==
思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截访问该服务的一切请求。而当设定熔断时间结束后它会尝试放行一次请求测试,如果成功就是服务恢复时,断路器会放行访问该服务的请求; 如果放行不通过继续走熔断状态,所有请求走fallback快速失败。
2. 常见服务保护技术
目前我们常见的就是以下两种,我们都推荐使用Sentinel:【实习公司用Hystrix】
==Sentinel==
==服务保护方案–Sentinel基础使用==
1.介绍
Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中。官方网站:
Sentinel 的使用可以分为两个部分:
- 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
2.安装
为了方便监控微服务,我们先把Sentinel的控制台搭建出来
2.1 下载jar包
下载地址:Releases · alibaba/Sentinel · GitHub
2.2 启动测试
- 1.存放jar:将jar包放在任意非中文、不包含特殊字符的目录下,重命名为
sentinel-dashboard.jar
:
- 2.启动:在当前目录下cmd打开命令行输入指令启动:
1 | 输入以下命令: |
运行结果:
- 3.访问:访问http://localhost:8090页面,就可以看到sentinel的控制台了:
需要输入账号和密码,默认都是:sentinel
登录后,即可看到控制台,默认会监控sentinel-dashboard服务本身
3.微服务整合
我们以微服务-黑马商城中的cart-service购物车模块为例:
3.1 引入依赖
我们在cart-service服务pom.xml文件引入依赖
1 | <!--sentinel--> |
因为整合到springalibaba,所以依赖名也是spring-cloud-starter前缀
3.2 yml配置控制台
我们在cart-service服务yml文件引入依赖
1 | spring: |
3.3 测试
重启cart-service
,然后访问查询购物车接口,sentinel的客户端就会将服务访问的信息提交到sentinel-dashboard
控制台。并展示出统计信息:
点击cart-service的簇点链路菜单,会看到下面的页面:
所谓簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel
监控的资源。【默认情况下,Sentinel
会监控SpringMVC
的每一个Endpoint
(接口)】
因此,我们看到/carts
这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。
3.4 簇点链路
【默认情况下】Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源(我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是/carts
路径)
解决方案:我们可以选择打开Sentinel的请求方式前缀,把请求方式 + 请求路径
作为簇点资源名
然后,重启服务,通过页面访问购物车的相关接口,可以看到sentinel控制台的簇点链路发生了变化:
==服务保护方案–Sentinel四大解决方案==
我们以微服务-黑马商城中的cart-service购物车模块为例:
1.请求限流
前提:我们已经将cart-service购物车模块和sentinel建立连接,我们就可以通过控制台进行操作
1.1 控制台设置限流QPS
把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6.
1.2 Jmeter测试
【可参考笔记-jmeter快速入门】
我们利用Jemeter做限流测试,我们每秒发出10个请求:
最终在sentinel监控结果如下:
可以看出GET:/carts
这个接口的通过QPS稳定在6附近,而拒绝的QPS在4附近,符合我们的预
2.线程隔离
2.1 控制台设置并发线程数
2.2 Jmeter测试
【可参考笔记-jmeter快速入门】
我们利用Jemeter测试,每秒发送100个请求:
最终在sentinel监控结果如下:
进入查询购物车的请求每秒大概在100,而在查询商品时却只剩下每秒10左右,符合我们的预期。
此时如果我们通过页面访问购物车的其它接口,例如添加购物车、修改购物车商品数量,发现不受影响:
利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。由于查询商品的功能耗时较高(我们模拟了500毫秒延时),再加上线程隔离限定了线程数为5,导致接口吞吐能力有限,最终QPS只有10左右。这就导致了几个问题:
第一,超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个降级处理逻辑【fallback快速失败】。
第二,由于查询商品的延迟较高(模拟的500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。
3.快速失败-Fallback(后备方案)
触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
3.1 两种配置方式
3.2 举例-以方式二为例:
3.2.1 yml导入依赖
修改cart-service模块的application.yml文件,开启Feign的sentinel功能:
1 | feign: |
3.2.2 编写降级处理类
在hm-api模块中给ItemClient
定义降级处理类,实现FallbackFactory
3.2.3 注入bean
在hm-api
模块中的com.hmall.api.config.DefaultFeignConfig
类中将ItemClientFallback
注册为一个Bean
:
3.2.4 给对应Openfeign添加属性
在hm-api
模块中的ItemClient
接口中使用ItemClientFallbackFactory
:
3.2.5 Jmeter测试
但是未被限流的请求延时依然很高:
导致最终的平均响应时间较长。
4.服务熔断
查询商品的RT较高(模拟的500ms),从而导致查询购物车的RT也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。
对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。当商品服务接口恢复正常后,再允许调用。这其实就是断路器的工作模式了。
Sentinel中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例。当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。
断路器的工作状态切换有一个状态机来控制:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态【正常状态】
- 请求失败:则切换到open状态【熔断状态】
4.1 控制台设置熔断规则
这种是按照慢调用比例来做熔断,上述配置的含义是:
- RT超过200毫秒的请求调用就是慢调用
- 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断
- 熔断持续时长20s
4.2 Jmeter测试
此时整个购物车查询服务的平均RT影响不大:
==Sentinel服务保护总结==
1.具体使用操作
- 1.启动sentinel控制台(jar包)
到这已经可以打开sentinel控制台
- 2.具体使用的微服务pom.xml文件导入sentinel依赖
- 3.具体使用的微服务yml文件配置sentinel控制台信息
到这已经可以在sentinel控制台查看
具体的四大解决方案【1请求限流,2线程隔离,4服务熔断在平台设置就行】
1和2.控制台通过具体微服务-簇点链路的流控按钮设置请求限流(设置QPS)和线程隔离(设置并发线程数)
3.fallback快速失败需要编写代码(Openfeign所在微服务进行编写)
①在Openfeign所在微服务编写降级处理类实现FallbackFactory重写create方法里面将ItemClient被其他微服务调用的方法都重写处理(后备方案,可以是信息提示等)
②将降级处理类加入bean
③在微服务ItemClient的FeignClient注解添加属性fallbackFactory=①降级处理类
- 4.控制台通过具体微服务-簇点链路的熔断按钮设置熔断策略
2.四大解决方案总结
1.请求限流(限制访问量)和线程隔离(限制线程数,不让此服务占用所有资源)相当于对单个微服务调用做保护
2.快速失败(如果请求限流/线程隔离资源没了,那就可以走这个fallback后备方案,不至于直接抛出异常)和服务熔断(提前对这个服务判断如果经常出异常,那就熔断)是在微服务调用其他微服务情况时候做保护
如果比喻成买火车票乘车:
1.请求限流就是限制购买人数
2.线程隔离就是车次分到A候车厅(六个通道)和B候车厅(四个通道),如果B满了或者速度太慢可以去A候车厅的通道,不至于直接瘫痪。
3.服务熔断就是我提前预测哪里经常会出问题,我可以一段时间内不让大家去那候车可以去备用候车厅;如果一段时间后,我可以测试走一个人如果通那就打开去候车,如果不行那就继续用备用候车厅。
4.快速失败就是如果这个通道走不了,那你不能让乘客不上车啊,所有可以大喇叭提示等待或者提供备用候车厅上车
==底层原理==
无论是①Hystix还是②Sentinel都支持线程隔离,实现方式不同。
①Hystix:线程池隔离[默认],信号量隔离
②Sentinel:信号量隔离
1.线程隔离
线程隔离有两种方式实现:
- 线程池隔离:给每个微服务调用业务分配一个线程池【利用线程池本身实现隔离效果】
- 信号量隔离:给每个业务设定线程数量,达到信号量上限时就禁止新的请求【不创建线程池,而是使用计数器模式】
两者的优缺点:
信号量隔离 | 线程池隔离 | |
---|---|---|
优点 | 轻量级,无额外开销 | 支持主动超时,支持异步调用 |
缺点 | 不支持主动超时,不支持异步调用 | 线程的额外开销比较大 |
场景 | 高频调用,高扇出 | 低扇出 |
2.四种算法
在熔断功能中,需要统计异常请求或慢请求比例,也就是计数。在限流的时候,要统计每秒钟的QPS,同样是计数。可见计数算法在熔断限流中的应用非常多。sentinel中采用的计数器算法就是滑动窗口计数算法。
2.1 固定窗口计数
- 每个窗口维护1个计数器,每有1次请求就将计数器
+1
。限流就是设置计数器阈值,本例为3,图中红线标记 - 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃。
说明:
- 第1、2秒,请求数量都小于3,没问题
- 第3秒,请求数量为5,超过阈值,超出的请求被拒绝
特殊情况:【无法结合前后的时间窗口的数据做综合统计】—只能统计当前某1个时间窗的请求数量是否到达阈值
说明:
- 假如在第5、6秒,请求数量都为3,没有超过阈值,全部放行
- 但是,如果第5秒的三次请求都是在4.5-5s之间进来;第6秒的请求是在5-5.5s之间,那么4.5-5s之间就有6次请求!也就是说每秒的QPS达到了6,远超阈值。
2.2 滑动窗口计数
固定时间窗口算法中窗口有很多,其跨度和位置是与时间区间绑定,因此是很多固定不动的窗口。而滑动时间窗口算法中只包含1个固定跨度的窗口,但窗口是可移动动的,与时间区间无关。
具体规则如下:
- 窗口时间跨度
Interval
大小固定,例如1秒 - 时间区间跨度为
Interval / n
,例如n=2,则时间区间跨度为500ms - 窗口会随着当前请求所在时间
currentTime
移动,窗口范围从currentTime-Interval
时刻之后的第一个时区开始,到currentTime
所在时区结束。
限流阈值依然为3,绿色小块就是请求,上面的数字是其currentTime
值。
- 在第1300ms时接收到一个请求,其所在时区就是1000~1500
- 按照规则,currentTime-Interval值为300ms,300ms之后的第一个时区是500
1000,因此窗口范围包含两个时区:5001000、1000~1500,也就是粉红色方框部分 - 统计窗口内的请求总数,发现是3,未达到上限。
若第1400ms又来一个请求,会落在1000~1500时区,虽然该时区请求总数是3,但滑动窗口内总数已经达到4,因此该请求会被拒绝:
假如第1600ms又来的一个请求,处于15002000时区,根据算法,滑动窗口位置应该是10001500和1500~2000这两个时区,也就是向后移动:
这就是滑动窗口计数的原理,解决了我们之前所说的问题。而且滑动窗口内划分的时区越多,这种统计就越准确。
2.3 令牌桶算法(Sentinel的热点参数)
其基本思路如图:【Sentinel中的热点参数(一段时间内频繁访问的用户id)限流】
说明:
- 生成令牌:以固定的速率生成令牌,存入令牌桶【令牌桶满了以后,多余令牌丢弃】
- 进入请求:必须先尝试从桶中获取令牌,①获取到令牌后才可以被处理②令牌桶中没有令牌,则请求等待或丢弃
基于令牌桶算法,每秒产生的令牌数量==QPS上限
当然也有例外情况,例如:
- 某一秒令牌桶中产生了很多令牌,达到令牌桶上限N,缓存在令牌桶中,但是这一秒没有请求进入。
- 下一秒的前半秒涌入了超过2N个请求,之前缓存的令牌桶的令牌耗尽,同时这一秒又生成了N个令牌,于是总共放行了2N个请求。超出了我们设定的QPS阈值。
因此,在使用令牌桶算法时,尽量不要将令牌上限设定到服务能承受的QPS上限。而是预留一定的波动空间,这样我们才能应对突发流量。
2.4 漏桶算法(Sentinel的排队等待)
漏桶算法与令牌桶相似,但在设计上更适合应对并发波动较大的场景,解决令牌桶中的问题。
简单来说就是请求到达后不是直接处理,①放入一个队列。②固定的速率从队列中取出并处理请求。[叫漏桶算法,就是把请求看做水,队列看做是一个漏了的桶]
漏桶的优势就是流量整型,桶就像是一个大坝,请求就是水。并发量不断波动,就如图水流时大时小,但都会被大坝拦住。而后大坝按照固定的速度放水,避免下游被洪水淹没。
因此,不管并发量如何波动,经过漏桶处理后的请求一定是相对平滑的曲线: