Sentinel進化指南:dashbaord改造,叢集流控,監控持久化

jian發表於2024-11-11

前言

我們的專案為了方便移植,所以選擇了阿里雲來進行部署,脫離的公司自己的技術能力平臺。專案中使用sentinel做 限流,單原本的sentinel只有基於的記憶體儲存的單機限流攻擊,無法滿足線上軟體的要求。我們需要在sentinel的基礎上,改造dashboard完成如下能力。

  1. 接入Sentinel-Dashboard提供更靈活的限流配置管理和更直觀的檢視系統資源的入口。
  2. 接入nacos 提供持久化的限流配置儲存能力。
  3. 接入token-server,提供叢集限流的能力。
  4. Sentinel-Dashboard 接入sqllite,持久化度量展示。

本文則會詳細展開來講解每一項的改動過程。

sentinel 初識

在Sentinel改造之前,我們來簡單看一下sentinel官方提供了什麼的基礎能力。
sentinel官網

在sentinel中,限流的基本單位就是資源。使用者可以自己將應用程式中的任何內容定義為一個資源,並圍繞這個資源實時設定相關的限流規則。從而實現系統的限流保護,熔斷降級等高可用的保障能力。下面是官方提供的一個簡單例子。


private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    //指定資源
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //設定qps的限流值
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

public static void main(String[] args) {
    
    // 配置規則.
    initFlowRules();

    //定義Hello World資源
    try (Entry entry = SphU.entry("HelloWorld")) {
            // 被保護的邏輯
            System.out.println("hello world");
    } catch (BlockException ex) {
            // 處理被流控的邏輯
        System.out.println("blocked!");
    }
}

至此,我們就簡單的實現了使用sentinel做一個單機限流的一個程式碼例項,只不過這個限流的規則是透過硬編碼的方式儲存在應用啟動的記憶體當中的。當應用啟動以後將無法實時的檢視資源的使用情況和動態調整資源的限流規則。為此Sentinel提供了一個dashbaord 啟動控制檯,可以實時監控各個資源的執行情況,並且可以實時地修改限流規則。

接入dashboard

sentinel控制檯官網

首先我們將sentinel-dashboard下載到本地。先在github上找到sentinel的原始碼,地址:https://github.com/alibaba/Sentinel。並單獨開啟sentinel-dashboard工程,

image.png

可以看到sentinel-dashboard是一個包含了前端和後端資源的springBoot工程,我們直接執行DashboardApplication就可以直接啟動sentinel的dashboard。

image.png

我們把使用到限流的業務工程稱為sentienl客戶端, sentinel-dashboard啟動後,我們先把dashboard放在一邊,來看一下sentinel客戶端如何接入dashboard.

sentienl的客戶端和dashboard之間透過 sentinel-transport-simple-http 模組通訊,sentinel客戶端每秒講資源的實時情況透過http彙總到dashboard,並監聽來自於dashboard的規則修改指令。因此客戶端需要引入如下依賴。


<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-core</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-transport-simple-http</artifactId>  
    <version>1.8.6</version>  
</dependency>

客戶端啟動時,需要額外增加Sentinel相關的引數

-Dproject.name=client-biz-test # 指定當前客戶端的名稱
-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定遠端的dashoard的地址。

sentinel的資源建立是一個懶載入的過程,在客戶端啟動後,一定要先觸發一次請求。否則在dashboard講檢視不到任何的客戶端資訊。觸發一次後,再開啟dashboard地址,透過預設的賬號和密碼(sentinel:sentinel)就可以檢視到當前資源的資訊了。

image

但目前來說,所有的規則配置資訊都是儲存著客戶端記憶體中,如果客戶端重啟,所有的配置資訊將全部丟失。如果想要線上上生成環境更好的使用sentinel就需要進行深入的改造工程。

開始sentinel改造之旅

接入nacos

