1.環境準備
1)代碼準備
筆者目前是基於Sentinel-release-1.7的源碼進行測試的,在原sentinel-demo-cluster的基礎上,適配筆者本地的環境進行修改的。源碼地址如下:https://github.com/alibaba/Sentinel/tree/release-1.7
2)Nacos環境準備
由於Sentinel集羣測試是基於Nacos配置中心的,所以需要在本地啓動一個Nacos服務,這裏就不再贅述Nacos的搭建。可參考:https://github.com/alibaba/nacos
3)Sentinel控制檯準備
由於我們需要Sentinel控制檯去觀察我們的集羣流控信息,所以需要提前把這個控制檯準備好。
這個啓動方式官網有介紹,筆者不再贅述
2.基礎概念
* token server:集羣流控服務端,用來處理token client的請求,根據集羣規則來判斷是否允許通過
* token client:集羣流控客戶端,向token server發送請求,詢問是否允許該請求通過,token server返回client結果,決定是否限流
* 集羣模式支持的規則:限流、熱點
* 規則閾值計算方式:
集羣總體模式(限定集羣內某資源的所有qps不超過此閾值);
單機均攤模式(配置的閾值爲單機能夠承受的限額,集羣總閾值=單機閾值*機器數量)
* token server部署方式
獨立部署(單獨啓動一個token server服務來處理client的請求)
嵌入模式(將token server與應用一起部署)
3.client-server交互原理及必備項
原理:類似於單機限流,單機限流統計的qps在每個實例中單獨統計,集羣限流是有一個專門的實例(token server)來進行qps統計。其他實例(token client)在處理真正的業務之前會先向token server發送一個請求,如果server返回一個token,則說明集羣qps未達到閾值,則可以繼續處理業務,否則拋錯。
token server配置項:
* namespace(爲token server的一個抽象概念,代表一個應用/服務,我們可以主動指定namespace,不指定的話就是${project.name})
* ServerTransportConfig(指定了server服務的port和idleSeconds)
* FlowRule/ParamFlowRule(集羣限流規則或參數限流規則)
* 表明當前服務爲token server(通過ClusterStateManager.registerProperty()方法來確認)
token client配置項:
* ClusterClientAssignConfig配置(指定token server的host和port)
* ClusterClientConfig配置(指定requestTimeout時間)
* 加載client降級處理FlowRule(在連接token server失敗時降級方案,使用本地FlowRule方案)
* 表明當前服務爲token client
4.Nacos準備
爲何要準備這個?筆者打算使用Nacos當做配置中心,然後上述所有需要的配置項都在Nacos直接加載即可,下面展示下需要提前在Nacos做的配置項:
1)先根據項目名clusterDemo來創建命名服務,如下即可
2)在命名空間clusterDemo的配置列表中添加配置
* 集羣FlowRule規則 DataId:clusterDemo-flow-rules,配置內容如下:
[
{
"resource" : "cluster-resource",
"grade" : 1,
"count" : 10,
"clusterMode" : true,
"clusterConfig" : {
"flowId" : 111,
"thresholdType" : 1,
"fallbackToLocalWhenFail" : true
}
}
]
* ClusterClientConfig配置 DataId:clusterDemo-cluster-client-config,內容如下:
{"requestTimeout":3000}
* 用於指定server和client的host:port信息 DataId:clusterDemo-cluster-map(後續會在代碼中解析這個json,獲獲取server和port,並確認當前應用身份[client/server]),內容如下:
[{
"clientSet": ["169.254.207.96@8729", "169.254.207.96@8727"],// client的集合
"ip": "169.254.207.96",// server ip
"machineId": "169.254.207.96@8720",// server ip:port
"port": 7717// server所在服務的啓動port
}]
結果如下:
5.嵌入式集羣模式
1)主代碼
根據原來的DemoClusterInitFunc.java 創建的,代碼如下
package com.alibaba.csp.sentinel.demo.cluster.init;
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.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.demo.cluster.DemoConstants;
import com.alibaba.csp.sentinel.demo.cluster.entity.ClusterGroupEntity;
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.csp.sentinel.transport.config.TransportConfig;
import com.alibaba.csp.sentinel.util.AppNameUtil;
import com.alibaba.csp.sentinel.util.HostNameUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.alibaba.nacos.api.PropertyKeyConst;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
/**
* 簡單版本
* @author Eric Zhao
*/
public class DemoClusterInitFuncSimple implements InitFunc {
private static final String APP_NAME = AppNameUtil.getAppName();
private final String remoteAddress = "localhost";
// 這裏需要注意的是,我們使用Nacos指定namespace後,這裏獲取的實際不是namespace的名稱clusterDemo,
// 而是其對應的一串ID,大家可以從各自的Nacos上找到
private final String nacosNamespace = "11f4068f-d5e7-4c97-af73-b9b1b037f5bd";// clusterDemo
private final String groupId = "DEFAULT_GROUP";
private Properties properties = new Properties();
private final String flowDataId = APP_NAME + DemoConstants.FLOW_POSTFIX;
private final String paramDataId = APP_NAME + DemoConstants.PARAM_FLOW_POSTFIX;
private final String configDataId = APP_NAME + "-cluster-client-config";
private final String clusterMapDataId = APP_NAME + DemoConstants.CLUSTER_MAP_POSTFIX;
@Override
public void init() throws Exception {
// 使用namespace的方式加載Nacos配置
properties.put(PropertyKeyConst.SERVER_ADDR, remoteAddress);
properties.put(PropertyKeyConst.NAMESPACE, nacosNamespace);
// client:加載FlowRule(降級規則)
initDynamicRuleProperty();
// client:加載ClusterClientConfig(requestTimeout)
initClientConfigProperty();
// client:加載ClusterClientAssignConfig(serverHost、serverPort)
initClientServerAssignProperty();
// server:加載集羣規則,namespace下對應的FlowRule
registerClusterRuleSupplier();
// server:從assignMap中獲取ServerTransportConfig(port、idleSeconds)
initServerTransportConfigProperty();
// 根據我們的clusterDemo-cluster-map配置,設置當前應用狀態(CLIENT/SERVER/NOT_STARTED)
initStateProperty();
}
// 這個最簡單,本地加載降級規則,沒啥好說的
private void initDynamicRuleProperty() {
ReadableDataSource<String, List<FlowRule>> ruleSource = new NacosDataSource<>(properties, groupId,
flowDataId, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(ruleSource.getProperty());
}
// client端加載requestTimeout配置
private void initClientConfigProperty() {
ReadableDataSource<String, ClusterClientConfig> clientConfigDs = new NacosDataSource<>(properties, groupId,
configDataId, source -> JSON.parseObject(source, new TypeReference<ClusterClientConfig>() {}));
ClusterClientConfigManager.registerClientConfigProperty(clientConfigDs.getProperty());
}
// server端加載port
private void initServerTransportConfigProperty() {
ReadableDataSource<String, ServerTransportConfig> serverTransportDs = new NacosDataSource<>(properties, groupId,
clusterMapDataId, source -> {
List<ClusterGroupEntity> groupList = JSON.parseObject(source, new TypeReference<List<ClusterGroupEntity>>() {});
return Optional.ofNullable(groupList)
// 主要在這裏,通過clusterDemo-cluster-map配置的值中的machineID來比對當前應用IP:port是否符合,符合則代表是server端
// 獲取配置中的port值
.flatMap(this::extractServerTransportConfig)
.orElse(null);
});
ClusterServerConfigManager.registerServerTransportProperty(serverTransportDs.getProperty());
}
// 這個是最關鍵的,根據namespace來動態從Nacos中獲取FlowRule
// namespace可以主動加載,通過代碼ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton(APP_NAME));
// 也可以不寫,在啓動項中添加project.name=xxx,則namespace默認取該配置項值
private void registerClusterRuleSupplier() {
// Register cluster flow rule property supplier which creates data source by namespace.
// Flow rule dataId format: ${namespace}-flow-rules
ClusterFlowRuleManager.setPropertySupplier(namespace -> {
ReadableDataSource<String, List<FlowRule>> ds = new NacosDataSource<>(properties, groupId,
namespace + DemoConstants.FLOW_POSTFIX, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
return ds.getProperty();
});
}
// 這裏主要是通過map配置項中的clientSet,比對當前應用的ip:port來確認當前是否client端,如果是,則設置serverIp:serverPort爲配置中的ip:port
private void initClientServerAssignProperty() {
// Cluster map format:
// [{"clientSet":["169.254.207.96@8729","169.254.207.96@8727"],"ip":"169.254.207.96","machineId":"169.254.207.96@8720","port":7717}]
// machineId: <ip@commandPort>, commandPort for port exposed to Sentinel dashboard (transport module)
ReadableDataSource<String, ClusterClientAssignConfig> clientAssignDs = new NacosDataSource<>(properties, groupId,
clusterMapDataId, source -> {
List<ClusterGroupEntity> groupList = JSON.parseObject(source, new TypeReference<List<ClusterGroupEntity>>() {});
return Optional.ofNullable(groupList)
// 主要在這裏
.flatMap(this::extractClientAssignment)
.orElse(null);
});
ClusterClientConfigManager.registerServerAssignProperty(clientAssignDs.getProperty());
}
// 這裏同樣很關鍵,通過map配置中提前設定好的clientSet,machineID來確定當前應用是server還是client
private void initStateProperty() {
// Cluster map format:
// [{"clientSet":["169.254.207.96@8729","169.254.207.96@8727"],"ip":"169.254.207.96","machineId":"169.254.207.96@8720","port":7717}]
// machineId: <ip@commandPort>, commandPort for port exposed to Sentinel dashboard (transport module)
ReadableDataSource<String, Integer> clusterModeDs = new NacosDataSource<>(properties, groupId,
clusterMapDataId, source -> {
List<ClusterGroupEntity> groupList = JSON.parseObject(source, new TypeReference<List<ClusterGroupEntity>>() {});
return Optional.ofNullable(groupList)
// 主要在這裏
.map(this::extractMode)
.orElse(ClusterStateManager.CLUSTER_NOT_STARTED);
});
ClusterStateManager.registerProperty(clusterModeDs.getProperty());
}
private int extractMode(List<ClusterGroupEntity> groupList) {
// If any server group machineId matches current, then it's token server.
if (groupList.stream().anyMatch(this::machineEqual)) {
return ClusterStateManager.CLUSTER_SERVER;
}
// If current machine belongs to any of the token server group, then it's token client.
// Otherwise it's unassigned, should be set to NOT_STARTED.
boolean canBeClient = groupList.stream()
.flatMap(e -> e.getClientSet().stream())
.filter(Objects::nonNull)
.anyMatch(e -> e.equals(getCurrentMachineId()));
return canBeClient ? ClusterStateManager.CLUSTER_CLIENT : ClusterStateManager.CLUSTER_NOT_STARTED;
}
private Optional<ServerTransportConfig> extractServerTransportConfig(List<ClusterGroupEntity> groupList) {
return groupList.stream()
.filter(this::machineEqual)
.findAny()
.map(e -> new ServerTransportConfig().setPort(e.getPort()).setIdleSeconds(600));
}
private Optional<ClusterClientAssignConfig> extractClientAssignment(List<ClusterGroupEntity> groupList) {
if (groupList.stream().anyMatch(this::machineEqual)) {
return Optional.empty();
}
// Build client assign config from the client set of target server group.
for (ClusterGroupEntity group : groupList) {
if (group.getClientSet().contains(getCurrentMachineId())) {
String ip = group.getIp();
Integer port = group.getPort();
return Optional.of(new ClusterClientAssignConfig(ip, port));
}
}
return Optional.empty();
}
private boolean machineEqual(/*@Valid*/ ClusterGroupEntity group) {
return getCurrentMachineId().equals(group.getMachineId());
}
private String getCurrentMachineId() {
// Note: this may not work well for container-based env.
// return HostNameUtil.getIp() + SEPARATOR + TransportConfig.getRuntimePort();
return HostNameUtil.getIp() + SEPARATOR + TransportConfig.getPort();
}
private static final String SEPARATOR = "@";
}
2)啓動應用配置
上述主代碼寫完之後,適用於集羣中的所有應用。接下來我們要去在應用啓動之前加載這個主類。
通過在ClusterDemoApplication.main()方法中加載這個類,代碼如下(當然也可以配置在文件中,通過SPI的方式加載):
@SpringBootApplication
public class ClusterDemoApplication {
public static void main(String[] args) {
try {
// 這裏我們要主動加載DemoClusterInitFuncSimple.init()方法
// 也可以通過SPI的方式加載
new DemoClusterInitFuncSimple().init();
} catch (Exception e) {
e.printStackTrace();
}
SpringApplication.run(ClusterDemoApplication.class, args);
}
}
* 下面就是啓動server,照常啓動SpringBoot項目的方式(也就是執行ClusterDemoApplication.main()方法)即可,啓動配置爲
-Dserver.port=7718 // 應用服務器啓動端口
-Dproject.name=clusterDemo // 應用名稱(也對應着我們在Nacos中配置的namespace)
-Dcsp.sentinel.dashboard.server=localhost:8080 // 指定Sentinel控制檯的地址(本地啓動)
-Dcsp.sentinel.api.port=8720 // 本地啓動 HTTP API Server 的端口號
-Dcsp.sentinel.log.use.pid=true // 日誌文件名中是否加入進程號,用於單機部署多個應用的情況
這裏需要注意的是:
我們的project.name一定要與Nacos中配置的namespace一致,否則會拉取不到配置信息
csp.sentinel.api.port配置的8720端口需要與clusterDemo-cluster-map配置中machineID中的port保持一致,否則無法判斷當前爲server端;
* 啓動client1,啓動配置爲
-Dserver.port=7719 // 應用服務器端口,其他與server基本類似
-Dproject.name=clusterDemo
-Dcsp.sentinel.dashboard.server=localhost:8080
-Dcsp.sentinel.api.port=8729 // 與上述的注意事項一致,這裏的port需要與map配置中的clientSet中的當前ip對應的port保持一致,否則無法判斷當前爲client
-Dcsp.sentinel.log.use.pid=true
* 啓動client2,啓動配置爲
-Dserver.port=7720 // 應用服務器端口,其他與server基本類似
-Dproject.name=clusterDemo // 三個項目的project.name要保持一致
-Dcsp.sentinel.dashboard.server=localhost:8080
-Dcsp.sentinel.api.port=8727 // 與上述的注意事項一致,這裏的port需要與map配置中的clientSet中的當前ip對應的port保持一致,否則無法判斷當前爲client
-Dcsp.sentinel.log.use.pid=true
在這裏,把我們的Nacos中clusterDemo-cluster-map的配置項再回顧一下
[{
"clientSet": ["169.254.207.96@8729", "169.254.207.96@8727"],// 這裏的ip:port與我們client1IP:csp.sentinel.api.port是保持一致的
"ip": "169.254.207.96",// server ip
"machineId": "169.254.207.96@8720",// serverIp:csp.sentinel.api.port
"port": 7717 //server應用服務器port
}]
3)Sentinel控制檯驗證
在啓動上面三個應用後,我們可以在Sentinel控制檯看到以下展示
可以看到項目名爲clusterDemo的機器列表中有我們啓動的三個應用ip和port,需要注意的是這裏的port是我們在應用啓動參數中指定的csp.sentinel.api.port。
在集羣流控頁面中可以看到以下頁面:
在管理信息中如下所示:可以看到token server和token client與我們在之前map配置中指定的是一致的
6.測試驗證
下面我們通過請求server和client的接口來驗證下集羣限流是否生效
1)測試代碼
// 新建類,用於測試
public class HttpRequestDemo {
// 注意這裏的port爲我們應用服務器的啓動port,對應應用啓動參server.port
public static final String requestPath = "http://localhost:7720/hello/jack";
public static final String requestPath2 = "http://localhost:7719/hello/jack";
public static final String requestPath3 = "http://localhost:7718/hello/jack";
private static RestTemplate restTemplate = new RestTemplate();
private static RestTemplate restTemplate2 = new RestTemplate();
private static RestTemplate restTemplate3 = new RestTemplate();
public static void main(String[] args) {
int i = 0;
while (true) {
executeGet();
i++;
if (i % 10 == 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i == 2000) {
break;
}
}
}
// 連續執行get請求
private static void executeGet() {
restTemplate.getForEntity(requestPath, String.class);
restTemplate2.getForEntity(requestPath2, String.class);
restTemplate3.getForEntity(requestPath3, String.class);
}
}
// com.alibaba.csp.sentinel.demo.cluster.app.service.DemoService
@Service
public class DemoService {
// 特別注意下,這裏的value值要修改爲我們定義的resource,也就是我們在Nacos clusterDemo-flow-rules配置項對應的resource
@SentinelResource(value = "cluster-resource", blockHandler = "sayHelloBlockHandler")
public String sayHello(String name) {
return "Hello, " + name;
}
public String sayHelloBlockHandler(String name, BlockException ex) {
// This is the block handler.
ex.printStackTrace();
return String.format("Oops, <%s> blocked by Sentinel", name);
}
}
2)Sentinel控制檯查看限流結果
可以看到通過的QPS基本維持在10左右。
總結:
筆者在參考Sentinel wiki進行集羣限流驗證的時候還是很痛苦的,因爲wiki中給的比較簡略,再看代碼,代碼後來可以慢慢看懂,但是在真正運行的時候卻總是失敗。終於在弄明白了代碼的含義尤其是如何確定應用是token server還是token client的時候才明白,這個demo缺少了運行時的參數指定,所以總也失敗。
還有就是通過頁面去訪問的話,無法體現出集羣限流的功能,所以筆者補了一個用於連續發送請求的demo。用於驗證限流功能。
參考:
https://github.com/alibaba/Sentinel/wiki/%E9%9B%86%E7%BE%A4%E6%B5%81%E6%8E%A7
https://www.jianshu.com/p/bb198c08b418