阿里熔斷限流Sentinel研究


1. 阿里熔斷限流Sentinel研究

1.1. 功能特點

  1. 豐富的應用場景:例如秒殺(即突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用
  2. 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機器秒級數據,甚至 500 台以下規模的集群的匯總運行情況。
  3. 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
  4. 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定制邏輯。例如定制規則管理、適配動態數據源等。

開源生態

Sentinel 分為兩個部分:

  • 核心庫(Java 客戶端)不依賴任何框架/庫,能夠運行於所有 Java 運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。
  • 控制台(Dashboard)基於 Spring Boot 開發,打包后可以直接運行,不需要額外的 Tomcat 等應用容器。

1.2. 快速開始

1.2.1. 公網接入

  • 根據該步驟,建立demo

  • 控制台效果圖

1.3. 手動接入

  1. 引入pom
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.6.2</version>
</dependency>
  1. 定義資源:也就是對哪個資源進行流量控制,現在已經提供了注解形式,所以新的接入直接用注解,@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寫入

  1. 這個功能是可以有的,只是官方文檔我沒找到,我直接debug源碼查看哪里可以把apollo寫入加進去,果然發現它是提供了寫入接口的
  2. 寫入接口為WritableDataSource,參考上面的完整代碼

1.5. 控制台

  1. 下載啟動控制台,下載地址
  2. 啟動命令java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

登錄名密碼都是sentinel

1.6. 工作原理

  1. 官方說明

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端口號映射需要一樣


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM