RPC服務熔斷、限流、降級等容災技術

本篇博客主要包括:限流方式、微服務容災技術選型、Sentinel的使用、Sentinel動態規則持久化Zookeeper 等。

Sentinel動態規則持久化到Zookeeper的代碼實現我已經共享到本人的github,有需要的可以下載使用。

github地址:https://github.com/Force-King/sentinel-dashboard-zk

持久化zk的sentinel-dashboard項目下載地址:https://github.com/Force-King/sentinel-dashboard-zk.git

 

一、限流方式

服務端限流

如果在服務端做限流,無論有多少個客戶端,總的提供能力是固定的,所以不會因爲客戶端數量過多而導致資源不足,因爲處理不過來的請求會被阻塞等待獲取資源。

缺點

缺點也比較明顯,由於服務提供者整體設置了最大限流數,此時所有的客戶端共享同一份限流數據,那麼有可能導致有的服務能分配到資源有些服務請求分配不到資源導致無法請求的情況。

 

客戶端限流

客戶端限流解決上服務端限流提到的問題,它能保證每個客戶端都能得到響應。但是從其它方面考慮,必須針對不同的客戶端做不同的限流策略:

  • 請求量大,但時效性不高,此時將限流數控制小一些會比較合適
  • 請求量大,但時效性高,此時將限流數適當調高
  • 響應時間長,即慢接口,適當降低
  • 主流業務,核心業務,適當調高
  • 非主流業務,適當降低

缺點

  • 如果客戶端的數量不固定,那麼有可能導致客戶端數量過多造成大量請求打到服務端導致處理不了的結果,所以需要嚴格監控客戶端的調用情況。
  • 配置複雜,需要針對每個客戶端做相對精準的判斷

 

二、選型

1. 目前開源容災框架有Hystrix、Sentinel、Resilience4j

 

Hystrix:常用於Spring Cloud的一個熔斷降級組件。主要功能是不同服務之間的資源隔離、失敗降級。底層實現是Rxjava。它提供兩種資源隔離的模式:信號量隔離和線程池隔離。一般使用線程池隔離。耗費一定資源,但相比之下支持超時和異步執行。聽起來可以覆蓋大部分場景,但它不支持更高要求的流控,如qps的控制。所以需要單獨採用令牌漏桶來做流量控制。

Sentinel:阿里開源的分佈式流量控制組件。支持流控、熔斷降級、系統保護等。所有的資源都對應一個資源名稱以及一個Entry。每一個Entry創建的時候,同時也會創建一系列插件(系統保護插件:SystemSlot、流控插件:FlowSlot、熔斷降級插件LDegradeSlot等)。每個插件會監控自己職責範圍內的指標。NodeSelectorSlot將各個資源的調用路徑以樹狀存儲,用於限流降級。調用者通過創建上下文、請求token來執行方法。若沒有拋出BlockException,表示請求成功。它支持併發數/qps的流量控制、也支持熔斷降級。

Resilience4j:一個比較輕量的熔斷降級庫。模塊化做的比較好,將每個功能點(如熔斷、限速器、自動重試)都拆成了單獨的模塊,這樣整體結構很清晰,用戶也只需要引入相應功能的依賴即可;另外resilience4j 是針對 Java 8 和函數式編程設計的,API 比較簡潔優雅。同時與 Hystrix 相比,Resilience4j 增加了簡單的限速器和自動重試特性,使用場景更加豐富。Resilience4j 屬於一個新興項目,社區也在蓬勃發展。總的來說,Resilience4j 是比較輕量的庫,在較小較新的項目中使用還是比較方便的,但是 Resilience4j 只包含限流降級的基本場景,對於非常複雜的企業級服務架構可能無法很好地 cover 住;同時 Resilience4j 缺乏生產級別的配套設施(如提供規則管理和實時監控能力的控制檯)。

