Sentinel 系列教程,現(xiàn)已上傳到 github 和 gitee 中:
- GitHub:https://github.com/all4you/sentinel-tutorial
- Gitee:https://gitee.com/all_4_you/sentinel-tutorial

Sentinel 能夠被大家所認(rèn)可,除了他自身的輕量級,高性能,可擴(kuò)展之外,跟控制臺的好用和易用也有著莫大的關(guān)系,因為通過控制臺極大的方便了我們?nèi)粘5倪\(yùn)維工作。
我們可以在控制臺上操作各種限流、降級、系統(tǒng)保護(hù)的規(guī)則,也可以查看每個資源的實時數(shù)據(jù),還能管理集群環(huán)境下的服務(wù)端與客戶端機(jī)器。
但是控制臺只是一個獨(dú)立的 spring boot 應(yīng)用,他本身是沒有任何數(shù)據(jù)的,他的數(shù)據(jù)都是從其他的 sentinel 實例中獲取的,那他是如何獲取到這些數(shù)據(jù)的呢?帶著這個疑問我們從源碼中尋找答案。
最簡單的方法莫過于啟動一個控制臺的實例,然后從頁面上查看每個接口請求的url,然后再到 dashboard 的代碼中去深挖下去。
怎么啟動控制臺,這里就不再詳細(xì)描述了,大家可以看 Sentinel實戰(zhàn):使用控制臺管理規(guī)則 這篇文章去了解下,簡單的幾步就可以啟動一個控制臺了。
我們就以一個簡單的查看【流控規(guī)則】為例來描述,點擊【流控規(guī)則】進(jìn)入頁面后,按F11打開network就可以看到請求的url了,如下圖所示:

可以看到,請求的 url 是 /v1/flow/rules 我們直接在源碼中全局搜索 /rules ,為什么不搜索 /v1/flow/rules 呢,因為有可能 url 被拆分成兩部分,我們直接搜完整的 url 可能搜不到結(jié)果。如下圖所示:

我們要找的應(yīng)該就是 FlowControllerV1 這個類了,打開這個類看下類上修飾的值是不是 /v1/flow 如下圖所示:

從圖中可以看出來,dashboard 是通過一個叫 SentinelApiClient 的類去指定的 ip 和 port 處獲取數(shù)據(jù)的。這個 ip 和 port 是前端頁面直接提交給后端的,而前端頁面又是通過 /app/{app}/machines.json 接口獲取機(jī)器列表的。
連接 dashboard
這里的機(jī)器列表中展示的就是所有連接到 dashboard 上的 sentinel 的實例,包括普通限流的 sentinel-core 和集群模式下的 token-server 和 token-client。我們可以回想一下,一個 sentinel-core 的實例要接入 dashboard 的幾個步驟:
- 引入 dashboard 的依賴
- 配置 dashboard 的 ip 和 port
- 初始化 sentinel-core,連接 dashboard
sentinel-core 在初始化的時候,通過 JVM 參數(shù)中指定的 dashboard 的 ip 和 port,會主動向 dashboard 發(fā)起連接的請求,該請求是通過 HeartbeatSender 接口以心跳的方式發(fā)送的,并將自己的 ip 和 port 告知 dashboard。這里 sentinel-core 上報給 dashboard 的端口是 sentinel 對外暴露的自己的 CommandCenter 的端口。
HeartbeatSender 有兩個實現(xiàn)類,一個是通過 http,另一個是通過 netty,我們看 http 的實現(xiàn)類:
SimpleHttpHeartbeatSender.java
private final HeartbeatMessage heartBeat = new HeartbeatMessage();
private final SimpleHttpClient httpClient = new SimpleHttpClient();
@Override
public boolean sendHeartbeat() throws Exception {
if (TransportConfig.getRuntimePort() <= 0) {
RecordLog.info("[SimpleHttpHeartbeatSender] Runtime port not initialized, won't send heartbeat");
return false;
}
InetSocketAddress addr = getAvailableAddress();
if (addr == null) {
return false;
}
SimpleHttpRequest request = new SimpleHttpRequest(addr, HEARTBEAT_PATH);
request.setParams(heartBeat.generateCurrentMessage());
try {
SimpleHttpResponse response = httpClient.post(request);
if (response.getStatusCode() == OK_STATUS) {
return true;
}
} catch (Exception e) {
RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addr + " : ", e);
}
return false;
}
通過一個 HttpClient 向 dashboard 發(fā)送了自己的信息,包括 ip port 和版本號等信息。
其中 consoleHost 和 consolePort 的值就是從 JVM 參數(shù) csp.sentinel.dashboard.server 中獲取的。
dashboard 在接收到 sentinel-core 的連接之后,就會與 sentinel-core 建立連接,并將 sentinel-core 上報的 ip 和 port 的信息包裝成一個 MachineInfo 對象,然后通過 SimpleMachineDiscovery 將該對象保存在一個 map 中,如下圖所示:

