目录
(1). CtSph.lookProcessChain() 核心方法!
一、基本使用
Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
下面示例是SpringBoot结合Sentinel,使用上比较简单。
(1)限流方式一:捕获异常的方式定义资源
1. pom文件引入
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
</dependencies>
2. 代码示例准备
// 项目启动类
@SpringBootApplication
public class SentinelApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelApplication.class, args);
}
}
// service层
@Service
public class OrderQueryService {
public String queryOrderInfo(String orderId) {
System.out.println("获取订单信息:" + orderId);
return "return OrderInfo :" + orderId;
}
}
// controller层
@Slf4j
@RestController
public class OrderCreateController {
@Autowired
private OrderQueryService orderQueryService;
/**
* 最原始方法--需要捕获异常
* @param orderId
* @return
*/
@RequestMapping("/getOrder")
@ResponseBody
public String queryOrder1(@RequestParam("orderId") String orderId) {
Entry entry = null;
try {
// 传入资源
entry = SphU.entry(SentinelConfigs.KEY);
return orderQueryService.queryOrderInfo(orderId);
} catch (BlockException e) {
// 接口被限流的时候, 会进入到这里
log.warn("---queryOrder1接口被限流了---, exception: ", e);
return "接口限流, 返回空";
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
sentinel规则设置:
@Component
public class SentinelConfigs {
public static final String KEY = "flowSentinelKey";
@Bean
public void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(KEY);
// QPS控制在2以内
rule1.setCount(2);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
}
3. 运行代码
当qps>2时,就会触发限流,如下:
(2)限流方式二:注解方式:
上面是最原始的方式实现限流,但是对代码侵入性太高,我们推荐使用注解方式!
1. 需要引入支持注解的jar包:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.0</version>
</dependency>
/**
* Sentinel切面类配置
*/
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect getSentinelResource() {
return new SentinelResourceAspect();
}
}
2. 注解方式代码:
@SentinelResource(value = "getOrderInfo",
blockHandler = "flowQpsException",
fallback = "queryOrder2FallBack")
public String queryOrder2(String orderId) {
// 模拟接口运行时抛出代码异常
if ("0".equals(orderId)) {
throw new RuntimeException();
}
System.out.println("获取订单信息:" + orderId);
return "return OrderInfo :" + orderId;
}
/**
* qps异常,被限流
* 注意: 方法参数、返回值要与原函数保持一致
*
* @param orderId
* @param e
* @return
*/
public String flowQpsException(String orderId, BlockException e) {
e.printStackTrace();
return "flowQpsException for queryOrder22: " + orderId;
}
/**
* 运行时异常的fallback方式
* 注意: 方法参数、返回值要与原函数保持一致
*
* @param orderId
* @param a
* @return
*/
public String queryOrder2FallBack(String orderId, Throwable a) {
return "fallback queryOrder2: " + orderId;
}
controller方法测试:
/**
* 限流方式二:注解方式定义
*
* @param orderId
* @return
*/
@RequestMapping("/query/order2")
@ResponseBody
public String queryOrder3(@RequestParam("orderId") String orderId) {
return orderQueryService.queryOrder2(orderId);
}
3. 运行代码
模拟运行时异常,执行fallback方法
请求:http://localhost:8080//query/order2?orderId=0
模拟限流:
请求:http://localhost:8080//query/order2?orderId=1223
正常执行了限流操作。
(3)熔断降级
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
直接上代码:
@Slf4j
@RestController
public class DegradeController {
@Autowired
private DegradeService degradeService;
@RequestMapping("/query/info/v1")
public String queryInfo(@RequestParam("spuId") String spuId) {
String res = degradeService.queryInfo(spuId);
return res;
}
}
降级这里有三种设置方式,这里只写一个例子:设置异常比例。
@Configuration
public class DegradeConfigs {
/**
* Sentinel切面类配置
* @return
*/
@Bean
public SentinelResourceAspect getSentinelResource() {
return new SentinelResourceAspect();
}
/**
* DEGRADE_GRADE_EXCEPTION_RATIO
* 将比例设置成0.1将全部通过, exception_ratio = 异常/通过量
* 当资源的每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态
*/
@PostConstruct
public void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("queryInfo");
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
// 错误比例设置为0.1
rule.setCount(0.1);
// 设置时间窗口:10s
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
}
@Slf4j
@Service
public class DegradeService {
private static final String KEY = "queryInfo";
@SentinelResource(value = KEY,
blockHandler = "blockHandlerMethod",
fallback = "queryInfoFallback")
public String queryInfo(String spuId) {
// 模拟调用服务出现异常
if ("0".equals(spuId)) {
throw new RuntimeException();
}
return "query goodsinfo success, " + spuId;
}
public String blockHandlerMethod(String spuId, BlockException e) {
log.warn("queryGoodsInfo222 blockHandler", e.toString());
return "已触发限流, blockHandlerMethod res: " + spuId;
}
public String queryInfoFallback(String spuId, Throwable e) {
log.warn("queryGoodsInfo222 fallback", e.toString());
return "业务异常, return fallback res: " + spuId;
}
}
测试的时候,可以使用jmeter压一下,我设置的比较简单,使用postman或者网页直接请求都行,多点几次,就能看到降级的执行:
测试请求:http://localhost:8080/query/info/v1?spuId=0
官方也有降级例子,可以看下:
熔断降级 · alibaba/Sentinel Wiki · GitHub
二、源码解析
说明:
1. 先概括性的看源码的整个调用链路,下面三部分介绍几个常用的重要slot的源码
2. StatisticSlot 用于存储资源的统计信息,例如资源的RT、QPS、thread count等等,这些信息将用作多维度限流,降级的依据(fireEntry用来触发下一个规则的调用)
3. FlowSlot 校验资源流控规则
4. DegradeSlot 校验资源流降级规则
1. 整体看源码链路
SentinelResourceAspect
// 请关注这行代码
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
一直点进去,看里面的方法
CtSph.entryWithType
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, Object[] args) throws BlockException {
// 将资源名称包装成StringResourceWrapper
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
return this.entryWithPriority(resource, count, prioritized, args);
}
CtSph.asyncEntryWithPriorityInternal()
// 前面的if else边缘逻辑,先忽略
private AsyncEntry asyncEntryWithPriorityInternal(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
return this.asyncEntryWithNoChain(resourceWrapper, context);
} else {
if (context == null) {
context = CtSph.InternalContextUtil.internalEnter("sentinel_default_context");
}
if (!Constants.ON) {
return this.asyncEntryWithNoChain(resourceWrapper, context);
} else {
// 核心方法!!!
ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);
if (chain == null) {
return this.asyncEntryWithNoChain(resourceWrapper, context);
} else {
AsyncEntry asyncEntry = new AsyncEntry(resourceWrapper, chain, context);
try {
// 对整个链条的调用,下面详细讲解
chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args);
asyncEntry.initAsyncContext();
asyncEntry.cleanCurrentEntryInLocal();
} catch (BlockException var9) {
asyncEntry.exitForContext(context, count, args);
throw var9;
} catch (Throwable var10) {
RecordLog.warn("Sentinel unexpected exception in asyncEntryInternal", var10);
asyncEntry.cleanCurrentEntryInLocal();
}
return asyncEntry;
}
}
}
}
(1). CtSph.lookProcessChain() 核心方法!
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap();
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
// double check 提升加锁性能【想想单例模式】
if (chain == null) {
synchronized(LOCK) {
chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
if (chain == null) {
if (chainMap.size() >= 6000) {
return null;
}
// pipeline 联想下责任链模式,后面会对这个方法进行详细讲解
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap(chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
SlotChainProvider.newSlotChain()
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
} else {
slotChainBuilder = (SlotChainBuilder)SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
if (slotChainBuilder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default", new Object[0]);
// 初始化DefaultSlotChainBuilder
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " + slotChainBuilder.getClass().getCanonicalName(), new Object[0]);
}
// 构建一个链条
return slotChainBuilder.build();
}
}
DefaultSlotChainBuilder.build()
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
Iterator var3 = sortedSlotList.iterator();
while(var3.hasNext()) {
ProcessorSlot slot = (ProcessorSlot)var3.next();
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain", new Object[0]);
} else {
// 就是在链表上增加一个节点
chain.addLast((AbstractLinkedProcessorSlot)slot);
}
}
return chain;
}
(2). chain.entry 整个链条的调用
DefaultProcessorSlotChain.entry()
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {
// 链条的第一个
this.first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}
AbstractLinkedProcessorSlot.transformEntry
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args) throws Throwable {
this.entry(context, resourceWrapper, o, count, prioritized, args);
}
first是什么:
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
super.fireExit(context, resourceWrapper, count, args);
}
};
fireEntry()方法
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {
if (this.next != null) {
// next的transformEntry方法
this.next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
2. StatisticSlot.entry
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
Iterator var8;
ProcessorSlotEntryCallback handler;
try {
// do some checking
this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 增加线程数(LongAdder加一)
node.increaseThreadNum();
// 统计通过的请求
node.addPassRequest(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
Iterator var13 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();
while(var13.hasNext()) {
ProcessorSlotEntryCallback<DefaultNode> handler = (ProcessorSlotEntryCallback)var13.next();
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException var10) {
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
}
var8 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();
while(var8.hasNext()) {
handler = (ProcessorSlotEntryCallback)var8.next();
handler.onPass(context, resourceWrapper, node, count, args);
}
// 要注意看这个异常捕获
} catch (BlockException var11) {
BlockException e = var11;
context.getCurEntry().setBlockError(var11);
// 增加block的统计
node.increaseBlockQps(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseBlockQps(count);
}
var8 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();
while(var8.hasNext()) {
handler = (ProcessorSlotEntryCallback)var8.next();
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
// 抛出业务异常
} catch (Throwable var12) {
context.getCurEntry().setError(var12);
throw var12;
}
}
3. FlowSlot.entry
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
this.checkFlow(resourceWrapper, context, node, count, prioritized);
this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
FlowRuleChecker.checkFlow
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider != null && resource != null) {
// 从内存中拿流控规则
Collection<FlowRule> rules = (Collection)ruleProvider.apply(resource.getName());
if (rules != null) {
Iterator var8 = rules.iterator();
// 遍历规则,看能否通过校验
while(var8.hasNext()) {
FlowRule rule = (FlowRule)var8.next();
// 对资源逐条校验每个规则
if (!this.canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
}
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
return selectedNode == null ? true : rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
DefaultController.canPass
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = this.avgUsedTokens(node);
if ((double)(curCount + acquireCount) > this.count) {
// 先不用关注
if (prioritized && this.grade == 1) {
long currentTime = TimeUtil.currentTimeMillis();
long waitInMs = node.tryOccupyNext(currentTime, acquireCount, this.count);
if (waitInMs < (long)OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
this.sleep(waitInMs);
throw new PriorityWaitException(waitInMs);
}
}
return false;
} else {
return true;
}
}
4. DegradeSlot.entry
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
this.performChecking(context, resourceWrapper);
this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
三、限流算法滑动时间窗口
滑动时间窗口计数器算法思想:它将时间窗口划分为更小的时间片断,每过一个时间片断,时间窗口就会往右滑动一格,每一个时间片断都有独立的计数器。在计算整个时间窗口内的请求总数会累加全部的时间片断内的计数器。时间窗口划分的越细,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
ps: 图片来源于网络
1. StatisticSlot.entry
node.addPassRequest(count);
public void addPassRequest(int count) {
super.addPassRequest(count);
this.clusterNode.addPassRequest(count);
}
StatisticNode.addPassRequest
public void addPassRequest(int count) {
// 滑动时间窗口实现
this.rollingCounterInSecond.addPass(count);
this.rollingCounterInMinute.addPass(count);
}
StatisticNode类:
1)rollingCounterInSecond
private transient volatile Metric rollingCounterInSecond;
private transient Metric rollingCounterInMinute;
private LongAdder curThreadNum;
private long lastFetchTime;
public StatisticNode() {
// 看下传入的这两个参数 2 1000ms
this.rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);
this.rollingCounterInMinute = new ArrayMetric(60, 60000, false);
this.curThreadNum = new LongAdder();
this.lastFetchTime = -1L;
}
看下ArrayMetric类:
public ArrayMetric(int sampleCount, int intervalInMs) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
OccupiableBucketLeapArray的构造方法:
public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
// 看下super
super(sampleCount, intervalInMs);
this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
}
public LeapArray(int sampleCount, int intervalInMs) {
AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");
// 一些初始化
// 500:时间窗口的长度
// windowLengthInMs = 1000 / 2 = 500
this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.sampleCount = sampleCount;
// new一个大小为2的数组 ==》sampleCount就是桶的个数
this.array = new AtomicReferenceArray(sampleCount);
}
2)StatisticNode.addPass()
public void addPassRequest(int count) {
// 滑动时间窗口实现
this.rollingCounterInSecond.addPass(count);
this.rollingCounterInMinute.addPass(count);
}
2. ArrayMetric.addPass()
@Override
public void addPass(int count) {
// 下面来看下currentWindow()方法
// 获取当前时间对应的时间窗口
WindowWrap<MetricBucket> wrap = this.data.currentWindow();
((MetricBucket)wrap.value()).addPass(count);
}
LeapArray.currentWindow()
其实注释写的已经很清楚了
public WindowWrap<T> currentWindow() {
return this.currentWindow(TimeUtil.currentTimeMillis());
}
/**
* Get bucket item at provided timestamp.
*
* @param timeMillis a valid timestamp in milliseconds
* @return current bucket item at provided timestamp if the time is valid; null if time is invalid
*/
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 计算落在哪个时间窗口里
int idx = calculateTimeIdx(timeMillis);
// Calculate current bucket start time.
// 落在这个时间窗口的具体坐标
long windowStart = calculateWindowStart(timeMillis);
/*
* Get bucket item at given time from the array.
*
* (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
* (2) Bucket is up-to-date, then just return the bucket.
* (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
*/
while (true) {
WindowWrap<T> old = array.get(idx);
if (old == null) {
/*
* B0 B1 B2 NULL B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
* time=888
* bucket is empty, so create new and update
*
* If the old bucket is absent, then we create a new bucket at {@code windowStart},
* then try to update circular array via a CAS operation. Only one thread can
* succeed to update, while other threads yield its time slice.
*/
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
// Successfully updated, return the created bucket.
return window;
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
/*
* B0 B1 B2 B3 B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
* time=888
* startTime of Bucket 3: 800, so it's up-to-date
*
* If current {@code windowStart} is equal to the start timestamp of old bucket,
* that means the time is within the bucket, so directly return the bucket.
*/
return old;
} else if (windowStart > old.windowStart()) {
/*
* (old)
* B0 B1 B2 NULL B4
* |_______||_______|_______|_______|_______|_______||___
* ... 1200 1400 1600 1800 2000 2200 timestamp
* ^
* time=1676
* startTime of Bucket 2: 400, deprecated, should be reset
*
* If the start timestamp of old bucket is behind provided time, that means
* the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
* Note that the reset and clean-up operations are hard to be atomic,
* so we need a update lock to guarantee the correctness of bucket update.
*
* The update lock is conditional (tiny scope) and will take effect only when
* bucket is deprecated, so in most cases it won't lead to performance loss.
*/
if (updateLock.tryLock()) {
try {
// Successfully get the update lock, now we reset the bucket.
// 重置时间窗口
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// Should not go through here, as the provided time is already behind.
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
再回到ArrayMetric.addPass()方法来看:
@Override
public void addPass(int count) {
WindowWrap<MetricBucket> wrap = this.data.currentWindow();
// 计算时间窗口里面的统计数据
((MetricBucket)wrap.value()).addPass(count);
}
MetricBucket.addPass()
下面分析一个统计pass的指标,其他的类型,比如抛异常时的block指标,可以类比来看
public void addPass(int n) {
// 增加一个pass的统计指标
add(MetricEvent.PASS, n);
}
MetricEvent枚举类:
public enum MetricEvent {
/**
* Normal pass.
*/
// 代表通过的所有校验规则
PASS,
/**
* Normal block.
*/
// 没有通过校验规则,抛出BlockException的调用
BLOCK,
// 发生了正常业务异常的调用
EXCEPTION,
// 调用完成的情况,不管是否抛异常了
SUCCESS,
// 所有的success调用耗费的总时间
RT,
/**
* Passed in future quota (pre-occupied, since 1.5.0).
*/
OCCUPIED_PASS
}
MetricBucket.add(MetricEvent event, long n)
private final LongAdder[] counters;
private volatile long minRt;
public MetricBucket() {
MetricEvent[] events = MetricEvent.values();
this.counters = new LongAdder[events.length];
for (MetricEvent event : events) {
counters[event.ordinal()] = new LongAdder();
}
initMinRt();
}
// 增加时间窗口里的通过请求数
public MetricBucket add(MetricEvent event, long n) {
counters[event.ordinal()].add(n);
return this;
}
3. 自己实现滑动时间窗口算法
代码逻辑:新建一个本地缓存,每5s为一个时间窗口,每1s为一个时间片断,时间片断作为缓存的key,原子类计数器做为缓存的value。每秒发送随机数量的请求,计算每一个时间片断的前5秒内的累加请求数量,超出阈值则限流。
先引入guava依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
@Slf4j
public class WindowLimitTest {
private LoadingCache<Long, AtomicLong> counter =
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
// 线程池建议使用手动创建,线程池不是这篇博客的重点,为了简便,就简单来写了
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
// 限流阈值
private long limit = 15;
/**
* 每隔1s累加前5s内每1s的请求数量,判断是否超出限流阈值
*/
public void slideWindow() {
scheduledExecutorService.scheduleWithFixedDelay(() -> {
try {
long time = System.currentTimeMillis() / 1000;
//每秒发送随机数量的请求
int reqs = (int) (Math.random() * 5) + 1;
counter.get(time).addAndGet(reqs);
long nums = 0;
// time windows 5 s
for (int i = 0; i < 5; i++) {
nums += counter.get(time - i).get();
}
log.info("time=" + time + ",nums=" + nums);
if (nums > limit) {
log.info("限流了,nums=" + nums);
}
} catch (Exception e) {
log.error("slideWindow() error", e);
} finally {
}
}, 5000, 1000, TimeUnit.MILLISECONDS);
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
WindowLimitTest limit = new WindowLimitTest();
limit.slideWindow();
}
}
输出结果:
四、漏桶算法
RateLimiterController
类比消息队列;
简单写下思路,感兴趣的可以自己完善
public class LeakyBucket {
// 当前时间
public long timeStamp = System.currentTimeMillis();
// 桶的容量
public long capatity;
// 水漏出的速度(每秒系统能处理的请求数)
public long rate;
// 当前水量(当前累积请求数)
public long water;
public boolean limit() {
long now = System.currentTimeMillis();
// 先执行漏水,计算剩余水量
water = Math.max(0, water - (now - timeStamp) / 1000 * rate);
timeStamp = now;
if ((water + 1) < capatity) {
// 尝试加水,并且还未满
water += 1;
return true;
} else {
// 水满,拒绝加水
return false;
}
}
}
五、令牌桶算法
WarmUpController.canPass
warm up--预热
简单写下思路,感兴趣的可以自己完善
public class TokenBucket {
// 当前时间
public long timeStamp = System.currentTimeMillis();
// 桶的容量
public long capatity;
// 令牌进入速度
public long rate;
// 当前令牌数量
public long tokens;
public boolean grant() {
long now = System.currentTimeMillis();
// 先执行漏水,计算剩余水量
tokens = Math.min(capatity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if (tokens < 1) {
// 若不到1个令牌,则拒绝
return false;
} else {
// 还有令牌,领取令牌
tokens -= 1;
return true;
}
}
}
六、限流算法对比
1. 计数器 VS 滑动窗口
1)计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。
2)滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。
3)也就是说,如果滑动窗口的精度越高,需要的存储空间越大。
2. 漏桶算法 VS 令牌桶算法
1)漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。
2)因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。
3)当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。