2. Sentinel 的優勢和特性

  • 輕量級,核心庫無多餘依賴,性能損耗小。
  • 方便接入,開源生態廣泛。

              Sentinel 對 Dubbo、Spring Cloud、Web Servlet、gRPC 等常用框架提供適配模塊,只需引入相應依賴並簡單配置即可快  速接入;同時針對自定 義的場景 Sentinel 還提供低侵入性的註解資源定義方式,方便自定義接入。

  • 豐富的流量控制場景。

              Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,流控維度包括流控指標、流控效果(塑形)、調用關係、熱點、集羣等各種維度,針對系統維度也提供自適應的保護機制。

  • 易用的控制檯,提供實時監控、機器發現、規則管理等能力。
  • 完善的擴展性設計,提供多樣化的 SPI 接口,方便用戶根據需求給 Sentinel 添加自定義的邏輯。

 

Sentinel 的主要特性

 

Sentinel 的開源生態

目前使用Sentinel 的公司

阿里巴巴、順豐、愛奇藝、vivo、每日優鮮、拼多多、易企秀、360金融、麻袋財富、喜馬拉雅FM 等。

Sentinel的性能

官方測試,單機25w qps以下基本沒啥影響,超過了這個值會有一個比較明顯的5%-10%的下降。

 

三、Sentinel 使用

1. maven 配置pom.xml

<properties>

<sentinel.version>1.6.2</sentinel.version>

</properties>

<!-- 限流降級 -->

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-core</artifactId>

<version>${sentinel.version}</version>

</dependency>

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-annotation-aspectj</artifactId>

<version>${sentinel.version}</version>

</dependency>

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-transport-simple-http</artifactId>

<version>${sentinel.version}</version>

</dependency>

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-datasource-zookeeper</artifactId>

<version>${sentinel.version}</version>

</dependency>



<!-- 熱點規則 -->

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-parameter-flow-control</artifactId>

<version>${sentinel.version}</version>

</dependency>

 

2. 添加AOP配置類

spring boot 項目:

@SpringBootConfiguration

public class AopConfiguration {

@Bean

public SentinelResourceAspect sentinelResourceAspect() {

return new SentinelResourceAspect();

}

}

spring 項目:

@Configuration

public class SentinelAspectConfiguration {

@Bean

public SentinelResourceAspect sentinelResourceAspect() {

return new SentinelResourceAspect();

}

}

3. 初始化

jdk 1.8:

Sentinel 初始化

jdk 1.7:

 

4. 使用註解在客戶端項目中進行埋點 

@RequestMapping(value = "/query", method = RequestMethod.GET)
@SentinelResource(value = "/query", entryType= EntryType.IN, blockHandler = "queryExceptionHandler", blockHandlerClass = {ControllerExceptionUtil.class})
public String query (@RequestParam(value = "appid", required = true) Integer appid,
                         @RequestParam(value = "idfa", required = true) String idfa){
  
     JSONObject obj = new JSONObject();
     try {
         int ret = 0;
         if(null != repeatService.findIdfa(appid,idfa)){
             ret = 1;
         }
         obj.put(idfa,ret);
     } catch (Exception e){
         logger.error(e);
         return JsonResult.buildEnum(RequestResultEnum.SERVER_EXP).toString();
     }
     return JsonResult.success(obj).toString();
}

 降級、限流處理類,如下:

 註解中blockHandlerClass的值就是上面的處理類,其中的方法就是blockHandler對應的值,名稱要一致。

service層使用:

5. 啓動配置

客戶端啓動參數:-Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.log.use.pid=true -Dproject.name=market

控制檯啓動:java -Dserver.port=8080 -Dcsp.sentinel.log.use.pid=true -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

查看控制檯訪問: http://localhost:8080/#/dashboard/home

用戶:sentinel

密碼:sentinel

控制檯顯示效果:

 在控制檯可以手動配置限流和降級規則:

如果不需要對特點請求來源做限流,來源應用就用默認即可,默認是指所有的應用。

 

四、Sentinel動態規則持久化Zookeeper

因爲目前阿里的Sentinel控制檯默認沒有持久化,是存在JVM內存,一旦控制檯重啓,之前配置的規則就會丟失。

