项目模块中引入 Sentinel限流组件
一、Sentinel简介
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
详情参见github部分链接:
- 源码[1]
- 源码说明文档[2]
- demo[3]
二、项目集成:
在我们的xxx项目最开始之前提出了对于外部的访问需要能够做到访问控制,要控制调用方、调用频率。因此这边选择了使用开源的sentinel流控组件来实现功能需求。
1.依赖
Gradle 依赖:
- 「spring-cloud-starter-alibaba-nacos-config」:实现项目可以以nacos作为配置中心。
- 「com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel」:实现项目接入sentinel流控组件。
- 「com.alibaba.csp:sentinel-datasource-nacos:1.5.2」:实现sentinel组件加载流控相关配置可以从nacos中获取。
2.配置
1)项目配置
- A.设置nacos
- B.配置文件
配置文件详情如下:
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
# 配置中心地址
server-addr: localhost:8848
# 默认分组策略(最简单)
group-id: DEFAULT_GROUP
# 配置文件类型
data-type: json
# 类似文件名
data-id: ${spring.applicaiton.name:xxx-service}-flowrule
# 规则类型:流量控制
rule-type: flow
ds2:
nacos:
server-addr: localhost:8848
# file:
group-id: DEFAULT_GROUP
data-id: ${spring.applicaiton.name:xxx-service}-degraderule
data-type: json
# file: 'classpath: sentinel/degraderule.json'
# file: './sentinel/degraderule.json'
# 熔断降级
rule-type: degrade
ds3:
nacos:
server-addr: localhost:8848
group-id: DEFAULT_GROUP
data-id: ${spring.applicaiton.name:xxx-service}-authorityrule
data-type: json
# 黑白名单控制
rule-type: authority
ds4:
nacos:
server-addr: localhost:8848
group-id: DEFAULT_GROUP
data-id: ${spring.applicaiton.name:xxx-service}-systemrule
data-type: json
# 系统自适应:模块级别总防
rule-type: system
# ds5:
# file:
# file: 'classpath: sentinel/param-flow.json'
## file: './sentinel/param-flow.json'
# rule-type: param-flow
eager: true
transport:
# sentinel 控制台地址
dashboard: localhost:8080
Nacos配置
直接在nacos中新增配置即可,图1为新增,图2为新增后,图3为sentinel控制台加载nacos中配置文件,展示的实际的线上规则(支持重写后立即生效)
不同流控规则配置文件的可选字段可以参见github相关章节。
3.架构
上述配置完成后,需要在代码中做简单编码,统一设置调用方(orign或者说limitApp)和命中响应。
1)调用方配置
package com.xxx.xxx.xxx.filter.sentinel;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import com.alibaba.csp.sentinel.context.ContextUtil;
/**
* sentinel web url 限流统一过滤器,针对直接以controller层的url进行限流处理的情况
*
* @Author:WWG
* @date:2020年9月16日
* @Version:1.0
*/
@Order(1)
@WebFilter(urlPatterns = "/*", filterName = "sentinelAuthorityFilter")
public class SentinelAuthorityFilter implements Filter {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(SentinelAuthorityFilter.class);
private static final String HTTP_HEAD_APP_ID_VALUE_DEAFAULT_ERROR = "errorAppId";// 缺省值,当调用方未声明自己时,默认给一个错误值
private static final String HTTP_HEAD_APP_ID_KEY = "appId";// 约定值 存放在http请求中自定义head
@Value("${server.servlet.context-path}")
private String contextPath;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest hSRequest = (HttpServletRequest)request;
// sentinel 设置限流入口origin(对应limitApp)
String appId = hSRequest.getHeader(HTTP_HEAD_APP_ID_KEY);
// sentinel 目前是真的粗,一个白名单,你orgin为null、为空,居然能够直接通过拦截 因此下方做一下适配
appId = StringUtils.isBlank(appId) ? HTTP_HEAD_APP_ID_VALUE_DEAFAULT_ERROR : appId;
// contextName 由自己定义,关键能唯一标识某一个方法 ,该过滤器为web 过滤器,因此,可以uri作为上下文名称
String contextName = hSRequest.getRequestURI();
ContextUtil.enter(contextName, appId);
chain.doFilter(request, response);
ContextUtil.exit();
}
}
2)限流命中处理
package com.xxx.xxx.xxx.filter.sentinel;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.xxx.xxx.common.constant.PopularStringConstant;
import com.xxx.xxx.common.util.StreamUtil;
/**
* 自定义SENTINEL限流响应
*
* @Author:WWG
* @date:2020年9月21日
* @Version:1.0
*/
@Component
public class XXXUrlBlockHandler implements BlockExceptionHandler {
private static final String COMMA = PopularStringConstant.COMMA;
private static final String SNETINEL_TIP_DEFAULT = "请稍候再试!!!";
private static final String SNETINEL_TIP_CHECK_OR_TRY_LATER = "请自查权限或稍候再试!!!";
private static final String SNETINEL_WARNING_DEFAULT = "请停止发送请求,您的信息已被追踪!!!";
private static final String SNETINEL_RULE_SYSTEM_BINGO = "请求被 系统规则 命中";
private static final String SENTINEL_RULE_AUTHORITY_BINGO = "请求被 授权规则 命中";
private static final String SENTINEL_RULE_FLOW_BINGO = "请求被 流控规则 命中";
private static final String SENTINEL_RULE_DEGRADE_BINGO = "请求被 降级规则 命中";
private static final String SENTINEL_RULE_PARAMFLOW_BINGO = "请求被 热点规则 命中";
private static final String SENTINEL_RULE_BINGO = "请求被 SENTINEL 规则 命中";
private static final Logger LOGGER = LoggerFactory.getLogger(WSNUrlBlockHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String warning;
String errorMsg;
if (e instanceof AuthorityException) {
// 授权规则命中
warning = SENTINEL_RULE_AUTHORITY_BINGO;
errorMsg = warning + COMMA + SNETINEL_WARNING_DEFAULT;
} else if (e instanceof SystemBlockException) {
// 流控规则命中
warning = SNETINEL_RULE_SYSTEM_BINGO;
errorMsg = warning + COMMA + SNETINEL_TIP_DEFAULT;
} else if (e instanceof FlowException) {
// 流控规则命中
warning = SENTINEL_RULE_FLOW_BINGO;
errorMsg = warning + COMMA + SNETINEL_TIP_DEFAULT;
} else if (e instanceof DegradeException) {
// 流控规则命中
warning = SENTINEL_RULE_DEGRADE_BINGO;
errorMsg = warning + COMMA + SNETINEL_TIP_DEFAULT;
} else if (e instanceof ParamFlowException) {
// 热点规则命中
warning = SENTINEL_RULE_PARAMFLOW_BINGO;
errorMsg = warning + COMMA + SNETINEL_TIP_DEFAULT;
} else {
// 其他规则命中
warning = SENTINEL_RULE_BINGO;
errorMsg = warning + COMMA + SNETINEL_TIP_CHECK_OR_TRY_LATER;
}
LOGGER.debug(warning);
StreamUtil.writeResponse(response, errorMsg);
}
}
三、实现
最终项目可以实现根据不同的调用方设置不同的限流策略:
1.例1
前文中设置了针对来源为ZCB的请求,在请求如下资源:/enterprises(http://127.0.0.1:8802/xxx/enterprises)时超过阈值(qps为2)就会被命中如图:
请求头中设置了来源方,设计由网关鉴权之后设置传递,保证可信度:
2.例2
新增一个授权规则(黑白名单规则):
Sentinel 控制台查看规则是否已生效:
工具调用:
四、概述
Sentinel 功能还有很多,上述例子只是一小部分,而且随着项目的壮大,后续的功能会越来越强大。
1.一些问题点的折射
1)版本
- 存在版本高版本不适配低版本的情况,这确实可以称做一个吐槽点,成熟的项目一般不会出现这种情况,不过从侧面也反应出这个项目的年轻与潜力,面对未来,充满信息即可,实在不行,源码自己拉下来改。
- 还行,目前也就两次截屏就能看到所有,当然还不包括子模块中展开,是否还有子模块。
2)代码
代码严谨度目前不高,一开始使用的file类型读取sentinel规则,eclipse中直接跑能行,但是打成jar包后,源码中读取file的方法根本不适用于读取jar包中的文件,后来被逼着使用nacos加载配置,加载本地文件相关问题代码如图:
五、待续。。。。
后续有更多的发掘会更新进来
Reference
[1]github源码: https://github.com/alibaba/Sentinel
[2]github源码对应的说明文档: https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
[3]spring-cloud-alibaba集成sentinel的demo: https://github.com/alibaba/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-examples/sentinel-example