SpringCloud 微服务_注册中心_配置中心_网关__熔断
2023-10-22 22:03:18 0 举报
AI智能生成
登录查看完整内容
SpringCloud 微服务 注册中心 配置中心 网关 熔断 OpenFeign Nacos SpringCloudGateway Sentinel 面试题
作者其他创作
大纲/内容
微服务是一种软件架构风格,它是以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。
什么是微服务
分支主题
国内从自2016年底开始,微服务热度突然暴涨,到现在2023热度不减
谷歌搜索指数
微服务的趋势是什么样的?
以电商项目举例,后期项目功能模块会越来越多,不管模块怎么增加,都是写在一个项目中的
单体架构的特点就是不管你的业务功能有多少模块,有多少复杂,它都是写在一个项目当中的。
将业务的所有功能集中在一个项目中开发,打成一个包部署。
什么是单体架构
架构简单
部署成本低
优点
单体架构适合开发功能相对简单,规模较小的项目。
2个后端1个前端的项目,一般用单体
单体项目开发人数
团队数十个人同时协作开发同一个项目,由于所有模块都在一个 项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入 到解决冲突的泥潭之中。
10人以上的团队、几百人的团队,如果大家都在一个项目中开发,协作成本有多高?
开发人数
微服务项目
团队协作成本高
任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较 多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时
系统发布效率低
单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能 会耗尽系统资源,导致其它服务低可用。
某些业务,它的发量会比较高一点。比如说下单。下单它的这个并发量很高。在某一时刻,很多用户涌进来了,都来访问订单,结果这个订单业务就把200个链接。占满了。这个时候如果有人来登录,还能登录进来吗?那肯定进不来了,因为资源被占完了。
一台tomcat服务器,它的并发最多能有多少?内部的连接默认是多少个?200个,那他的内部最大连接只有200
并发量
系统可用性差
缺点
单体架构
服务拆分
远程调用
服务治理
请求路由
身份认证
配置管理
服务保护
分布式事务
异步通信
消息可靠性
延迟消息
分布式搜索
倒排索引
数据聚合
微服务项目可能会存在那些问题
一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
单一职责:
每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
团队自治:
每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
服务自治:
微服务架构有什么特点
概述
SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合,微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
SpringCloud是什么?
Eureka、
Nacos、
Consul
注册中心(服务注册发现)
SpringCloudConfig、
Nacos
配置中心(统一配置管理)
OpenFeign
Dubbo
服务远程调用
SpringCloudGateway、
Zuul
网关(统一网关路由)
Hystix、
Sentinel
熔断限流(流控、降级、保护)
Zipkin、
Sleuth
链路追踪(服务链路监控)
SpringCloud各种微服务功能组件 有那些?
目前SpringCloud最新版本为 2022.0.x 版本,对应的SpringBoot版本为 3.x 版本,但它们全部依赖于JDK17,目前在企业中使用相对较少。因此,我们推荐使用次新版本:Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本。
SpringCloud和SpringBoot版本对应关系是什么样的?
指定 版本号
SpringCloudAlibaba目前也成为了SpringCloud组件中的一员,项目中也会使用其中的部分组件。在我们的工程中也配置了SpringCloud以及SpringCloudAlibaba的依赖,这样,我们在后续需要使用SpringCloud或者SpringCloudAlibaba组件时,就无需单独指定版本了。
怎么在maven 的pom文件中指定SpringCloud 的版本?
SpringCloud
一般情况下,对于一个初创的项目,首先要做的是验证项目的可行性。因此这一阶段的首要任务是敏捷 开发,快速产出生产可用的产品,投入市场做验证。为了达成这一目的,该阶段项目架构往往会比较简 单,很多情况下会直接采用单体架构,这样开发成本比较低,可以快速产出结果,一旦发现项目不符合 市场,损失较小。如果这一阶段采用复杂的微服务架构,投入大量的人力和时间成本用于架构设计,最终发现产品不符合 市场需求,等于全部做了无用功。所以,对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分 为微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分 时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。
先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
大多数中小型项目:
而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架 构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。
资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
大型项目:
什么时候拆?
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖。
从拆分目标来说,要做到:
高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前 提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们 不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且 一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时 无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。
所谓纵向拆分,就是按照项目的功能模块来拆分。例如商城中,就有用户管理功能、订单管理功 能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵 向拆分。这种拆分模式可以尽可能提高服务的内聚性。
纵向拆分:按照业务模块来拆分
而横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如 用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、 风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服 务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之 间过分耦合。
横向拆分:抽取公共服务,提高复用性
从拆分方式来说,一般包含两种方式:
如何拆?
完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完 全解耦。
优点:服务之间耦合度低
缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
独立Project
Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
优点:项目代码集中,管理和运维方便
缺点:服务之间耦合,编译时间较长
Maven聚合
微服务项目有那两种不同的工程结构?
购物车业务中需要查询商品信息,但商品信息查询的逻辑全部 迁移到了item-service 服务,导致我们无法查询。最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原 本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)
服务远程调用问题
查询购物车列表的流程
代码中需要变化的就是这一步
我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通 过Swagger测试商品查询接口,而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各 种远程请求。
该如何跨服务调用,准确的说,如何在cart-service 中获取item-service 服务中的提供的商品数据呢?
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
我们该如何用Java代码发送Http的请求呢?
电商项目把商品管理功能、购物车功能抽取为两个独立服务
服务拆分示例:
是什么
可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方 法来构造请求。
将RestTemplate注册为一个Bean
在`cart-service`服务中定义一个配置类:
怎么用?
请求方式
请求路径
请求参数
返回值类型
可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息
修改cart-service 中的CartServiceImpl 的handleCartItems 方法,发送http请求到 item-service :
getForObject:发送Get请求并返回指定类型对象
delete:发送Delete请求
exchange:发送任意类型请求,返回ResponseEntity
PostForObject:发送Post请求并返回指定类型对象
put:发送PUT请求
调用RestTemplate的API发送请求,常见方法有
RestTemplate
假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署
item-service(商品)这么多实例,cart-service如何知道每一个实例的地址?
http请求要写url地址, cart-service 服务到底该调用哪个实例呢?
如果在运行过程中,某一个item-service (商品)实例宕机, cart-service 依然在调用该怎么办?
如果并发太高, item-service(商品) 临时多部署了N台实例, cart-service 如何知道新实例的地址?
每个item-service(商品)的实例其IP或端口不同,问题来了
为了解决上述问题,就必须引入注册中心的概念
使用RestTemplate 服务调用存在的问题
微服务拆分
大型微服务项目中,服务提供者的数量会非常多,注册中心可以管理这些服务
是什么?有什么用?
服务提供者:提供接口供其它微服务访问,比如item-service(商品服务)
服务提供者:暴露服务接口,供其它服务调用
服务消费者:调用其它微服务提供的接口,比如cart-service(购物车服务)
服务消费者:调用其它服务提供的接口
注册中心:记录并监控微服务各实例状态,推送服务变更信息
服务治理中的三个角色分别是什么?
注册中心、服务提供者、服务消费者三者间关系是什么样的?
服务启动时,服务提供者:就会注册自己的服务信息(服务名、IP、端口)到注册中心
服务调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
服务调用者自己对实例列表负载均衡,挑选一个实例
服务调用者向该实例发起远程调用
注册中心的整个运作流程是什么样的?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何知道提供者的地址?
服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
消费者如何得知服务状态变更?
消费者可以通过负载均衡算法,从多个实例中选择一个
当提供者有多个实例时,消费者该选择哪一个?
注册中心原理
- Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
- Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
- Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言
国内比较常见的,开源的注册中心框架有那些?
以上几种注册中心都遵循SpringCloud中的API规范,因此在业务开发使用上没有太大差异。由于Nacos是国内产品,中文文档比较丰富,而且同时具备**配置管理**功能,因此在国内使用较多,我们后面以Nacos为例来学习。
注册中心
Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品,目前已经加入SpringCloudAlibaba中
在`item-service`(商品服务)的`pom.xml`中添加依赖:
引入nacos discovery依赖:
在`item-service`(商品服务)的`application.yml`中添加nacos地址配置:
配置Nacos地址
为了测试一个服务多个实例的情况,我们再配置一个item-service 的部署实例:
然后配置启动项,注意重命名并且配置新的端口,避免冲突:
重启item-service 的两个实例:
访问nacos控制台,可以发现服务注册成功:
点击详情,可以查看到item-service 服务的两个实例信息:
启动服务实例
服务注册
消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样,后面再加上服务调用即可:
服务的消费者要去nacos订阅服务,这个过程就是服务发现
是什么?
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
可以发现,这里Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
因此,等一会儿`cart-service`启动,同样会注册到Nacos
我们在`cart-service`中的`pom.xml`中添加下面的依赖:
引入nacos discovery依赖
在`cart-service`的`application.yml`中添加nacos地址配置:
配置nacos地址
服务调用者`cart-service`就可以去订阅`item-service`服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问
- 随机
- 轮询
- IP的hash
- 最近最少访问
常见的负载均衡算法有:
这里我们可以选择最简单的随机负载均衡。
负载均衡
服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
修改之前的接口调用方式
服务发现工具,DiscoveryClient
发现并调用服务
步骤
服务发现
Nacos注册中心
OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。官方地址:https://github.com/OpenFeign/feign
OpenFeign其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送。
而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。因此,我们必须想办法改变远程调用的开发模式,让**远程调用像本地方法调用一样简单**。而这就要用到OpenFeign组件了。
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
其实远程调用的关键点就在于四个:
OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
在上一章,我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:
OpenFeign是什么? 有什么用?
包括OpenFeign和负载均衡组件SpringCloudLoadBalancer
在`cart-service`服务的pom.xml中引入`OpenFeign`的依赖和`loadBalancer`依赖:
引入依赖
接下来,我们在`cart-service`的`CartApplication`启动类上添加注解,启动OpenFeign功能:
通过@EnableFeignClients注解,启用OpenFeign功能
在`cart-service`中,定义一个新的接口,编写Feign客户端:
- `@FeignClient("item-service")` :声明服务名称
- `@GetMapping` :声明请求方式
- `@GetMapping("/items")` :声明请求路径
- `@RequestParam("ids") Collection<Long> ids` :声明请求参数
- `List<ItemDTO>` :返回值类型
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
其中代码如下:
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向`http://item-service/items`发送一个`GET`请求,携带ids为请求参数,并自动将返回值处理为`List<ItemDTO>`。我们只需要直接调用这个方法,即可实现远程调用了。
编写FeignClient客户端
最后,我们在`cart-service`的`com.hmall.cart.service.impl.CartServiceImpl`中改造代码,直接调用`ItemClient`的方法:
feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来优雅多了。
而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。
使用FeignClient,实现远程调用
案例:购物车服务调用商品服务接口
Feign底层发起http请求,依赖于其它的框架。
- HttpURLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
Feign其底层支持的http客户端实现包括:
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OKHttp.
在`cart-service`的`pom.xml`中引入依赖:
重启服务,连接池就生效了。
在`cart-service`的`application.yml`配置文件中开启Feign的连接池功能:
开启连接池
我们可以打断点验证连接池是否生效,在`org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient`中的`execute`方法中打断点:
可以发现这里底层的实现已经改为`OkHttpClient`
Debug方式启动cart-service,请求一次查询我的购物车方法,进入断点:
验证
连接池
将来我们要把与下单有关的业务抽取为一个独立微服务:`trade-service`(订单微服务),
也就是说,如果拆分了订单微服务(`trade-service`),它也需要远程调用`item-service`(商品服务)中的根据id批量查询商品功能。这个需求与`cart-service`中是一样的。
因此,我们就需要在`trade-service`中再次定义`ItemClient`接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?
问题
订单微服务(trade-service) 和 购物车微服务(cart-service),都要调用商品微服务(item-service)的接口,怎么避免调用商品微服务的接口的代码,在订单、购物车微服务中重复编码。
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
- 思路1:抽取到微服务之外的公共module
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
- 思路2:每个微服务自己抽取一个module
避免重复编码的办法就是**抽取**。不过这里有两种抽取思路:
思路分析
其依赖如下:
在`hmall`下定义一个新的module,命名为hm-api
然后把ItemDTO和ItemClient都拷贝过来,最终结构如下:
现在,任何微服务要调用`item-service`中的接口,只需要引入`hm-api`模块依赖即可,无需自己编写Feign客户端了。
采用方案1:抽取Feign客户端
我们在`cart-service`的`pom.xml`中引入`hm-api`模块:
方式1:声明扫描包:
方式一:指定FeignClient所在包
方式2:声明要用的FeignClient
方式二:指定FeignClient字节码
两种方式:
这里因为`ItemClient`现在定义到了`com.hmall.api.client`包下,而cart-service的启动类定义在`com.hmall.cart`包下,扫描不到`ItemClient`,所以报错了。解决办法很简单,在cart-service的启动类上添加声明即可
删除`cart-service`中原来的ItemDTO和ItemClient,重启项目,发现报错了:
扫描包
实践:抽取公共接口
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。
由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
其日志级别有4级:
代码如下:
在hm-api模块下新建一个配置类,定义Feign的日志级别:
局部生效:在某个`FeignClient`中配置,只对当前`FeignClient`生效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
全局生效:在`@EnableFeignClients`中配置,针对所有`FeignClient`生效。
要让日志级别生效,还需要配置这个类。有两种方式:
配置
日志格式:
自定义日志级别
日志
引入OpenFeign和SpringCloudLoadBalancer依赖
利用@EnableFeignClients注解开启OpenFeign功能
编写FeignClient
如何利用OpenFeign实现远程调用?
引入http客户端依赖,例如OKHttp、HttpClient
配置yaml文件,打开OpenFeign连接池开关
如何配置OpenFeign的连接池?
由服务提供者编写独立module,将FeignClient及DTO抽取
OpenFeign使用的最佳实践方式是什么?
声明类型为Logger.Level的Bean
在@FeignClient或@EnableFeignClients注解上使用
如何配置OpenFeign输出日志的级别?
面试问答
注册中心(服务注册和发现)
网关:就是网络的关口,负责数据的路由、转发、安全校验。
- 网关可以做安全控制,也就是登录身份校验,校验通过才放行
- 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
现在,微服务网关就起到同样的作用。前端请求不能直接访问微服务,而是要请求网关:
- 外面的人要想进入园区,必须经过大爷的认可,如果你是不怀好意的人,肯定被直接拦截。
- 外面的人要传话或送信,要找大爷。大爷帮你带给目标人。
顾明思议,网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来 做数据的路由和转发以及数据安全的校验。更通俗的来讲,网关就像是以前园区传达室的大爷。
类比理解
什么是网关
- 请求不同数据时要访问不同的入口,需要维护多个入口地址,麻烦
- 前端无法调用nacos,无法实时更新服务列表
每个微服务都有不同的地址或端口,入口不同,相信大家在与前端联调的时候发现了一些问题:
网关路由,解决前端请求入口的问题。
- 每个微服务都需要编写登录校验、用户信息获取的功能吗?
- 当微服务之间调用时,该如何传递用户信息?
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,这就存在一些问题:
网关鉴权,解决统一登录校验和用户信息获取的问题。
- 用户服务- 商品服务- 购物车服务- 交易服务- 支付服务
将商城拆分为5个微服务之后:
网关有什么用?解决了什么问题?
- Netflix Zuul:早期实现,目前已经淘汰
超链接
官方网站:
- SpringCloudGateway:基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强
在SpringCloud当中,提供了两种网关实现方案:
网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。
总体流程
首先,我们要在hmall下创建一个新的module,命名为hm-gateway,作为网关微服务:
创建项目
创建网关微服务
- 引入SpringCloudGateway、NacosDiscovery依赖
在hm-gateway 模块的pom.xml 文件中引入依赖:
代码
在hm-gateway 模块的com.hmall.gateway 包下新建一个启动类:
编写启动类
接下来,在hm-gateway 模块的resources 目录新建一个application.yaml 文件,内容如下:
配置网关路由
启动GatewayApplication,以 http://localhost:8080 拼接微服务接口路径来测试。
例如:[http://localhost:8080/items/page?pageNo=1&pageSize=1]
测试
大概步骤如下:
实战:如何利用网关实现请求路由
引入nacos服务发现、负载均衡和gateway依赖
配置application.yml,包括服务基本信息、nacos地址、路由
网关搭建步骤有哪些?
路由id:路由的唯一标示
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
路由断言(predicates):判断路由的规则
路由配置包括哪些?
id:路由唯一标示
uri:路由目的地,支持lb和http两种
predicates:路由断言,断言配置都有路由断言工厂(RoutePredicateFactory)来处理。
filters:路由过滤器,处理请求或响应
网关路由对应的Java类型是RouteDefinition,其中可配置的属性有:
`lb://`代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
这里我们重点关注`predicates`,也就是路由断言。SpringCloudGateway中支持的断言类型有很多
路由规则的定义语法如下:
Spring提供了12种基本的RoutePredicateFactory的默认实现:
是一个集合,也就是说可以定义很多路由规则。集合中的`RouteDefinition`就是具体的路由规则定义,其中常见的属性如下:
其中routes对应的类型如下:
路由断言
网关路由
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
- 每个微服务都需要知道JWT的秘钥,不安全
- 每个微服务重复编写登录校验代码、权限校验代码,麻烦
我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
- 只需要在网关和用户服务保存秘钥
- 只需要在网关开发登录校验功能
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:
鉴权思路分析
单体项目sagg,就是spring AOP 面向切面编程中,做鉴权的 逻辑,校验接口request 的 信息,是否有权访问后端模块的接口,是否过期
登录校验的流程如图:
- 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
- 网关校验JWT之后,如何将用户信息传递给微服务?
- 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
这里存在几个问题:
JWT,全称 JSON Web Token,是一个开发标准(rfc7519),它定义了一种紧凑的,自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密(使用HMAC算法)或者使用RSA或ECDSA的公钥/私钥对进行签名。
通俗来讲,就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密,签名等相关处理。
什么是JWT
JWT
1. 客户端请求进入网关后由`HandlerMapping`对请求做判断,找到与当前请求匹配的路由规则(`**Route**`),然后将请求交给`WebHandler`去处理。
2. `WebHandler`则会加载当前路由下需要执行的过滤器链(`**Filter chain**`),然后按照顺序逐一执行过滤器(后面称为`**Filter**`)。
3. 图中`Filter`被虚线分为左右两部分,是因为`Filter`内部的逻辑分为`pre`和`post`两部分,分别会在请求路由到微服务**之前**和**之后**被执行。
4. 只有所有`Filter`的`pre`逻辑都依次顺序执行通过后,请求才会被路由到微服务。
5. 微服务返回结果后,再倒序执行`Filter`的`post`逻辑。
6. 最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter 的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将 过滤器执行顺序定义到 **NettyRoutingFilter** 之前,这就符合我们的需求了!
什么是网关过滤器?
`Gateway`内置的`GatewayFilter`过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个`Route`下,就作用于哪个`Route`
常见的GatewayFilter有:
使用的使用只需要在application.yaml中这样配置:
如果想要让过滤器作用于所有的路由,则可以这样配置:
例如,有一个过滤器叫做`AddRequestHeaderGatewayFilterFacotry`,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。
**GatewayFilter** :路由过滤器,作用范围比较灵活,可以是任意指定的路由Route .
**GlobalFilter** :全局过滤器,作用范围是所有路由,不可配置。
该如何实现一个网关过滤器呢?
其实`GatewayFilter`和`GlobalFilter`这两种过滤器的方法签名完全一致:
网关过滤器
无论是`GatewayFilter`还是`GlobalFilter`都支持自定义,只不过**编码**方式、**使用**方式略有差别。
**注意**:该类的名称一定要以`GatewayFilterFactory`为后缀!
然后在yaml配置中这样使用:
在yaml文件中使用:
上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。还有一种用法,无需按照这个顺序,就是手动指定参数名:
这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
自定义`GatewayFilter`不是直接实现`GatewayFilter`,而是实现`AbstractGatewayFilterFactory`。最简单的方式是这样的:
自定义GatewayFilter
自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:
自定义GlobalFilter
自定义过滤器
完成登录校验并获取登录用户身份信息
需求:在gateway模块基于过滤器实现登录校验功能
提示:商城是基于JWT实现的登录校验,目前相关功能在hm-service模块。我们可以将其中的JWT工具拷贝到gateway模块,然后基于GlobalFilter来实现登录校验。
- `AuthProperties`:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
- `JwtProperties`:定义与JWT工具有关的属性,比如秘钥文件位置
- `SecurityConfig`:工具的自动装配
- `JwtTool`:JWT工具,其中包含了校验和解析`token`的功能
- `hmall.jks`:秘钥文件
具体作用如下:
其中`AuthProperties`和`JwtProperties`所需的属性要在`application.yaml`中配置:
登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在`hm-service`中已经有了,我们直接拷贝过来:
JWT工具
定义一个登录校验的过滤器:
重启测试,会发现访问/items开头的路径,未登录状态下不会被拦截:
访问其他路径则,未登录状态下请求会被拦截,并且返回`401`状态码:
登录校验过滤器
利用自定义`GlobalFilter`来完成登录校验。
案例:利用网关实现登录校验
网关登录校验
由于网关发送请求到微服务依然采用的是`Http`请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
程图如下:
网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
目前存在的问题
- 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
- 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行
解决问题的步骤
需求:修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中。
提示:要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API,示例如下:
一、在网关的登录校验过滤器中,把获取到的用户写入请求头
需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写。
提示:获取到用户后需要保存到ThreadLocal,对应的工具类在hm-common中已经定义好了:
接下来,我们只需要编写拦截器,获取用户信息并保存到`UserContext`,然后放行即可。由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在`hm-common`中,并写好自动装配。这样微服务只需要引入`hm-common`就可以直接具备拦截器功能,无需重复编写。我们在`hm-common`模块下定义一个拦截器:
接着在`hm-common`模块下编写`SpringMVC`的配置类,配置登录拦截器:
内容如下:
不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是`com.hmall.common.config`,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。基于SpringBoot的自动装配原理,我们要将其添加到`resources`目录下的`META-INF/spring.factories`文件中:
二、在hm-common中编写SpringMVC拦截器,获取登录用户
网关传递用户信息到微服务
下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,**订单服务调用购物车时并没有传递用户信息**,购物车服务无法知道当前用户是谁!
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就**必须在微服务发起调用时把用户信息存入请求头**。微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:
目前项目还存在的问题
我们只需要实现这个接口,然后实现apply方法,利用`RequestTemplate`类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求:
其中的RequestTemplate类中提供了一些方法可以让我们修改请求头:
在`com.hmall.api.config.DefaultFeignConfig`中添加一个Bean:
由于`FeignClient`全部都是在`hm-api`模块,因此我们在`hm-api`模块的`com.hmall.api.config.DefaultFeignConfig`中编写这个拦截器:
现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。
代码实现
OpenFeign在微服务之间传递用户信息
微服务重复配置过多,维护成本高
业务配置经常变动,每次修改都要重启服务
网关路由在配置文件中写死了,如果变更必须重启微服务
网关路由配置写死,如果变更要重启网关
配置中心能解决什么问题?
配置中心是什么? 有什么用?
Nacos不仅仅具备注册中心功能,也具备配置管理的功能
微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。
网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。
有那些配置中心?
- 在Nacos中添加共享配置
- 微服务拉取配置
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
首先是jdbc相关配置:
然后是日志配置:
然后是swagger以及OpenFeign的配置:
添加一些共享配置到Nacos中,包括:Jdbc、MybatisPlus、日志、Swagger、OpenFeign等配置
以cart-service为例,我们看看有哪些配置是重复的,可以抽取的
在弹出的表单中填写信息:
注意这里的jdbc的相关参数并没有写死,例如:
- `数据库ip`:通过`${hm.db.host:192.168.150.101}`配置了默认值为`192.168.150.101`,同时允许通过`${hm.db.host}`来覆盖默认值
- `数据库端口`:通过`${hm.db.port:3306}`配置了默认值为`3306`,同时允许通过`${hm.db.port}`来覆盖默认值
- `数据库database`:可以通过`${hm.db.database}`来设定,无默认值
其中详细的配置如下:
我们在nacos控制台分别添加这些配置。首先是jdbc相关配置,在`配置管理`->`配置列表`中点击`+`新建一个配置:
然后是统一的日志配置,命名为`shared-log.yaml`,配置内容如下:
注意,这里的swagger相关配置我们没有写死,例如:
- `title`:接口文档标题,我们用了`${hm.swagger.title}`来代替,将来可以有用户手动指定
- `email`:联系人邮箱,我们用了`${hm.swagger.email:zhanghuyi@jates.cn}`,默认值是`zhanghuyi@jates.cn`,同时允许用户利用`${hm.swagger.email}`来覆盖。
然后是统一的swagger配置,命名为`shared-swagger.yaml`,配置内容如下:
在Nacos中添加共享配置
拉取共享配置基于NacosConfig拉取共享配置代替微服务的本地配置。
我们要在微服务拉取共享配置。将拉取到的共享配置与本地的`application.yaml`配置合并,完成项目上下文的初始化。不过,需要注意的是,读取Nacos配置是SpringCloud上下文(`ApplicationContext`)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取`application.yaml`。
基于NacosConfig拉取共享配置代替微服务的本地配置。
在cart-service模块引入依赖:
引入依赖:
在cart-service中的resources目录新建一个bootstrap.yaml文件:
新建bootstrap.yaml
重启服务,发现所有配置都生效了。
由于一些配置挪到了bootstrap.yaml,因此application.yaml需要修改为:
修改application.yaml
微服务整合Nacos配置管理的步骤如下:
拉取共享配置
Nacos 实现:配置共享
现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?
- 在Nacos中添加配置
- 在微服务读取配置
这就要用到Nacos的配置热更新能力了,分为两步:
有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10,对应代码如下:
注意文件的dataId格式:```yaml[服务名]-[spring.active.profile].[后缀名]
- `**服务名**`:我们是购物车服务,所以是`cart-service`
- `**spring.active.profile**`:就是spring boot中的`spring.active.profile`,可以省略,则所有profile共享该配置
- `**后缀名**`:例如yaml
文件名称有三部分组成:
配置内容如下:
这里我们直接使用`cart-service.yaml`这个名称,则不管是dev还是local环境都可以共享该配置。
提交配置,在控制台能看到新添加的配置:
首先,我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中:
添加配置到Nacos
接着,我们在微服务中读取配置,实现配置热更新。在`cart-service`中新建一个属性读取类:
接着,在业务中使用该属性加载类:
测试,向购物车中添加多个商品:
配置热更新
Nacos 实现:配置热更新
网关的路由配置全部是在项目启动时由`org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator`在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用之前学习的配置热更新来实现路由更新。
- 如何监听Nacos配置变更?
- 如何把路由信息更新到路由表?
我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:
在Nacos官网中给出了手动监听Nacos配置变更的SDK:
如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。
- 创建ConfigService,目的是连接到Nacos
- 添加配置监听器,编写配置变更的通知处理逻辑
这里核心的步骤有2步:
示例代码:
由于我们采用了`spring-cloud-starter-alibaba-nacos-config`自动装配,因此`ConfigService`已经在`com.alibaba.cloud.nacos.NacosConfigAutoConfiguration`中自动创建好了:
监听Nacos配置变更
更新路由要用到`org.springframework.cloud.gateway.route.RouteDefinitionWriter`这个接口:
- id:路由id
- predicates:路由匹配规则
- filters:路由过滤器
- uri:路由目的地
这里更新的路由,也就是RouteDefinition,之前我们见过,包含下列常见字段:
将来我们保存到Nacos的配置也要符合这个对象结构,将来我们以JSON来保存,格式如下:
以上JSON配置就等同于:
更新路由
首先, 我们在网关gateway引入依赖:
然后在网关`gateway`的`resources`目录创建`bootstrap.yaml`文件,内容如下:
接着,修改`gateway`的`resources`目录下的`application.yml`,把之前的路由移除,最终内容如下:
在`gateway`中定义配置监听器:
发现是404,无法访问。
重启网关,任意访问一个接口,比如 [http://localhost:8080/search/list?pageNo=1&pageSize=1](http://localhost:8080/search/list?pageNo=1&pageSize=1):
接下来,我们直接在Nacos控制台添加路由,路由文件名为`gateway-routes.json`,类型为`json`:
无需重启网关,稍等几秒钟后,再次访问刚才的地址:
实现动态路由
Nacos 实现:动态路由
配置中心
查询购物车列表业务中,购物车服务需要查询最新的商品信息,与购物车数据做对比,提醒用户。大家设想一下,如果商品服务查询时发生故障,查询购物车列表在调用商品服务时,是不是也会异常?从而导致购物车查询失败。但从业务角度来说,为了提升用户体验,即便是商品查询失败,购物车列表也应该正确展示出来,哪怕是不包含最新的商品信息。
业务健壮性问题:
还是查询购物车的业务,假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。
此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。
级联失败问题(雪崩问题):
熔断限流解决的问题是什么?
保证服务运行的健壮性,避免级联失败导致的雪崩问题,就属于微服务保护。
这些方案或多或少都会导致服务的体验上略有下降,比如请求限流,降低了并发上限;
- 请求限流
线程隔离,降低了可用资源数量;
- 线程隔离
服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。因此这些方案都属于服务降级的方案。
- 服务熔断
微服务保护的方案有那些
微服务保护
请求限流:限制流量在服务可以处理的范围,避免因突发流量而故障
线程隔离:控制业务可用的线程数量,将故障隔离在一定范围
失败处理:定义fallback逻辑,让业务失败时不再抛出异常,而是走fallback逻辑
服务熔断:将异常比例过高的接口断开,拒绝所有请求,直接走fallback
解决服务雪崩问题的常见方案有哪些?
请求限流:限制访问接口的请求的并发量,避免服务因流量激增出现故障。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
请求限流
线程隔离:也叫做舱壁模式,模拟船舱隔板的防水原理。通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散。
轮船的船舱会被隔板分割为N个相互隔离的密闭舱,假如轮船触礁进水,只有损坏的部分密闭舱会进水,而其他舱由于相互隔离,并不会进水。这样就把进水控制在部分船体,避免了整个船舱进水而沉没。
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。
如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。
图示
线程隔离
服务熔断:由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求。熔断期间,所有请求快速失败,全都走fallback逻辑。
- 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
-异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
我们要做两件事情:
服务熔断
服务保护方案
Hystrix
对比
有那些服务保护技术
服务保护技术
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址: https://sentinelguard.io/zh-cn/index.html
- **核心库**(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- **控制台**(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
Sentinel 的使用可以分为两个部分:
下载地址:[Sentinel Release](https://github.com/alibaba/Sentinel/releases)
1)下载jar包
将jar包放在任意非中文、不包含特殊字符的目录下,重命名为`sentinel-dashboard.jar`:
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
然后运行如下命令启动控制台:
[Sentinel启动配置项](https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9)
其它启动时可配置参数可参考官方文档:
2)运行
需要输入账号和密码,默认都是:sentinel
访问[http://localhost:8080](http://localhost:8080)页面,就可以看到sentinel的控制台了:
登录后,即可看到控制台,默认会监控sentinel-dashboard服务本身:
3)访问
Sentinel:安装
1)引入sentinel依赖
修改application.yaml文件,添加下面内容:
2)配置控制台
重启`cart-service`,然后访问查询购物车接口,sentinel的客户端就会将服务访问的信息提交到`sentinel-dashboard`控制台。并展示出统计信息:
所谓簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被`Sentinel`监控的资源。默认情况下,`Sentinel`会监控`SpringMVC`的每一个`Endpoint`(接口)。
簇点链路,就是单机调用链路。是一次请求进入服务后经过的每一个被Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个Endpoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径
什么是簇点链路
我们看到`/carts`这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。
点击簇点链路菜单,会看到下面的页面:
默认情况下Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源,这显然是不合适的。
我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是`/carts`路径:
提出问题
首先,在`cart-service`的`application.yml`中添加下面的配置:
然后,重启服务,通过页面访问购物车的相关接口,可以看到sentinel控制台的簇点链路发生了变化:
我们可以选择打开Sentinel的请求方式前缀,把`请求方式 + 请求路径`作为簇点资源名:
解决问题
修改配置,区别Restful风格设计不同接口
簇点链路
3)访问`cart-service`的任意端点
我们在`cart-service`模块中整合sentinel,连接`sentinel-dashboard`控制台,步骤如下:
Sentinel:微服务整合
在簇点链路后面点击流控按钮,即可对其做限流配置:
这样就把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6.
我们利用Jemeter做限流测试,我们每秒发出10个请求:
在弹出的菜单中这样填写:
可以看出`GET:/carts`这个接口的通过QPS稳定在6附近,而拒绝的QPS在4附近,符合我们的预期。
最终监控结果如下:
Sentinel:请求限流
限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。
线程隔离就是 限制某个服务的 线程使用的数量。那这样一来的话,即便某个服务它出现了故障,它也不可能把tomcat资源给耗尽。
什么是线程隔离
这样,即便商品服务出现故障,最多导致查询购物车业务故障,并且可用的线程资源也被限定在一定范围,不会导致整个购物车服务崩溃。
比如,查询购物车的时候需要查询商品,为了避免因商品服务出现故障导致购物车服务级联失败,我们可以把购物车业务中查询商品的部分隔离起来,限制可用的线程资源:
所以,我们要对查询商品的FeignClient接口做线程隔离。
修改cart-service模块的application.yml文件,开启Feign的sentinel功能:
然后重启cart-service服务,可以看到查询商品的FeignClient自动变成了一个簇点资源:
OpenFeign整合Sentinel
点击查询商品的FeignClient对应的簇点资源后面的流控按钮:
注意,这里勾选的是并发线程数限制,也就是说这个查询功能最多使用5个线程(每秒最多5个线程 调用查询接口),而不是5 QPS(每秒调用5次查询接口)。如果查询商品的接口每秒处理2个请求,则5个线程的实际QPS在10左右,而超出的请求自然会被拒绝。
在弹出的表单中填写下面内容:
流控规则
我们利用Jemeter测试,每秒发送100个请求:
此时如果我们通过页面访问购物车的其它接口,例如添加购物车、修改购物车商品数量,发现不受影响
进入查询购物车的请求每秒大概在100,而在查询商品时却只剩下每秒10左右,符合我们的预期。
响应时间非常短,这就证明线程隔离起到了作用,尽管查询购物车这个接口并发很高,但是它能使用的线程资源被限制了,因此不会影响到其它接口。
最终测试结果如下:
实战:配置线程隔离
Sentinel:线程隔离
第一,超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个**降级处理**逻辑。
第二,由于查询商品的延迟较高(模拟的500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口**熔断**。
上面我们利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。由于查询商品的功能耗时较高(我们模拟了500毫秒延时),再加上线程隔离限定了线程数为5,导致接口吞吐能力有限,最终QPS只有10左右。这就导致了几个问题:
存在的问题
触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。
给FeignClient编写失败后的降级逻辑有两种方式:
步骤一:在hm-api模块中给`ItemClient`定义降级处理类,实现`FallbackFactory`:
步骤二:在`hm-api`模块中的`com.hmall.api.config.DefaultFeignConfig`类中将`ItemClientFallback`注册为一个`Bean`:
步骤三:在`hm-api`模块中的`ItemClient`接口中使用`ItemClientFallbackFactory`:
重启后,再次测试,发现被限流的请求不再报错,走了降级逻辑:
导致最终的平局响应时间较长。
但是未被限流的请求延时依然很高:
演示方式二的失败降级处理。
服务降级
熔断降级是解决雪崩问题的重要手段。思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
查询商品的RT较高(模拟的500ms),从而导致查询购物车的RT也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。
对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口**熔断**。当商品服务接口恢复正常后,再允许调用。这其实就是*断路器*的工作模式了。
Sentinel中的断路器不仅可以统计某个接口的*慢请求比例*,还可以统计*异常请求比例*。当这些比例超出阈值时,就会*熔断*该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。
- **closed**:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- **open**:打开状态,服务调用被**熔断**,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
- **half-open**:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
状态机包括三个状态:
断路器的工作状态切换有一个状态机来控制:
在弹出的表格中这样填写:
- RT超过200毫秒的请求调用就是慢调用
- 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断
- 熔断持续时长20s
这种是按照慢调用比例来做熔断,上述配置的含义是:
在一开始一段时间是允许访问的,后来触发熔断后,查询商品服务的接口通过QPS直接为0,所有请求都被熔断了。而查询购物车的本身并没有受到影响。
此时整个购物车查询服务的平均RT影响不大:
配置完成后,再次利用Jemeter测试,可以发现:
在控制台通过点击簇点后的`**熔断**`按钮来配置熔断策略
Sentinel:服务降级、熔断
熔断限流(限流、降级、熔断)
SpringCloud 微服务_注册中心_配置中心_网关__熔断
0 条评论
回复 删除
下一页