集群限流

时间:2022-06-19
本文章向大家介绍集群限流,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

准备工作

基于sentine-1.4.2,在dashboard想要更好的查看集群限流相关配置,需要一些小修改

你也可以直接从github上拉取我的代码: git@github.com:spilledyear/Sentinel.git,对应的分支是 1.4.2

  • 开启集群规则界面 修改:resources/app/views/flow_v1.html,将其中和集群相关的按钮打开,最终效果如下:
  • 规则持久化 dashboard默认没有对规则持久化,但在集群规则界面添加的规则,其实是可以持久化到nacos的,只需要做一些简单的修改。将dashboard模块test目录下的com.alibaba.csp.sentinel.dashboard.rule.nacos类拷贝到java目录,如下:

然后修改com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2文件,将其中的ruleProvider和rulePublisher改成刚刚新增的那两个

  • 启动nacos nacos的部署就不过多介绍,可以看官方文档 nacos手册

下面将从以下几个方面简单介绍集群限流

启动测试案例

以嵌入式模式为例,在源码的sentinel-demo模块种,已经准备好了相关测试案例,启动两个实例:ClusterDemoApplication,启动参数分别如下:

  • 实例一
-Dproject.name=clusterapp -Dserver.port=8081 -Dcsp.sentinel.dashboard.server=localhost:8080
  • 实例二
-Dproject.name=clusterapp -Dserver.port=8082 -Dcsp.sentinel.dashboard.server=localhost:8080

为了能够方便的修改规则信息,直观的观察效果,需要启动控制台

  • 启动控制台
-Dserver.port=8080

此时通过localhost:8080访问控制台,还无法看到任何应用信息,因为此时还没有任何的服务调用,通过以下快捷方式访问两个服务实例

curl localhost:8081/hello/luo

curl localhost:8082/hello/luo

这时候查看机器列表菜单选项,发现已经有两个实例了(端口区分):

但这时候还没有server和client的概念,需要简单配置:点击集群限流菜单项,然后点击右上角的"新增Toeken Server"

从中选取一台server,另一台指定为client,即:

此时,再查看集群流控菜单项,发现已经有了server信息,通过连接详情发现已有两个连接,这是这是嵌入式,server端本身也是一个应用实例

规则的推送

新建规则

以上准备工作完成之后,下面可以新建资源了。为了观察限流效果光差,新建的资源名与测试案例中的资源名一致:点击流控规则菜单项,然后点击右上角的回到集群界面

为什么这里要在集群界面新建规则呢?上面已经说过了,针对集群规则界面已经做了修改,规则可以持久化到nacos配置中心

然后新建一个规则,有关于规则的使用这里就不展开了

以上操作完成之后,会发现nacos中多了一条配置,具体内容就是规则的具体信息

查看限流效果

通过jmeter测试,让两个请求都分别请求不同的实例各20次:

发现每个请求都通过了10次,加起来刚好20次,多出来的请求抛出了FlowException异常,执行了blockHandler对应的逻辑,初步符合集群限流的效果

推送原理

在保存规则信息的时候,发现请求了以下接口:http://localhost:8080/v2/flow/rule/29 对应FlowControllerV2中的apiUpdateFlowRule,主要逻辑如下:

  • 将规则信息更新到dashboard的内存中,用于界面展示,这一部分主要和InMemoryRuleRepositoryAdapter的save方法相关;
  • 推送规则信息到nacos注册中心,这一部分和FlowRuleNacosPublisher相关;

如果dashboard使用了nacos持久化规则,对应的,在嵌入式模式下应该也会在server和client端使用NacosDatasource作为数据源,对应的源码在sentinel-datasource-nacos模块的NacosDataSource类中:

public NacosDataSource(final Properties properties, final String groupId, final String dataId,Converter<String, T> parser) {
    super(parser);
    this.configListener = new Listener() {
        @Override
        public Executor getExecutor() {
            return pool;
        }
        @Override
        public void receiveConfigInfo(final String configInfo) {
            RecordLog.info(String.format("[NacosDataSource] New property value received for (properties: %s) (dataId: %s, groupId: %s): %s",
                properties, dataId, groupId, configInfo));
            T newValue = NacosDataSource.this.parser.convert(configInfo);
            // Update the new value to the property.
            getProperty().updateValue(newValue);
        }
    };
    initNacosListener();
    loadInitialConfig();
}

从上可以看出,当规则信息更新了的时候,会同步到sentinel的内存结构中。

这里有一个小问题,如果没有使用注册中心,规则将怎么进行推送? 答案其实在FlowRuleApiPublisher中,如果没有使用注册中心,将通过SentinelApiClient发送http请求,将规则推送到各个服务实例,服务实例收到规则信息之后再加载到sentinel相关的内存结构,核心代码如下:

for (MachineInfo machine : set) {
    if (!MachineUtils.isMachineHealth(machine)) {
        continue;
    }
    // TODO: parse the results
    sentinelApiClient.setFlowRuleOfMachine(app, machine.getIp(), machine.getPort(), rules);
}

如果针对这个问题再次延申,还会有一些疑问,SentinelApiClient怎么就知道要将规则信息发送到哪里呢?哪个端口?这一部分肯定是sentine为我们隐藏起来了。

  • 第一个问题,哪个端口?这个端口其实是commandPort,即应用端暴露给 Sentinel 控制台的端口,ip@commandPort,其实就是界面上看到的那两个,分别为8720和8721;
  • 第二个问题,隐藏了哪些细节?其实就是隐藏了暴露端口的这部分细节,都在sentinel-transport模块中,提供了两种实现方式。
  • 方式一,sentinel-transport-simple-http模块中,通过ServerSocket方式暴露,对应的核心类为SimpleHttpCommandCenter,核心代码如下
