Stories

Detail Return Return

Sentinel進化指南:dashbaord改造,集羣流控,監控持久化 - Stories Detail

前言

我們的項目為了方便移植,所以選擇了阿里雲來進行部署,脱離的公司自己的技術能力平台。項目中使用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

Add a new Comments

Some HTML is okay.