Sentinel 系列教程,現已上傳到 github 和 gitee 中:
GitHub:
https://github.com/all4you/sentinel-tutorial
Gitee:
https://gitee.com/all_4_you/sentinel-tutorial
Sentinel 能夠被大家所認可,除了他自身的輕量級,高性能,可擴展之外,跟控制臺的好用和易用也有著莫大的關系,因為通過控制臺極大的方便了我們日常的運維工作。
我們可以在控制臺上操作各種限流、降級、系統保護的規則,也可以查看每個資源的實時數據,還能管理集群環境下的服務端與客戶端機器。
但是控制臺只是一個獨立的 spring boot 應用,他本身是沒有任何數據的,他的數據都是從其他的 sentinel 實例中獲取的,那他是如何獲取到這些數據的呢?帶著這個疑問我們從源碼中尋找答案。
最簡單的方法莫過于啟動一個控制臺的實例,然后從頁面上查看每個接口請求的url,然后再到 dashboard 的代碼中去深挖下去。
怎么啟動控制臺,這里就不再詳細描述了,大家可以看 Sentinel實戰:使用控制臺管理規則 這篇文章去了解下,簡單的幾步就可以啟動一個控制臺了。
我們就以一個簡單的查看【流控規則】為例來描述,點擊【流控規則】進入頁面后,按F11打開network就可以看到請求的url了,如下圖所示:
可以看到,請求的 url 是 /v1/flow/rules 我們直接在源碼中全局搜索 /rules ,為什么不搜索 /v1/flow/rules 呢,因為有可能 url 被拆分成兩部分,我們直接搜完整的 url 可能搜不到結果。如下圖所示:
我們要找的應該就是 FlowControllerV1 這個類了,打開這個類看下類上修飾的值是不是 /v1/flow 如下圖所示:
從圖中可以看出來,dashboard 是通過一個叫 SentinelApiClient 的類去指定的 ip 和 port 處獲取數據的。這個 ip 和 port 是前端頁面直接提交給后端的,而前端頁面又是通過 /app/{app}/machines.json 接口獲取機器列表的。
連接 dashboard
這里的機器列表中展示的就是所有連接到 dashboard 上的 sentinel 的實例,包括普通限流的 sentinel-core 和集群模式下的 token-server 和 token-client。我們可以回想一下,一個 sentinel-core 的實例要接入 dashboard 的幾個步驟:
引入 dashboard 的依賴
配置 dashboard 的 ip 和 port
初始化 sentinel-core,連接 dashboard
sentinel-core 在初始化的時候,通過 JVM 參數中指定的 dashboard 的 ip 和 port,會主動向 dashboard 發起連接的請求,該請求是通過 HeartbeatSender 接口以心跳的方式發送的,并將自己的 ip 和 port 告知 dashboard。這里 sentinel-core 上報給 dashboard 的端口是 sentinel 對外暴露的自己的 CommandCenter 的端口。
HeartbeatSender 有兩個實現類,一個是通過 http,另一個是通過 netty,我們看 http 的實現類:
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 發送了自己的信息,包括 ip port 和版本號等信息。
其中 consoleHost 和 consolePort 的值就是從 JVM 參數 csp.sentinel.dashboard.server 中獲取的。
dashboard 在接收到 sentinel-core 的連接之后,就會與 sentinel-core 建立連接,并將 sentinel-core 上報的 ip 和 port 的信息包裝成一個 MachineInfo 對象,然后通過 SimpleMachineDiscovery 將該對象保存在一個 map 中,如下圖所示:
定時發送心跳
sentinel-core 連接上 dashboard 之后,并不是就結束了,事實上 sentinel-core 是通過一個 ScheduledExecutorService 的定時任務,每隔 10 秒鐘向 dashboard 發送一次心跳信息。發送心跳的目的主要是告訴 dashboard 我這臺 sentinel 的實例還活著,你可以繼續向我請求數據。
這也就是為什么 dashboard 中每個 app 對應的機器列表要用 Set 來保存的原因,如果用 List 來保存的話就可能存在同一臺機器保存了多次的情況。
心跳可以維持雙方之間的連接是正常的,但是也有可能因為各種原因,某一方或者雙方都離線了,那他們之間的連接就丟失了。
1.sentinel-core 宕機
如果是 sentinel-core 宕機了,那么這時 dashboard 中保存在內存里面的機器列表還是存在的。目前 dashboard 只是在接收到 sentinel-core 發送過來的心跳包的時候更新一次機器列表,當 sentinel-core 宕機了,不再發送心跳數據的時候,dashboard 是沒有將 “失聯” 的 sentinel-core 實例給去除的。而是頁面上每次查詢的時候,會去用當前時間減去機器上次心跳包的時間,如果時間差大于 5 分鐘了,才會將該機器標記為 “失聯”。
所以我們在頁面上的機器列表中,需要至少等到 5 分鐘之后,才會將具體失聯的 sentinel-core 的機器標記為 “失聯”。如下圖所示:
2.dashboard 宕機
如果 dashboard 宕機了,sentinel-core 的定時任務實際上是會一直請求下去的,只要 dashboard 恢復后就會自動重新連接上 dashboard,雙方之間的連接又會恢復正常了,如果 dashboard 一直不恢復,那么 sentinel-core 就會一直報錯,在 sentinel-record.log 中我們會看到如下的報錯信息:
不過實際生產中,不可能出現 dashboard 宕機了一直沒人去恢復的情況的,如果真出現這種情況的話,那就要吃故障了。
請求數據
當 dashboard 有了具體的 sentinel-core 實例的 ip 和 port 之后,就可以去請求所需要的數據了。
讓我們再回到最開始的地方,我在頁面上查詢某一臺機器的限流的規則時,是將該機器的 ip 和 port 以及 appName 都傳給了服務端,服務端通過這些信息去具體的遠程實例中請求所需的數據,拿到數據后再封裝成 dashboard 所需的格式返回給前端頁面進行展示。
具體請求限流規則列表的代碼在 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 請求的數據,然后再對結果進行轉換,具體請求的過程是在 httpGetContent 方法中進行的,我們看下該方法,如下所示:
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 再結合 CountDownLatch 等待 5 秒的超時時間去獲取結果的。
獲取數據的請求從 dashboard 中發出去了,那 sentinel-core 中是怎么進行相應處理的呢?看過我其他文章的同學肯定還記得, sentinel-core 在啟動的時候,執行了一個 InitExecutor.init 的方法,該方法會觸發所有 InitFunc 實現類的 init 方法,其中就包括兩個最重要的實現類:
HeartbeatSenderInitFunc
CommandCenterInitFunc
HeartbeatSenderInitFunc 會啟動一個 HeartbeatSender 來定時的向 dashboard 發送自己的心跳包,而 CommandCenterInitFunc 則會啟動一個 CommandCenter 對外提供 sentinel-core 的數據服務,而這些數據服務是通過一個一個的 CommandHandler 來提供的,如下圖所示:
總結
現在我們已經知道了 dashboard 是如何獲取到實時數據的了,具體的流程如下所示:
1.首先 sentinel-core 向 dashboard 發送心跳包
2.dashboard 將 sentinel-core 的機器信息保存在內存中
3.dashboard 根據 sentinel-core 的機器信息通過 httpClient 獲取實時的數據
4.sentinel-core 接收到請求之后,會找到具體的 CommandHandler 來處理
5.sentinel-core 將處理好的結果返回給 dashboard
思考
1.數據安全性
sentinel-dashboard 和 sentinel-core 之間的通訊是基于 http 的,沒有進行加密或鑒權,可能會存在數據安全性的問題,不過這些數據并非是很機密的數據,對安全性要求并不是很高,另外增加了鑒權或加密之后,對于性能和實效性有一定的影響。
2.SentinelApiClient
目前所有的數據請求都是通過 SentinelApiClient 類去完成的,該類中充斥著大量的方法,都是發送 http 請求的。代碼的可讀性和可維護性不高,所以需要對該類進行重構,目前我能夠想到的有兩種方法:
1)通過將 sentinel-core 注冊為 rpc 服務,dashboard 就像調用本地方法一樣去調用 sentinel-core 中的方法,不過這樣的話需要引入服務注冊和發現的依賴了。
2)通過 netty 實現私有的協議,sentinel-core 通過 netty 啟動一個 CommandCenter 來對外提供服務。dashboard 通過發送 Packet 來進行數據請求,sentinel-core 來處理 Packet。不過這種方法跟目前的做法沒有太大的區別,唯一比較好的可能就是不需要為每種請求都寫一個方法,只需要定義好具體的 Packet 就好了。
更多原創好文
請關注「逅弈逐碼」