@Override
public void run() {
    boolean success = false;
    ServerSocket serverSocket = getServerSocketFromBasePort(port);

    if (serverSocket != null) {
        CommandCenterLog.info("[CommandCenter] Begin listening at port " + serverSocket.getLocalPort());
        socketReference = serverSocket;
        executor.submit(new ServerThread(serverSocket));
        success = true;
        port = serverSocket.getLocalPort();
    } else {
        CommandCenterLog.info("[CommandCenter] chooses port fail, http command center will not work");
    }

    if (!success) {
        port = PORT_UNINITIALIZED;
    }

    TransportConfig.setRuntimePort(port);
    executor.shutdown();
}
  • 方式二,在sentinel-transport-netty-http模块中,通过netty暴露,核心类是NettyHttpCommandCenter,核心代码如下:
@Override
public void start() throws Exception {
    pool.submit(new Runnable() {
        @Override
        public void run() {
            try {
                server.start();
            } catch (Exception ex) {
                RecordLog.info("Start netty server error", ex);
                ex.printStackTrace();
                System.exit(-1);
            }
        }
    });
}

内部通过SPI机制加载,引用了哪个模块就会使用哪种机制。

节点发现

dashboard是如何获取节点信息并将其展示在界面上的?核心原理还是在sentinel-transport模块中,不管是在sentinel-transport-simple-http还是sentinel-transport-netty-http中,都会向dashboard发送心跳上报当前节点信息,请求地址即:

dashboardIp:port/registry/machine,这里代表 localhost:8080/registry/machine

dashboar收到请求后会将节点信息保存到内存中。

有关于这一部分,sentinel-transport-simple-http模块中的核心类是SimpleHttpHeartbeatSender;sentinel-transport-netty-http模块中的核心类是HttpHeartbeatSender;

dashboard相关的逻辑如下

public Result<?> receiveHeartBeat(String app, Long version, String v, String hostname, String ip, Integer port) {
    if (app == null) {
        app = MachineDiscovery.UNKNOWN_APP_NAME;
    }
    if (ip == null) {
        return Result.ofFail(-1, "ip can't be null");
    }
    if (port == null) {
        return Result.ofFail(-1, "port can't be null");
    }
    if (port == -1) {
        logger.info("Receive heartbeat from " + ip + " but port not set yet");
        return Result.ofFail(-1, "your port not set yet");
    }
    String sentinelVersion = StringUtil.isEmpty(v) ? "unknown" : v;
    long timestamp = version == null ? System.currentTimeMillis() : version;
    try {
        MachineInfo machineInfo = new MachineInfo();
        machineInfo.setApp(app);
        machineInfo.setHostname(hostname);
        machineInfo.setIp(ip);
        machineInfo.setPort(port);
        machineInfo.setTimestamp(new Date(timestamp));
        machineInfo.setVersion(sentinelVersion);
        appManagement.addMachine(machineInfo);
        return Result.ofSuccessMsg("success");
    } catch (Exception e) {
        logger.error("Receive heartbeat error", e);
        return Result.ofFail(-1, e.getMessage());
    }
}

所以,整个过程看起来是这样子的:

配置项

  • NameSpace NameSpace主要是用于区分不同的应用,其实在嵌入式的模式下作用不大,嵌入式模式下一般是一种对等结构,这时候NameSpace一般就是一个,即:应用名。只有在独立模式下才能体现它的作用:区分不同的应用。
// 如果不配置默认default
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("cluster-" + appId));
  • Supplier 主要作用就是就是根据NameSpace找到一个DynamicSentinelProperty,其实在嵌入式模式下,一般也就是写死一个DynamicSentinelProperty,因为这时候的NameSpace也就只有一个
// 集群限流规则配置,根据namespace动态生成Supplier,其实子
ClusterFlowRuleManager.setPropertySupplier(dataSource.getClusterFlowSupplier());
  • ServerTransportProperty 作用比较大,针对server端,会根据ServerTransportProperty中的信息在server端通过netty开启一个端口,用于和client交互
// 配置ServerTransportConfig:port、idleSeconds
ClusterServerConfigManager.registerServerTransportProperty(dataSource.getServerTransportConfigProperty());
  • ClientConfigProperty client端的相关配置,其实只有一个属性:请求server端的超时时间(requestTimeout)
// 为client设置requestTimeout
ClusterClientConfigManager.registerClientConfigProperty(dataSource.getClusterClientConfigProperty());
  • ServerAssignProperty client端的相关配置,里面保存的是server端的相关信息:server的host和port
// 为client设置server的host和port,即serverHost、serverPort
ClusterClientConfigManager.registerServerAssignProperty(dataSource.getClusterClientAssignConfigProperty());
  • ClusterStateManager 在嵌入式模式下,可以通过API来改变client和server的身份,大致逻辑就是:将server中的那个netty服务stop,然后根据新的配置在client开启一个新的netty服务(注意,服务开启成功之后,client就转变成server了)
// 用于设置mode,设置0 代表client, 设置1代表 server
ClusterStateManager.registerProperty(dataSource.getClusterStateProperty());