SpringCloudAlibaba之集群流控

写在前面

本文参照Spring官方文档以及 Sentinel 文档,并实现了代码,在此做下笔记;

学习微服务最好使用容器来搭建,对于正在学习编程的小伙伴推荐买上属于自己的一台服务器,用来练练手,也顺带学习 Docker,这很重要。最近,阿里在搞活动,新用户 1C2G 只要 98 一年,我也比较了很多,还是比较划算的,我自己也入手了,可以点进来看看,对了,最便宜的一款在 【全部必抢爆款】 里面: 阿里云服务器,助力云上云!


为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门统计总的调用量,其他的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控方式。

另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

集群流控中共用两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

模块结构

Sentinel 1.4.0 开始引入集群流控模块,主要包含以下几部分:

  • sentinel-cluster-common-default:公共模块,包含公共接口和实体
  • sentinel-cluster-client-default:默认集群流控 client 模块,使用 Netty 进行通信,提供接口方便序列化协议拓展
  • sentinel-cluster-server-default: 默认集群流控 server 模块,使用 Netty 进行通信,提供接口方便序列化协议拓展;同时提供拓展接口对接规则判断的具体实现(TokenServer),默认实现是复用 sentinel-core 的相关逻辑

我们在引入了 spring-cloud-starter-alibaba-sentinel 模块后,已经引入这三个模块。后文在说到 cluster-server 的独立部署时,可以通过如下的依赖管理,删掉 sentinel-cluster-server-default 模块的依赖:

		<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.csp</groupId>
                    <artifactId>sentinel-cluster-server-default</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

既然提到 Netty 通信,那我们自然能够想到,需要在 client 端进行 server 端的 host 和 port 配置。先不要着急这个,让我们先看下 sentinel-cluster-client-default 下的 command 包,因为该包下的类会提供给我们一些获取系统内部配置的 API。该包下提供了 cluster/client/fetchConfig"cluster/client/modifyConfig。一个是用于抓取配置的,另一个是用于修改配置的。

抓取配置的代码如下:

@CommandMapping(name = "cluster/client/fetchConfig", desc = "get cluster client config")
public class FetchClusterClientConfigHandler implements CommandHandler<String> {

    @Override
    public CommandResponse<String> handle(CommandRequest request) {
        ClusterClientStateEntity stateVO = new ClusterClientStateEntity()
            .setServerHost(ClusterClientConfigManager.getServerHost())
            .setServerPort(ClusterClientConfigManager.getServerPort())
            .setRequestTimeout(ClusterClientConfigManager.getRequestTimeout());
        if (TokenClientProvider.isClientSpiAvailable()) {
            stateVO.setClientState(TokenClientProvider.getClient().getState());
        } else {
            stateVO.setClientState(ClientConstants.CLIENT_STATUS_OFF);
        }
        return CommandResponse.ofSuccess(JSON.toJSONString(stateVO));
    }
}

访问该接口结果如下:
在这里插入图片描述
发现了没,如果我们想修改所连接 server 的 host 和 port ,参考上面的代码便可实现。更详细的过程,我将在后续的集群限流客户端中给出。

之所以有这个探索过程,是因为 spring 的配置文件中并没有发现可以进行 Token server 服务端的配置。

集群流控规则

流控规则对应的 Java 类 是 FlowRule,该类中有两个字段用于集群流控相关配置:

private boolean clusterMode; //标识是否是集群限流配置
private ClusterFlowConfig clusterConfig; //集群限流相关配置项

public class ClusterFlowConfig {

    /**
     * 全局唯一的规则ID,由集群限流管控端分配
     */
    private Long flowId;

    /**
     * 阈值模式,默认(0)为单机均摊,1 为全局阈值
     * 单机均摊下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace 下的连接数来计算总的阈值;而全局模式下配置的阈值等于整个集群的总阈值
     */
    private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;
    private boolean fallbackToLocalWhenFail = true;

    /**
     * 0: normal.
     */
    private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;

    private int sampleCount = ClusterRuleConstant.DEFAULT_CLUSTER_SAMPLE_COUNT;
    /**
     * The time interval length of the statistic sliding window (in milliseconds)
     */
    private int windowIntervalMs = RuleConstant.DEFAULT_WINDOW_INTERVAL_MS;
}

直到现在,我们仍然可以通过控制台来配置集群限流规则。

上面有几个参数并没有给出具体的解释,我在文档中也并未看见。如果要理解它们的作用,只能深挖源码,但现在我还不想这么做。

集群限流客户端

前文也提到过,在 Spring Cloud 中引入 sentinel ,会默认引入 cluster-clientcluster-server。很显然1,一个 Sentinel 既支持做集群限流的客户端也支持做服务端。并且,我们可以通过 ClusterStateManager.applyState 指定模式。也可以通过访问 API 来指定模式:

http://192.168.3.18:8720/setClusterMode?mode=0

通过 API 的方式,很显然,是能够支持动态切换的。那切换为客户端后,如何为其配置服务端连接信息呢?这些肯定也需要动态支持。看了后续,你便明白,整个流程是能够形成闭环的。

