18-Sentinel集群限流的实现(上)

原创 吴就业 161 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/19e0b627b12649b7953843e8c8af6dd2

作者:吴就业
链接:https://wujiuye.com/article/19e0b627b12649b7953843e8c8af6dd2
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

由于请求倾斜的存在,分发到集群中每个节点上的流量不可能均匀,所以单机限流无法实现精确的限制整个集群的整体流量,导致总流量没有到达阈值的情况下一些机器就开始限流。例如服务A部署了3个节点,规则配置限流阈值为200qps,理想情况下集群的限流阈值为600qps,而实际情况可能某个节点先到达200qps,开始限流,而其它节点还只有100qps,此时集群的QPS为400qps。

Sentinel 1.4.0版本开始引入集群限流功能,目的就是实现精确地控制整个集群的QPS。

sentinel-cluster包含以下几个重要模块:

我们回顾一下单机限流的整个工作流程:

实现集群限流是在第(3)步的基础上,如果限流规则的clusterMode配置为集群限流模式,则向集群限流服务端发起远程调用,由集群限流服务端判断是否拒绝当前请求,流量效果控制在集群限流服务端完成。我们结合下图理解。

18-01-cluster-flow-lct

如图所示,当规则配置为集群限流模式时,通过TokenService向集群限流服务端发起请求,根据响应结果决定如何控制当前请求。

集群限流服务端的两种模式

Sentinel支持两种模式启动集群限流服务端,分别是嵌入模式、独立模式,两种模式都有各种的优缺点。

嵌入模式(Embedded)

集群限流服务端作为应用的内置服务同应用一起启动,与应用在同一个进程,可动态的挑选其中一个节点作为集群限流服务端,如下图所示。

18-02-集群限流服务端-01

独立模式(Alone)

集群限流服务端作为一个独立的应用部署,如下图所示。

18-02-集群限流服务端-02

Sentinel集群限流客户端与集群限流服务端通信只保持一个长连接,底层通信基于Netty框架实现,自定义通信协议,并且数据包设计得足够小,网络I/O性能方面降到最低影响。而限流服务端处理一次请求都是访问内存,并且计算量少,响应时间短,对限流客户端性能的影响不大,可以参考Redis一次hget对应用性能的影响。

Sentinel集群限流对限流服务端的可用性要求不高,当限流服务端挂掉时,可回退为本地限流;嵌入模式并未实现类似于主从自动切换的功能,当服务端挂掉时,客户端并不能自动切换为服务端。所以选择哪种限流服务端启动模式更多的是考虑使用嵌入模式是否会严重影响应用的性能,以及应用是否有必要严重依赖集群限流。

如果服务是部署在Kubernetes集群上,使用嵌入模式就可能需要频繁的调整配置,以选择一个节点为集群限流服务端,并且需要调整其它客户端的连接配置,才能让其它客户端连接上服务端。试想一下,当半夜某个节点挂了,而该节点正好是作为集群限流的服务端,Kubernetes新起的POD变成集群限流客户端,此时,所有集群限流客户端都连接不上服务端,也只能退回本地限流。嵌入模式在Kubernetes集群上其弊端可谓表现得淋漓尽致。

集群限流规则

集群限流规则也是FlowRule,当FlowRule的clusterMode配置为true时,表示这个规则是一个集群限流规则。

如果将一个限流规则配置为集群限流规则,那么FlowRule的clusterConfig就必须要配置,该字段的类型为ClusterFlowConfig。

ClusterFlowConfig可配置的字段如下源码所示。

public class ClusterFlowConfig {
    private Long flowId;
    private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;
    private boolean fallbackToLocalWhenFail = true;
    // 当前版本未使用
    private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;
    private int sampleCount = ClusterRuleConstant.DEFAULT_CLUSTER_SAMPLE_COUNT;
    private int windowIntervalMs = RuleConstant.DEFAULT_WINDOW_INTERVAL_MS;
}

当限流规则配置为集群模式时,限流规则的阈值类型(grade)将弃用,而是使用集群限流配置(ClusterFlowConfig)的阈值类型(thresholdType),支持单机均摊和集群总阈值两种集群限流阈值类型:

集群限流规则的动态配置

集群限流规则需要在集群限流客户端配置一份,同时集群限流服务端也需要配置一份,缺一不可。客户端需要取得集群限流规则才会走集群限流模式,而服务端需要取得同样的限流规则,才能正确的回应客户端。为了统一规则配置,我们应当选择动态配置,让集群限流客户端和集群限流服务端去同一数据源取同一份数据。

Sentinel支持使用名称空间(namespace)区分不同应用之间的集群限流规则配置,如服务A的集群限流规则配置和服务B的集群限流规则配置使用名称空间隔离。

