目录导读
- 熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 1. 开源代码整体架构设计
- 2. 微服务逻辑架构设计
- 3. 微服务熔断降级与限流规划
- 3.1 微服务熔断降级与限流场景分析
- 3.2 微服务熔断降级与限流技术栈规划
- 3.3 微服务熔断降级与限流技术选型
- 3.3.1 熔断降级中间件选型
- 3.3.2 限流中间件选型
- 4. 微服务熔断降级与限流实现
- 4.1 网关微服务的熔断降级限流实现
- 4.1.1 网关微服务的限流验证
- 4.1.2 网关微服务的熔断降级验证
- 4.2 业务服务的非功能熔断降级与限流实现
- 4.2.1 业务微服务的非功能接口限流实现
- 4.2.2 业务微服务的非功能接口熔断降级实现
- 4.2.3 业务微服务的非功能资源限流实现
- 4.2.3.1 业务微服务的非功能资源限流实现方案一
- 4.2.3.2 业务微服务的非功能资源限流实现方案二
- 4.2.4 业务微服务的非功能资源熔断降级实现
- 4.3 业务微服务功能性限流实现
- 4.3.1 业务微服务的客户限流实现
- 4.3.1.1 业务微服务的客户限流实现的详细逻辑
- 4.3.1.2 业务微服务的客户限流验证
- 4.3.2 业务微服务的渠道限流实现
- 4.3.2.1 业务微服务的渠道限流的详细逻辑
- 4.3.2.2 业务微服务的渠道限流验证
- 5. 本文小结
- 6. 参考资料
熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践
前期内容导读:
- Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍
- Java开源AES/SM4/3DES对称加密算法介绍及其实现
- Java开源AES/SM4/3DES对称加密算法的验证说明
- Java开源RSA/SM2非对称加密算法对比介绍
- Java开源RSA非对称加密算法实现
- Java开源SM2非对称加密算法实现
- Java开源接口微服务代码框架
- Json在开源SpringBoot/SpringCloud微服务框架中的最佳实践
- 加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 链路追踪在开源SpringBoot/SpringCloud微服务框架的最简实践
- OAuth2在开源SpringBoot/SpringCloud微服务框架的最佳实践
- 在前面详细介绍的基础上,且代码全部开源后,这次来完整介绍下熔断降级与限流在微服务解决方案中到底是如何改造的。
- 应该是先有业务,才会有微服务设计。此开源的微服务设计见Java开源接口微服务代码框架 文章,现把核心设计摘录如下:
1. 开源代码整体架构设计
+------------+
| bq-log |
| |
+------------+
Based on SpringBoot
|
|
v
+------------+ +------------+ +------------+ +-------------------+
|bq-encryptor| +-----> | bq-base | +-----> |bq-boot-root| +-----> | bq-service-gateway|
| | | | | | | |
+------------+ +------------+ +------------+ +-------------------+
Based on BouncyCastle Based on Spring Based on SpringBoot Based on SpringBoot-WebFlux
+
|
v
+------------+ +-------------------+
|bq-boot-base| +-----> | bq-service-auth |
| | | | |
+------------+ | +-------------------+
ased on SpringBoot-Web | Based on SpringSecurity-Authorization-Server
|
|
|
| +-------------------+
+-> | bq-service-biz |
| |
+-------------------+
说明:
bq-encryptor
:基于BouncyCastle
安全框架,已开源 ,加解密介绍
,支持RSA
/AES
/PGP
/SM2
/SM3
/SM4
/SHA-1
/HMAC-SHA256
/SHA-256
/SHA-512
/MD5
等常用加解密算法,并封装好了多种使用场景、做好了为SpringBoot所用的准备;bq-base
:基于Spring框架的基础代码框架,已开源 ,支持json
/redis
/DataSource
/guava
/http
/tcp
/thread
/jasypt
等常用工具API;bq-log
:基于SpringBoot框架的基础日志代码,已开源 ,支持接口Access日志、调用日志、业务操作日志等日志文件持久化,可根据实际情况扩展;bq-boot-root
:基于SpringBoot,已开源 ,但是不包含spring-boot-starter-web
,也不包含spring-boot-starter-webflux
,可通用于servlet
和netty
web容器场景,封装了redis
/http
/定时器
/加密机
/安全管理器
等的自动注入;bq-boot-base
:基于spring-boot-starter-web
(servlet,BIO),已开源 ,提供常规的业务服务基础能力,支持PostgreSQL
/限流
/bq-log
/Web框架
/业务数据加密机加密
等可配置自动注入;bq-service-gateway
:基于spring-boot-starter-webflux
(Netty,NIO),已开源 ,提供了Jwt Token安全校验能力,包括接口完整性校验
/接口数据加密
/Jwt Token合法性校验等;bq-service-auth
:基于spring-security-oauth2-authorization-server
,已开源 ,提供了JwtToken生成和刷新的能力;bq-service-biz
:业务微服务参考样例,已开源 ;
2. 微服务逻辑架构设计
+-------------------+
| Web/App Client |
| |
+-------------------+
|
|
v
+--------------------------------------------------------------------+
| | Based On K8S |
| |1 |
| v |
| +-------------------+ 2 +-------------------+ |
| | bq-service-gateway| +-------> | bq-service-auth | |
| | | | | |
| +-------------------+ +-------------------+ |
| |3 |
| +-------------------------------+ |
| v v |
| +-------------------+ +-------------------+ |
| | bq-service-biz1 | | bq-service-biz2 | |
| | | | | |
| +-------------------+ +-------------------+ |
| |
+--------------------------------------------------------------------+
说明:
bq-service-gateway
:基于SpringCloud-Gateway
(底层是基于spring-boot-starter-webflux
),用作JwtToken鉴权,并提供了接口、数据加解密的安全保障能力;bq-service-auth
:基于spring-security-oauth2-authorization-server
,提供了JwtToken生成和刷新的能力;bq-service-biz
:基于spring-boot-starter-web
,业务微服务参考样例;k8s
在上述微服务架构中,承担起了服务注册和服务发现的作用,鉴于k8s
云原生环境构造较为复杂,实际开源的代码时,以Nacos
(为主)/Eureka
做服务注册和服务发现中间件;- 以上所有服务都以docker容器作为载体,确保服务有较好地集群迁移和弹性能力,并能够逐步平滑迁移至k8s的终极目标;
- 逻辑架构不等同于物理架构(部署架构),实际业务部署时,还有DMZ区和内网区,本逻辑架构做了简化处理;
3. 微服务熔断降级与限流规划
- 个人理解的相关概念:
概念 | 处理终端 | 处理措施 | 具体指标 | 恢复措施 |
---|---|---|---|---|
熔断 | 客户端 | 当客户端发起请求时,一旦服务方响应比较慢或者发生了异常,其数值超过了阈值, 则客户端在后续的请求中就直接跳过请求服务端,直接响应预设的失败结果 | 1.按照一定的响应时限熔断; 2.按照异常的比例或者类型熔断; | 一般支持半熔断状态,可自动恢复 |
降级 | 服务端/客户端 | 1.作为服务端,当资源紧张时,主动停掉部分不重要的服务,直接响应预设的异常数据给客户端; 2.作为客户端,作用类似于熔断,但是比熔断的作用范围要小。比如:熔断前会先变成半熔断状态,就可以认为是服务降级; | 1.按照业务重要程度来区分 | 自动或者手动恢复 |
限流 | 服务端/客户端 | 1.作为服务端,当客户在特定时间内的请求达到一定阈值时,直接响应超限的异常结果; 2.作为客户端,在特定时间内,达到服务端允许的调用阈值时,直接响应超限异常或者更换调用其他服务方; | 1.服务端约定时间内的最大调用量限流,如:允许客户A每天调用/xx接口100万次; 2.服务端约定QPS限流,如:允许客户A最大的QPS为100; 3.客户端被约定时间内的最大调用量限流,如:每天被允许调用/xx接口500万次; 4.客户端被约定QPS限流,如:被允许的最大QPS为200; | 自动恢复 |
基于此分析,后面就不单独区分熔断和降级了。在不少开源实现上,熔断降级也是共代码共配置,如:nacos。
- 本解决方案微服务主要为网关服务(
bq-service-gateway
)、认证服务(bq-service-auth
)、业务服务(bq-service-biz
)三类,今天就好好梳理下微服务熔断降级、限流的设计初衷; - 主要从业务场景和实现微服务的技术栈2个方面去展开分析;
3.1 微服务熔断降级与限流场景分析
- 微服务中的每个接口服务是否要做熔断和限流,是值得探讨的事情。如果一股脑都做了,固然好,但是必然耗费更多的精力,同时也会把架构搞得更复杂,比如原本不需要限流的服务,要加入限流能力,就要额外加入限流组件,如果限流的业务诉求是需要根据不同的客户来配置不同的限流规则,那这个服务的依赖就会更复杂,事情做复杂了就非常容易出错;
- 以下是根据个人多年经验,从场景出发,分别对各个微服务的接口是否要做熔断和限流展开分析:
接口 | 调用频率 | 调用链路 | 接口分析 | 是否熔断 | 熔断所在服务 | 是否限流 | 限流所在服务 |
---|---|---|---|---|---|---|---|
/oauth/enc/token | 低 | bq-service-gateway 解密 -> bq-service-auth 生成会话 | auth服务生成JwtToken和刷新JwtToken | ✔ | bq-service-gateway | ✔ | bq-service-gateway |
/oauth/token | 低 | bq-service-gateway 透传 -> bq-service-auth 生成会话 | auth服务生成JwtToken和刷新JwtToken | ✔ | bq-service-gateway | ✔ | bq-service-gateway |
/auth/user/get | 高 | bq-service-gateway JwtToken校验 -> bq-service-auth 获取用户信息 | auth服务提供基础的用户获取数据 | ✔ | bq-service-gateway | ✔ | bq-service-gateway |
/demo/enc/qr | 高 | bq-service-gateway JwtToken校验和解密 -> bq-service-biz 生成二维码数据 | biz服务提供的1个业务能力 | ✔ | bq-service-gateway | ✔ | bq-service-gateway /bq-service-biz |
/demo/qr | 高 | bq-service-gateway JwtToken校验 -> bq-service-biz 生成二维码数据 | biz服务提供的1个业务能力 | ✔ | bq-service-gateway | ✔ | bq-service-gateway /bq-service-biz |
- 表格是按照个人在金融场景下的项目经验做了简化处理,每个微服务的职责非常清晰,这也是我对整个系统架构的强制约束,可能和互联网电商等场景下的微服务之间的调用关系偏差比较大。做此表的目的,是希望大家能够从中学会分析场景,建立自己的思维体系,不要一开始什么都想做,要多思考为什么要做;
- 本微服务解决方案产生的根本原因是:先根据业务诉求,设计出了网关、认证服务和业务服务,先满足了业务诉求;在此基础上,为了系统的稳健和合理,才去思考的哪些服务要做熔断降级,哪些服务要做限流。本文也是照着这个顺序来记录的;
- 熔断降级是系统稳定性需求,限流则要分2个场景来看。限流作为系统稳定性需求(一般叫非功能需求,或者DFX),应该由公共模块来承担,保证系统不被破坏;限流作为业务需求(一般叫做功能性需求,比如限制客户群体中的A、B客户的QPS分别为100、200),则应该由业务服务来承担。不是每个服务都有限流需求;
- 如果对这个表格的每个服务是否要做熔断和降级、限流还不是很理解,可以结合下章的技术栈规划来理解;
3.2 微服务熔断降级与限流技术栈规划
- 基于上面的场景分析,继续分析下每个微服务需要依赖哪些中间件:
服务 | 提供能力 | 依赖中间件情况 |
---|---|---|
bq-service-gateway | 1.提供JwtToken/JwtToken刷新校验; 2.提供接口数据解密/加密能力; | 1.Nacos(开发业务功能时选型的服务注册/发现组件); 2.网关在前期设计就是无状态的轻量级应用,不使用DB; 3.熔断降级限流组件; 4.限流组件; |
bq-service-auth | 1.提供生成JwtToken的能力; 2.提供获取用户信息的能力; | 1.Nacos(开发业务功能时选型的服务注册/发现组件); 2.按照上述场景分析,不需要熔断降级限流组件; |
bq-service-biz | 1.提供获取业务二维码生成的能力; | 1.Nacos(开发业务功能时选型的服务注册/发现组件); 2.按照上述场景分析,不依赖熔断降级组件; 3.按照上述场景分析,依赖限流组件; |
- 以上是微服务解决方案新增熔断及限流能力初期的设计,实际上,考虑到后续业务场景的扩展,还是会把熔断降级和限流代码写在微服务的公共基础代码包中,以后就算是要扩展,微服务从没做限流到支持限流的改造也会非常简单,此处先不展开叙述了;
3.3 微服务熔断降级与限流技术选型
3.3.1 熔断降级中间件选型
- 熔断降级中间件选型(有人做了比较全面的对比,参见链接 ):
优劣势对比 | Hystrix | resilience4j | Sentinel |
---|---|---|---|
优势 | 活跃度非常高,成熟度高 | Spring推荐取代Hystrix的熔断组件,轻量级,功能强大 | 中文社区活跃度非常高,成熟度高; 鉴于系统已经使用了Nacos,再使用alibaba出品的Sentinel的成本相对较低; 考虑到以后微服务可能会切换到Dubbo框架,也会更丝滑 |
缺点 | 已经停止维护,在大公司是禁用状态,此处也不选 | 新组件,国内使用还不算多; 本人专门写的预研总结如下: resilience4j使用指南 | 为了支持复杂的业务场景,导致熔断规则较为复杂 |
结论 | ✖ | ✖ | ✔ |
3.3.2 限流中间件选型
- 限流场景分析:
- 限流分为非功能需求和功能需求2部分。考虑到系统的简化,因为上述的熔断降级组件都具备限流能力,所以非功能需求的限流应该由熔断降级组件承担;功能需求的限流则需要继续深入业务诉求进行评估;
- 因为是分布式微服务架构,考虑到服务实例的伸缩,无论上述哪种限流,都应该是集群限流;
- 因为功能需求的限流需要对每个不同的客户设置不同的阈值,且该阈值配置需要很方便的查看和更改,所以需要先把限流阈值放在DB中,然后再把DB的限流阈值刷入限流组件中,且要分布式的所有业务服务实例立即生效;
- 功能需求的限流除了要支持对客户的请求限流外,还需要把控我们调用第三方的请求限流(这可不是我杜撰的需求,在Java图片压缩/加密处理实践
一文中有提及,当我们调用受限的第三方权威数据源时,就必须对我们发起请求的并发和每天调用量做控制); - 功能需求的限流还要支持多种限流规则,比如:每天调用量限流、QPS限流,还要方便以后扩展,比如:支持月调用量限流;
- 基于上述限流场景+开源技术组件对比如下:
场景支持情况对比 | Hystrix | Resilience4j | Sentinel | Redis | Guava |
---|---|---|---|---|---|
作为服务端,接口被调用的天调用量限流 | ✖ | ✖ | ✖ | ✔ | ✔ |
作为服务端,接口被调用的QPS限流 | ✔ | ✔ | ✔ | ✔ | ✔ |
作为服务端,接口被客户ID调用的天调用量限流 | ✖ | ✖ | ✖ | ✔ | ✖ |
作为服务端,接口被客户ID调用的QPS限流 | ✖ | ✖ | ✖ | ✔ | ✖ |
作为客户端,调用第三方接口的天调用量限流 | ✖ | ✖ | ✖ | ✔ | ✖ |
作为客户端,调用第三方接口的QPS限流 | ✖ | ✖ | ✖ | ✔ | ✖ |
集群限流 | ✖ | ✖ | ✔ | ✔ | ✖ |
结论 | ✖ | ✖ | ✔ | ✔ | ✖ |
综上以上诉求:
- 非功能需求的限流组件选择Sentinel,理由是支持集群限流,同时正好反向印证了上面的熔断降级组件选择Sentinel是合理的
- 功能需求的限流组件选择redis,理由如下:
- redis可以充当数据库的限流阈值缓存(限流阈值放在数据库是为了方便管理,但是限流使用阈值又非常频繁,不能一直查数据库,但是又要求阈值一直是最新的,否则就起不到快速流控的效果了。所以一般限流阈值是放在数据库,只有第一次从数据库查入缓存,后面就一直从缓存获取;当阈值更新时,更新数据库后,立即更新redis,则限流阈值也随之生效了。能起到同等效果的是Guava+分布式消息通知组件);
- redis对限流场景非常友好,API非常简洁;
- redis还支持lua脚本编写限流逻辑,以后要加新的限流逻辑时,只需要往代码框架添加新的lua脚本即可,代码本身只需要做很小的改动,甚至没有改动;
4. 微服务熔断降级与限流实现
- 汇总下各微服务熔断降级限流的技术栈
服务 | 提供能力 | 依赖中间件情况 |
---|---|---|
bq-service-gateway | 1.提供JwtToken/JwtToken刷新校验; 2.提供接口数据解密/加密能力; | 依赖Sentinel熔断降级及做非功能需求的限流 |
bq-service-auth | 1.提供生成JwtToken的能力; 2.提供获取用户信息的能力; | - |
bq-service-biz | 1.提供获取业务二维码生成的能力; | 依赖Redis做业务限流; |
考虑以后的扩展,实际上
bq-service-biz
也有添加熔断降级组件Sentinel,bq-service-auth
也引入了Sentinel和Redis,只是没有启用相应的规则而已;
- 做好前置准备:
- 因为sentinel是独立安装的,需要提前准备好。下载至~/opensource目录,启动sentinel:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar ~/opensource/sentinel-dashboard*.jar
- 同理启动Nacos:
sh ~/opensource/nacos-2.2.1/distribution/target/nacos-server-2.2.1/nacos/bin/startup.sh -m standalone
- 同理启动zipkin
java -jar ~/opensource/zipkin-server/target/zipkin-server-*exec.jar
- 启动redis
redis-server
- 因为sentinel是独立安装的,需要提前准备好。下载至~/opensource目录,启动sentinel:
以上所有启动命名均是基于本人的Mac电脑验证的,系统不同时命令可能有差异,请参考其文字说明。
4.1 网关微服务的熔断降级限流实现
- 引入Sentinel Pom依赖配置
<!--sentinel熔断降级--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.8.6</version> </dependency>
bq-service-gateway
:基于spring-cloud-gateway
,已开源 ,其引入Sentinel的yaml配置 如下:spring: cloud: sentinel: transport: #sentinel服务地址 dashboard: localhost:8080 #默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口 port: 8719 #规则持久化配置 datasource: ds1: nacos: #nacos服务地址 server-addr: localhost:8848 #nacos配置文件的名称 dataId: ${spring.application.name} groupId: DEFAULT_GROUP #持久化为json文件 data_type: json rule_type: flow
bq-service-gateway
编写限流的异常响应服务SentinelConfigurer(本例是参考官方例子):@Slf4j @Configuration public class SentinelConfigurer { /** * 定义网关异常时的处理器(使用自定义的错误码) * * @return 熔断降级异常时的处理器 */ @Bean public BlockRequestHandler blockRequestHandler() { return (exchange, e) -> { log.error("happened block exception.", e); ResultCode<?> resultCode = ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode()); int httpCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); ServerResponse.BodyBuilder bodyBuilder = ServerResponse.status(httpCode); bodyBuilder.contentType(MediaType.APPLICATION_JSON); return bodyBuilder.body(BodyInserters.fromValue(resultCode)); }; } }
各位注意:网上有各种Sentinel版本的初始化配置,截止目前我使用的版本,仅需要如此配置即可。不需要定义
SentinelGatewayFilter
/SentinelGatewayBlockExceptionHandler
等,一定不要盲从。- 至此,网关的限流和熔断代码全部编写完毕,依次启动
bq-service-auth
和bq-service-gateway
微服务。
4.1.1 网关微服务的限流验证
- 新增网关流控规则,配置针对Route ID(整个服务)的限流:
- 验证步骤:
-
- curl命令调用加密的auth服务认证接口 获取JwtToken:
curl --location --request POST 'http://localhost:9992/oauth/enc/token?scope=read&grant_type=client_credentials' \ --header 'bq-integrity: ef5b4373e5c24c2c0ccd6700a3e9b70f2b3a80268f9d4b1cc5bf8740e5dd81ca' \ --header 'bq-enc: app001' \ --header 'Authorization: 04d726fd8806b1c0763fead3a90592e592d8abb8290a6b638208c19977ae2d52e3553e507ad72f7e62b33a70fdaca4d8416ec091a59fe7eb8246745b5b6c88c15db580847186e895b29b47a569d2fc4e70e1e63e44aa529b396db710663d9a0c0c0f3a171885f74e0d8eec10469cb757d02b57a850547dc03bfa23'
-
- 获取相应的JwtToken报文为:
{ "code": "100001", "msg": "通过", "data": { "access_token": "eyJraWQiOiI2ZTNjNmYzMWI2ODk0MjU0YWUwY2Q4ODdkZWFmMzMxOCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhcHAwMDEiLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTk5MSIsInJlc291cmNlcyI6WyJcL2F1dGhcL3d4Il0sInNvdXJjZV90eXBlIjoiU0RLIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImF1ZCI6ImFwcDAwMSIsIm5iZiI6MTY4ODA4NzA5Nywic2NvcGUiOlsicmVhZCJdLCJqd3RfdHlwZSI6InRva2VuIiwiZXhwIjoxNjg4MDg4ODk3LCJpYXQiOjE2ODgwODcwOTcsImp0aSI6IjE3ZGQwZTUxZDExNDRiOGJiNzczNmI0YjgyM2RkNDI4In0.ju7NxhV8HmLct3afTrRxtj2KhUBLBekFAZ5bMq-K2yAW-2R8hJr6cKLy9F3WvMS_RsBRfoPx9e3Kn5glG6FzGBjwGJ08IFfGa3ufurFG5wQZZ39MIBu0hwuSuHZa6FFym51ZxY6QM0AMdgVdJOQL8gKVTH-Ui5lNtGDmqsuc89b1HEwORs-Or7jch1BrLJwNijTzrQN3lcdOXuuOuCh4claNxopuxswz3N2p-DTDWZJVnBh4TvtzJN-ycH5Xsy93a64iJCcrgXczoOi-FCUi6UFsly1WBnYkju09iJvrrJ1AkosZ9L-md4xSK2XrA2VEGUroaXITUlrzKoFRSiC2fA", "token_type": "Bearer", "scope": "read", "refresh_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhcHAwMDEiLCJhdWQiOiJhcHAwMDEiLCJuYmYiOjE2ODgwODcwOTcsInNjb3BlIjpbInJlYWQiXSwiand0X3R5cGUiOiJyZWZyZXNoIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0Ojk5OTEiLCJzb3VyY2VfdHlwZSI6IlNESyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJleHAiOjE2ODgwOTA2OTcsImlhdCI6MTY4ODA4NzA5NywianRpIjoiM2QyMzY0MzljOTNiNDhhNjgyYzViMGY5ZmY0MTk0ODcifQ.Cpz0O3UGpGNkirTkcHqOaJgfyTK8HNaM0PybS3SIylwM98gFtf8pyx2geOMc4UDj4qIfCFwhbigGr8Vq3qvbO7BXNDZENyG17Y65TmHoc2SfedBbpsnJZJ9wcAPoCfR-4pD1_5cX6VvZEjm6NXv_2BCKbaP11Z7qMNCB_iI0gA61HV6_13MzWVSNVd8ukYHdyy3LbbmgkwTcAiw16VbTDmgrcCEHX4vdXXDd8OzmT5QpnsPFLKi3y0nHN4R73U7Sdv3gRCCStmZyBYj4EYpXf__5WIU-2emcYgK-7mtiZx09ZJXJThw47QDG1QtAMy_7r4zTRsTiPahIULBJu2XISA", "client_id": "app001", "jti": "17dd0e51d1144b8bb7736b4b823dd428", "resources": [ "/auth/wx" ], "expires_in": 1800 }, "cost": 0 }
-
- 把JwtToken填入请求报文header中,并执行获取用户信息接口的curl命令:
curl --location 'http://localhost:9992/auth/user/get' \ --header 'Authorization: Bearer eyJraWQiOiI2ZTNjNmYzMWI2ODk0MjU0YWUwY2Q4ODdkZWFmMzMxOCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhcHAwMDEiLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTk5MSIsInJlc291cmNlcyI6WyJcL2F1dGhcL3d4Il0sInNvdXJjZV90eXBlIjoiU0RLIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImF1ZCI6ImFwcDAwMSIsIm5iZiI6MTY4ODA4NzA5Nywic2NvcGUiOlsicmVhZCJdLCJqd3RfdHlwZSI6InRva2VuIiwiZXhwIjoxNjg4MDg4ODk3LCJpYXQiOjE2ODgwODcwOTcsImp0aSI6IjE3ZGQwZTUxZDExNDRiOGJiNzczNmI0YjgyM2RkNDI4In0.ju7NxhV8HmLct3afTrRxtj2KhUBLBekFAZ5bMq-K2yAW-2R8hJr6cKLy9F3WvMS_RsBRfoPx9e3Kn5glG6FzGBjwGJ08IFfGa3ufurFG5wQZZ39MIBu0hwuSuHZa6FFym51ZxY6QM0AMdgVdJOQL8gKVTH-Ui5lNtGDmqsuc89b1HEwORs-Or7jch1BrLJwNijTzrQN3lcdOXuuOuCh4claNxopuxswz3N2p-DTDWZJVnBh4TvtzJN-ycH5Xsy93a64iJCcrgXczoOi-FCUi6UFsly1WBnYkju09iJvrrJ1AkosZ9L-md4xSK2XrA2VEGUroaXITUlrzKoFRSiC2fA' \ --header 'Content-Type: application/json' \ --data '{ "app_id":"app001" }'
-
- 响应的成功结果为:
{ "code": "100001", "msg": "通过", "data": { "start": 0, "id": "a4b42fa45e6f4dd8a942c34c62a6bf57", "app_id": "app001", "app_key": "hao123", "app_name": "bq-app", "expire_time": 1715348560590, "create_time": 1683812560590, "status": 1 }, "cost": 0 }
-
- 再次快速多次执行获取用户信息接口的curl命令,则会出现如下响应结果:
{ "code": "100098", "msg": "内部错误", "cost": 0 }
-
- 查看运行日志,看到上述自定义的异常Handler(
SentinelConfigurer#blockRequestHandler
)的日志已打印:
23-06-30 21:16:40.800[bq-gateway][Tid:,Sid:][ERROR][c.b.b.s.g.c.SentinelConfigurer_lambda$blockRequestHandler$0] - happened block exception. com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException: $D Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.cloud.sleuth.instrument.web.TraceWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ HTTP POST "/auth/user/get" [ExceptionHandlingWebHandler] Original Stack Trace:
- 查看运行日志,看到上述自定义的异常Handler(
-
- 新增针对单个接口的网关流控规则,步骤如下:
- 在API管理中新增一个API分组:
- 新增网关流控规则,选择这个API分组:
- 在API管理中新增一个API分组:
- 单个接口的网关限流验证步骤同上,略。
总结下sentinel限流的验证过程:
- 只需要简单的引入sentinel依赖,并编写自定义的异常响应结果(该异常结果也可以配置,本解决方案是为了统一错误码,所以通过代码来控制)。
- 在通过Sentinel管理界面来做流控时,需要注意一旦重启了网关,其流控配置规则就丢失了,需要重新在Sentinel面板刷新确认下规则在不在,如果不在就需要重新创建,否则限流就不会生效。也相信聪明的你,很快就能想到怎么做规则的持久化。
4.1.2 网关微服务的熔断降级验证
- 在上述API分组的基础上,创建针对单个接口的熔断降级规则:
- 熔断降级配置(步骤见截图框中部分,也可以直接在熔断规则里面去创建):
- 为了保证验证效果,特意删除上述创建的2个限流规则;
- 为了模拟大概率出现慢请求的效果,需要改造下
bq-service-auth
的/auth/user/get
rest接口代码:@Slf4j @RestController public class ClientResourceController { @PostMapping("/auth/user/get") public ResultCode<ClientResource> get(@RequestBody ClientResource client) { long simulatorMills = RandomUtils.nextInt(8, 15) * 100; log.info("current user:{},with simulator:{}ms", JsonUtil.toJson(client), simulatorMills); try { Thread.sleep(simulatorMills); } catch (InterruptedException e) { log.error("InterruptedException:", e); } ClientResource result = dao.get(client); log.info("from db user:{}", JsonUtil.toJson(result)); return ResultCode.ok(result); } /** * dao操作 */ @Autowired private BizDao<ClientResource> dao; }
- 熔断降级配置(步骤见截图框中部分,也可以直接在熔断规则里面去创建):
- 反复执行上面的curl命令请求
http://localhost:9992/auth/user/get
,会返回异常错误码,观测日志异常结果如下:23-06-30 21:00:10.023[bq-gateway][Tid:,Sid:][ERROR][c.b.b.s.g.c.SentinelConfigurer_lambda$blockRequestHandler$0] - happened block exception. com.alibaba.csp.sentinel.slots.block.degrade.DegradeException: null Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.cloud.sleuth.instrument.web.TraceWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ HTTP POST "/auth/user/get" [ExceptionHandlingWebHandler] Original Stack Trace:
- 针对Route ID(整个服务)的熔断降级过程同上,就不再单独介绍了。
总结下sentinel熔断降级的验证过程:
- 除了配置规则有区别外,其他的方面基本上都是相同的。
- 在通过Sentinel管理界面来做熔断时,需要注意一旦重启了网关,其熔断配置规则就丢失了,需要重新在Sentinel面板刷新确认下规则在不在,如果不在就需要重新创建,否则熔断就不会生效。也相信聪明的你,很快就能想到怎么做规则的持久化。
4.2 业务服务的非功能熔断降级与限流实现
- 按照前面的场景分析,除了
bq-service-gateway
需要熔断降级和限流外,就只有bq-service-biz
做功能性限流了,不涉及熔断降级。但是为了验证下Sentinel在常规服务的熔断降级用法,特意又新增了一个/demo/jwk
接口(bq-service-gateway
->bq-service-biz
->bq-service-auth
),在bq-service-biz
服务上做熔断降级和限流; bq-service-biz
服务中引入Sentinel Pom依赖配置:<!--sentinel熔断降级--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId> <version>2021.0.5.0</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.8.6</version> </dependency>
bq-service-biz
服务中引入限流的异常响应服务SentinelWebConfigurer:@Slf4j @Configuration public class SentinelWebConfigurer { /** * 定义异常时的处理器(使用自定义的错误码) * * @return 熔断降级异常时的处理器 */ @Bean public BlockExceptionHandler blockSentinelHandler() { return (request, response, e) -> { log.error("limit block happened.", e); ResultCode<?> resultCode = ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); ResponseUtil.writeErrorBody(response, JsonUtil.toJson(resultCode, snakeCase)); }; } /** * 是否驼峰式json(默认支持) */ @Value("${bq.json.snake-case:true}") private boolean snakeCase; }
4.2.1 业务微服务的非功能接口限流实现
- 因为sentinel默认是懒加载,需要先请求下接口。先执行命令获取JwtToken:
curl --location --request POST 'http://localhost:9992/oauth/enc/token?scope=read&grant_type=client_credentials' \ --header 'bq-integrity: ef5b4373e5c24c2c0ccd6700a3e9b70f2b3a80268f9d4b1cc5bf8740e5dd81ca' \ --header 'bq-enc: app001' \ --header 'Authorization: 04d726fd8806b1c0763fead3a90592e592d8abb8290a6b638208c19977ae2d52e3553e507ad72f7e62b33a70fdaca4d8416ec091a59fe7eb8246745b5b6c88c15db580847186e895b29b47a569d2fc4e70e1e63e44aa529b396db710663d9a0c0c0f3a171885f74e0d8eec10469cb757d02b57a850547dc03bfa23'
- 替换如下命令中的JwtToken后,执行命令调用接口
/demo/jwk
:curl --location 'http://localhost:9992/demo/jwk' \ --header 'Authorization: Bearer eyJraWQiOiI2ZTNjNmYzMWI2ODk0MjU0YWUwY2Q4ODdkZWFmMzMxOCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhcHAwMDEiLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTk5MSIsInJlc291cmNlcyI6WyJcL2F1dGhcL3d4Il0sInNvdXJjZV90eXBlIjoiU0RLIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImF1ZCI6ImFwcDAwMSIsIm5iZiI6MTY4ODE2Njg3NSwic2NvcGUiOlsicmVhZCJdLCJqd3RfdHlwZSI6InRva2VuIiwiZXhwIjoxNjg4MTY4Njc1LCJpYXQiOjE2ODgxNjY4NzUsImp0aSI6ImViZjZjNTBiYTk0YjRjM2FiNjQ4MGY5Yjk3YmM4ODIyIn0.SsFSmUHVUjPi-Xdu8jYl4VV3epZoGeyQSsQPsRGE4nyEjiHoME01j9JF8J3iwvdy9sJhQhol8eV2-_t69doQhTT-Faj_Q6kS4Bjr4A5UZknZ5LPHa-gUX_wyxwpZzkto2ynxvyyGIm2sXhsZJapNdDNpM5-skF_ts8aC3BcNx4qk18e9ZEG6-3V1Oc9IDJJ2G3kGqFRXh52Ot0cS61clXk-2BzYHZ6PDBKhB8pjA7_cGgqzqYBF6IXObyH-XfKIROtnl5gHKpUq69Re-ffjInY4hOEGXssb8bzuzt8nAOLSt15_Td_wKYI1MiVuUtx3fqJcH4mJC7dcPqqojjpY4wg' \ --header 'Content-Type: application/json' \ --data '{ "code":"test123" }'
- 打开Sentinel控制面板就可以看到如下请求:
- 继续点击图注的流控按钮,添加限流规则:
- 多次执行上述命令,请求
/demo/jwk
接口,会看到如下返回结果:{ "code": "100003", "msg": "流量超限", "cost": 0 }
- 观测运行日志,发现也从我们自定义的handler中打印出了相关异常:
[bq-biz][Tid:2e534ec2f199a69b,Sid:c64a17f11048a522][ERROR][c.b.b.c.SentinelWebConfigurer_lambda$blockSentinelHandler$0] - limit block happened. com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
总结:Sentinel对单个接口的限流已经演示完毕。但是Sentinel熔断降级和限流并不仅限于接口,还可以对里面的部分资源进行熔断降级和限流。比如接口中,有查询数据库/请求其他服务时,可以在其Service方法上添加
@SentinelResource
注解。
4.2.2 业务微服务的非功能接口熔断降级实现
- 接口的熔断降级实现过程和上述的限流逻辑相比,除了配置规则不一样,其它完全一致,验证过程略。
4.2.3 业务微服务的非功能资源限流实现
4.2.3.1 业务微服务的非功能资源限流实现方案一
- Sentinel中可以使用
@SentinelResource
注解对资源做限流(参见文档 )。简单总结下:@SentinelResource
使用场景比较苛刻,要求方法的参数名和接口的入参一致,没法做全局统一的异常管控,也就是每个限流的@SentinelResource
资源都得写个异常处理逻辑,非常不优雅。代码如下:@Slf4j @RestController public class QrCodeController extends BaseBizController<QrCodeResult, QrCode, QrCodeInner> { @SentinelResource(value = "demo_jwk", blockHandler = "blockHandler") @PostMapping("/demo/jwk") @Override public ResultCode<QrCodeResult> execute(@RequestBody QrCodeInner inner) { log.info("current inner:{}", JsonUtil.toJson(inner)); return restService.execute(inner.toModel()); } protected ResultCode<QrCodeResult> blockHandler(QrCodeInner inner, BlockException e) { log.error("current inner:{},block exception.", JsonUtil.toJson(inner), e); return ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); } /** * 注入自定义的Rest服务 */ @Resource(name = DemoConst.DEMO_REST_SERVICE) private RestService<QrCodeResult, QrCode> restService; }
- 配置资源相关的限流见下图:
- 多次执行上述命令,请求
/demo/jwk
接口,会看到如下返回结果:{ "code": "100003", "msg": "流量超限", "cost": 0 }
- 观测运行日志,发现也从我们自定义的handler中打印出了相关异常:
[ERROR][c.b.b.w.d.QrCodeController_blockHandler] - current inner:{"code":"test123"},block exception. com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
- 按照常规的限流方式,验证完毕。但是为每个
@SentinelResource
都要写一个异常处理逻辑实在难以接受,下面介绍另外一种解决方案。
4.2.3.2 业务微服务的非功能资源限流实现方案二
- 还是如上先定义
@SentinelResource
资源,但是不给对应的异常实现,代码如下:@Slf4j @RestController public class QrCodeController extends BaseBizController<QrCodeResult, QrCode, QrCodeInner> { @SentinelResource(value = "demo_jwk", blockHandler = "blockHandler") @PostMapping("/demo/jwk") @Override public ResultCode<QrCodeResult> execute(@RequestBody QrCodeInner inner) { log.info("current inner:{}", JsonUtil.toJson(inner)); return restService.execute(inner.toModel()); } /** * 注入自定义的Rest服务 */ @Resource(name = DemoConst.DEMO_REST_SERVICE) private RestService<QrCodeResult, QrCode> restService; }
- 不写任何的
blockHandler
逻辑。配置了资源限流规则,阈值达到时,异常就会抛到SpringBoot框架中。 - 在SpringBoot全局异常处理器
GlobalExceptionHandler
中,新增熔断降级与限流的中断异常BlockException处理逻辑:@Slf4j @ControllerAdvice public class GlobalExceptionHandler extends BaseExceptionHandler { @ExceptionHandler({CommonException.class, NoHandlerFoundException.class, Exception.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ResultCode<?> handleErr(HttpServletRequest req, Exception e) { Throwable ex = e; if (e instanceof UndeclaredThrowableException) { UndeclaredThrowableException realEx = (UndeclaredThrowableException)e; ex = realEx.getUndeclaredThrowable(); } if (ex instanceof BlockException) { log.error("sentinel block happened.", ex); return ResultCode.error(ErrCodeEnum.LIMIT_ERROR.getCode()); } return handle(req.getRequestURI(), e); } }
- 之后继续按照方案一的步骤创建限流配置,执行相关的命令,观测日志结果如下:
[ERROR][c.b.b.h.GlobalExceptionHandler_handleErr] - sentinel block happened. com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
4.2.4 业务微服务的非功能资源熔断降级实现
- 资源的熔断降级实现过程和上述的限流逻辑相比,除了配置规则不一样,其它完全一致,验证过程略。
总结下
4.2章节
的主要内容:
- 本章节本来在场景分析中,是不需要做非功能熔断降级与限流的,但是考虑到后续业务的复杂性,还是把相关的能力给补充上了,只要Sentinel面板中不配置相关的规则,也不会有什么影响;
- 接口(Rest)的非功能熔断降级与限流使用非常简单,且全局配置一个异常Handler(参见本开源项目中的SentinelWebConfigurer类),再在Sentinel面板中配置规则即可;
- 资源(
@SentinelResource
注解标注)的非功能熔断降级与限流使用则较为复杂与苛刻,本意是接口中可能会涉及查询数据库、查询缓存、调用第三方接口等,可以分别定义多个资源的熔断降级与限流资源(一般是在对应的Service上添加@SentinelResource
注解)来实现,再在Sentinel面板配置对应规则,同时还要求Service的方法入参和异常Handler的参数一致,这样就很难做到像上面的接口那样由一个异常Handler统一处理。本方案目前采取了通过全局异常来做统一处理,但是在某些降级场景下,服务的某几个资源降级了,整体服务还要求是成功时,全局异常的方案就不适用了。
4.3 业务微服务功能性限流实现
- 通过上面的介绍,相信大家会对非功能的熔断降级与限流有了较为清晰的认识,再概括来讲,这些非功能的架构设计属于基础架构的范畴;
- 业务在落地的时候,需要考虑更深层次的用户需求和全盘可行性,拿限流来讲,除了非功能限流外,还有功能性限流,而且功能性限流还会多种多样(前面的章节有介绍),这方面的架构设计属于业务架构的范围;
- 基础架构和业务架构师一般分属不同的团队,技术各有千秋,二者兼修才是王道。当然这是题外话,说回正题。
4.3.1 业务微服务的客户限流实现
4.3.1.1 业务微服务的客户限流实现的详细逻辑
-
先讲下客户限流的核心设计思路:
- 基于spring-boot-starter-web框架的HandlerInterceptor切面,添加自定义的限流逻辑,且该限流切面可根据yaml配置自动开启和关闭;
- 系统中的接口有很多,支持配置部分接口做限流,部分接口不做限流,这个简单配置放在字典表即可;
- 支持对QPS和每天调用量2个维度的限流,且该限流逻辑完全是通过数据库配置和Redis Lua脚本组合实现,方便后续继续扩展不同的限流维度;
- 支持如下场景的客户限流,且优先级从高到底:
- 客户调用接口限流(单用户单接口精准限流);
- 对客户做整体限流(客户调用的所有接口共享限流阈值);
- 对接口做整体限流(所有客户共享限流阈值);
- 对所有接口所有客户做整体限流(所有客户、所有接口共享限流阈值);
通过一个表结构设计就搞定了数据存储,查询优先级时略微有点复杂;
-
核心代码如下;
- 基于spring-boot-starter-web的服务可通过DelegatingWebMvcConfiguration来扩展,本开源框架实现
WebMvcConfigurer
的代码如下:@Slf4j @Configuration public class WebMvcConfigurer extends BaseWebConfigurer { @Override protected void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); //默认关闭限流,只有业务微服务才需要打开 if (!limitEnabled) { log.info("disabled limit access."); return; } //添加一个限流器,仅拦截特定的url InterceptorRegistration registration = registry.addInterceptor(limitHandler); GlobalDict param = new GlobalDict().toDict(); List<GlobalDict> batchDict = dictService.getBatch(param); Set<String> urls = Sets.newHashSet(); if (!CollectionUtils.isEmpty(batchDict)) { for (GlobalDict dict : batchDict) { urls.add(dict.getValue()); } } registration.addPathPatterns(Lists.newArrayList(urls)); } /** * 字典服务 */ @Resource(name = BootConst.GLOBAL_DICT_SVC) private BaseBizService<GlobalDict> dictService; /** * 限流handler */ @Resource(name = BootConst.CLIENT_LIMIT_SVC) private HandlerInterceptor limitHandler; /** * 直接拦截的url */ @Value("${bq.limit.enabled:false}") private boolean limitEnabled; }
- 可支持每个服务配置是否开启限流;
- 限流的接口在字典表中配置;
- 查询多种场景的阈值配置
ConfigBizServiceImpl
代码为:@Service(BootConst.GLOBAL_CONF_SVC) public class ConfigBizServiceImpl extends BaseBizService<GlobalConfig> { @Override public GlobalConfig get(GlobalConfig model) { List<GlobalConfig> batch = this.getBatch(model); return getBest(batch); } @Override protected List<GlobalConfig> queryBatchByKey(String key) { return dao.getBatch(GlobalConfig.toBean(key)); } @Override protected List<GlobalConfig> queryBatchByKeys(Iterable<? extends String> keys) { List<GlobalConfig> configs = Lists.newArrayList(); for (Iterator<? extends String> iterator = keys.iterator(); iterator.hasNext(); ) { String key = iterator.next(); configs.add(GlobalConfig.toBean(key)); } return dao.batchGet(configs); } @Override protected List<GlobalConfig> bestChoose(List<GlobalConfig> batch) { List<GlobalConfig> bestResults = Lists.newArrayList(); GlobalConfig best = getBest(batch); if (!best.isEmpty()) { bestResults.add(best); } return super.bestChoose(batch); } /** * 从匹配的4种组合数据下获取最佳的配置 * <p> * 4种配列组合及其优选顺序(从高到低): * 1.clientId和urlId都存在 * 2.clientId存在,urlId不存在 * 3.clientId不存在,urlId存在 * 4.clientId和urlId都不存在 * * @param batch 批量结果 * @return 最佳结果 */ private GlobalConfig getBest(List<GlobalConfig> batch) { if (CollectionUtils.isEmpty(batch)) { //没数据时返回空对象,避免数据库被击穿(此服务因为涉及接口限流,执行频率太高) return new GlobalConfig(); } for (GlobalConfig config : batch) { if (!StringUtils.isEmpty(config.getClientId()) && !StringUtils.isEmpty(config.getUrlId())) { return config; } } for (GlobalConfig config : batch) { if (!StringUtils.isEmpty(config.getClientId()) && StringUtils.isEmpty(config.getUrlId())) { return config; } } for (GlobalConfig config : batch) { if (StringUtils.isEmpty(config.getClientId()) && !StringUtils.isEmpty(config.getUrlId())) { return config; } } return batch.get(0); } /** * 全局配置dao */ @Autowired private BizDao<GlobalConfig> dao; }
- Redis限流
LimitServiceImpl
代码如下:@Service public class LimitServiceImpl implements LimitService { @Override public boolean qpsLimit(LimitConfig config) { List<String> keys = Lists.newArrayList(config.toKey()); Object[] params = new Object[Const.THREE]; int i = 0; //限流大小 params[i++] = MathUtil.getLong(config.getSvcValue(), qps); //限流单位时间(默认为1000,即秒,也可以支持更大时间粒度) params[i++] = MathUtil.getLong(config.getUnit(), qpsUnit); //当前时间 params[i] = System.currentTimeMillis(); Boolean result = redis.execute(CommonBootConst.QPS_REDIS_SCRIPT_SVC, Boolean.class, keys, params); return Boolean.TRUE.equals(result); } @Override public boolean maxLimit(LimitConfig config) { List<String> keys = Lists.newArrayList(config.toKey()); Object[] params = new Object[Const.TWO]; int i = 0; params[i++] = MathUtil.getLong(config.getSvcValue(), max); //计算过期时间为当天的00:00:00对应的毫秒+配置的阈值(默认支持天,也可以支持更大粒度,比如月) params[i] = TimeUtil.getTodayUtcMills() + MathUtil.getLong(config.getUnit(), maxUnit); Boolean result = redis.execute(CommonBootConst.MAX_REDIS_SCRIPT_SVC, Boolean.class, keys, params); return Boolean.TRUE.equals(result); } }
- QPS限流的
access_limit.lua
脚本代码如下:-- 获取限流key local limitKey = KEYS[1] --redis.log(redis.LOG_WARNING,'access limit key is:',limitKey) -- 调用脚本传入的限流大小 local limitNum = tonumber(ARGV[1]) -- 传入数据的有效期(ms,比如1秒) local expireMills = tonumber(ARGV[2]) -- 传入当前时间(ms) local nowMills = tonumber(ARGV[3]) --清除过期数据 local expiredTimeMills = nowMills-expireMills; redis.call('zremrangebyscore',limitKey,0,expiredTimeMills); local count = redis.call('zcard',limitKey); -- 获取当前流量大小 local countNum = tonumber(count or "0") --redis.log(redis.LOG_WARNING,'access countNum=',countNum) --是否超出限流值 if countNum + 1 > limitNum then -- 拒绝访问 return true else -- 没有超过阈值,设置当前访问数量+1 redis.call('zadd',limitKey,nowMills,nowMills) -- 设置过期时间(ms,相当于给这个zset key自动续期) redis.call('pexpire',limitKey,expireMills) -- 放行 return false end
- 每天最大调用量限流的
max_limit.lua
脚本代码如下:-- 获取限流key local limitKey = KEYS[1] --redis.log(redis.LOG_WARNING,'max limit key is:',limitKey) -- 调用脚本传入的限流大小 local limitNum = tonumber(ARGV[1]) --redis.log(redis.LOG_WARNING,'max limit num is:',limitNum) -- 传入过期时间(ms) local expireMills = tonumber(ARGV[2]) --redis.log(redis.LOG_WARNING,'max limit expire is:',expireMills) local count = redis.call('get',limitKey); --redis.log(redis.LOG_WARNING,'max limit count is:',count) if count then -- 获取当前流量大小 local countNum = tonumber(count or "0") -- redis.log(redis.LOG_WARNING,'max countNum=',countNum) --是否超出限流值 if countNum + 1 > limitNum then -- 拒绝访问 return true else -- 没有超过阈值,设置当前访问数量+1 redis.call('incrby',limitKey,1) -- 放行 return false end else -- 没有超过阈值,设置当前访问数量+1 redis.call('set',limitKey,1) -- 设置过期时间(ms) redis.call('pexpire',limitKey,expireMills) -- 放行 return false end
- 基于spring-boot-starter-web的服务可通过DelegatingWebMvcConfiguration来扩展,本开源框架实现
4.3.1.2 业务微服务的客户限流验证
- 以
bq-service-biz
的/demo/jwk
接口为例进行验证; - 查询字典表,查看接口URL对应的接口ID:
select * from bq_global_dict where value='/demo/jwk';
- 获取接口ID如下:
[ { "id": "d211", "key": "DEMO_QR_API", "value": "/demo/jwk", "type": "ClientUrl" } ]
- 再在灵活的限流阈值配置表中查询出限流配置:
select * from bq_global_config where url_id='DEMO_QR_API' or client_id='DEMO_QR_API';
- 可知接口的阈值配置结果如下,即客户限流的QPS和天最大调用量都是20:
[ { "id": "svc301", "svc_id": "client.to.channel", "client_id": "app001", "url_id": "DEMO_QR_API", "svc_value": "DEMO_CHANNEL_JWK_API", "create_time": 1566382443412 }, { "id": "svc431", "svc_id": "client.limit.qps", "client_id": "app001", "url_id": "DEMO_QR_API", "svc_value": "20", "create_time": 1566382443412 }, { "id": "svc432", "svc_id": "client.limit.max", "client_id": "app001", "url_id": "DEMO_QR_API", "svc_value": "20", "create_time": 1566382443412 }, { "id": "svc434", "svc_id": "channel.limit.qps", "client_id": "DEMO_QR_API", "url_id": "DEMO_CHANNEL_JWK_API", "svc_value": "20", "create_time": 1566382443412 }, { "id": "svc435", "svc_id": "channel.limit.max", "client_id": "DEMO_QR_API", "url_id": "DEMO_CHANNEL_JWK_API", "svc_value": "20", "create_time": 1566382443412 } ]
- 为了单独验证功能限流,特意删除前面网关、业务服务的非功能限流逻辑,然后再持续疯狂执行请求上述
/demo/jwk
接口的curl命令,会看到如下返回结果:{ "code": "100003", "msg": "流量超限", "cost": 0 }
- 其对应的异常日志如下:
[bq-biz][Tid:21ebec3d3b78cbbd,Sid:00c5db8e37856b77][ERROR][c.b.b.h.i.LimitHandlerImpl_limit] - [{"accessId":"app001","urlId":"DEMO_QR_API","config":{"start":0,"clientId":"app001","urlId":"DEMO_QR_API","svcId":"client.limit.max.unit","createTime":0}}]reach max limit:{"start":0,"clientId":"app001","urlId":"DEMO_QR_API","svcId":"client.limit.max","svcValue":"20","createTime":0,"unit":"86400000"}.
总结下客户限流的特点:
- 客户限流可能存在较多维度,需要考虑比较灵活的方案才能较好满足后续的场景扩展;
- 客户限流跟前面的熔断降级思考方式和实现完全不同,前者是要熟悉相关的框架,后者则需要找到合适的框架,还有有业务解决方案的设计和整合能力;
4.3.2 业务微服务的渠道限流实现
4.3.2.1 业务微服务的渠道限流的详细逻辑
- 渠道限流的核心设计思路:
- 基于Spring AOP,对要调用的远程服务做切面;
- 完全复用了客户限流的实现逻辑,主要是把渠道的接口ID看成是之前客户限流的客户ID,把渠道ID看成是客户限流的接口ID;
- 渠道限流的切面
ChannelLimitAop
代码:@Component @Aspect public class ChannelLimitAop extends BaseAop { @Before("execution (* com.biuqu.boot.remote.RemoteService+.*invoke*(..))") @Override public void before(JoinPoint joinPoint) { super.before(joinPoint); } @Override protected void doBefore(Method method, Object[] args) { boolean isLimit = limitHandler.limit(method, args); if (isLimit) { throw new CommonException(ErrCodeEnum.LIMIT_ERROR.getCode()); } } /** * 注入渠道限流 */ @Autowired private ChannelLimitHandler limitHandler; }
- 渠道限流的实现类
ChannelLimitHandler
代码如下:@Component public class ChannelLimitHandler { /** * 限流执行方法 * * @param method 切面拦截的方法对象 * @param args 方法参数 * @return true表示限流, false表示不限流 */ public boolean limit(Method method, Object[] args) { if (null == args || args.length <= 0) { return false; } Object element = args[0]; if (element instanceof BaseBiz) { BaseBiz biz = (BaseBiz)element; if (StringUtils.isEmpty(biz.getChannelId()) || StringUtils.isEmpty(biz.getUrlId())) { return false; } AccessLimit model = new AccessLimit(); model.setAccessId(biz.getUrlId()); model.setUrlId(biz.getChannelId()); model.setConfig(LimitConfig.channelConf()); return limitHandler.limit(model); } return false; } /** * 注入接入限流服务(封装了qps限流和最大调用量限流) */ @Autowired private LimitHandler limitHandler; }
细心的朋友会发现,其服务引入的
LimitHandler
和客户限流的LimitHandler
是同一个服务。 - 切面关注的Remote服务代码如下:
@Slf4j @Service(DemoConst.DEMO_REMOTE_SERVICE) public class QrRemoteServiceImpl extends BaseRemoteService<QrCodeResult, QrCode> { @Override protected String call(QrCode model, boolean snake) { log.info("current param1:{},snake:{}", JsonUtil.toJson(model), snake); return callRemote(model, snake); } protected String callRemote(QrCode model, boolean snake) { log.info("current param2:{},snake:{}", JsonUtil.toJson(model), snake); String channelUrl = this.getChannelUrl(model); if (StringUtils.isEmpty(channelUrl)) { log.error("[{}]no channel[{}] url found.", model.getUrlId(), model.getChannelId()); return null; } ResponseEntity<String> jwkJson = restTemplate.getForEntity(channelUrl, String.class); log.info("remote result:{}", jwkJson.getBody()); return jwkJson.getBody(); } @Override protected ResultCode<QrCodeResult> toModel(String json, TypeReference<ResultCode<QrCodeResult>> typeRef, boolean snake) { if (null == json) { return ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode()); } QrCodeResult result = new QrCodeResult(); result.setOpenId(Hex.toHexString(json.getBytes(StandardCharsets.UTF_8))); return ResultCode.ok(result); } /** * 注入远程服务 */ @Autowired private CommonRestTemplate restTemplate; }
4.3.2.2 业务微服务的渠道限流验证
- 渠道限流的验证同客户限流的过程完全一致,仅需要把渠道限流的阈值改小即可,验证步骤略。
5. 本文小结
- 本文从0开始讲解什么是熔断降级与限流,也逐步分析了怎么去做的方法论(先明确需求,再去做业务分析、技术选型与验证),而且还要最终高效优雅地实现它;
- 熔断降级分为网关熔断与常规的业务熔断,主要是底层的技术栈实现不同(webflux和web),位于微服务架构的层级也不同(一个是入口,一个是其后端的服务),对应的开源框架Sentinel的用法也不同,网关熔断的使用更优雅简洁,常规业务熔断兼顾优雅简洁和灵活的定制扩展,整体表现非常好;
- 服务限流分为非功能限流和功能限流,前者只关注技术本身,不关注业务使用(一般限流专项的开发者和服务真正落地的开发者不是同一个人);后者除了需要前者的配置能力外,还需要设计灵活的可配置方法,保证业务落地。后者还要关注功能限流的业务特性,有时候还要做新技术方案来完善业务场景。比如按照不同客户ID做QPS和每天最大调用量限流就是这种场景;
- 本文所有的代码全部开源了,上述所有代码和配置,都可以在代码库中获取到,由于时间有限,暂未全部链接好,非常抱歉;
- 会说和会做完全是两码事,有些人会说,但是做的效果不太好,甚至做不出来;有些人会做,但是想不明白。行万里路,知行合一才是王道。
6. 参考资料
- [1]熔断框架比较
- [2]Spring Cloud Gateway 整合阿里 Sentinel网关限流实战!
- [3]Spring Cloud Gateway 整合 sentinel 实现流控熔断
- [4]Spring Cloud Alibaba Sentinel @SentinelResource