我们也可以通过 /getClusterMode 来获取集群状态:
在这里插入图片描述
尽管在这里我们能够看见为 true,但实际上并不可用。这个地方的可用状态判断是根据 client!=null 来判断的,但 client 的内部状态为 0 ,即关闭的状态。

我想这里表明的是能够启用的意思,即通过下面的方法,能够在运行时将其启动。

我们在应用程序的启动类中的 main 方法添加如下代码:

	public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        DynamicSentinelProperty<ClusterClientAssignConfig> serverAssignProperty = new DynamicSentinelProperty<>(new ClusterClientAssignConfig("192.168.3.18", 18730));
        ClusterClientConfigManager.registerServerAssignProperty(serverAssignProperty);
        ClusterClientConfig clusterClientConfig = new ClusterClientConfig();
        clusterClientConfig.setRequestTimeout(5000);
        DynamicSentinelProperty<ClusterClientConfig> clientConfigProperty = new DynamicSentinelProperty<>(clusterClientConfig);
        ClusterClientConfigManager.registerClientConfigProperty(clientConfigProperty);
        ClusterStateManager.applyState(0);
    }

这种代码的形式和调用 /setClusterMode?mode=0cluster/client/modifyConfig API 达到的效果相同。一个是在启动时,一个是在运行时。

因为我们并没有相应的服务端,所以,上面的连接并不会成功。但奇怪的是,在控制台我们看不见连接失败的日志。这是因为日志记录在 System.getProperty("user.home")/logs/csp/ 目录下的具体文件中,源码方面可以参考 LogBase#init() 或者 RecordLog

我们可以在 yaml 配置文件中指定 sentinel 的日志目录,并且应用程序启动时,控制台有打印具体的日志目录(头两行)位置;

这里失败了也不要紧,client 端有重连策略。在我们完成下文的集群限流服务端后,它便会自动连接成功,我们可以通过日志文件观察到这一结果。

集群限流服务端

集群限流服务端,需要引入集群限流 server 相关依赖:sentinel-cluster-server-default。如果是通过 Spring Cloud 引入的 sentinel ,那么该依赖已经有了。

启动方式

Sentinel 集群限流服务端有两种启动方式:

  • 独立模式:即作为独立的 token server 进程启动,独立部署,隔离性好。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。

    这是因为 Token server 支持 namespace ,所以能够给不同的应用集群提供流控服务,但目前只支持全局的流控配置,并不支持 namespace 级别的流控配置。不过源码里已经留有 TODO 了,相信在不久的将来就能够支持。

  • 嵌入模式,即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不加,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。

测试用例 - 独立模式

新建 token-server 项目,导入如下依赖:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <exclusions>
            	<!-- 不依赖 Client -->
                <exclusion>
                    <groupId>com.alibaba.csp</groupId>
                    <artifactId>sentinel-cluster-client-default</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

新建启动类:

public class TokenServerApplication {

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
        //传输配置
        ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig()
                .setIdleSeconds(600).setPort(18270));
        ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("sentinel-app"));
        tokenServer.start();
    }
}

启动上面的类,观察日志文件,我们能看到下面这样的日志:

2020-07-29 14:35:54.660 INFO [NettyTransportServer] Token server started success at port 18730
2020-07-29 14:36:02.694 INFO [ServerEntityCodecProvider] Response entity writer resolved: com.alibaba.csp.sentinel.cluster.server.codec.DefaultResponseEntityWriter
2020-07-29 14:36:02.695 INFO [ServerEntityCodecProvider] Request entity decoder resolved: com.alibaba.csp.sentinel.cluster.server.codec.DefaultRequestEntityDecoder
2020-07-29 14:36:02.697 INFO [ConnectionManager] Client <192.168.3.18:57772> registered with namespace <com.duofei.Application>

可以发现集群限流服务端启动成功,并且,集群限流客户端已经成功注册。

现在,我们可以通过控制台为成功注册的集群限流客户端配置如下规则:
在这里插入图片描述
不过在这之前,有一点需要明白,流控规则现在是推送到具体应用上才生效的。既然 token server 负责流控的校验,那么它也需要流控规则的配置。客户端的流控规则我们已经在上面配置了,那集群限流服务端怎么配置呢?

这里还有一点限制:“客户端流控规则中的集群流控规则的 flowId 应该和集群限流服务端中相同规则的 flowId 一样”。鉴于现在,我们没有使用动态的配置源,所以,我们将直接在集群限流服务端中创建一个和客户端一模一样的流控规则。重新修改集群限流服务端启动类:

public class TokenServerApplication {

