Spring Cloud 系列之 Netflix Hystrix 服務容錯(二)

本篇文章爲系列文章,未讀第一集的同學請猛戳這裏:Spring Cloud 系列之 Netflix Hystrix 服務容錯(一)

本篇文章講解 Hystrix 服務隔離中的線程池隔離與信號量隔離。


服務隔離

點擊鏈接觀看:服務隔離視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

線程池隔離

沒有線程池隔離的項目所有接口都運行在一個 ThreadPool 中,當某一個接口壓力過大或者出現故障時,會導致資源耗盡從而影響到其他接口的調用而引發服務雪崩效應。我們在模擬高併發場景時也演示了該效果。

通過每次都開啓一個單獨線程運行。它的隔離是通過線程池,即每個隔離粒度都是個線程池,互相不干擾。線程池隔離方式,等於多了一層的保護措施,可以通過 hytrix 直接設置超時,超時後直接返回。

隔離前

隔離後

優點:

  • 使用線程池隔離可以安全隔離依賴的服務(例如圖中 A、C、D 服務),減少所依賴服務發生故障時的影響面。比如 A 服務發生異常,導致請求大量超時,對應的線程池被打滿,這時並不影響 C、D 服務的調用。
  • 當失敗的服務再次變得可用時,線程池將清理並立即恢復,而不需要一個長時間的恢復。
  • 獨立的線程池提高了併發性

缺點:

  • 請求在線程池中執行,肯定會帶來任務調度、排隊和上下文切換帶來的 CPU 開銷。
  • 因爲涉及到跨線程,那麼就存在 ThreadLocal 數據的傳遞問題,比如在主線程初始化的 ThreadLocal 變量,在線程池線程中無法獲取。

點擊鏈接觀看:線程池隔離視頻(獲取更多請關注公衆號「哈嘍沃德先生」)

添加依賴

服務消費者 pom.xml 添加 hystrix 依賴。

<!-- spring-cloud netflix hystrix 依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

業務層

服務消費者業務層代碼添加線程隔離規則。

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查詢商品列表
     *
     * @return
     */
    // 聲明需要服務容錯的方法
    // 線程池隔離
    @HystrixCommand(groupKey = "order-productService-listPool",// 服務名稱,相同名稱使用同一個線程池
            commandKey = "selectProductList",// 接口名稱,默認爲方法名
            threadPoolKey = "order-productService-listPool",// 線程池名稱,相同名稱使用同一個線程池
            commandProperties = {
                    // 超時時間,默認 1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000")
            },
            threadPoolProperties = {
                    // 線程池大小
                    @HystrixProperty(name = "coreSize", value = "6"),
                    // 隊列等待閾值(最大隊列長度,默認 -1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"),
                    // 線程存活時間,默認 1min
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出隊列等待閾值執行拒絕策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            }, fallbackMethod = "selectProductListFallback")
    @Override
    public List<Product> selectProductList() {
        System.out.println(Thread.currentThread().getName() + "-----selectProductList-----");
        // ResponseEntity: 封裝了返回數據
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }
    
    // 託底數據
    private List<Product> selectProductListFallback() {
        System.out.println("-----selectProductListFallback-----");
        return Arrays.asList(
                new Product(1, "託底數據-華爲手機", 1, 5800D),
                new Product(2, "託底數據-聯想筆記本", 1, 6888D),
                new Product(3, "託底數據-小米平板", 5, 2020D)
        );
    }

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    // 聲明需要服務容錯的方法
    // 線程池隔離
    @HystrixCommand(groupKey = "order-productService-singlePool",// 服務名稱,相同名稱使用同一個線程池
            commandKey = "selectProductById",// 接口名稱,默認爲方法名
            threadPoolKey = "order-productService-singlePool",// 線程池名稱,相同名稱使用同一個線程池
            commandProperties = {
                    // 超時時間,默認 1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", 
                            value = "5000")
            },
            threadPoolProperties = {
                    // 線程池大小
                    @HystrixProperty(name = "coreSize", value = "3"),
                    // 隊列等待閾值(最大隊列長度,默認 -1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"),
                    // 線程存活時間,默認 1min
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出隊列等待閾值執行拒絕策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            })
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName() + "-----selectProductById-----");
        return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
    }

}

@HystrixCommand 註解各項參數說明如下:

啓動類

服務消費者啓動類開啓熔斷器註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

// 開啓熔斷器註解 2 選 1,@EnableHystrix 封裝了 @EnableCircuitBreaker
// @EnableHystrix
@EnableCircuitBreaker
@SpringBootApplication
public class OrderServiceRestApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceRestApplication.class, args);
    }

}

測試

服務提供者接口添加 Thread.sleep(2000),模擬服務處理時長。

JMeter 開啓 20 線程循環 50 次訪問:http://localhost:9090/order/1/product/list

瀏覽器訪問:http://localhost:9090/order/1/product 控制檯打印結果如下:

hystrix-order-productService-listPool-1-----selectProductList-----
hystrix-order-productService-listPool-4-----selectProductList-----
hystrix-order-productService-listPool-2-----selectProductList-----
hystrix-order-productService-listPool-3-----selectProductList-----
hystrix-order-productService-singlePool-1-----selectProductById-----
hystrix-order-productService-listPool-5-----selectProductList-----
hystrix-order-productService-listPool-6-----selectProductList-----