定時發(fā)送心跳
sentinel-core 連接上 dashboard 之后,并不是就結(jié)束了,事實上 sentinel-core 是通過一個 ScheduledExecutorService 的定時任務(wù),每隔 10 秒鐘向 dashboard 發(fā)送一次心跳信息。發(fā)送心跳的目的主要是告訴 dashboard 我這臺 sentinel 的實例還活著,你可以繼續(xù)向我請求數(shù)據(jù)。
這也就是為什么 dashboard 中每個 app 對應(yīng)的機(jī)器列表要用 Set 來保存的原因,如果用 List 來保存的話就可能存在同一臺機(jī)器保存了多次的情況。
心跳可以維持雙方之間的連接是正常的,但是也有可能因為各種原因,某一方或者雙方都離線了,那他們之間的連接就丟失了。
1.sentinel-core 宕機(jī)
如果是 sentinel-core 宕機(jī)了,那么這時 dashboard 中保存在內(nèi)存里面的機(jī)器列表還是存在的。目前 dashboard 只是在接收到 sentinel-core 發(fā)送過來的心跳包的時候更新一次機(jī)器列表,當(dāng) sentinel-core 宕機(jī)了,不再發(fā)送心跳數(shù)據(jù)的時候,dashboard 是沒有將 “失聯(lián)” 的 sentinel-core 實例給去除的。而是頁面上每次查詢的時候,會去用當(dāng)前時間減去機(jī)器上次心跳包的時間,如果時間差大于 5 分鐘了,才會將該機(jī)器標(biāo)記為 “失聯(lián)”。
所以我們在頁面上的機(jī)器列表中,需要至少等到 5 分鐘之后,才會將具體失聯(lián)的 sentinel-core 的機(jī)器標(biāo)記為 “失聯(lián)”。如下圖所示:

2.dashboard 宕機(jī)
如果 dashboard 宕機(jī)了,sentinel-core 的定時任務(wù)實際上是會一直請求下去的,只要 dashboard 恢復(fù)后就會自動重新連接上 dashboard,雙方之間的連接又會恢復(fù)正常了,如果 dashboard 一直不恢復(fù),那么 sentinel-core 就會一直報錯,在 sentinel-record.log 中我們會看到如下的報錯信息:

不過實際生產(chǎn)中,不可能出現(xiàn) dashboard 宕機(jī)了一直沒人去恢復(fù)的情況的,如果真出現(xiàn)這種情況的話,那就要吃故障了。
請求數(shù)據(jù)
當(dāng) dashboard 有了具體的 sentinel-core 實例的 ip 和 port 之后,就可以去請求所需要的數(shù)據(jù)了。
讓我們再回到最開始的地方,我在頁面上查詢某一臺機(jī)器的限流的規(guī)則時,是將該機(jī)器的 ip 和 port 以及 appName 都傳給了服務(wù)端,服務(wù)端通過這些信息去具體的遠(yuǎn)程實例中請求所需的數(shù)據(jù),拿到數(shù)據(jù)后再封裝成 dashboard 所需的格式返回給前端頁面進(jìn)行展示。
具體請求限流規(guī)則列表的代碼在 SentinelApiClient 中,如下所示:
SentinelApiClient.java
public List<FlowRuleEntity> fetchFlowRuleOfMachine(String app, String ip, int port) {
String url = "http://" + ip + ":" + port + "/" + GET_RULES_PATH + "?type=" + FLOW_RULE_TYPE;
String body = httpGetContent(url);
logger.info("FlowRule Body:{}", body);
List<FlowRule> rules = RuleUtils.parseFlowRule(body);
if (rules != null) {
return rules.stream().map(rule -> FlowRuleEntity.fromFlowRule(app, ip, port, rule))
.collect(Collectors.toList());
} else {
return null;
}
}
可以看到也是通過一個 httpClient 請求的數(shù)據(jù),然后再對結(jié)果進(jìn)行轉(zhuǎn)換,具體請求的過程是在 httpGetContent 方法中進(jìn)行的,我們看下該方法,如下所示:
private String httpGetContent(String url) {
final HttpGet httpGet = new HttpGet(url);
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<String> reference = new AtomicReference<>();
httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
@Override
public void completed(final HttpResponse response) {
try {
reference.set(getBody(response));
} catch (Exception e) {
logger.info("httpGetContent " + url + " error:", e);
} finally {
latch.countDown();
}
}
@Override
public void failed(final Exception ex) {
latch.countDown();
logger.info("httpGetContent " + url + " failed:", ex);
}
@Override
public void cancelled() {
latch.countDown();
}
});
try {
latch.await(5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.info("wait http client error:", e);
}
return reference.get();
}
從代碼中可以看到,是通過一個異步的 httpClient 再結(jié)合 CountDownLatch 等待 5 秒的超時時間去獲取結(jié)果的。
獲取數(shù)據(jù)的請求從 dashboard 中發(fā)出去了,那 sentinel-core 中是怎么進(jìn)行相應(yīng)處理的呢?看過我其他文章的同學(xué)肯定還記得, sentinel-core 在啟動的時候,執(zhí)行了一個 InitExecutor.init 的方法,該方法會觸發(fā)所有 InitFunc 實現(xiàn)類的 init 方法,其中就包括兩個最重要的實現(xiàn)類:
- HeartbeatSenderInitFunc
- CommandCenterInitFunc
HeartbeatSenderInitFunc 會啟動一個 HeartbeatSender 來定時的向 dashboard 發(fā)送自己的心跳包,而 CommandCenterInitFunc 則會啟動一個 CommandCenter 對外提供 sentinel-core 的數(shù)據(jù)服務(wù),而這些數(shù)據(jù)服務(wù)是通過一個一個的 CommandHandler 來提供的,如下圖所示:

總結(jié)
現(xiàn)在我們已經(jīng)知道了 dashboard 是如何獲取到實時數(shù)據(jù)的了,具體的流程如下所示:
1.首先 sentinel-core 向 dashboard 發(fā)送心跳包
2.dashboard 將 sentinel-core 的機(jī)器信息保存在內(nèi)存中
3.dashboard 根據(jù) sentinel-core 的機(jī)器信息通過 httpClient 獲取實時的數(shù)據(jù)
4.sentinel-core 接收到請求之后,會找到具體的 CommandHandler 來處理
5.sentinel-core 將處理好的結(jié)果返回給 dashboard
思考
1.數(shù)據(jù)安全性
sentinel-dashboard 和 sentinel-core 之間的通訊是基于 http 的,沒有進(jìn)行加密或鑒權(quán),可能會存在數(shù)據(jù)安全性的問題,不過這些數(shù)據(jù)并非是很機(jī)密的數(shù)據(jù),對安全性要求并不是很高,另外增加了鑒權(quán)或加密之后,對于性能和實效性有一定的影響。
2.SentinelApiClient
目前所有的數(shù)據(jù)請求都是通過 SentinelApiClient 類去完成的,該類中充斥著大量的方法,都是發(fā)送 http 請求的。代碼的可讀性和可維護(hù)性不高,所以需要對該類進(jìn)行重構(gòu),目前我能夠想到的有兩種方法:
1)通過將 sentinel-core 注冊為 rpc 服務(wù),dashboard 就像調(diào)用本地方法一樣去調(diào)用 sentinel-core 中的方法,不過這樣的話需要引入服務(wù)注冊和發(fā)現(xiàn)的依賴了。
2)通過 netty 實現(xiàn)私有的協(xié)議,sentinel-core 通過 netty 啟動一個 CommandCenter 來對外提供服務(wù)。dashboard 通過發(fā)送 Packet 來進(jìn)行數(shù)據(jù)請求,sentinel-core 來處理 Packet。不過這種方法跟目前的做法沒有太大的區(qū)別,唯一比較好的可能就是不需要為每種請求都寫一個方法,只需要定義好具體的 Packet 就好了。