1. 阿里熔斷限流Sentinel研究
1.1. 功能特點
- 豐富的應用場景:例如秒殺(即突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等
- 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機器秒級數據,甚至 500 台以下規模的集群的匯總運行情況。
- 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
- 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定制邏輯。例如定制規則管理、適配動態數據源等。
開源生態
Sentinel 分為兩個部分:
- 核心庫(Java 客戶端)不依賴任何框架/庫,能夠運行於所有 Java 運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
- 控制台(Dashboard)基於 Spring Boot 開發,打包后可以直接運行,不需要額外的 Tomcat 等應用容器。
1.2. 快速開始
1.2.1. 公網接入
-
根據該步驟,建立demo
-
控制台效果圖
1.3. 手動接入
- 引入pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.6.2</version>
</dependency>
-
定義資源:也就是對哪個資源進行流量控制,現在已經提供了注解形式,所以新的接入直接用注解,
@SentinelResource
- 關於SentinelResource注解,這里列出幾個好用和必填的參數,具體參考這里
1.3.1. apollo接入
由於我的系統使用的是apollo管理配置,所以我用apollo來管理規則,官方也提供了apollo的接入說明,然后我想到需要自動提交規則,而不是自己手動去配,又找到了官方的推送例子,可是該例子還是存在問題的,或者說要運行該例子需要對apollo的開放設置有進一步了解
期間我遇到了401問題,是因為需要apollo授權第三方應用,配置token后才能起效;
之后又遇到400問題,是因為openItemDTO.setDataChangeCreatedBy("apollo");
和namespaceReleaseDTO.setReleasedBy("apollo");
該配置項,例子並不是這么寫的,需要將參數改成apollo才行
下面給出完整配置,結合apollo讀取配置
@Configuration
@ConditionalOnProperty(name = "sentinel.enabled",havingValue = "true")
@Slf4j
public class SentinelAspectConfiguration {
@Autowired
private ApolloOpenApiClient apolloOpenApiClient;
@Autowired(required = false)
private IRuleManage ruleManage;
@Value("${appId:}")
private String appId;
/**
* 已有配置,可直接使用
*/
@Value("${profile:}")
private String env;
/**
* 沒有profile屬性配置,則必須配置env
*/
@Value("${env:pro}")
private String envReal;
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
pushlish();
return new SentinelResourceAspect();
}
private void pushlish(){
List<FlowRule> flowRules = null;
if (ruleManage != null) {
flowRules = ruleManage.getFlowRules();
}
if (appId == null) {
return;
}
String flowDataId = appId+"-flow-rules";
String degradeDataId = appId+"-degrade-rules";
if("".equals(env)){
env = envReal;
}
env = env.toUpperCase();
//代碼級別的規則,初始化加載,可不實現IRuleManage接口
setRules(flowRules, flowDataId);
//流控
flowConfig(flowDataId);
//降級
degradeConfig(degradeDataId);
}
private void degradeConfig(String degradeDataId) {
//讀取
String namespaceName = "application";
String defaultFlowRules = "[]";
ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new ApolloDataSource<>(namespaceName,
degradeDataId, defaultFlowRules, source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
}));
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
//寫入配置
WritableDataSource<List<DegradeRule>> wds = new WritableDataSource<List<DegradeRule>>() {
@Override
public void write(List<DegradeRule> rules) throws Exception {
setRules(rules, degradeDataId);
}
@Override
public void close() throws Exception {
log.info("WritableDataSource DegradeRule close");
}
};
WritableDataSourceRegistry.registerDegradeDataSource(wds);
}
private void flowConfig(String flowDataId) {
//讀取
String namespaceName = "application";
String defaultFlowRules = "[]";
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ApolloDataSource<>(namespaceName,
flowDataId, defaultFlowRules, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
//寫入配置
WritableDataSource<List<FlowRule>> wds = new WritableDataSource<List<FlowRule>>() {
@Override
public void write(List<FlowRule> rules) throws Exception {
setRules(rules, flowDataId);
}
@Override
public void close() throws Exception {
log.info("WritableDataSource FlowRule close");
}
};
WritableDataSourceRegistry.registerFlowDataSource(wds);
}
private void setRules(List rules, String flowDataId) {
if (rules != null && rules.size() > 0) {
// Increase the configuration
OpenItemDTO openItemDTO = new OpenItemDTO();
openItemDTO.setKey(flowDataId);
openItemDTO.setValue(JSON.toJSONString(rules));
openItemDTO.setComment("Program auto-join");
openItemDTO.setDataChangeCreatedBy("apollo");
apolloOpenApiClient.createOrUpdateItem(appId, env, "default", "application", openItemDTO);
// Release configuration
NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
namespaceReleaseDTO.setEmergencyPublish(true);
namespaceReleaseDTO.setReleaseComment("Modify or add configurations");
namespaceReleaseDTO.setReleasedBy("apollo");
namespaceReleaseDTO.setReleaseTitle("Modify or add configurations");
apolloOpenApiClient.publishNamespace(appId, env, "default", "application", namespaceReleaseDTO);
}
}
}
和
@Configuration
@EnableApolloConfig(value = "application", order = 10)
public class AppBaseConfig {
@Value("${apollo.token}")
private String token;
@Value("${apollo.portalUrl}")
private String portalUrl;
@Value("${sentinel.project.name:}")
private String projectName;
@Value("${spring.application.name:}")
private String applicationName;
@Value("${sentinel.console.server:}")
private String consoleServer;
@Value("${sentinel.heartbeatClient:}")
private String heartbeatClient;
@Bean
@ConditionalOnProperty(name = "sentinel.enabled",havingValue = "true")
public ApolloOpenApiClient apolloOpenApiClient() {
System.setProperty(AppNameUtil.APP_NAME, "".equals(projectName) ? applicationName : projectName);
System.setProperty(TransportConfig.CONSOLE_SERVER, consoleServer);
if (!"".equals(heartbeatClient)) {
String[] split = heartbeatClient.split(":");
System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, split[0].trim());
System.setProperty(TransportConfig.SERVER_PORT, split[1].trim());
}
ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder()
.withPortalUrl(portalUrl)
.withToken(token)
.build();
return client;
}
}
1.4. 控制台修改規則apollo寫入
- 這個功能是可以有的,只是官方文檔我沒找到,我直接debug源碼查看哪里可以把apollo寫入加進去,果然發現它是提供了寫入接口的
- 寫入接口為
WritableDataSource
,參考上面的完整代碼
1.5. 控制台
- 下載啟動控制台,下載地址
- 啟動命令
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
登錄名密碼都是sentinel
1.6. 工作原理
1.7. 問題
1.7.1. 將控制台部署在公網,本機啟動連接出現錯誤日志
2019-07-23 14:57:33.256 ERROR 14788 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from <http://172.16.100.141:8721/metric?startTime=1563865044000&endTime=1563865050000&refetch=false> (ConnectionException: Connection refused: no further information)
說明發送了內網地址,導致fetch拉取埋點信息不通
我通過System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, split[0].trim());
設置心跳地址為外網地址解決這個問題
本質上是因為控制台主動通過接口來客戶端拉信息,但若是訪問不通,也是沒轍,所以本地測試部在服務器上的控制台,除非外網映射
1.7.2. 部署上去后發現可以訪問通,且項目注冊進來了,但沒有任何調用信息,且沒有任何規則信息
我的這個問題基礎是因為我部署到docker上的,之后debug源碼,發現控制台調用客戶端的地址是我根本沒配過的,深入后發現如下代碼段
Runnable serverInitTask = new Runnable() {
int port;
{
try {
port = Integer.parseInt(TransportConfig.getPort());
} catch (Exception e) {
port = DEFAULT_PORT;
}
}
@Override
public void run() {
boolean success = false;
ServerSocket serverSocket = getServerSocketFromBasePort(port);
if (serverSocket != null) {
CommandCenterLog.info("[CommandCenter] Begin listening at port " + serverSocket.getLocalPort());
socketReference = serverSocket;
executor.submit(new ServerThread(serverSocket));
success = true;
port = serverSocket.getLocalPort();
} else {
CommandCenterLog.info("[CommandCenter] chooses port fail, http command center will not work");
}
if (!success) {
port = PORT_UNINITIALIZED;
}
TransportConfig.setRuntimePort(port);
executor.shutdown();
}
};
new Thread(serverInitTask).start();
該代碼段的作用是為客戶端在分配一個socketServer,之后的信息交互都是通過該服務提供的端口來提供;
這樣一來客戶端需要額外提供一個端口了,而我的docker只暴露了1個服務端口,所以不可避免的會出現問題,以上是我到目前的思路,正在驗證中
至於端口如何決定,它是用了一個簡單的技巧,若設置了csp.sentinel.api.port
配置項,則會取該配置端口,若沒有設,則是默認端口8719;但如果你用的是官網的啟動方式,那8719應該是被控制台占用了,所以進入小技巧getServerSocketFromBasePort
方法,內容如下
private static ServerSocket getServerSocketFromBasePort(int basePort) {
int tryCount = 0;
while (true) {
try {
ServerSocket server = new ServerSocket(basePort + tryCount / 3, 100);
server.setReuseAddress(true);
return server;
} catch (IOException e) {
tryCount++;
try {
TimeUnit.MILLISECONDS.sleep(30);
} catch (InterruptedException e1) {
break;
}
}
}
return null;
}
它會循環嘗試端口是否被占用,每個端口嘗試三次,若被占用則取下一個+1端口,一直到可用的端口返回;所以如果我們的客戶端應用放到了docker,而開放的端口只有一個,那就獲取不了信息了
這里csp.sentinel.api.port
配置項很容易理解成客戶端的端口地址,因為啟動也不會報錯啥的,會誤讓我們誤會這個參數可以不填,雖然文檔上寫着必填,但本地測試的時候可沒影響-_-||,所有都注意了,這個配置項是必填的
- 還要注意一點,因為是socket連接,兩邊端口要一致,所以docker端口號映射需要一樣