(因网上大多数案例都是从官网抄的,看的特别费劲,所以学习完了以后特意整理了一个自己的案例,介绍和理论的东西还是要从官网抄,描述的很好,很精辟)
为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
集群流控中共有两种身份:
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。
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
相似。
这儿我就不抄了,我是这样理解的,集群流控分为客户端和服务端,一旦服务端挂了,客户端会自动退回本地模式,所以我们会看到客户端和服务端都会加载流控规则。
在集群流控的场景下,我们推荐使用动态规则源来动态地管理规则。
对于客户端,我们可以按照原有的方式来向 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 生成动态规则源并进行自动监听,并删除旧的不需要的规则源。
!!!!相信你看到这儿,又懵了。!!!! 没关系,看了下面就不懵了
客户端都干了什么呢,我们一一分解下。
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;
}
}
以下为客户端的配置,均持久化在nacos中。
配置的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
}
}
]
配置的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
}
]
其实配的就是客户端与服务端的请求超时时间,配置的dataId为:cluster-client,可参考代码阅读。
配置如下:
{
"requestTimeout":5000
}
配置服务端的ip和端口,方便客户端与服务端通过netty建立长连接,配置的dataId为:token-server-config,可参考代码阅读。
配置如下:
{
"serverHost":"127.0.0.1",
"serverPort":8071
}
此处的8071端口并不是服务端应用的端口,上面也有提到,客户端和服务端的通信是通过netty框架进行长连接的,所以这是netty的端口。稍后服务端会进行配置。
此刻,客户端的配置基本上就完了,记得使用的时候,先启动服务端,毕竟客户端需要和netty进行长连接的。
此处附上@Value注解用到的yml配置,大概率不用看,毕竟代码里@Value注解都有默认值
服务端做的事情,我们也一个一个分解下,官网的东西看着头大,不好理解。
namespace集合重点说明下:
集群流控规则的dataId格式为:${namespace}-sentinel-flow
集群热点流控规则的dataId格式为:${namespace}-sentinel-param-flow
其中的${namespace}在动态加载规则的时候,会使用namespace集合中的元素进行替换,以循环遍历的方式动态加载。
例如:新的应用需要加流控规则,只要新应用的流控规则的dataId符合格式,再往namespace集合里补上一个namespace即可。例如OA系统,流控规则的dataId为“oa-sentinel-flow”,namespace集合中再添加一个["warehouse","oa"]即可。
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;
}
}
此处的配置均为服务端(token server)的相关配置。配置均持久化在nacos中。
dataId为:namespace-set,可参考代码阅读
[
"warehouse"
]
流控规则的dataId为:warehouse--sentinel-flow,可参考代码阅读
[
{
"resource":"getGoods",
"limitApp":"default",
"grade":1,
"count":1,
"strategy":0,
"controlBehavior":0,
"clusterMode":true,
"clusterConfig":{
"flowId":111,
"thresholdType":0
}
}
]
流控规则的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
}
}
]
dataId为:sentinel-cluster-server,可参考代码阅读。
{
"port":8071,
"idleSeconds":600
}
此处的port为服务端(token server)netty的端口,用于和客户端进行长连接。
此处应设置为服务端,代码如下:
/**
* 指定当前服务的角色,此处为Token Server
*/
public void initRole(){
ClusterStateManager.applyState(ClusterStateManager.CLUSTER_SERVER);
}
此处附上@Value注解用到的yml配置,大概率不用看,毕竟代码里@Value注解都有默认值
注意,按照官方要求:初始化的类应实现com.alibaba.csp.sentinel.init.InitFunc接口,在resources目录下建一个com.alibaba.csp.sentinel.init.InitFunc文件,文件内容为实现类的全路径。
但我个人想把一些配置放到yml文件里,需要用到spring注解,所以就使用了spring的组件注解。此处如有错误,望评论区告知,谢谢!
文中所说的客户端和服务端是分开来写的,按照官方的说法就是服务端为独立模式,除了独立模式,还有一种为嵌入式模式。其实就是把上述客户端的代码和服务端的代码放在一起。
不同之处在于,假如集群了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
获取当前服务的ip和运行时端口(格式为:ip@port),若和json中的serverId一致,则认为当前服务为服务端。否则循环遍历clientSet数组,判断是否有匹配的客户端,如有,则为客户端。若两者皆无,还有一个状态叫未开始,变量如下图:
获取当前服务的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));
}
作为客户端,肯定是要连接服务端的,服务端的信息直接获取json中的ip和port字段即可。