前面我们分析了Sentinel动态数据源的实现原理,并且也基于Spring Cloud提供的动态配置功能完成一个动态数据源。为了便于理解,也为了便于测试,我们选择自己实现一个简单的动态数据源(SimpleLocalDataSource),实现根据名称空间加载集群限流规则。

SimpleLocalDataSource继承AbstractDataSource,同时构造方法要求传入名称空间,用于指定一个动态数据源对象只负载加载指定名称空间的集群限流规则。SimpleLocalDataSource实现代码如下所示。

public class SimpleLocalDataSource
        extends AbstractDataSource<String, List<FlowRule>> 
        implements Runnable{
  
    public SimpleLocalDataSource(String namespace) {
        super(new SimpleConverter<String, List<FlowRule>>() {});
        // 模拟Spring容器刷新完成初始化加载一次限流规则
        new Thread(this).start();
    }
  
    @Override
    public String readSource() throws Exception {
        // 获取动态配置,
        return "";
    }
  
    @Override
    public void close() throws Exception {
    }
  
    @Override
    public void run() {
        try {
            // 休眠6秒
            Thread.sleep(6000);
            getProperty().updateValue(loadConfig());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

SimpleLocalDataSource构造方法中启动一个线程,用于实现等待动态数据源对象注册到ClusterFlowRuleManager之后再模拟加载一次规则。由于是测试,所以readSource方法并未实现,我们直接在SimpleConverter这个转换器中虚构一个集群限流规则,代码如下。

public class SimpleConverter extends Converter<String, List<FlowRule>>() {
    @Override
    public List<FlowRule> convert(String source) {
          List<FlowRule> flowRules = new ArrayList<>();
          FlowRule flowRule = new FlowRule();
          flowRule.setCount(200);
          flowRule.setResource("GET:/hello");
          // 集群限流配置
          flowRule.setClusterMode(true);
          ClusterFlowConfig clusterFlowConfig = new ClusterFlowConfig();
          clusterFlowConfig.setFlowId(10000L); // id确保全局唯一
          flowRule.setClusterConfig(clusterFlowConfig);
          flowRules.add(flowRule);
          return flowRules;
    }
}

接下来,我们将使用这个动态数据源实现集群限流客户端和服务端的配置。

集群限流客户端配置

在需要使用集群限流功能的微服务项目中添加sentinel-cluster-client-default的依赖。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-client-default</artifactId>
    <version>${version}</version>
</dependency>

将身份设置为集群限流客户端(CLUSTER_CLIENT),并且注册客户端配置(ClusterClientConfig),代码如下。

@SpringBootApplication
public class WebMvcDemoApplication {
    static {
			// 指定当前身份为 Token Client
			ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
      // 集群限流客户端配置,ClusterClientConfig目前只支持配置请求超时
      ClusterClientConfig clientConfig = new ClusterClientConfig();
      clientConfig.setRequestTimeout(1000);
      ClusterClientConfigManager.applyNewConfig(clientConfig);
    }
}

在Spring 项目中,可通过监听ContextRefreshedEvent事件,在Spring容器启动完成后再初始化创建动态数据源、为FlowRuleManager注册动态数据源的SentinelProperty,代码如下。

@SpringBootApplication
public class WebMvcDemoApplication implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
      // 指定名称空间为serviceA,只加载这个名称空间下的限流规则
      SimpleLocalDataSource ruleSource = new SimpleLocalDataSource("serviceA");
      FlowRuleManager.register2Property(ruleSource.getProperty());
    }
}

最后注册用于连接到集群限流服务端的配置(ClusterClientAssignConfig),指定集群限流服务端的IP和端口,代码如下。

@SpringBootApplication
public class WebMvcDemoApplication {
    static {
			ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
      assignConfig.setServerHost("127.0.0.1");
      assignConfig.setServerPort(11111);
      // 先指定名称空间为serviceA
      ConfigSupplierRegistry.setNamespaceSupplier(()->"serviceA"); 
      ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
    }
}

当ClusterClientConfigManager#applyNewAssignConfig方法被调用时,会触发Sentinel初始化或重新连接到集群限流服务端,所以我们看不到启动集群限流客户端的代码。Sentinel还支持当客户端与服务端意外断开连接时,让客户端不断的重试重连。

注意看,我们在调用ClusterClientConfigManager#applyNewAssignConfig方法之前,先调用了ConfigSupplierRegistry#setNamespaceSupplier方法注册名称空间,这是非常重要的一步。当客户端连接上服务端时,会立即发送一个PING类型的消息给服务端,Sentinel将名称空间携带在PING数据包上传递给服务端,服务端以此获得每个客户端连接的名称空间。

完成以上步骤,集群限流客户端就已经配置完成,但这些步骤都只是完成集群限流客户端的配置,如果集群限流服务端使用嵌入模式启动,那么还需要在同一个项目中添加集群限流服务端的配置。

集群限流服务端配置

如果使用嵌入模式,则可直接在微服务项目中添加sentinel-cluster-server-default的依赖;如果是独立模式,则单独创建一个项目,在独立项目中添加sentinel-cluster-server-default的依赖。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-server-default</artifactId>
    <version>${version}</version>
</dependency>

在独立模式下,需要我们自己手动创建ClusterTokenServer并启动,在启动之前需指定服务监听的端口和连接最大空闲等待时间等配置,代码如下。

public class ClusterServerDemo {

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
        // 配置短裤和连接最大空闲时间
        ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig()
            .setIdleSeconds(600)
            .setPort(11111));
        // 启动服务
        tokenServer.start();
    }
}

