基于aop & 代理 & Sentinel & Nacos配置控制包装类实现原理
阿昌 Java小菜鸡

基于aop & 代理 & Sentinel & Nacos配置控制包装类实现原理

Hi,我是阿昌,今天记录下看sentinel源码结合业务实现的思路基于aop & 代理 & Sentinel & Nacos配置控制包装类实现原理;下面并不会手把手的记录方案的实现流程,而是记录流程的重要环节和举例,方便自己理解和回顾。

一、涉及知识点

  • SpringBoot
  • Nacos
  • Sentinel
  • AOP
  • 代理拦截器

二、正文

0、Sentinel的总体框架图

image

1、基于Nacos配置控制资源json信息

在集成Nacos了之后,在对应的DataId#Group下配置JSON类型的文件如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"flowRules": [
{
"enabled": false,
"clusterMode": false,
"controlBehavior": 0,
"count": 200,
"grade": 1,
"limitApp": "default",
"maxQueueingTimeMs": 500,
"resource": "com.achang.UserService",
"strategy": 0,
"warmUpPeriodSec": 10
},
{
"enabled": false,
"clusterMode": false,
"controlBehavior": 2,
"count": 0.1,
"grade": 1,
"limitApp": "default",
"maxQueueingTimeMs": 30000,
"resource": "achang:1",
"strategy": 0,
"warmUpPeriodSec": 10
}
],
"sentinelEnabled": true
}

以上分总开关和对应sentinelSlot开关

2、如何加载以上的配置?

利用hutool的spi包;

SPI机制中的服务加载工具类,流程如下
1、创建接口,并创建实现类
2、ClassPath/META-INF/services下创建与接口全限定类名相同的文件
3、文件内容填写实现类的全限定类名

通过Java的Spi机制加载对应的NacosSpiService类

1
2
3
4
5
public interface NacosSpiService {
void loadRules(String content);
String getDataId();
String getGroupId();
}

META-INF/services下声明需要加载的类

1
com.achang.core.sentinel.NacosSpiSentinelImpl

然后在Nacos的@Configuration类中声明方法Spi加载,增加监听器监听Nacos配置变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void refreshNacosConfigBySpi() {
try {
ServiceLoaderUtil.loadList(NacosSpiService.class)
.stream()
.filter(nacosSpiService -> nacosSpiService != null && StringUtils.isNotBlank(nacosSpiService.getDataId())).forEach(new Consumer<NacosSpiService>() {
@SneakyThrows
@Override
public void accept(NacosSpiService nacosSpiService) {
try {
// nacosSpiService.getGroupId()暂时不用spi的group
String content = configService.getConfigAndSignListener(nacosSpiService.getDataId(),
group, 5000, new AbstractListener() {
@Override
public void receiveConfigInfo(String content) {
try {
nacosSpiService.loadRules(content);
log.info("nacos配置初始化" + nacosSpiService.getDataId() + ":" + content);
} catch (Exception e) {
log.error(nacosSpiService.getDataId() + "配置解析失败:{}", e.getMessage(), e);
}
}
});
try {
nacosSpiService.loadRules(content);
log.info("nacos配置初始化" + nacosSpiService.getDataId() + ":" + content);
} catch (Exception e) {
log.error(nacosSpiService.getDataId() + "配置解析失败:{}", e.getMessage(), e);
}
} catch (Throwable throwable) {
log.error("nacos register listener:{},{} failed:{}", group, nacosSpiService.getDataId(), throwable.getMessage(), throwable);
}
}
});
} catch (Throwable throwable) {
log.error("refreshNacosConfigBySpi failed:{}", throwable.getMessage(), throwable);
}

以上会最终通过loadRules方法来加载nacos传来的配置信息,来初始化成sentinel对应的资源控制Rule:

  • com.alibaba.csp.sentinel.slots.system.SystemRule
  • com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule
  • com.alibaba.csp.sentinel.slots.block.flow.FlowRule
  • com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule

通过以上对应Rule的Manager的loadRules方法来加载为一个HashMap


以下以com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager为例子;

  • FlowRuleManager#flowRules来存储控制资源的映射关系

  • FlowRuleManager#FlowPropertyListener来更新&加载配置

    • FlowRuleUtil.buildFlowRuleMap方法来转化为一个ConcurrentHashMap,并对其进行用hash进行去重和排序,排序规则用的是FlowRuleComparator

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      {
      "enabled": false,
      "clusterMode": false,
      "controlBehavior": 0,
      "count": 200,
      "grade": 1,
      "limitApp": "default",
      "maxQueueingTimeMs": 500,
      "resource": "com.achang.UserService",
      "strategy": 0,
      "warmUpPeriodSec": 10
      }

      以上资源会被转化为:

      • Key:com.achang.UserService

      • Value:[{“enabled”:false,”clusterMode”:false,”controlBehavior”:0,”count”:200,”grade”:1,”limitApp”:”default”,”maxQueueingTimeMs”:500,”resource”:”com.achang.UserService”,”strategy”:0,”warmUpPeriodSec”:10}]

3、如何使用加载后的资源

通过Nacos配置的json字符串转化为对应的RuleMap,然后通过getFlowRuleMap()来获取规则Map;这里涉及到Sentinel中的Slot责任链,依然用的com.alibaba.csp.sentinel.slots.block.flow.FlowSlot举例。

  • com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#ruleProvider来获取对应资源的规则;
  • com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#checkFlow会将上面获取的资源包装成resourceWrapper
  • 一个代理方法会调用每一个责任链的com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#entry方法来执行是否符合这个slot的逻辑来进行限流/降级/熔断等

在getFlowRuleMap方法中会去根据资源的配置来组装对应的Map,其中generateRater会去设置对应的controlBehavior字段来对应TrafficShapingController(匀速器com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController/预热器com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController等)具体的逻辑参考官方文档https://sentinelguard.io/zh-cn/docs/flow-control.html

下面举例RateLimiterController的核心代码canPass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// Pass when acquire count is less or equal than 0.
if (acquireCount <= 0) {
return true;
}
// Reject when count is less or equal than 0.
// Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
if (count <= 0) {
return false;
}

long currentTime = TimeUtil.currentTimeMillis();
// Calculate the interval between every two requests.
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);

// Expected pass time of this request.
long expectedTime = costTime + latestPassedTime.get();

if (expectedTime <= currentTime) {
// Contention may exist here, but it's okay.
latestPassedTime.set(currentTime);
return true;
} else {
// Calculate the time to wait.
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}

在对应的slot的入口会执行com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#entry

1
2
3
4
5
6
7
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized);

fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#checkFlow中会有com.alibaba.csp.sentinel.slots.block.flow.FlowRuleChecker来根据对应的FlowRule规则来判断是否通过或者执行对于的降级逻辑等;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) {
return;
}
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}

然后根据对应的TrafficShapingController来执行对应的逻辑;


4、如何在正确的地方执行上面的降级/熔断等判断?

sentinel有基于aop的方式使用@SentinelResource注解实现,但就不能动态的对配置进行修改,不灵活

那可以对一个模版类进行包装【阿昌之丑陋代码优化】通过策略模式&模版模式来优化Controller执行流程,然后用代理对象的方式代理这个模版类来在目标方式执行前后进行自定义降级/熔断等;


用Interceptor拦截器等方式来写对于的前后逻辑,实现InvocationHandler类重写invoke方法

com.alibaba.csp.sentinel.SphU的entry方法来传递资源名来降级/熔断等逻辑

1
2
3
4
5
6
7
@Slf4j
public class TemplateInterceptor implements InvocationHandler{
try (Entry entry = SphU.entry(actionTemplateRequestInfo.getResource())) {
// 调用目标方法
return method.invoke(target, args);
}
}

在对应配置类中声明代理这个包装类,如下:

1
2
3
4
5
6
7
8
9
10
11
@Bean
Template template() {
Template template = new Template();
Template interceptor = new TemplateInterceptor(template);
// 创建代理对象
return (Template) Proxy.newProxyInstance(
Template.class.getClassLoader(),
new Class[]{Template.class},
interceptor
);
}

这样子就可以用代理结合aop的形式并通过Nacos动态配置的方式结合了sentinel框架灵活控制资源。

参考


 请作者喝咖啡