sentinel-集群流量控制

1.介绍

        (因网上大多数案例都是从官网抄的,看的特别费劲,所以学习完了以后特意整理了一个自己的案例,介绍和理论的东西还是要从官网抄,描述的很好,很精辟)

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

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

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

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

2.模块结构

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

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

注意:集群流控模块要求 JDK 版本最低为 1.7。

3.集群流控规则

3.1 规则

FlowRule 添加了两个字段用于集群限流相关配置:

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

其中 用一个专门的 ClusterFlowConfig 代表集群限流相关配置项,以与现有规则配置项分开:

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

// 阈值模式,默认(0)为单机均摊,1 为全局阈值.
private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;

private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;

// 在 client 连接失败或通信失败时,是否退化到本地的限流模式
private boolean fallbackToLocalWhenFail = true;
  • flowId 代表全局唯一的规则 ID,Sentinel 集群限流服务端通过此 ID 来区分各个规则,因此务必保持全局唯一。一般 flowId 由统一的管控端进行分配,或写入至 DB 时生成。
  • thresholdType 代表集群限流阈值模式。其中单机均摊模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于整个集群的总阈值

ParamFlowRule 热点参数限流相关的集群配置与 FlowRule 相似。

3.2 配置方式

        这儿我就不抄了,我是这样理解的,集群流控分为客户端和服务端,一旦服务端挂了,客户端会自动退回本地模式,所以我们会看到客户端和服务端都会加载流控规则。

        在集群流控的场景下,我们推荐使用动态规则源来动态地管理规则。

        对于客户端,我们可以按照原有的方式来向 FlowRuleManager 和 ParamFlowRuleManager 注册动态规则源,这儿说的就是客户端加载本地规则。

例如:

ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

        下面则是集群流控服务端(也称为token server)需要加载的集群流控规则,与本地规则不同的是,该规则开启了集群,并配置了集群相关配置。对于集群流控 token server,由于集群限流服务端有作用域(namespace)的概念,因此我们需要注册一个自动根据 namespace 生成动态规则源的 PropertySupplier:

// Supplier 类型:接受 namespace,返回生成的动态规则源,类型为 SentinelProperty>
// ClusterFlowRuleManager 针对集群限流规则,ClusterParamFlowRuleManager 针对集群热点规则,配置方式类似
ClusterFlowRuleManager.setPropertySupplier(namespace -> {
    return new SomeDataSource(namespace).getProperty();
});

然后每当集群限流服务端 namespace set 产生变更时,Sentinel 会自动针对新加入的 namespace 生成动态规则源并进行自动监听,并删除旧的不需要的规则源。

!!!!相信你看到这儿,又懵了。!!!! 没关系,看了下面就不懵了

3.3 集群限流客户端

        客户端都干了什么呢,我们一一分解下。

  1. 客户端需要加载本地流控规则,是为了防止token server挂了,客户端可以使用本地规则进行流量控制。        
  2. 配置客户端请求服务端(token server)的超时时间,配置持久化在nacos中。
  3. 配置服务端(token server)的地址
  4. 设置角色,告诉sentinel,当前服务为客户端还是服务端,或者未启动。此处我们肯定设置为客户端

 3.3.1 具体的代码示例。


import com.alibaba.csp.sentinel.cluster.ClusterStateManager;
import com.alibaba.csp.sentinel.cluster.client.config.ClusterClientAssignConfig;
import com.alibaba.csp.sentinel.cluster.client.config.ClusterClientConfig;
import com.alibaba.csp.sentinel.cluster.client.config.ClusterClientConfigManager;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.alibaba.nacos.api.PropertyKeyConst;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;

/**
 * 从nacos中读取配置信息,并加载至sentinel的监听中
 */
@Component
public class ApplicationInitializer{

    @Value("${sentinel.nacos.address:127.0.0.1:8848}")
    private  String nacosAddress;

    @Value("${sentinel.nacos.namespace:zshop-api-sentinel}")
    private  String namespace;

    @Value("${sentinel.nacos.flow.common-group:ZSHOP_SENTINEL_GROUP}")
    private String commonFlowGroup;

    @Value("${sentinel.nacos.flow.flow-dataId:sentinel-flow}")
    private String flowDataId;

    @Value("${sentinel.nacos.flow.param-flow-dataId:sentinel-param-flow}")
    private String paramFlowDataId;

    @Value("${sentinel.nacos.client.group:ZSHOP_CLIENT_GROUP}")
    private String clientGroup;

    @Value("${sentinel.nacos.client.dataId:cluster-client}")
    private String clusterClientDataId ;