接着我们需要为服务端创建用于加载集群限流规则的动态数据源,在创建动态数据源时,需指定数据源只加载哪个名称空间下的限流规则配置,如下代码所示。

ClusterFlowRuleManager.setPropertySupplier(new Function<String, SentinelProperty<List<FlowRule>>>() {
      // ClusterFlowRuleManager会给apply方法返回的SentinelProperty注册监听器
      @Override
      public SentinelProperty<List<FlowRule>> apply(String namespace) {
            // 创建动态数据源
            SimpleLocalDataSource source = new SimpleLocalDataSource(namespace);
            // 返回数据源的SentinelProperty
            return source.getProperty();
      }
});

从代码中可以看出,我们注册的是一个Java8的Function,这个Function的apply方法将在我们注册名称空间时触发调用。

现在,我们为集群限流服务端注册名称空间,以触发动态数据源的创建,从而使ClusterFlowRuleManager拿到动态数据源的SentinelProperty,将规则缓存更新监听器注册到动态数据源的SentinelProperty上。注册名称空间代码如下。

// 多个应用应该对应多个名称空间,应用之间通过名称空间互相隔离
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("serviceA"));

名称空间可以有多个,如果存在多个名称空间,则会多次调用ClusterFlowRuleManager#setPropertySupplier注册的Function对象的apply方法创建多个动态数据源。多个应用应该对应多个名称空间,应用之间通过名称空间互相隔离。

由于我们在SimpleLocalDataSource的构造方法中创建一个线程并延迟执行,当以上步骤完成后,也就是SimpleLocalDataSource的延时任务执行时,SimpleLocalDataSource会加载一次限流规则,并调用SentinelProperty#updateValue方法通知ClusterFlowRuleManager更新限流规则缓存。

在实现项目中,我们自定义的动态数据源可通过定时拉取方式从配置中心拉取规则,也可以结合Spring Cloud动态配置使用,通过监听动态配置改变事件,获取最新的规则配置,而规则的初始化加载,可通过监听Spring容器刷新完成事件实现。

动态配置为嵌入模式提供的支持

如果是嵌入模式启动,除非一开始我们就清楚的知道应用会部署多少个节点,这些节点的IP是什么,并且不会改变,否则我们无法使用静态配置的方式去指定某个节点的角色。Sentinel为此提供了支持动态改变某个节点角色的API,使用方式如下。

http://<节点ip>:<节点port>/setClusterMode?mode={state}

其中{state}为0代表集群限流客户端,为1代表集群限流服务端。当一个新的节点被选为集群限流服务端后,旧的集群限流服务端节点也应该变为集群限流客户端,并且其它的节点都需要作出改变以连接到这个新的集群限流服务端。

Sentinel提供动态修改ClusterClientAssignConfig、ClusterClientConfig的API,使用方式如下。

http://<节点ip>:<节点port>/cluster/client/modifyConfig?data={body}

其中{body}要求是json格式的字符串,支持的参数配置如下:

除使用API可动态修改节点角色、客户端连接到服务端的配置之外,Sentinel还支持通过动态配置方式修改,但无论使用哪种方式修改都有一个弊端:需要人工手动配置。

虽然未能实现自动切换,但不得不称赞的是,Sentinel将动态数据源与SentinelProperty结合使用,通过SentinelProperty实现的观察者模式,提供更为灵活的嵌入模式集群限流角色转换功能,支持以动态修改配置的方式去重置嵌入模式集群中任一节点的集群限流角色。

ClusterClientAssignConfig(客户端连接服务端配置)、ServerTransportConfig(服务端传输层配置:监听端口、连接最大空闲时间)、ClusterClientConfig(客户端配置:请求超时)、ClusterState(节点状态:集群限流客户端、集群限流服务端)都支持使用动态数据源方式配置。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。