信號量隔離

每次調用線程,當前請求通過計數信號量進行限制,當信號量大於了最大請求數 maxConcurrentRequests 時,進行限制,調用 fallback 接口快速返回。信號量的調用是同步的,也就是說,每次調用都得阻塞調用方的線程,直到結果返回。這樣就導致了無法對訪問做超時(只能依靠調用協議超時,無法主動釋放)。

添加依賴

服務消費者 pom.xml 添加 hystrix 依賴。

<!-- spring-cloud netflix hystrix 依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

業務層

服務消費者業務層代碼添加信號量隔離規則。

package com.example.service.impl;

import com.example.pojo.Product;
import com.example.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 查詢商品列表
     *
     * @return
     */
    // 聲明需要服務容錯的方法
    // 信號量隔離
    @HystrixCommand(commandProperties = {
            // 超時時間,默認 1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                    value = "5000"),
            // 信號量隔離
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,
                    value = "SEMAPHORE"),
            // 信號量最大併發,調小一些方便模擬高併發
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
                    value = "6")
    }, fallbackMethod = "selectProductListFallback")
    @Override
    public List<Product> selectProductList() {
        // ResponseEntity: 封裝了返回數據
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }).getBody();
    }

    // 託底數據
    private List<Product> selectProductListFallback() {
        System.out.println("-----selectProductListFallback-----");
        return Arrays.asList(
                new Product(1, "託底數據-華爲手機", 1, 5800D),
                new Product(2, "託底數據-聯想筆記本", 1, 6888D),
                new Product(3, "託底數據-小米平板", 5, 2020D)
        );
    }

}

@HystrixCommand 註解各項參數說明如下:

啓動類

服務消費者啓動類開啓熔斷器註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

// 開啓熔斷器註解 2 選 1,@EnableHystrix 封裝了 @EnableCircuitBreaker
// @EnableHystrix
@EnableCircuitBreaker
@SpringBootApplication
public class OrderServiceRestApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceRestApplication.class, args);
    }

}

測試

服務提供者接口添加 Thread.sleep(2000),模擬服務處理時長。

服務消費者信號量最大併發設置爲 6,方便模擬高併發。

JMeter 開啓 20 線程循環 50 次訪問:http://localhost:9090/order/1/product/list

瀏覽器也訪問:http://localhost:9090/order/1/product/list 結果如下:

線程池隔離 vs 信號量隔離

隔離方式 是否支持超時 是否支持熔斷 隔離原理 是否是異步調用 資源消耗
線程池隔離 支持 支持 每個服務單獨用線程池 支持同步或異步
信號量隔離 不支持 支持 通過信號量的計數器 同步調用,不支持異步

線程池隔離

  • 請求線程和調用 Provider 線程不是同一條線程

  • 支持超時,可直接返回;

  • 支持熔斷,當線程池到達最大線程數後,再請求會觸發 fallback 接口進行熔斷;

  • 隔離原理:每個服務單獨用線程池;

  • 支持同步和異步兩種方式;

  • 資源消耗大,大量線程的上下文切換、排隊、調度等,容易造成機器負載高;

  • 無法傳遞 Http Header。

信號量隔離

  • 請求線程和調用 Provider 線程是同一條線程
  • 不支持超時;
  • 支持熔斷,當信號量達到 maxConcurrentRequests 後。再請求會觸發 fallback 接口進行熔斷;
  • 隔離原理:通過信號量的計數器;
  • 同步調用,不支持異步;
  • 資源消耗小,只是個計數器;
  • 可以傳遞 Http Header。

總結

  • 請求併發大,耗時長(計算大,或操作關係型數據庫),採用線程隔離策略。這樣可以保證大量的線程可用,不會由於服務原因一直處於阻塞或等待狀態,快速失敗返回。還有就是對依賴服務的網絡請求的調用和訪問,會涉及 timeout 這種問題的都使用線程池隔離。
  • 請求併發大,耗時短(計算小,或操作緩存),採用信號量隔離策略,因爲這類服務的返回通常會非常的快,不會佔用線程太長時間,而且也減少了線程切換的開銷,提高了緩存服務的效率。還有就是適合訪問不是對外部依賴的訪問,而是對內部的一些比較複雜的業務邏輯的訪問,像這種訪問系統內部的代碼,不涉及任何的網絡請求,做信號量的普通限流就可以了,因爲不需要去捕獲 timeout 類似的問題,併發量突然太高,稍微耗時一些導致很多線程卡在這裏,所以進行一個基本的資源隔離和訪問,避免內部複雜的低效率的代碼,導致大量的線程被夯住。

下一篇我們講解 Hystrix 的服務熔斷和服務降級以及基於 Feign 的服務熔斷處理,記得關注噢~

本文采用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議

大家可以通過 分類 查看更多關於 Spring Cloud 的文章。


🤗 您的點贊轉發是對我最大的支持。

📢 掃碼關注 哈嘍沃德先生「文檔 + 視頻」每篇文章都配有專門視頻講解,學習更輕鬆噢 ~

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