    @Value("${sentinel.nacos.client.token-server.dataId:token-server-config}")
    private String tokenServerConfigDataId ;

    @PostConstruct
    public void init() {
        System.out.println("-----------初始化加载Sentinel服务端!!!!");
        initRole(); //初始化角色为客户端
        registerFlow();  //加载本地流量控制规则
        registerParamFlow();  //加载本地热点流控规则
        initClientConfigProperty(); //配置客户端连接服务端的超时时间
        initClientServerAssignProperty(); //配置服务端(token server)的连接,例如:ip、port等
    }


    /**
     * 注册限流规则
     */
    public void registerFlow(){

        ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(this.buildProperties(), commonFlowGroup, flowDataId,
                o -> JSON.parseObject(o, new TypeReference>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    /**
     * 注册热点限流规则
     */
    public void registerParamFlow(){
        ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(this.buildProperties(), commonFlowGroup, paramFlowDataId,
                (Converter) o -> JSON.parseObject(o.toString(), new TypeReference>() {
                }));
        ParamFlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    /**
     * 初始化角色为客户端
     */
    public void initRole(){
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
    }


    /**
     * 客户端与服务端通讯的配置:请求超时时间
     */
    private void initClientConfigProperty() {

        ReadableDataSource clientConfigDs = new NacosDataSource<>(this.buildProperties(), clientGroup,
                clusterClientDataId, source -> JSON.parseObject(source, new TypeReference() {}));
        ClusterClientConfigManager.registerClientConfigProperty(clientConfigDs.getProperty());
    }

    /**
     * 配置token server的连接地址
     */
    private void initClientServerAssignProperty() {

        ReadableDataSource clientAssignDs = new NacosDataSource<>(this.buildProperties(), clientGroup,tokenServerConfigDataId
                , source -> JSON.parseObject(source, new TypeReference(){}));
        ClusterClientConfigManager.registerServerAssignProperty(clientAssignDs.getProperty());
    }



    /**
     * 该方法构造nacos的地址、命名空间、账号、密码等,因为我是匿名的,所以只需要两个地址和命名空间
     * @return
     */
    private Properties buildProperties(){
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosAddress);
        properties.setProperty(PropertyKeyConst.NAMESPACE, namespace);

        return properties;
    }
}

3.3.2 nacos配置

        以下为客户端的配置,均持久化在nacos中。

3.3.2.1. 限流规则配置

        配置的dataId为:sentinel-flow,可参考代码阅读

[
    {
        "resource":"updateNum", 
        "limitApp":"default",  
        "grade":1,            
        "count":1,            
        "strategy":0,         
        "controlBehavior":0,  
        "clusterMode":false
    },
    {
        "resource":"getGoods", 
        "limitApp":"default",  
        "grade":1,             
        "count":1,            
        "strategy":0,         
        "controlBehavior":0,  
        "clusterMode":false
    }
    }

]
3.3.2.2.热点参数限流配置

        配置的dataId为:sentinel-param-flow,可参考代码阅读,与3.3.2.1的流控规则类似。

[
    {
        "resource":"getGoods",   
        "paramIdx":0,           
        "count":50,           
        "grade":1,          
        "durationInSec":2,      
        "controlBehavior":0,    
        "maxQueueingTimeMs":1000,
        "paramFlowItemList":[
            {
               "object":1,        
               "count":10,           
               "classType":"long"                
            }        
        ],
        "clusterMode":false    
    }
]
 3.3.2.3.客户端与服务器的通讯配置

        其实配的就是客户端与服务端的请求超时时间,配置的dataId为:cluster-client,可参考代码阅读。

配置如下:

{
    "requestTimeout":5000
}
3.3.2.4.配置服务端(token server)连接信息

        配置服务端的ip和端口,方便客户端与服务端通过netty建立长连接,配置的dataId为:token-server-config,可参考代码阅读。

配置如下:

{
    "serverHost":"127.0.0.1",
    "serverPort":8071
}

此处的8071端口并不是服务端应用的端口,上面也有提到,客户端和服务端的通信是通过netty框架进行长连接的,所以这是netty的端口。稍后服务端会进行配置。

此刻,客户端的配置基本上就完了,记得使用的时候,先启动服务端,毕竟客户端需要和netty进行长连接的。

此处附上@Value注解用到的yml配置,大概率不用看,毕竟代码里@Value注解都有默认值

sentinel-集群流量控制_第1张图片

3.4 集群限流服务端

        服务端做的事情,我们也一个一个分解下,官网的东西看着头大,不好理解。

  1. 从nacos获取namespace集合数据,并注册。
  2. 从nacos获取集群流控规则并注册
  3. 从nacos加载与客户端的传输配置
  4. 设置角色为服务端

namespace集合重点说明下

        集群流控规则的dataId格式为:${namespace}-sentinel-flow

        集群热点流控规则的dataId格式为:${namespace}-sentinel-param-flow

        其中的${namespace}在动态加载规则的时候,会使用namespace集合中的元素进行替换,以循环遍历的方式动态加载。

        例如:新的应用需要加流控规则,只要新应用的流控规则的dataId符合格式,再往namespace集合里补上一个namespace即可。例如OA系统,流控规则的dataId为“oa-sentinel-flow”,namespace集合中再添加一个["warehouse","oa"]即可。

3.4.1 具体的服务端代码如下

package com.jc.sentinel.init;

import com.alibaba.csp.sentinel.cluster.ClusterStateManager;
import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager;
import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterParamFlowRuleManager;
import com.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager;
import com.alibaba.csp.sentinel.cluster.server.config.ServerTransportConfig;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.alibaba.nacos.api.PropertyKeyConst;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * token server初始化类
 */
@Component
public class SentinelInitializer {

    @Value("${sentinel.nacos.address:127.0.0.1:8848}")
    private  String nacosAddress;

    @Value("${sentinel.nacos.namespace:zshop-api-sentinel}")
    private  String namespace;

    @Value("${sentinel.nacos.flow.common-group:ZSHOP_SENTINEL_GROUP}")
    private String commonFlowGroup;

    @Value("${sentinel.nacos.flow.flow-postfix:-sentinel-flow}")
    private String FLOW_POSTFIX;

    @Value("${sentinel.nacos.flow.param-flow-postfix:-sentinel-param-flow}")
    private String PARAM_FLOW_POSTFIX;

    @Value("${sentinel.nacos.server.group:ZSHOP_SERVER_GROUP}")
    private String serverGroup;

    @Value("${sentinel.nacos.server.namespace-set:namespace-set}")
    private  String namespaceSetDataId ;

    @Value("${sentinel.nacos.server.cluster-server:sentinel-cluster-server}")
    private  String serverTransportDataId;

    @PostConstruct
    public void init(){

        System.out.println("-----------初始化加载TokenServer!!!!");
        registerClusterRuleSupplier();//从nacos注册动态集群规则
        registerServerNamespaceDatasource();//从nacos注册并加载 Namespace Set 数据
        registerServerTransportDataSource();//从nacos注册并加载传输配置
        initRole();             //设置角色为服务端
    }

    /**
     * 加载动态流控规则
     */
    public void registerClusterRuleSupplier(){
        //注册流控规则
        ClusterFlowRuleManager.setPropertySupplier(namespace -> {
            ReadableDataSource> ds = new NacosDataSource<>(this.buildProperties(), commonFlowGroup,
                    namespace + FLOW_POSTFIX,
                    source -> JSON.parseObject(source, new TypeReference>() {}));
            return ds.getProperty();
        });

        //注册热点流控规则
        ClusterParamFlowRuleManager.setPropertySupplier(namespace -> {
            ReadableDataSource> ds = new NacosDataSource<>(this.buildProperties(), commonFlowGroup,
                    namespace + PARAM_FLOW_POSTFIX,
                    source -> JSON.parseObject(source, new TypeReference>() {}));
            return ds.getProperty();
        });
    }

    /**
     * 注册namespace集合
     */
    public void registerServerNamespaceDatasource(){
        // Server namespace set (scope) data source.
        ReadableDataSource> namespaceDs = new NacosDataSource<>(this.buildProperties(), serverGroup,
                namespaceSetDataId, source -> JSON.parseObject(source, new TypeReference>() {}));
        ClusterServerConfigManager.registerNamespaceSetProperty(namespaceDs.getProperty());

    }

    /**
     * 注册服务端传输配置
     */
    public void registerServerTransportDataSource(){

        ReadableDataSource transportConfigDs = new NacosDataSource<>(this.buildProperties(),
                serverGroup, serverTransportDataId,
                source -> JSON.parseObject(source, new TypeReference() {}));
        ClusterServerConfigManager.registerServerTransportProperty(transportConfigDs.getProperty());

    }


    /**
     * 指定当前服务的角色,此处为Token Server
     */
    public void initRole(){
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_SERVER);
    }


    private Properties buildProperties(){
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosAddress);
        properties.setProperty(PropertyKeyConst.NAMESPACE, namespace);

        return properties;
    }
}

