文章目录
嵌入式集群限流的不足之处
在之前一篇嵌入式模式实现Sentinel集群限流的方案中已经介绍过Sentinel集群限流的大致实现思路
文章路径如下
Sentinel-集成Apollo配置中心实现嵌入式集群流控
对Sentinel集群限流有疑问的可以先看下这篇文章属性几个概念,最主要的是如下图片的理解
但是嵌入式模式的实现方式有明显几个缺点
- 每个应用集群内部需要单独配置自己集群内部的TokenServer(在Sentinel的控制台中指定),是隐形的人工成本
- 假设集群内部该台TokenServer依附的应用服务宕机,则TokenServer不可用,不满足应用高可用的需求
- 假设应用中某接口(配置了集群限流规则)的调用量特别大,每个接口都去请求TokenServer,无形中增加了应用服务的压力(是个安全隐患,可能压垮应用服务),限流服务本不应该对业务产生影响
实现独立部署TokenServer的关键
正好Sentinel中也有提供独立模式的TokenServer启动类,因此前端时间琢磨了下如何将TokenServer剥离到应用之外,即独立集群模式部署
分为几个方面来介绍TokenServer独立模式部署时的实现思路
Apollo规则存储
由于配置中心采用的是Apollo,Apollo相关OPEN API不懂的这里不做具体介绍,可以看Apollo官方介绍。这里只介绍规则是如何存储的在Apollo当中的,其他配置中心思路类似,但是相关Apollo的坑我会做说明,防止大家踩坑
在这次针对TokenServer独立模式部署的探索、修改中,我发现把所有业务应用的规则放在一个AppId(Apollo中的一个应用)中来管理是比较方便的,什么意思呢?直接上图说明,我把所有业务应用的规则都放到一个名为Sentinel的AppId下管理,不同应用的规则放到不同nameSpace下
几个关键点如下
一、不同业务下的限流规则如何存储?
-
Apollo中,我新建了个一个名为Sentinel的AppId来存储所有业务应用的规则,不同的业务用不同的NameSpace来区分,图中的sentinel-test就是一个演示测试用的业务应用名称(这个名称和注册到Sentinel控制台的保持一致,方便控制台写入规则到Apollo)。
-
具体业务下的限流规则以sentinel-xxxx-rules为key写入该nameSpace,xxxx可能是普通限流规则(flow),或者降级规则(degrade)等等
-
然后nameSpace一定要设置成public的,这算是Apollo里的一个坑,因为业务应用首先要监听自己的业务规则(比如sentinel-test这个应用要监听appId=sentinel-test的业务appId下的规则),同时也需要监听Sentinel这个appId下名为sentinel-test的nameSpace下的限流规则变化,但是实际应用代码中,只能指定一个appId,所以只能指定Sentinel限流规则这个appId下的规则所在的nameSpace为public,这样就能被业务应用所监听到
二、tokenServer这个nameSpace下存储什么东西?
- 首先要存储的是nameSpaceSet,什么是nameSpaceSet?在我的理解中,Sentinel中每个业务就是一个nameSpace,有的应用需要集群限流,有的业务不需要集群限流。假设某个业务中是需求集群限流的,那么TokenServer就需要监听这个业务(这个nameSpace)下的限流规则变化,因此 Sentinel中nameSpaceSet即标识TokenServer端需要监听的业务应用有哪些。在我的实现中,Apollo中的nameSpace是业务应用名称,同时也是Sentinel中的nameSpace名称,如下图设置时即标识TokenServer端目前监听了sentinel-test这个nameSpace下的所有集群限流规则变化
- 第二个要存储的是token-server-cluster-map,我这的token-server-cluster-map又是存储的什么鬼?我这存储的是TokenServer应用的部署信息,即TokenServer的ip、端口信息,存储格式如下(假设我部署了2个TokenServer,分别部署在192.168.0.1、192.168.0.2上)
[
{
"ip": "192.168.0.1",
//最大允许qps
"maxAllowedQps": 20000,
"port": 18730
}
]
同时我默认这个数组中的第一个TokenServer配置信息即为客户端需要连接的TokenServer地址信息
客户端也会监听这个nameSpace下的这个tokenServer地址的规则配置,TokenServer地址变化时(比如宕机后重新选举出了一个TokenServer,并且ip、端口有了修改),则客户端动态重新连接这个新的TokenServer
为什么我只指定一个TokenServer?
考虑到不同应用集群连接不同TokenServer实现的复杂度(因为要考虑TokenServer故障切换及Qps数据同步的问题),在我的公司应用中,需求集群限流的场景暂时不多,因此我暂定:所有的业务客户端都只连接一个TokenServer(这个TokenServer是通过Master选举出来的),否则怕出现集群限流不生效的场景
关于TokenServer的Master选举实现,在下面做具体介绍
客户端设计与实现
客户端的实现中,有如下几个关键处
- 监听业务应用的限流规则变化(即监听配置中心中的限流规则变化),这个不多说了,之前文章介绍过,不懂的去翻一翻,或者看下官网针对不同配置中心的适配
- 设置业务端状态为集群限流客户端
每个接入了Sentinel的业务应用,其角色要么是TokenClient,要么是TokenServer,但在TokenServer独立部署时,业务端只能是TokenClient,故直接写死客户端角色为TokenClient(我的代码实现中是根据nameSpaceSet来设置的,本质上差不多)
ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
-
监听TokenServer地址的变化
Apollo规则存储时介绍了,通过Master选举出来的TokenServer地址信息存储在NameSpace为token-server中,key为token-server-cluster-map,因此客户端需要监听这个配置,客户端的关键类为ClusterClientConfigManager.registerServerAssignProperty
,这个方法是用来指定客户端连接的TokenServer地址的直接上代码:
appId即为上图中的token-server(别忘了指定为public的,不然监听不到)
ApolloConfigUtil.getTokenServerClusterMapDataId()即为token-server-cluster-map
private void initClientServerAssignProperty(String appId) {
ReadableDataSource<String, ClusterClientAssignConfig> clientAssignDs = new ApolloDataSource<>(appId,
ApolloConfigUtil.getTokenServerClusterMapDataId(), defaultRules, new ClusterAssignConfigParser());
ClusterClientConfigManager.registerServerAssignProperty(clientAssignDs.getProperty());
}
关键点就是如何从规则中解析出地址信息,我的实现类为ClusterAssignConfigParser
,看下代码
public class ClusterAssignConfigParser implements Converter<String, ClusterClientAssignConfig> {
@Override
public ClusterClientAssignConfig convert(String source) {
if (source == null) {
return null;
}
RecordLog.info("[ClusterClientAssignConfigParser] Get data: " + source);
//转换成对象List
List<ClusterGroupEntity> groupList = JSON.parseObject(source, new TypeReference<List<ClusterGroupEntity>>() {
});
if (groupList == null || groupList.isEmpty()) {
return null;
}
return extractClientAssignment(groupList);
}
private ClusterClientAssignConfig extractClientAssignment(List<ClusterGroupEntity> groupList) {
//获取第一个配置的TokenServer地址信息,解析出ip,端口
ClusterGroupEntity group = groupList.get(0);
String ip = group.getIp();
Integer port = group.getPort();
return new ClusterClientAssignConfig(ip, port);
}
}
同时,贴上我的客户端接入Apollo的实现,本身是个Starter的实现,但是涉及到一些配置相关信息,就不全部贴上来了,只贴出关键部分代码。
/**
* @author chenyin
*/
public class ApolloInitFunc implements InitFunc {
private String tokenServerNameSpace = ApolloConfigUtil.getTokenServerNamespaceName();
private String defaultRules = "[]";
@Override
public void init() throws Exception {
initClientRules();
initClusterConfig();
}
/**
* 初始化普通限流规则
*/
private void initClientRules() {
String appId = System.getProperty(AppNameUtil.APP_NAME);
//流控规则
registerFlowRuleProperty(appId);
//系统规则
registerSystemRuleProperty(appId);
//热点规则
registerParamFlowRuleProperty(appId);
//降级规则
registerDegradeRuleProperty(appId);
//授权规则
registerAuthorityRuleProperty(appId);
}
/**
* 初始化集群限流规则
*/
private void initClusterConfig() {
String tokenServerNameSpaceId = ApolloConfigUtil.getTokenServerNamespaceName();
//等待transport端口分配完毕
while (TransportConfig.getRuntimePort(