通过sentinel前世今生介绍,我们知道了sentinel流控主要是依赖sentinel-core,但是我们生产环境往往需要动态更新流控规则所以需要集成nacos、zookeeper、redis、mysql、等中间存储。配置的复杂性和规则的复杂性我们需要可视化的方式对规则进行管理,我们需要集成dashboard。
这些sentinel都为我们提供了解决方案
集成这些组件我们需要复杂的配置,sentinel为了方便接入提供的spring-cloud-starter-alibaba-sentinel 自动装配进行快速服务整合
服务如何接入
版本选择
https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明
1.pom引入依赖
<!--使用nacos持久化规则--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.8.0</version> </dependency> |
yml配置
server: port: 9000 spring: main: allow-circular-references: true application: name: sentinel-nacos-starter-datasource-demo cloud: nacos: discovery.server-addr: 127.0.0.1:8848 sentinel: web-context-unify: false # 关闭context整合 避免链路失效 默认会由context为根链路 eager: true #启动立即加载规则,而不是懒加载 transport: dashboard: 127.0.0.1:8080 #dashboard地址 port: 8719 #dashboard通信端口,如果冲突则会自动+1 寻找可用端口 heartbeat-interval-ms: 5000 #心跳秒数 datasource: flow-rule: nacos: #流控规则的nacos配置文件 dashbard上报推送的是这个格式 data-id: ${spring.application.name}-flow-rules #流控规则格式 XML OR JSON dashbard配置的json data-type: json namespace: 04415d0c-9d8e-4e32-81a7-c0d737bb0063 group-id: SENTINEL_GROUP server-addr: 127.0.0.1:8848 #规则类型控制规则更新的策略 rule-type: flow param-flow-rule: nacos: data-id: ${spring.application.name}-param-rules data-type: json namespace: 04415d0c-9d8e-4e32-81a7-c0d737bb0063 group-id: SENTINEL_GROUP server-addr: 127.0.0.1:8848 rule-type: param-flow authority-rule: nacos: data-id: ${spring.application.name}-authority-rules data-type: json namespace: 04415d0c-9d8e-4e32-81a7-c0d737bb0063 group-id: SENTINEL_GROUP server-addr: 127.0.0.1:8848 rule-type: authority system-rule: nacos: data-id: ${spring.application.name}-system-rules data-type: json namespace: 04415d0c-9d8e-4e32-81a7-c0d737bb0063 group-id: SENTINEL_GROUP server-addr: 127.0.0.1:8848 rule-type: system |
接入原理
规则是如何实现从nacos自动加载和监听的
com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler 源码,利用spring 生命周期提供的SmartInitializingSingleton 回调
com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler#parseBeanDefinition
public void afterSingletonsInstantiated() { //遍历配置文件规则 sentinelProperties.getDatasource() .forEach((dataSourceName, dataSourceProperties) -> { try { List<String> validFields = dataSourceProperties.getValidField(); if (validFields.size() != 1) { log.error("[Sentinel Starter] DataSource " + dataSourceName + " multi datasource active and won't loaded: " + dataSourceProperties.getValidField()); return; } AbstractDataSourceProperties abstractDataSourceProperties = dataSourceProperties .getValidDataSourceProperties(); abstractDataSourceProperties.setEnv(env); abstractDataSourceProperties.preCheck(dataSourceName); //注册 registerBean(abstractDataSourceProperties, dataSourceName + "-sentinel-" + validFields.get(0) + "-datasource"); } catch (Exception e) { log.error("[Sentinel Starter] DataSource " + dataSourceName + " build error: " + e.getMessage(), e); } }); } |
private void registerBean(final AbstractDataSourceProperties dataSourceProperties, String dataSourceName) { //根据配置构建 DataSource 的BeanDefinition BeanDefinitionBuilder builder = parseBeanDefinition(dataSourceProperties, dataSourceName); //进行初始化 this.beanFactory.registerBeanDefinition(dataSourceName, builder.getBeanDefinition()); // 获取对应的dataSource AbstractDataSource newDataSource = (AbstractDataSource) this.beanFactory .getBean(dataSourceName); // 执行注入 dataSourceProperties.postRegister(newDataSource); } |
public void postRegister(AbstractDataSource dataSource) { switch (this.getRuleType()) { case FLOW: FlowRuleManager.register2Property(dataSource.getProperty()); break; case DEGRADE: DegradeRuleManager.register2Property(dataSource.getProperty()); break; case PARAM_FLOW: ParamFlowRuleManager.register2Property(dataSource.getProperty()); break; case SYSTEM: SystemRuleManager.register2Property(dataSource.getProperty()); break; case AUTHORITY: AuthorityRuleManager.register2Property(dataSource.getProperty()); break; case GW_FLOW: GatewayRuleManager.register2Property(dataSource.getProperty()); break; case GW_API_GROUP: GatewayApiDefinitionManager.register2Property(dataSource.getProperty()); break; default: break; } } |
spring mvc 资源是如何实现自动注册的
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration 会初始化一个spring 拦截器 并注入容器
@Autowired private SentinelProperties properties; @Autowired private Optional<UrlCleaner> urlCleanerOptional; @Autowired private Optional<BlockExceptionHandler> blockExceptionHandlerOptional; @Autowired private Optional<RequestOriginParser> requestOriginParserOptional; /** * 基于下面配置 初始化一个spring mvc拦截器 * @param sentinelWebMvcConfig * @return */ @Bean @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true) public SentinelWebInterceptor sentinelWebInterceptor( SentinelWebMvcConfig sentinelWebMvcConfig) { return new SentinelWebInterceptor(sentinelWebMvcConfig); } /** * 拦截器的一些自定义配置,如 自定义sentinel流控处理器 * @return */ @Bean @ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true) public SentinelWebMvcConfig sentinelWebMvcConfig() { //...... } |
com.alibaba.csp.sentinel.adapter.spring.webmvc.AbstractSentinelInterceptor#preHandle
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { //根据url获取资源名字 String resourceName = this.getResourceName(request); if (StringUtil.isEmpty(resourceName)) { return true; } else if (this.increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) { return true; } else { String origin = this.parseOrigin(request); String contextName = this.getContextName(request); ContextUtil.enter(contextName, origin); Entry entry = SphU.entry(resourceName, 1, EntryType.IN); request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry); return true; } } catch (BlockException var12) { BlockException e = var12; try { //执行自定义的 sentinel流控处理器 this.handleBlockException(request, response, e); } finally { ContextUtil.exit(); } return false; } } |
如何自定义spring MVC 流控处理器
从容器获取,我们可以根据这个钩子方法进行统一扩展,比如权限的,指定url的流控降级规则
@Autowired
private Optional<BlockExceptionHandler> blockExceptionHandlerOptional;
实现BlockExceptionHandler自定义流控处理器
@Component public class CustomBlockExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { Map<String, Object> resData=new HashMap<>(); if (e instanceof FlowException) { resData.put("status",100); resData.put("msg","触发限流规则|"+Thread.currentThread().getId()+"|"+ DateFormatUtils.format(System.currentTimeMillis() , "yyyy-MM-dd HH:mm:ss:SSS")); } else if (e instanceof DegradeException) { resData.put("status",101); resData.put("msg","降级了|" +Thread.currentThread().getId()+"|"+ DateFormatUtils.format(System.currentTimeMillis() , "yyyy-MM-dd HH:mm:ss:SSS") ); } else if (e instanceof ParamFlowException) { resData.put("status",102); resData.put("msg","热点参数限流"+Thread.currentThread().getId()+"|"+ DateFormatUtils.format(System.currentTimeMillis() , "yyyy-MM-dd HH:mm:ss:SSS") ); } else if (e instanceof SystemBlockException) { resData.put("status",103); resData.put("msg","系统规则(负载/...不满足要求"+Thread.currentThread().getId()+"|"+ DateFormatUtils.format(System.currentTimeMillis() , "yyyy-MM-dd HH:mm:ss:SSS") ); } else if (e instanceof AuthorityException) { resData.put("status",104); resData.put("msg","授权规则不通过"+Thread.currentThread().getId()); } // http状态码 httpServletResponse.setStatus(500); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); new ObjectMapper() .writeValue( httpServletResponse.getWriter(), resData ); } } |
如何整合Fegin
yml配置
feign.sentinel.enabled=true
代码使用
文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
源码处
通过扩展 Feign.Builder实现
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
@Configuration(proxyBeanMethods = false) @ConditionalOnClass({ SphU.class, Feign.class }) public class SentinelFeignAutoConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.sentinel.enabled") public Feign.Builder feignSentinelBuilder() { return SentinelFeign.builder(); } } |
注解支持
参考文档
https://github.com/alibaba/Sentinel/wiki/注解支持
自动装配源码处
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration#sentinelResourceAspect
控制台各个规则使用
流控规则
qps
我们最常用的限流规则,限制我们的资源能够接收的每秒最大请求数。防止超过资源接收到自身最大的请求流量。
比如我们抢购功能,压测接口最大峰值qps是1000,我们应该设置每秒超过1000的快速失败,避免大量请求引起的阻塞排队。
并发线程数
与qps不同的是,是每秒基于线程维度统计,线程数并不等于qps,比如设置5,接口性能是500毫秒。那么在不排队情况,1个线程每秒能执行2个请求,5个线程则是10的qps
下面例子20个线程并发抢占最终成功10个
高级设置流控模式
- 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
- 关联:统计与当前资源相关的另一个资源,触发阈值时。限流自己
比如商品详情本身支持2000qps。但是当商品有促销活动时会调用促销资源。促销资源只支持100qps。那么这种时候我们接口也就支持100qp,超过100则会触发等待。则可以通过关联来限制。(关联资源单独设置了流控,则取最小那个)
或者有下单接口,下单完之后会调用支付接口。当支付接口出现限流,我们可以触发限流下单。
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
自下而上精细化控制,我们的资源,比如我们查询订单接口。支持200qps,在支付的时候会查询订单。订单详情也会查询订单。这个时候我们的支付优先级肯定更高,则我们设置订单资源关联非支付的则只允许50qps
高级设置流控效果
- 快速失败:默认效果,达到阈值则快速失败
- Warm Up:预热模式,我们接口能够支持2000qps,但是刚启动的时候很多缓存没加载。需要预热一段时间,则缓慢的放流量最终到2000qps,比如一心助手门店维度缓存数据
热点规则
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
场景
抢购活动,针对茅台系列商品 0005,0006.可以通过首页活动广告页进入活动,也可以通过商品详情进入活动页。商品详情这个系列商品会匹配到活动,会走抢购活动的分支,所以根据0005,0006 进行单独的qps设置
@Override public void run(ApplicationArguments args) throws Exception { initParamFlowRules(); } /** * com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot#checkFlow * 默认是string.valueof * @throws IOException */ public void initParamFlowRules() throws IOException { ParamFlowRule rule = new ParamFlowRule("flowRuleParamsString") .setParamIdx(0) .setCount(20);//总阀20 按各个参数分别统计 // 针对 String 类型的参数,参数值为李强 单独设置限流 QPS 阈值为 1,而不是全局的阈值 20. ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf("李强")) .setClassType(String.class.getName()) .setCount(1); rule.setParamFlowItemList(Collections.singletonList(item)); ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); } |
授权规则
很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的黑白名单控制的功能。黑白名单根据资源的请求来源(origin
)限制资源是否通过,
若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
源码
/** * 资源应用仅允许appA,appB访问 * 调用方信息通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。 * ContextUtil.enter(resourceName, origin) */ public void intAuthorityRule(){ AuthorityRule rule = new AuthorityRule(); rule.setResource("test"); rule.setStrategy(RuleConstant.AUTHORITY_WHITE); rule.setLimitApp("appA,appB"); AuthorityRuleManager.loadRules(Collections.singletonList(rule)); } |
在starter的拦截器中提供RequestOriginParser 由我们扩展
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration
public class SentinelWebAutoConfiguration implements WebMvcConfigurer { ...... @Autowired private Optional<RequestOriginParser> requestOriginParserOptional; ...... } |
com.alibaba.csp.sentinel.adapter.spring.webmvc.AbstractSentinelInterceptor#preHandle
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { String resourceName = this.getResourceName(request); if (StringUtil.isEmpty(resourceName)) { return true; } else if (this.increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) { return true; } else { //获取origin String origin = this.parseOrigin(request); String contextName = this.getContextName(request); ContextUtil.enter(contextName, origin); Entry entry = SphU.entry(resourceName, 1, EntryType.IN); request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry); return true; } } catch (BlockException var12) { BlockException e = var12; try { this.handleBlockException(request, response, e); } finally { ContextUtil.exit(); } return false; } } protected String parseOrigin(HttpServletRequest request) { String origin = ""; //RequestOriginParser if (this.baseWebMvcConfig.getOriginParser() != null) { origin = this.baseWebMvcConfig.getOriginParser().parseOrigin(request); if (StringUtil.isEmpty(origin)) { return ""; } } return origin; } |
系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统保护规则我的理解是一个兜底策略。比如我们压测都是针对单一接口压测,如果我们压测接口本身是cpu密集型的高并发接口,我们把他压到极致,但是一个服务不仅仅是这一个接口提供能力。当时高并发接口和其他接口同时提供能力。往往还没到我们限流阀值系统负载已经上来了。
所以通过系统规则做一次兜底
LOAD:linxu的load指标
RT:平均响应时长
线程数:入口流量的并发线程数
CPU使用率
sentinel怎么区分入口流量
通过
Entry entry = SphU.entry(resourceName, 1, EntryType.IN);控制
com.alibaba.csp.sentinel.EntryType
public enum EntryType { IN, OUT; private EntryType() { } } |
sentinle的spring adapter模块在拦截器埋点的就是IN
com.alibaba.csp.sentinel.adapter.spring.webmvc.AbstractSentinelInterceptor#preHandle
在服务fegin埋点就是OUT
com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler#invoke
运维相关
规则是否加载成功
参考文档:https://sentinelguard.io/zh-cn/docs/metrics.html
我们如果使用nacos推的方式,容易推失败,比如我遇到过推失败后拉取的是nacos缓存
dashbard源码处:
com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient#executeCommand
更多端点?
sentinel日志查看
参考文档:https://sentinelguard.io/zh-cn/docs/logs.html
默认在${user_home}/logs/csp目录下,可通过sp.sentinel.log.dir进行修改