3.4.1 nacos配置

        此处的配置均为服务端(token server)的相关配置。配置均持久化在nacos中。

3.4.1.1.namespace集合配置

dataId为:namespace-set,可参考代码阅读

[
  
    "warehouse"
  
]
3.4.1.2.nacos集群流控规则配置

流控规则的dataId为:warehouse--sentinel-flow,可参考代码阅读

[
    
    {
        "resource":"getGoods", 
        "limitApp":"default",  
        "grade":1,             
        "count":1,            
        "strategy":0,         
        "controlBehavior":0,  
        "clusterMode":true,
        "clusterConfig":{
            "flowId":111,
            "thresholdType":0
        }
    }

]
  • flowId属性值,随便配置,但不能重复,记住,它是long型的
  • 其他属性都有默认值,如果想修改,可查看类:com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig
3.4.1.3.nacos集群热点流控规则

流控规则的dataId为:warehouse-sentinel-param-flow,可参考代码阅读

[
    {
        "resource":"getGoods",   
        "paramIdx":0,           
        "count":50,           
        "grade":1,          
        "durationInSec":2,      
        "controlBehavior":0,    
        "maxQueueingTimeMs":1000,
        "paramFlowItemList":[
            {
               "object":1,        
               "count":10,           
               "classType":"long"                
            }        
        ],
        "clusterMode":true,
        "clusterConfig":{
            "flowId":113,
            "thresholdType":0
        }    
    }
]
3.4.1.4.服务端配置