    private static final String SPECIAL_NAMESPACE = "sentinel-app";

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
        //传输配置
        ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig()
                .setIdleSeconds(600).setPort(18730));
        ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton(SPECIAL_NAMESPACE));

        ClusterFlowRuleManager.loadRules(SPECIAL_NAMESPACE, buildFlowRuleLikeClient());
        tokenServer.start();
    }
    
	//构建一个和客户端一模一样的配置规则
    private static List<FlowRule> buildFlowRuleLikeClient(){
        FlowRule flowRule = new FlowRule();
        flowRule.setResource("hello");
        flowRule.setGrade(1);
        flowRule.setCount(3.0);
        flowRule.setControlBehavior(0);
        flowRule.setWarmUpPeriodSec(10);
        flowRule.setMaxQueueingTimeMs(500);
        flowRule.setClusterMode(true);
        flowRule.setLimitApp("default");
        //这里的 187 是我在集群限流客户端断点发现的,具体 FlowRuleChecker#passClusterCheck()
        flowRule.setClusterConfig(new ClusterFlowConfig().setFlowId(187L).setThresholdType(1)
                .setFallbackToLocalWhenFail(true).setStrategy(0)
                .setSampleCount(10).setWindowIntervalMs(1000));
        return Collections.singletonList(flowRule);
    }
}

由于失败退化(Token Server不可用会退化到单机限流)是默认的,无法取消勾选。所以,很难判断到底是单机限流生效了还是集群限流生效的。但我们可以通过调试来验证我们的集群限流是否生效,具体见 FlowRuleChecker#passClusterCheck() 类;

Dashboard上的 Token Server

其实,sentinel 控制台有关于集群流控的 Token Server 列表。所以,我们可以将上面的 token-server 应用也连接上 Dashboard 。

token-server 项目新增 application.yml 配置文件:

server:
  port: 18086
spring:
  application:
    name: token-server
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
        heartbeat-interval-ms: 10000
        port: 8721
      eager: true
      filter:
        enabled: false
management:
  endpoint:
    sentinel:
      enabled: true

为启动类 TokenServerApplication.java 新增 @SpringBootApplication 注解,并在 main 方法的第一行添加如下代码:

SpringApplication.run(TokenServerApplication.class, args);

重新启动 token-server ,控制台上,我们得到了这样一个界面:
在这里插入图片描述
但在点击“管理”后,我们没法选取到 sentinel-app 中的 client,所以,我们需要将我们的 token-server 改名为 sentinel-app,然后,控制台能够自动识别是否为集群流控,这样我们就可以通过管理按钮来为集群流控客户端在运行时选择集群流控服务端。

修改后重新启动应用,得到的控制台界面如下:
在这里插入图片描述
上面是我在点击管理以后展示的界面。

属性配置

配置类型有以下几种:

  • namespace set:集群限流服务端的作用域(命名空间),用于指定该 token server 可以服务哪些应用或分组,嵌入模式下可以设置为自己的应用名。集群限流 client 在连接到 token server 后会报上自己的命名空间(默认为 project.name 配置的应用名),token server 会根据上报的命名空间名称统计连接数。

    但是,我发现在获取规则的时候,并没有使用到命名空间,前文也说过,目前还不支持命名空间级别的流控规则配置,这与这里提到的并不冲突。

  • transport config:集群限流服务端通信相关配置,如 server port。

  • flow config:集群限流服务端限流相关配置,如滑动窗口统计时长、格子数目、最大运行总 QPS 等。

我们可以通过 ClusterServerConfigManager 的各个 registerXxxProperty 方法来注册相关的配置源。

总结

文章开头的一句话很好地总结了为什么要使用集群流控,那是因为 集群流控更能够精确地控制整个集群的调用总量,再结合单机限流兜底的话,便可以更好地发挥流量控制的效果

集群流控分为单机均摊和总体阈值。并且在 Token Server 不可用时会退化到单机限流。

集群流控在架构上,分为 Token Client 和 Token Server,每一个服务如果需要使用集群流控,那么它需要是一个 Token Client。在 Spring Cloud 体系中,sentinel 同时引入了 Client 和 Server 包,我们可以切换身份来选择到底是作为 Client 还是 Server。这可以在运行时修改,通过访问 API 的方式。

Server的运行模式又分为嵌入式和独立模式,两种各有优劣。目前我不太理解嵌入式的用法。我是这样思考的,如果一个服务实例作为 Server,那么,这个服务实例是不是就没法支持集群流控了(因为它无法作为 client)。

独立模式就比较好理解,单独启动一个程序来做 Token Server。文档中也有提到 Token Server 的高可用,如何让某个 server 不可用时,自动 failover 到其它机器这点也很重要。

还有个点一直未提到,那就是动态配置源。现在,我们的规则一直都是由 Dashboard 推送到应用程序,应用程序保存在内存中;一旦重启,规则便丢失了。所以,我们需要外部化这个配置,并且支持动态修改,sentinel 能够很好地支持我们实现这样的功能。后续,我将在 Spring Cloud Alibaba 系列 中给出这一文章。

从这里考虑框架的设计就明白可拓展的重要性了,即使框架本身还未实现的功能,你依然能够通过拓展点来实现。

我与风来

如果你觉得我的文章对你有所帮助,欢迎关注我的公众号。赞!我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值