Sentinel实战
一、Sentinel简介
Sentinel是阿里开源的面向服务流量治理的框架,官方原文是Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel有两个重要的基本概念:
资源
资源就是需要进行流量管理的事物,可以是服务名也可以是接口地址URL等,如果你想根据某个接口进行限流,那资源就是该接口。
规则
规则就是进行流量管理的规则,比如用哪种限流算法,是根据QPS限流还是根据线程数限流等。
更多可以查看官方介绍https://sentinelguard.io/zh-cn/docs/introduction.html。
二、需求背景
微服务网关一般都会有限流的功能,防止后端服务被瞬时超高流量击垮,刚好之前开源的ship-gate网关支持自定义插件,那么就可以通过给ship-gate增加限流功能来学习Sentinel。
完整需求如下:
ship-gate需要支持服务(如订单服务)维度的限流,限流方式包括根据QPS和线程数这两种,本次改造内容全部在ship-server子模块。
三、编码实现
ship-server是基于Spring WebFlux实现的,Sentinel提供的有开源框架的适配sentinel-spring-webflux-adapter模块,但是看了下其实现与现有的设计不兼容,于是决定用原生的核心库sentinel-core。
同时ship-gate也需要Sentinel的控制台功能监控限流情况,所以pom文件需要引入以下依赖:
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.8.6</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>1.8.6</version> </dependency>
- 配置类增加限流配置
/** * Created by 2YSP on 2020/12/27 */ @ConfigurationProperties(prefix = "ship.gate") public class ServerConfigProperties { /** * 负载均衡算法,默认轮询 */ private String loadBalance = LoadBalanceConstants.ROUND; /** * 网关超时时间,默认3s */ private Long timeOutMillis = 3000L; /** * 缓存刷新间隔,默认10s */ private Long cacheRefreshInterval = 10L; /** * 限流方式QPS或THREAD,默认QPS */ private String rateLimitType = "QPS"; /** * 限流数量 */ private Integer rateLimitCount; // 省略getter setter方法 }
ShipPluginEnum增加对应枚举
public enum ShipPluginEnum { /** * DynamicRoute */ DYNAMIC_ROUTE("DynamicRoute", 2, "动态路由插件"), /** * Auth */ AUTH("Auth", 1, "鉴权插件"), RATE_LIMIT("RateLimit", 0, "限流插件"); private String name; private Integer order; private String desc; ShipPluginEnum(String name, Integer order, String desc) { this.name = name; this.order = order; this.desc = desc; } // 省略getter方法 }
注意: order越小越先执行(与Spring规则一致),限流插件最先执行所以定义为0
2.新建RateLimitPlugin类,继承AbstractShipPlugin
/** * @Author: Ship * @Description: * @Date: Created in 2020/12/29 */ public class RateLimitPlugin extends AbstractShipPlugin { private final Logger logger = LoggerFactory.getLogger(RateLimitPlugin.class); private static Map<String, Integer> rateLimitTypeMap = new HashMap<>(); static { rateLimitTypeMap.put(ServerConstants.LIMIT_BY_QPS, RuleConstant.FLOW_GRADE_QPS); rateLimitTypeMap.put(ServerConstants.LIMIT_BY_THREAD, RuleConstant.FLOW_GRADE_THREAD); } public RateLimitPlugin(ServerConfigProperties properties) { super(properties); } @Override public Integer order() { return ShipPluginEnum.RATE_LIMIT.getOrder(); } @Override public String name() { return ShipPluginEnum.RATE_LIMIT.getName(); } @Override public Mono<Void> execute(ServerWebExchange exchange, PluginChain pluginChain) { String appName = pluginChain.getAppName(); initFlowRules(appName); if (SphO.entry(appName)) { // 务必保证finally会被执行 try { /** * 被保护的业务逻辑 */ return pluginChain.execute(exchange, pluginChain); } finally { SphO.exit(); } } throw new ShipException(ShipExceptionEnum.REQUEST_LIMIT_ERROR); } private void initFlowRules(String resource) { Assert.hasText(properties.getRateLimitType(), "config ship.gate.rateLimitType required!"); Assert.notNull(properties.getRateLimitCount(), "config ship.gate.rateLimitCount required!"); List<FlowRule> list = new ArrayList<>(); FlowRule flowRule = new FlowRule(); flowRule.setResource(resource); flowRule.setGrade(rateLimitTypeMap.get(properties.getRateLimitType())); flowRule.setCount(properties.getRateLimitCount().doubleValue()); list.add(flowRule); FlowRuleManager.loadRules(list); } }
因为如果请求被限流了,RateLimitPlugin会抛出异常,为了处理这种异常需要增加全局异常处理配置。
类似SpringMVC框架,只需要实现WebExceptionHandler接口,然后在配置类注册对应的bean即可。
ShipExceptionHandler
public class ShipExceptionHandler implements WebExceptionHandler { private final Logger logger = LoggerFactory.getLogger(ShipExceptionHandler.class); @Override public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) { logger.error("ship server exception msg:{}", throwable.getMessage()); if (throwable instanceof ShipException) { ShipException shipException = (ShipException) throwable; return ShipResponseUtil.doResponse(serverWebExchange, new ApiResult(shipException.getCode(), shipException.getErrMsg())); } String errorMsg = "system error"; if (throwable instanceof IllegalArgumentException) { errorMsg = throwable.getMessage(); } return ShipResponseUtil.doResponse(serverWebExchange, new ApiResult(5000, errorMsg)); } }
配置类WebConfig
/** * @Author: Ship * @Description: * @Date: Created in 2020/12/25 */ @Configuration @EnableWebFlux @EnableConfigurationProperties(ServerConfigProperties.class) public class WebConfig { @Bean public PluginFilter pluginFilter(@Autowired ServerConfigProperties properties) { return new PluginFilter(properties); } /** * set order -2 to before DefaultErrorWebExceptionHandler(-1) ResponseStatusExceptionHandler(0) * @return */ @Order(-2) @Bean public ShipExceptionHandler shipExceptionHandler(){ return new ShipExceptionHandler(); } }
注意: 这里踩了一个小坑,开始发现自己写的WebExceptionHandler不生效,请求错误响应还是原来的格式,后来发现需要添加@order注解指定bean注入的优先级比默认的小。
- 插件责任链注册插件RateLimitPlugin
/** * @Author: Ship * @Description: * @Date: Created in 2020/12/25 */ public class PluginFilter implements WebFilter { private ServerConfigProperties properties; public PluginFilter(ServerConfigProperties properties) { this.properties = properties; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String appName = parseAppName(exchange); if (CollectionUtils.isEmpty(ServiceCache.getAllInstances(appName))) { throw new ShipException(ShipExceptionEnum.SERVICE_NOT_FIND); } PluginChain pluginChain = new PluginChain(properties, appName); pluginChain.addPlugin(new DynamicRoutePlugin(properties)); pluginChain.addPlugin(new AuthPlugin(properties)); pluginChain.addPlugin(new RateLimitPlugin(properties)); return pluginChain.execute(exchange, pluginChain); } private String parseAppName(ServerWebExchange exchange) { RequestPath path = exchange.getRequest().getPath(); String appName = path.value().split("/")[1]; return appName; } }
最后,Sentinel控制台支持JVM参数和sentinel.properties文件两种配置方式,方便起见在resource目录增加sentinel.properties配置文件,内容如下:
project.name=ship-server csp.sentinel.dashboard.server=127.0.0.1:8080
至此,代码已经写完了,下面进入测试环节。
四、测试总结
- 启动nacos
- 启动ship-admin,ship-gate-example工程
- 在t_plugin表插入限流插件
INSERT INTO `ship`.`t_plugin`(`id`, `name`, `code`, `description`, `created_time`) VALUES (3, '限流', 'RateLimit', '限流插件', '2023-04-16 11:06:39');
然后t_app_plugin表增加插件配置(插件管理这块暂时没有后台功能后面考虑增加)
通过nacos控制台查看实例详情,发现服务实例的插件配置已经有了。
- 在application.yml增加限流配置后就可以启动ship-server工程了
ship: gate: load-balance: round time-out-millis: 3000 cache-refresh-interval: 10 rate-limit-type: QPS rate-limit-count: 4
这里配置了QPS限制在4以内,相当于每秒最多4个请求通过。
-
官网下载sentinel-dashboard.jar(下载地址:https://github.com/alibaba/Sentinel/releases),然后
打开命令行输入java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar启动。 -
游览器访问http://localhost:8080/#/login(默认账号密码都是sentinel),就可以看到后台界面了。
因为ship-server还没收到请求,所以左侧菜单栏开始不会显示配置的projectName,相当于懒加载的原理。 -
使用wrk压测工具对接口进行压测,命令如下:
wrk -c 100 -t 20 -d 60s http://localhost:9000/order/user/test
IDEA控制台日志如下
第一张图说明触发了限流的保护机制,第二张图说明QPS确实被限制到了4以内,看日志打印的频率猜测用的是固定窗口计数器算法实现的,测试成功。
同时在Sentinel控制台也能看到监控数据
总结:
Sentinel还具备其他很强大的功能,需要慢慢摸索,Sentinel控制台有一个把配置推送到注册中心,然后服务监听流量规则配置的方案有时间看下怎么玩,如果觉得这篇文章对您有用希望可以点个赞让更多人看到😉。
参考文档:
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/17327799.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2018-04-18 【mysql】on duplicate key update的详解