改造的第一站,就是要實現 規則配置的持久化:將規則儲存在第三方的配置中心(比如nacos或zookeeper),我在專案中使用的是nacos,本文也講著重講解如何使用nacos來儲存規則。

sentienl的原始模式與push模式

上面的透過dashboard將配置寫入到客戶端記憶體中的這種方式就是原始模式。
而我們的目標:從nacos中實時感知規則配置的變更的這種模式就是push模式。

image.png

image.png

在push模式中控制檯將配置規則推送到遠端配置中心,例如Nacos。Sentinel客戶端監聽Nacos,獲取配置變更的推送訊息,完成本地配置更新。

sentinel的客戶端改造

參考官方網站的指引:動態規則擴充套件

首先我們先完成 sentinel的客戶端改造。只需要兩步:

首先引入依賴

<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-core</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-datasource-nacos</artifactId>  
    <version>1.8.6</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba.csp</groupId>  
    <artifactId>sentinel-transport-simple-http</artifactId>  
    <version>1.8.6</version>  
</dependency>

然後註冊nacos的資料來源


@Component
@Slf4j
public class SentinelNacosConfig {

    @Resource
    private Environment environment;

    private static final String PROJECT_NAME = "project.name";

    @PostConstruct
    public void sentinelConfigChange(String configJson) {

        //todo 這裡填入你自己的nacos資訊
        String remoteAddress = '';
        String groupId = '';
        String namespace = '';

        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, remoteAddress);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);

        //讀取jvm引數中的 -Dproject.name 每一個工程生成一個dataId
        String projectName = MoreObjects.firstNonNull(environment.getProperty(PROJECT_NAME), "default");
        String dataId = projectName + "-flow-rules";
        //註冊資料來源頭
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(properties, groupId, dataId,
                source -> JsonUtils.readObject(source, new TypeReference<>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

客戶端啟動時,仍然需要額外增加Sentinel相關的引數再啟動。

-Dproject.name=client-biz-test # 指定當前客戶端的名稱
-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定遠端的dashoard的地址。

sentinel的dashboard改造

dashbaord的改造就相對來說複雜一些。這裡我會盡量講清楚。

開啟我們之前下載的dashbord原始碼。sentinel團隊其實已經給我們預留好了接入nacos的程式碼。dashboard中

  • 透過DynamicRuleProvider來從第三方讀取規則資訊,預設的實現是透過http從sentinel客戶端中任意一臺節點來讀取規則資訊。(具體實現類:flowRuleDefaultProvider)
  • 透過DynamicRulePublisher來將頁面變更的規則資訊推送到第三方中。預設的實現是透過http推送給所有的sentinel客戶端節點。(具體實現類:flowRuleDefaultPublisher)

image.png

而我們要做的事情是

  1. pom中啟用nacos模組
    image.png
    刪除這個score
  2. 替換DynamicRuleProvider和DynamicRulePublisher的預設實現。只需要將原始碼中test目錄下的nacos實現並覆蓋rule工程下即可。

image.png

在NacosConfig中填入你自己的nacos的配置資訊


@Bean  
public ConfigService nacosConfigService() throws Exception {  
    Properties properties = new Properties();  
    properties.put(PropertyKeyConst.SERVER_ADDR, "");  
    properties.put(PropertyKeyConst.NAMESPACE, "");  
    return ConfigFactory.createConfigService(properties);  
}
  1. 將之前所有使用flowRuleDefaultProvider和flowRuleDefaultPublisher的地方改成使用flowRuleNacosProvider和flowRuleNacosPublisher。

    其實只有FlowControllerV2中有使用。FlowController是流控規則Controller入口,Sentinel Dashboard的流控規則下的所有操作,都會呼叫Sentinel-Dashboard原始碼中的FlowController類,這個類中包含流控規則本地化的CRUD操作。

    此時你會發現程式碼中既有 FlowControllerV1類,又有FlowControllerV2類,這裡其實是一個歷史原因。

    一開始只有FlowController類,該類的程式碼和現在看到的FlowControllerV1是相同的:http從sentinel客戶端中任意一臺節點來讀取規則資訊。再將變更透過http推送給所有的sentinel客戶端節點。從 Sentinel 1.4.0 開始,官方抽取出了介面用於向遠端配置中心推送規則以及拉取規則。即DynamicRuleProvider和DynamicRulePublisher。

    但即使是最新的版本,前端預設的情況在仍然是請求 FlowControllerV1 的介面的(主要是為了歷史相容)。因此我們需要繼續修改前端程式碼。

  2. 修改前端程式碼:Sentinel Dashboard前端sidebar.html頁面入口。在目錄resources/app/scripts/directives/sidebar找到sidebar.html,裡面有關於V1版本的請求入口:

    <li ui-sref-active="active" ng-if="!entry.isGateway">  
      <a ui-sref="dashboard.flowV1({app: entry.app})">  
    <i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控規則</a>  
    </li>

 對應的JS 請求是在 app.js 檔案下,搜尋關鍵字 dashboard.flowV1 ,可以看到下方會有一個類似的但請求FlowControllerV2 的 js程式碼。因此我們只需要將sidebar.html中的關於v1的版本請求改掉即可。

image.png

dashboard改造完成,啟動dashboard,嘗試調整一下限流配置觀察整改改造鏈路是否完成。在nacos頁面中可以檢視具體的配置專案是否已經建立成功。

image.png

更詳細的內容,可以檢視官方文件 Sentinel 控制檯

叢集流控

參考sentinel官方文件 叢集流量控制

即使經過上面的改造我們仍然只是解決單機限流的配置持久化和及時的資訊查詢能力。所有的限流仍然是單節點有效。在實際的高併發場景下,由於業務叢集需要能夠動態的擴容縮容,所以我們無法透過使用 單機限流 * 節點數 來實現一個叢集限流的能力。我們仍然是需要一個真正的叢集限流能力。如果想實現叢集流控,就避免不了需要有一個單點服務做統計。在Sentinel中,叢集流控中共有兩種身份:

  • Token Client:叢集流控客戶端,用於向所屬 Token Server 通訊請求 token。叢集限流服務端會返回給客戶端結果,決定是否限流。
  • Token Server:即叢集流控服務端,處理來自 Token Client 的請求,根據配置的叢集規則判斷是否應該發放 token(是否允許透過)。

若Token Sever 當機,則會使用Token Client的單機限流進行均攤。

Sentinel 叢集限流服務端有兩種啟動方式:

  • 獨立模式(Alone),即作為獨立的 token server 程序啟動,獨立部署,隔離性好,但是需要額外的部署操作。獨立模式適合作為 Global Rate Limiter 給叢集提供流控服務。

image

  • 嵌入模式(Embedded),即作為內建的 token server 與服務在同一程序中啟動。在此模式下,叢集中各個例項都是對等的,token server 和 client 可以隨時進行轉變,因此無需單獨部署,靈活性比較好。但是隔離性不佳,需要限制 token server 的總 QPS,防止影響應用本身。嵌入模式適合某個應用叢集內部的流控。

image

在生產環境中,我們更希望是有一個單獨的節點來作為的叢集流控的token-server角色。因此我們需要使用獨立模式。

指定Token Client角色

回到sentinel的客戶端改造 這個章節,我們需要指定我們目前的Sentinel客戶端為Token Client角色(我在實際改造過程中沒有主動指定身份,結果整個叢集怎麼都不通,然後卡了大半天)

在 SentinelNacosConfig 中 增加一行程式碼

@Component
@Slf4j
public class SentinelNacosConfig {

    @Resource
    private Environment environment;

    private static final String PROJECT_NAME = "project.name";

    @NacosConfigListener(dataId = "sentinel_nacos_config")
    public void sentinelConfigChange(String configJson) {
        //全部省略

        // 指定當前身份為 Token Client
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
    }
}

單獨搭建 Token-Server 工程

初始化一個簡單的springBoot工程。

  1. 引入Sentinel相關的依賴

    <dependency>  
     <groupId>com.alibaba.csp</groupId>  
     <artifactId>sentinel-datasource-nacos</artifactId>  
     <version>1.8.6</version>  
    </dependency>  
    <dependency>  
     <groupId>com.alibaba.csp</groupId>  
     <artifactId>sentinel-transport-simple-http</artifactId>  
     <version>1.8.6</version>  
    </dependency>
  2. 並新增如下檔案用於配置nacos並啟動token-server

@Slf4j  
@Component  
public class SentinelServer {  
  
    private String groupId = "SENTINEL_GROUP";  
  
    private String dataIdSuffix = " -flow-rules";  
  
    private static final String namespaceSetDataId = "token-server-namespace-set";  
  
    @PostConstruct  
    public void init() throws Exception {  
  
        Properties properties = new Properties();  
  
        //填入你的自己的nacos配置,  
        properties.put(PropertyKeyConst.SERVER_ADDR, "");  
        properties.put(PropertyKeyConst.NAMESPACE, "");  
  
        initPropertySupplier(properties);  
        initNamespaceSetProperty(properties);  
        runTokenServer();  
    }  
  
  
    private void initPropertySupplier(Properties properties) {  
        // 與sentinel client 共同讀取同一套限流規則配置 ,因此groupId和dataId的生成規則要和 sentinel-client 以及 sentinel-dashboard保持一致  
        ClusterFlowRuleManager.setPropertySupplier(namespace -> {  
            ReadableDataSource<String, List<FlowRule>> ds = new NacosDataSource<>(properties, groupId,  
                    namespace + dataIdSuffix,  
                    source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {  
                    }));  
            return ds.getProperty();  
        });  
    }  
  
    // 配置名稱空間  
    // 叢集限流服務端服務的作用域(名稱空間),可以設定為自己服務的應用名。  
    // 叢集限流 client 在連線到 token server 後會上報自己的應用名(預設為 project.name 配置的應用名),token server 會根據上報的應用名來統計連線數。  
    private void initNamespaceSetProperty(Properties properties) {  
        // Server namespace set (scope) data source.  
        ReadableDataSource<String, Set<String>> namespaceDs = new NacosDataSource<>(properties, groupId,  
                namespaceSetDataId, source -> JSON.parseObject(source, new TypeReference<Set<String>>() {  
        }));  
        ClusterServerConfigManager.registerNamespaceSetProperty(namespaceDs.getProperty());  
    }  
  
    //啟動token-server  
    private void runTokenServer() throws Exception {  
          
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();  
          
        ServerTransportConfig serverTransportConfig = new ServerTransportConfig();  
        serverTransportConfig.setPort(11111);  
        ClusterServerConfigManager.loadGlobalTransportConfig(serverTransportConfig);  
        // Start the server.  
        tokenServer.start();  
  
    }  
  
}
  1. 啟動部署Token-server
    -Dproject.name=client-biz-test # 指定當前客戶端的名稱
    -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 # 指定遠端的dashoard的地址。
  2. 分配client
    正常啟動以後,需要在dashboard中分配client和token-server之間的關係。dashboar叢集限流選擇項中可以調整,這裡不再贅述。

dashboard 監控資料持久化

Dashboard 中MetricsRepository 是用來儲存和查詢所有的監控入口的入口,預設的實現方式是InMemoryMetricsRepository,dashboard從client收集上來的監控資料只會在記憶體中儲存5分鐘的時間。有時候我們查詢歷史的qps情況就需要我們自己來實現一箇中MetricsRepository。

對於日誌類的資料,一般來說InfluxDB這種時序資料庫是再合適不過的。使用InfluxDB那麼就需要額外的啟動一個InfluxDB的程序。但我這裡希望dashboard能足夠的精簡獨立,而且經過上面的改造dashboard完全可以單節點部署(dashbaord掛了也沒關係,所有的規則配置都在nacos中),所以我選擇了嵌入式sqllite來作為持久化的儲存,直接將資料儲存到

如何在springBoot中引入sqllite的依賴,我這裡就不贅述了,我們完全可以讓chatgpt幫我們寫一個具體的例子來。

但有一點需要考慮的是,為了防止sqlite佔用的空間無限增大,我們需要定時來清理7天之前的資料。


@Component
@Slf4j
public class SqlLiteMetricsRepository implements MetricsRepository<MetricEntity> {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void initDb() {

        String createTableSql = "CREATE TABLE IF NOT EXISTS MetricEntity (\n" +
                "    id INTEGER,\n" +
                "    gmtCreate TIMESTAMP,\n" +
                "    gmtModified TIMESTAMP,\n" +
                "    app TEXT,\n" +
                "    timestamp TIMESTAMP,\n" +
                "    resource TEXT,\n" +
                "    passQps INTEGER,\n" +
                "    successQps INTEGER,\n" +
                "    blockQps INTEGER,\n" +
                "    exceptionQps INTEGER,\n" +
                "    rt REAL,\n" +
                "    count INTEGER,\n" +
                "    resourceCode INTEGER\n" +
                ");\n" +
                "CREATE INDEX IF NOT EXISTS idx_app_resource_timestamp ON MetricEntity (app, resource, timestamp); ";
        jdbcTemplate.execute(createTableSql);
    }

    @Override
    public void save(MetricEntity metricEntity) {
        String sql = "INSERT INTO MetricEntity (id, gmtCreate, gmtModified, app, timestamp, resource, passQps, successQps, blockQps, exceptionQps, rt, count, resourceCode) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        //此處省略操作sqllite相關的程式碼
    }

    @Override
    public void saveAll(Iterable<MetricEntity> metrics) {
        String sql = "INSERT INTO MetricEntity (id, gmtCreate, gmtModified, app, timestamp, resource, passQps, successQps, blockQps, exceptionQps, rt, count, resourceCode) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        //此處省略操作sqllite相關的程式碼
    }


    @Override
    public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
        String sql = "SELECT * FROM MetricEntity WHERE app = ? AND resource = ? AND `timestamp` BETWEEN ? AND ?";
        //此處省略操作sqllite相關的程式碼
        return metricEntityList;
    }

    @Override
    public List<String> listResourcesOfApp(String app) {
        String sql = "SELECT * FROM MetricEntity WHERE app = ?  ";
        //此處省略操作sqllite相關的程式碼
    }

    //滾動刪除7天前的資料
    @Scheduled(fixedRate = 86400000) // Every 24 hours
    public void cleanupOldData() {
        Instant cutoff = Instant.now().minusSeconds(7 * 24 * 60 * 60); // 7 days ago
        String sql = "DELETE FROM MetricEntity WHERE timestamp < ?";
        jdbcTemplate.update(sql, cutoff);
    }

}

然後將所有使用到MetricsRepository的地方,指明裝配sqlLiteMetricsRepository。
總共有兩個檔案用到了 MetricsRepository

  • MetricController:提供前端查詢用的介面
  • MetricFetcher:獲取Sentinel-client資訊的入口。

改造前端
我們需要給前端頁面增加 開始時間和結束時間的查詢範圍入參。讓前端頁面能夠選擇查詢的範圍。

相關的程式碼檔案在metric.html和metric.js中。可能大部分的後端開發對前端的相關知識並不是很清楚,改起來比較費勁,我們可以著前端同事幫忙修改前端頁面,增加這個邏輯。也可以像我一樣從github中找一份以及改造過的程式碼,然後將對應的metric.html、metric.js檔案直接覆蓋過去。再進行除錯,透過除錯反饋的問題,不斷的複製copy相關的程式碼。參考的github :https://github.com/shiyindaxiaojie/Sentinel

相關文章