高并发下存在的问题
- 微服务拆分多个系统,服务之间互相依赖,可能会由于系统负载过高,突发流量或者网络等各种异常情况,导致服务不可用。
核心思想——面向失败编程
- 不要外界影响
- 不被请求拖垮
- 上游服务
- 下游服务
高并发下的微服务容错方案
- 限流
- 漏斗,不管流量多大,均匀的流入容器,令牌桶算法,漏桶算法
- 熔断:
- 保险丝,熔断服务,为了防止整个系统故障,包含当前和下游服务。下单服务 -》商品服务-》用户服务 -》(出现异常-》熔断风控服务
- 降级:
- 抛弃一些非核心的接口和数据,返回兜底数据。旅行箱的例子:只带核心的物品,抛弃非核心的,等有条件的时候再去携带这些物品
- 隔离:
- 服务和资源互相隔离,比如网络资源,机器资源,线程资源等,不会因为某个服务的资源不足而抢占其他服务的资源
- 熔断和降级互相交集
- 相同点:
- 从可用性和可靠性触发,为了防止系统崩溃
- 最终让用户体验到的是某些功能暂时不能用
- 不同点
- 服务熔断一般是下游服务故障导致的,而服务降级一般是从整体系统负荷考虑,由调用方控制
- 相同点:
想进行微服务的容错,业界目前有 Sentinel、Hystrix,相对于 AlibabaCloud 而言,Sentinel 是最好的搭配
流量防卫兵Sentinel
什么是Sentinel
- 阿里巴巴开源的分布式系统流控工具
- 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
- 丰富的应用场景:消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
- 完备的实时监控:Sentinel 同时提供实时的监控功能
- 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合
官网:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
Sentinel 版本:2.2.1
核心概念:
- 资源:是 Sentinel 中的核心概念之一,可以是java程序中任何内容,可以是服务或者方法甚至代码,总结起来就是我们要保护的东西
- 规则:定义怎样的方式保护资源,主要包括流控规则、熔断降级规则等
Sentinel 和控制台搭建
Sentinel 分为两个部分
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo、Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
微服务引入 Sentinel 依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
Sentinel 控制台搭建
- 文档:https://github.com/alibaba/Sentinel/wiki/控制台
- 控制台包含如下功能:
- 查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
- 监控 (单机和集群聚合)通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。
- 规则管理和推送:统一管理推送规则。
- 鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。
注意:Sentinel 控制台目前仅支持单机部署
//启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本,
//-Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080
//默认用户名和密码都是 sentinel
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
多个微服务接入 Sentinel 配置
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080
port: 9999
#dashboard: 8080 控制台端口
#port: 9999 本地启的端口,随机选个不能被占用的(每个启动的微服务 port 都应该不同),与 dashboard 进行数据交互,会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描
微服务注册上去后,由于 Sentinel 是懒加载模式,所以需要访问微服务后才会在控制台出现
限流配置实操
- 控制台配置
- 浏览器刷新
Sentinel 流量控制功能
流量控制(flow control)
- 监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
两种规则
- 基于统计并发线程数的流量控制
- 并发数控制用于保护业务线程池不被慢调用耗尽
- Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目)
- 如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。
- 基于统计 QPS 的流量控制
- 当 QPS 超过某个阈值的时候,则采取措施进行流量控制
控制面板介绍
- 资源名:默认是请求路径,可自定义
- 针对来源:对哪个微服务进行限流,默认是不区分来源,全部限流,这个是针对区分上游服务进行限流,比如视频服务被订单服务、用户服务调用,就可以针对来源进行限流
基于并发线程进行限流配置实操
开发临时接口,方便测试
@RequestMapping("list") public Object list(){ try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } Map<String,String> map = new HashMap<>(); map.put("title1","ALibabaCloud微服务专题"); map.put("title2","小滴课堂面试专题第一季"); return map; }
如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置
流控规则会下发到微服务,微服务如果重启,则流控规则会消失(可以持久化配置)
选择阈值类型“线程数” ,配置是1
刷新浏览器
流量控制的效果
流量控制的效果包括以下几种:
- 直接拒绝:默认的流量控制方式,当 QPS 超过任意规则的阈值后,新的请求就会被立即拒绝
- Warm Up:冷启动/预热,如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值
- 匀速排队:严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法,主要用于处理间隔性突发的流量,如消息队列,想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求
注意:
- 匀速排队等待策略是 Leaky Bucket 算法结合虚拟队列等待机制实现的。
- 匀速排队模式暂时不支持 QPS > 1000 的场景
流控文档:https://github.com/alibaba/Sentinel/wiki/流量控制#基于调用关系的限流
Sentinel熔断降级规则
熔断降级(虽然是两个概念,基本都是互相配合)
- 对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一
- 对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩
- 熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置
什么是 Sentinel 降级规则
- 文档:https://github.com/alibaba/Sentinel/wiki/熔断降级
- 就是配置一定规则,然后满足之后就对服务进行熔断降级
Sentinel 熔断策略
- 慢调用比例(响应时间): 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用
- 比例阈值:修改后不生效-目前已经反馈给官方那边的bug
- 熔断时长:超过时间后会尝试恢复
- 最小请求数:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断
- 异常比例:当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断
- 比例阈值
- 熔断时长:超过时间后会尝试恢复
- 最小请求数:熔断触发的最小请求数,请求数小于该值时,即使异常比率超出阈值也不会熔断
- 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断
- 异常数
- 熔断时长:超过时间后会尝试恢复
- 最小请求数:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断
常见的熔断状态和恢复
服务熔断一般有三种状态(画图)
- 熔断关闭(Closed)
- 服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
- 熔断开启(Open)
- 后续对该服务接口的调用不再经过网络,直接执行本地的 fallback 方法
- 半熔断(Half-Open)
- 所谓半熔断就是尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率
熔断恢复:
- 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态)尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。
- 如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断状态
服务调用熔断例子
修改代码
int temp = 0; @RequestMapping("list") public Object list() { /*try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }*/ temp++; if (temp % 3 == 0) { throw new RuntimeException(); } Map<String, String> map = new HashMap<>(); map.put("title1", "ALibabaCloud微服务专题"); map.put("title2", "小滴课堂面试专题第一季"); return map; }
熔断测试
Sentinel自定义异常-整合Open-Feign
默认降级返回数据问题
- 限流和熔断返回的数据有问题
- 微服务交互基本都是 json 格式,需要自定义异常信息
AlibabCloud 版本升级,不兼容问题
- v2.1.0 到 v2.2.0 后,Sentinel 里面依赖进行了改动,且不向下兼容
自定义降级返回数据
- 【旧版】实现 UrlBlockHandler 并且重写 blocked 方法
- 【新版】实现 BlockExceptionHandler 并且重写 handle 方法
// 旧版本 @Component public class XdclassUrlBlockHandler implements UrlBlockHandler { @Override public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException { //降级业务处理 } } //新版本 public class XdclassUrlBlockHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { //降级业务处理 } }
异常种类
- FlowException //限流异常
- DegradeException //降级异常
- ParamFlowException //参数限流异常
- SystemBlockException //系统负载异常
- AuthorityException //授权异常
【新版】实现 BlockExceptionHandler 并且重写 handle 方法
@Component public class XdclassUrlBlockHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException, IOException { Map<String, Object> backMap = new HashMap<>(); if (e instanceof FlowException) { backMap.put("code", -1); backMap.put("msg", "限流-异常啦"); } else if (e instanceof DegradeException) { backMap.put("code", -2); backMap.put("msg", "降级-异常啦"); } else if (e instanceof ParamFlowException) { backMap.put("code", -3); backMap.put("msg", "热点-异常啦"); } else if (e instanceof SystemBlockException) { backMap.put("code", -4); backMap.put("msg", "系统规则-异常啦"); } else if (e instanceof AuthorityException) { backMap.put("code", -5); backMap.put("msg", "认证-异常啦"); } // 设置返回json数据 httpServletResponse.setStatus(200); httpServletResponse.setHeader("content-Type", "application/json;charset=UTF-8"); httpServletResponse.getWriter().write(JSON.toJSONString(backMap)); } }
Feign整合Sentinel配置
整合步骤
- 加入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency
- 开启 Feign 对 Sentinel 的支持
# 开启feign对Sentinel feign: sentinel: enabled: true
- 创建容错类,实现对应的服务接口,记得加注解 @Service
@Service public class VideoServiceFallback implements VideoService { @Override public Video findById(int videoId) { Video video = new Video(); video.setTitle("熔断降级数据"); return video; } }
- 配置 Feign 容错类
@FeignClient(name = "xdclass-video-service", fallback = VideoServiceFallback.class) public interface VideoService { @GetMapping(value = "/api/v1/video/find_by_id") Video findById(@RequestParam("videoId") int videoId); }
- 准备兜底数据