(demo源码地址:https://github.com/chenfei7357/chenfei-demo)
背景:
随着微服务的流行,服务和服务之间的稳定性变得越来越重要,每个服务的高可用也是业内最关注的指标之一。以流量为切入点,
从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性成为一个绕不开的话题。随着
Spring Cloud Hystrix宣布停止更新,阿里开源的sentinel也在寻找其替代里呼之欲出。
sentinel生态:
Sentinel 官网称为分布式系统的流量防卫兵,主要提供限流、熔断等服务治理相关的功能。
实践:
1、基本概念:
限流:我们通常使用QPS对流量来进行描述,限流就是现在服务被调用的并发QPS,从而对系统进行自我保护。
熔断:就是当系统中某一个服务出现性能瓶颈是,对这个服务的调用进行快速失败,避免造成连锁反应,从而影响整个链路的调用。
降级:当系统中某一个服务或着某个接口因为频繁报错或者RT过长被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断
本次分享主要是基于sentinel作为zuul网关层限流的实践、结合实际demo代码以及进行踩坑分享与部分核心源码的学习跟进
2、组件引用说明:
Eureka:注册中心。主要给zuul网关集成,做路由转发时,无需配置应用路由的相关信息
apollo:主要给zuul网关配置一些后续根据指定api路径做限流,可动态配置
以及作为sentinel做限流规则的动态持久化配置
sentinel针对在zuul网关限流主要有两个方式:
·针对不同 route限流
·自定义的 API 分组进行限流
支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,
请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
sentinel源码结构:
Sentinel 的核心模块说明如下:
·sentinel-core
Sentinel 核心模块,实现限流、熔断等基本能力。
·sentinel-dashboard(需要对源码进行做点改造)
Sentinel 可视化控制台,提供基本的管理界面,配置限流、熔断规则等,展示监控数据等。
·sentinel-adapter
Sentinel 适配,Sentinel-core 模块提供的是限流等基本API,主要是提供给应用自己去显示调用,对代码有侵入性,故该模块对主流框架进行了适配,目前已适配的模块如下:
osentinel-apache-dubbo-adapter
对 Apache Dubbo 版本进行适配,这样应用只需引入 sentinel-apache-dubbo-adapter 包即可对 dubbo 服务进行流控与熔断,大家可以思考会利用 Dubbo 的哪个功能特性。
osentinel-dubbo-adapter
对 Alibaba Dubbo 版本进行适配。
osentinel-grpc-adapter
对 GRPC 进行适配。
osentinel-spring-webflux-adapter
对响应式编程框架 webflux 进行适配。
osentinel-web-servlet
对 servlet 进行适配,例如 Spring MVC。
osentinel-zuul-adapter
对 zuul 网关进行适配。
·sentinel-cluster
提供集群模式的限流与熔断支持,因为通常一个应用会部署在多台机器上组成应用集群。
·sentinel-transport
网络通讯模块,提供 Sentinel 节点与 sentinel-dashboard 的通讯支持,主要有如下两种实现。
osentinel-transport-netty-http
基于 Netty 实现的 http 通讯模式。
osentinel-transport-simple-http
简单的 http 实现方式。
·sentinel-extension
Sentinel 扩展模式。主要提供了如下扩展(高级)功能:
osentinel-annotation-aspectj
提供基于注解的方式来定义资源等。
osentinel-parameter-flow-control
提供基于参数的限流(热点限流)。
osentinel-datasource-extension
限流规则、熔断规则的存储实现,默认是存储在内存中。
osentinel-datasource-apollo
基于 apollo 配置中心实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-consul
基于 consul 实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-etcd
基于 etcd 实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-nacos
基于 nacos 实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-redis
基于 redis 实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-spring-cloud-config
基于 spring-cloud-config 实现限流规则、熔断规则的存储,动态推送生效机制。
osentinel-datasource-zookeeper
基于 zookeeper 实现限流规则、熔断规则的存储,动态推送生效机制。
用法:
springboot项目为例引入相关依赖,使用apollo为持久化规则存储
<< span="">dependency> << span="">groupId>org.springframework.cloudgroupId> << span="">artifactId>spring-cloud-starter-netflix-zuulartifactId>dependency>
<< span="">dependency> << span="">groupId>org.springframework.cloudgroupId> << span="">artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>dependency>
<< span="">dependency>
<< span="">groupId>com.alibaba.cspgroupId>
<< span="">artifactId>sentinel-datasource-apolloartifactId>dependency>
<< span="">dependency>
<< span="">groupId>com.alibaba.cloudgroupId>
<< span="">artifactId>spring-cloud-starter-alibaba-sentinelartifactId>dependency>
<< span="">dependency>
<< span="">groupId>com.alibaba.cloudgroupId>
<< span="">artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>dependency>
动态数据源配置原理:
结合sentinel官方提供的控制台,通过控制台设置规则后将规则push到统一的规则中心,客户端实现 ReadableDataSource
接口端监听规则中心实时获取变更
目前支持对 ZooKeeper, Redis, Nacos, Apollo的动态规则数据源拓展
源码处理时序图:
配置限流的动态数据源:
真正处理限流的核心代码入口:
在链式插槽SlotChain(核心几个Slot):
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
StatisticsSlot 则用于记录,统计不同维度的 runtime 信息,基于滑动窗口统计;
GatewayFlowSlot 根据预设的网关限流规则来做checkGatewayParamFlow限流
匀速排队限流算法源码解析:
它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;
否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 MaxQueueingTimeMs 时间,则该请求会等待直到预设时间到来通过(排队等待处理);
若预期的通过时间超出最大排队时长,则直接拒接这个请求。
这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。
ParamFlowChecker#passThrottleLocalCheck:
static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount, Object value) { ParameterMetric metric = getParameterMetric(resourceWrapper); CacheMaptimeRecorderMap = metric ==null ? null : metric.getRuleTimeCounter(rule); if (timeRecorderMap == null) { return true; } // Calculate max token count (threshold) SetexclusionItems = rule.getParsedHotItems().keySet(); long tokenCount = (long)rule.getCount(); if (exclusionItems.contains(value)) { tokenCount = rule.getParsedHotItems().get(value); } //设置QPS为0 则直接返回拦截 if (tokenCount == 0) { return false; } //acquireCount 每次请求消耗数固定为1、时间单位为秒:rule.getDurationInSec()、 tokenCount通过数 //costTime算出来为每个请求过来需要的时间 单位为毫秒 long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount); while (true) { long currentTime = TimeUtil.currentTimeMillis(); //putIfAbsent同一个key不存在则返回null,存在把原来值返回,不会覆盖原来值 //timeRecorder为null 则为第一次访问 AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime)); if (timeRecorder == null) { return true; } //AtomicLong timeRecorder = timeRecorderMap.get(value); long lastPassTime = timeRecorder.get(); long expectedTime = lastPassTime + costTime; //计算expectedTime为上次请求+每个请求需要的时间是否大于当前时间,大于则说明请求过于频繁 if (expectedTime <= currentTime || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) { AtomicLong lastPastTimeRef = timeRecorderMap.get(value); //通过CAS比较通过,更新最近通过的毫秒值等下次请求进来当上次的请求时间 if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) { long waitTime = expectedTime - currentTime; if (waitTime > 0) { //waitTime > 0的情况只有在设置了匀速排队情况并且希望花费的时间与当前请求过来的时间要小于设置的排队的时间(默认是快速失败)对当前线程请求放过、 //线程休眠waitTime,重新设上次请求时间值 lastPastTimeRef.set(expectedTime); try { TimeUnit.MILLISECONDS.sleep(waitTime); } catch (InterruptedException e) { RecordLog.warn("passThrottleLocalCheck: wait interrupted", e); } } return true; } else { Thread.yield(); } } else { return false; } } }
动态规则控制台配置集成
配置规则:
实时结果:
A&Q:
1、既然网关层已经限流了,那应用层还需要限流吗?
需要的,双重保护是很有必要。同理,上游的聚合服务配置了限流,下游的基础服务也是需要配置限流的,
试想下如果只配置了上游的限流,如果上游发起大量重试岂不是依旧可能压垮下游的基础服务?而且这种情况,
我们在配置限流阈值时也需要特别注意,比如上游的A,B两个服务都依赖了下游Y服务,A,B分别配置的100QPS,
那么Y服务至少得配置为200QPS,要不然有部分请求额外的经过透传和处理但最终又被拒绝,不仅是浪费资源,严重了还可能导致数据不一致等问题。
所以,最好是根据总体的容量规划来配置,越早拦截越好,每一层都要配置限流。
2、你真的需要集群限流吗?
(1)、当想要配置单机QPS限制<1 时单机模式是无法满足的,只能使用集群限流模式来限制集群的总QPS。比如有个性能极差的接口单机最多只能扛住0.5QPS,
部署了10台机器那么需要将集群最大容量是5 QPS,当然这个例子有点极端。再比如我们希望某个客户调用某个接口总的最大QPS为10,
但是实际我们部署了20台机器,这种情况是真实存在的;
(2)、 单机限流阈值是10 QPS,部署了3个节点,理论上集群的总QPS可以达到30,但是实际上由于流量不均匀导致集群总QPS还没有达到30就已经触发限流了。
很多人会说这不合理,但我认为需要按真实情况来分析。如果这个 “10QPS”是根据容量规划的系统承载能力推算出来的阈值(或者说该接口请求如果超过10 QPS就可能会导致系统崩溃),
那这个限流的结果就是让人满意的。如果这个“10QPS”只是业务层面的限制,即便某个节点的QPS超过10了也不会导致什么问题,其实我们本质上是想限制整个集群总的QPS,
那么这个限流的结果就不是合理的,并没有达到最佳效果;
所以,实际取决于你的限流是为了实现“过载保护”,还是实现业务层的限制。
集群限流并无法解决流量不均匀的问题,限流组件并不能帮助你重新分配或者调度流量。集群限流只是能让流量不均匀场景下整体限流的效果更好。
交流会掠影