dataId为:sentinel-cluster-server,可参考代码阅读。

{
    "port":8071,
    "idleSeconds":600
}

此处的port为服务端(token server)netty的端口,用于和客户端进行长连接。

3.4.1.5.设置角色

此处应设置为服务端,代码如下:

/**
     * 指定当前服务的角色,此处为Token Server
     */
    public void initRole(){
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_SERVER);
    }

此处附上@Value注解用到的yml配置,大概率不用看,毕竟代码里@Value注解都有默认值

sentinel-集群流量控制_第2张图片

注意,按照官方要求:初始化的类应实现com.alibaba.csp.sentinel.init.InitFunc接口,在resources目录下建一个com.alibaba.csp.sentinel.init.InitFunc文件,文件内容为实现类的全路径。

但我个人想把一些配置放到yml文件里,需要用到spring注解,所以就使用了spring的组件注解。此处如有错误,望评论区告知,谢谢!

4.解读官方文档

        文中所说的客户端和服务端是分开来写的,按照官方的说法就是服务端为独立模式,除了独立模式,还有一种为嵌入式模式。其实就是把上述客户端的代码和服务端的代码放在一起。

        不同之处在于,假如集群了4个服务,那谁是客户端、谁是服务端呢?这个就需要代码进行控制了。首先我们看一个JSON数据结构:

[
    {
        "serverId":"33.44.55.66@8718",  //服务端id,格式为ip@port
        "ip":"33.44.55.66",             //服务端ip
        "port":18730,                   //服务端netty的端口
        "clientSet":[        //客户端集合
            "33.44.55.66@8719","33.44.55.66@8720","33.44.55.66@8721"
        ]
    
    }

]

官网的代码就是用这个数据结构玩的,个人觉得不咋地。我大概说一下官网嵌入式的逻辑,代码我就不提供了,官方有提供代码,地址:sentinel-demo-cluster-embedded

4.1. 当前服务的角色判断

        获取当前服务的ip和运行时端口(格式为:ip@port),若和json中的serverId一致,则认为当前服务为服务端。否则循环遍历clientSet数组,判断是否有匹配的客户端,如有,则为客户端。若两者皆无,还有一个状态叫未开始,变量如下图:

sentinel-集群流量控制_第3张图片

4.2 设置服务端配置(netty的端口号)

        获取当前服务的ip和运行时端口(格式为:ip@port),若和json中的serverId一致,则认为当前服务为服务端,将json中的port组装成“ServerTransportConfig”对象,代码如下图:

 private Optional extractServerTransportConfig(List groupList) {
        return groupList.stream()
            .filter(this::machineEqual)
            .findAny()
            .map(e -> new ServerTransportConfig().setPort(e.getPort()).setIdleSeconds(600));
    }

4.3 设置客户端的连接服务端配置

        作为客户端,肯定是要连接服务端的,服务端的信息直接获取json中的ip和port字段即可。

你可能感兴趣的:(微服务,sentinel,java,服务器)