不過控制檯提供了可擴展的接口,只要實現DynamicRulePublisher<T>接口的publish() 和 DynamicRuleProvider<T>接口getRules() 即可。

使用Zookeeper做配置中心,將限流規則、降級規則持久化到配置中心,配置中心統一push到集羣的每一臺機器。實現配置的一致性。

zookeeper配置:

#Zookeeper
zk.address=127.0.0.1:2181
zk.sentinel.path=/sentinel_rules
zk.sentinel.appName=/market

項目初始化加載配置中心的規則: 

@Component
public class ZookeeperSentinelConfig {

    private final Logger logger = LoggerFactory.getLogger(ZookeeperSentinelConfig.class);

    @Value("${zk.address}")
    private String zkAddress;
    @Value("${zk.sentinel.path}")
    private String zkPath;
    @Value("${zk.sentinel.appName}")
    private String appName;

    private final String FLOW_PATH = "/flow";
    private final String DEGRADE_PATH = "/degrade";

    @PostConstruct
    public void loadRules() {
        //限流Flow
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ZookeeperDataSource<>(zkAddress, zkPath + appName,
                source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                }));
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

        //降級Degrade
        String degradePath = zkPath + appName + DEGRADE_PATH;
        Converter<String, List<DegradeRule>> degradeRules = source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
        });
        ReadableDataSource<String, List<DegradeRule>> zkDataSourceDegrade = new ZookeeperDataSource<>(zkAddress, degradePath, degradeRules);
        DegradeRuleManager.register2Property(zkDataSourceDegrade.getProperty());
        logger.info("----------------- Sentinel DataSource Zookeeper Init Success -------------------");
    }


    public String getFlowRulePath(String appName) {
        if (appName.startsWith("/")) {
            return zkPath+appName+FLOW_PATH;
        } else {
            return zkPath + "/" + appName + FLOW_PATH;
        }
    }

    public String getDegradeRulePath(String appName) {
        if (appName.startsWith("/")) {
            return zkPath+appName+DEGRADE_PATH;
        } else {
            return zkPath + "/" + appName + DEGRADE_PATH;
        }
    }

    public String getZkAddress() {
        return zkAddress;
    }

    public String getZkPath() {
        return zkPath;
    }

}

getRules實現: 

@Component("flowRuleZookeeperProvider")
public class FlowRuleZookeeperProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    @Autowired
    private CuratorFramework zkClient;
    @Autowired
    private Converter<String, List<FlowRuleEntity>> converter;
    @Autowired
    private ZookeeperSentinelConfig zkConfig;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        String zkPath = zkConfig.getFlowRulePath(appName);
        byte[] bytes = zkClient.getData().forPath(zkPath);
        if (null == bytes || bytes.length == 0) {
            return new ArrayList<>();
        }
        String s = new String(bytes);

        return converter.convert(s);
    }
}

publish實現: 

@Component("flowRuleZookeeperPublisher")
public class FlowRuleZookeeperPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
    @Autowired
    private CuratorFramework zkClient;
    @Autowired
    private Converter<List<FlowRuleEntity>, String> converter;
    @Autowired
    private ZookeeperSentinelConfig zkConfig;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");

        String path = zkConfig.getFlowRulePath(app);
        Stat stat = zkClient.checkExists().forPath(path);
        if (stat == null) {
            zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
        }
        byte[] data = CollectionUtils.isEmpty(rules) ? "[]".getBytes() : converter.convert(rules).getBytes();
        zkClient.setData().forPath(path, data);
    }
}

如果使用持久化,Sentinel-dashboard需要修改代碼,限流Flow 使用FlowControllerV2,具體使用方法Sentinel的github上有,這裏就不再介紹了。需要注意的是,頁面的跳轉路徑需要修改。使用/v2,降級是Degrade, 也需要修改。

前端頁面需要修改 sidebar.html 的流控規則跳轉方法,identity.js 中的流控規則添加後跳轉路徑改爲/dashboard/v2/flow/,否則在簇點鏈路添加流控規則會有問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章