Based on aop & agent & Sentinel & Nacos configuration control packaging class implementation principle

Based on aop & amp; proxy & amp; Sentinel & amp; Nacos configuration control packaging class implementation principle

Hi, I am Achang. Today I will record the idea of looking at sentinel source code and combining business implementation Based on aop & amp; proxy & amp; Sentinel & amp; Nacos configuration control packaging class implementation principle ;The following does not record the implementation process of the plan step by step, but records the important links and examples of the process to facilitate your own understanding and review.

1. Knowledge points involved

  • SpringBoot
  • Nacos
  • Sentinel
  • AOP
  • proxy interceptor

2. Text

0. Overall framework diagram of Sentinel

1. Control resource json information based on Nacos configuration

After integrating Nacos, configure the JSON type file under the corresponding DataId#Group, such as

{<!-- -->
    "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
}

The above is divided into main switch and the corresponding sentinelSlot switch

2. How to load the above configuration?

Use hutool’s spi package;

Service loading tool class in SPI mechanism, the process is as follows
1. Create an interface and create an implementation class
2. Create a file with the same fully qualified class name as the interface under ClassPath/META-INF/services
3. Fill in the fully qualified class name of the implementation class in the file content.

Load the corresponding NacosSpiService class through Java’s Spi mechanism

public interface NacosSpiService {<!-- -->
    void loadRules(String content);
    String getDataId();
    String getGroupId();
}

Declare the classes that need to be loaded under META-INF/services

com.achang.core.sentinel.NacosSpiSentinelImpl

Then declare the method Spi loading in the @Configuration class of Nacos and add a listener to listen for Nacos configuration changes

 private void refreshNacosConfigBySpi() {<!-- -->
        try {<!-- -->
            ServiceLoaderUtil.loadList(NacosSpiService.class)
                    .stream()
                    .filter(nacosSpiService -> nacosSpiService != null & amp; & amp; StringUtils.isNotBlank(nacosSpiService.getDataId())).forEach(new Consumer<NacosSpiService>() {<!-- -->
                        @SneakyThrows
                        @Override
                        public void accept(NacosSpiService nacosSpiService) {<!-- -->
                            try {<!-- -->
                                // nacosSpiService.getGroupId() does not use spi group temporarily
                                String content = configService.getConfigAndSignListener(nacosSpiService.getDataId(),
                                        group, 5000, new AbstractListener() {<!-- -->
                                            @Override
                                            public void receiveConfigInfo(String content) {<!-- -->
                                                try {<!-- -->
                                                    nacosSpiService.loadRules(content);
                                                    log.info("nacos configuration initialization" + nacosSpiService.getDataId() + ":" + content);
                                                } catch (Exception e) {<!-- -->
                                                    log.error(nacosSpiService.getDataId() + "Configuration parsing failed: {}", e.getMessage(), e);
                                                }
                                            }
                                        });
                                try {<!-- -->
                                    nacosSpiService.loadRules(content);
                                    log.info("nacos configuration initialization" + nacosSpiService.getDataId() + ":" + content);
                                } catch (Exception e) {<!-- -->
                                    log.error(nacosSpiService.getDataId() + "Configuration parsing failed: {}", 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);
        }

The above will eventually load the configuration information from nacos through the loadRules method to initialize it into the resource control Rule corresponding to sentinel:

  • 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

Load it as a HashMap through the loadRules method of the above Manager corresponding to Rules

The following takes com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager as an example;

  • FlowRuleManager#flowRules to store the mapping relationship of control resources

  • FlowRuleManager#FlowPropertyListener to update & load configuration

    • The FlowRuleUtil.buildFlowRuleMap method is used to convert it into a ConcurrentHashMap, and use hash to deduplicate and sort it. The sorting rule is FlowRuleComparator.

      {<!-- -->
                  "enabled": false,
                  "clusterMode": false,
                  "controlBehavior": 0,
                  "count": 200,
                  "grade": 1,
                  "limitApp": "default",
                  "maxQueueingTimeMs": 500,
                  "resource": "com.achang.UserService",
                  "strategy": 0,
                  "warmUpPeriodSec": 10
      }
      

      The above resources will be converted into:

      • 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. How to use loaded resources

The json string configured through Nacos is converted into the corresponding RuleMap, and then the rule map is obtained through getFlowRuleMap(); this involves the Slot responsibility chain in Sentinel, which still uses com.alibaba.csp.sentinel.slots.block.flow. FlowSlot example.

  • com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#ruleProvider to obtain the rules of the corresponding resource;
  • com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#checkFlow will wrap the resources obtained above into resourceWrapper
  • A proxy method will call the com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#entry method of each responsibility chain to execute the logic of the slot to limit/downgrade/circuit, etc.

In the getFlowRuleMap method, the corresponding Map will be assembled according to the resource configuration, and generateRater will set the corresponding controlBehavior field to correspond to the TrafficShapingController (smoothizer com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController/pre Heater com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController, etc.) For specific logic, please refer to the official document https://sentinelguard.io/zh-cn/docs/flow-control.html

The following is an example of the core code canPass of RateLimiterController.

@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;
    }

At the entry of the corresponding slot, com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#entry will be executed.

 @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);
    }

There will be com.alibaba.csp.sentinel.slots.block.flow.FlowRuleChecker in com.alibaba.csp.sentinel.slots.block.flow.FlowSlot#checkFlow to determine whether to pass or perform the downgrade based on the corresponding FlowRule rules. Logic etc;

 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);
                }
            }
        }
    }

Then execute the corresponding logic according to the corresponding TrafficShapingController;

4. How to perform the above downgrade/circuit break judgments in the correct place?

Sentinel is implemented based on AOP using the @SentinelResource annotation, but the configuration cannot be modified dynamically, which is inflexible

Then you can wrap a template class [Achang’s Ugly Code Optimization] to optimize the Controller execution process through strategy mode & template mode, and then use a proxy object to proxy this template class to perform custom downgrades before and after the target method is executed/ Fusing, etc.;

Use Interceptor interceptors and other methods to write the before and after logic of the object, and implement the InvocationHandler class to override the invoke method.

The entry method of com.alibaba.csp.sentinel.SphU is used to pass the resource name to downgrade/circuit and other logic.

@Slf4j
public class TemplateInterceptor implements InvocationHandler{<!-- -->
  try (Entry entry = SphU.entry(actionTemplateRequestInfo.getResource())) {<!-- -->
            // Call target method
            return method.invoke(target, args);
  }
}

Declare the proxy wrapper class in the corresponding configuration class, as follows:

 @Bean
    Template template() {<!-- -->
        Template template = new Template();
        Template interceptor = new TemplateInterceptor(template);
        //Create proxy object
        return (Template) Proxy.newProxyInstance(
                Template.class.getClassLoader(),
                new Class[]{<!-- -->Template.class},
                interceptor
        );
    }

In this way, you can use agent combined with aop and combine it with the flexible sentinel framework through Nacos dynamic configuration. Control resources.

Reference:

  • sentinel official documentation
  • blog