天天看點

深入 Hystrix 線程池隔離與接口限流

深入 Hystrix 線程池隔離與接口限流

前面講了 Hystrix 的 request cache 請求緩存、fallback 優雅降級、circuit breaker 斷路器快速熔斷,這一講,我們來詳細說說 Hystrix 的線程池隔離與接口限流。

深入 Hystrix 線程池隔離與接口限流

Hystrix 通過判斷線程池或者信号量是否已滿,超出容量的請求,直接 Reject 走降級,進而達到限流的作用。

限流是限制對後端的服務的通路量,比如說你對 MySQL、Redis、Zookeeper 以及其它各種後端中間件的資源的通路的限制,其實是為了避免過大的流量直接打死後端的服務。

線程池隔離技術的設計

Hystrix 采用了 Bulkhead Partition 艙壁隔離技術,來将外部依賴進行資源隔離,進而避免任何外部依賴的故障導緻本服務崩潰。

艙壁隔離,是說将船體内部空間區隔劃分成若幹個隔艙,一旦某幾個隔艙發生破損進水,水流不會在其間互相流動,如此一來船舶在受損時,依然能具有足夠的浮力和穩定性,進而減低立即沉船的危險。

深入 Hystrix 線程池隔離與接口限流

Hystrix 對每個外部依賴用一個單獨的線程池,這樣的話,如果對那個外部依賴調用延遲很嚴重,最多就是耗盡那個依賴自己的線程池而已,不會影響其他的依賴調用。

Hystrix 應用線程池機制的場景

  • 每個服務都會調用幾十個後端依賴服務,那些後端依賴服務通常是由很多不同的團隊開發的。
  • 每個後端依賴服務都會提供它自己的 client 調用庫,比如說用 thrift 的話,就會提供對應的 thrift 依賴。
  • client 調用庫随時會變更。
  • client 調用庫随時可能會增加新的網絡請求的邏輯。
  • client 調用庫可能會包含諸如自動重試、資料解析、記憶體中緩存等邏輯。
  • client 調用庫一般都對調用者來說是個黑盒,包括實作細節、網絡通路、預設配置等等。
  • 在真實的生産環境中,經常會出現調用者,突然間驚訝的發現,client 調用庫發生了某些變化。
  • 即使 client 調用庫沒有改變,依賴服務本身可能有會發生邏輯上的變化。
  • 有些依賴的 client 調用庫可能還會拉取其他的依賴庫,而且可能那些依賴庫配置的不正确。
  • 大多數網絡請求都是同步調用的。
  • 調用失敗和延遲,也有可能會發生在 client 調用庫本身的代碼中,不一定就是發生在網絡請求中。

簡單來說,就是你必須預設 client 調用庫很不靠譜,而且随時可能發生各種變化,是以就要用強制隔離的方式來確定任何服務的故障不會影響目前服務。

線程池機制的優點

  • 任何一個依賴服務都可以被隔離在自己的線程池内,即使自己的線程池資源填滿了,也不會影響任何其他的服務調用。
  • 服務可以随時引入一個新的依賴服務,因為即使這個新的依賴服務有問題,也不會影響其他任何服務的調用。
  • 當一個故障的依賴服務重新變好的時候,可以通過清理掉線程池,瞬間恢複該服務的調用,而如果是 tomcat 線程池被占滿,再恢複就很麻煩。
  • 如果一個 client 調用庫配置有問題,線程池的健康狀況随時會報告,比如成功/失敗/拒絕/逾時的次數統計,然後可以近實時熱修改依賴服務的調用配置,而不用停機。
  • 基于線程池的異步本質,可以在同步的調用之上,建構一層異步調用層。

簡單來說,最大的好處,就是資源隔離,確定說任何一個依賴服務故障,不會拖垮目前的這個服務。

線程池機制的缺點

  • 線程池機制最大的缺點就是增加了 CPU 的開銷。

    除了 tomcat 本身的調用線程之外,還有 Hystrix 自己管理的線程池。

  • 每個 command 的執行都依托一個獨立的線程,會進行排隊,排程,還有上下文切換。
  • Hystrix 官方自己做了一個多線程異步帶來的額外開銷統計,通過對比多線程異步調用+同步調用得出,Netflix API 每天通過 Hystrix 執行 10 億次調用,每個服務執行個體有 40 個以上的線程池,每個線程池有 10 個左右的線程。)最後發現說,用 Hystrix 的額外開銷,就是給請求帶來了 3ms 左右的延時,最多延時在 10ms 以内,相比于可用性和穩定性的提升,這是可以接受的。

我們可以用 Hystrix semaphore 技術來實作對某個依賴服務的并發通路量的限制,而不是通過線程池/隊列的大小來限制流量。

semaphore 技術可以用來限流和削峰,但是不能用來對調研延遲的服務進行 timeout 和隔離。

execution.isolation.strategy

 設定為 

SEMAPHORE

,那麼 Hystrix 就會用 semaphore 機制來替代線程池機制,來對依賴服務的通路進行限流。如果通過 semaphore 調用的時候,底層的網絡調用延遲很嚴重,那麼是無法 timeout 的,隻能一直 block 住。一旦請求數量超過了 semaphore 限定的數量之後,就會立即開啟限流。

接口限流 Demo

假設一個線程池大小為 8,等待隊列的大小為 10。timeout 時長我們設定長一些,20s。

在 command 内部,寫死代碼,做一個 sleep,比如 sleep 3s。

  • withCoreSize:設定線程池大小。
  • withMaxQueueSize:設定等待隊列大小。
  • withQueueSizeRejectionThreshold:這個與 withMaxQueueSize 配合使用,等待隊列的大小,取得是這兩個參數的較小值。

如果隻設定了線程池大小,另外兩個 queue 相關參數沒有設定的話,等待隊列是處于關閉的狀态。

public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {

    private Long productId;

    private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");

    public GetProductInfoCommand(Long productId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
                .andCommandKey(KEY)
                // 線程池相關配置資訊
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        // 設定線程池大小為8
                        .withCoreSize(8)
                        // 設定等待隊列大小為10
                        .withMaxQueueSize(10)
                        .withQueueSizeRejectionThreshold(12))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerEnabled(true)
                        .withCircuitBreakerRequestVolumeThreshold(20)
                        .withCircuitBreakerErrorThresholdPercentage(40)
                        .withCircuitBreakerSleepWindowInMilliseconds(3000)
                        // 設定逾時時間
                        .withExecutionTimeoutInMilliseconds(20000)
                        // 設定fallback最大請求并發數
                        .withFallbackIsolationSemaphoreMaxConcurrentRequests(30)));
        this.productId = productId;
    }

    @Override
    protected ProductInfo run() throws Exception {
        System.out.println("調用接口查詢商品資料,productId=" + productId);

        if (productId == -1L) {
            throw new Exception();
        }

        // 請求過來,會在這裡hang住3秒鐘
        if (productId == -2L) {
            TimeUtils.sleep(3);
        }

        String url = "http://localhost:8081/getProductInfo?productId=" + productId;
        String response = HttpClientUtils.sendGetRequest(url);
        System.out.println(response);
        return JSONObject.parseObject(response, ProductInfo.class);
    }

    @Override
    protected ProductInfo getFallback() {
        ProductInfo productInfo = new ProductInfo();
        productInfo.setName("降級商品");
        return productInfo;
    }
}      

我們模拟 25 個請求。前 8 個請求,調用接口時會直接被 hang 住 3s,那麼後面的 10 個請求會先進入等待隊列中等待前面的請求執行完畢。最後的 7 個請求過來,會直接被 reject,調用 fallback 降級邏輯。

@SpringBootTest
@RunWith(SpringRunner.class)
public class RejectTest {

    @Test
    public void testReject() {
        for (int i = 0; i < 25; ++i) {
            new Thread(() -> HttpClientUtils.sendGetRequest("http://localhost:8080/getProductInfo?productId=-2")).start();
        }
        // 防止主線程提前結束執行
        TimeUtils.sleep(50);
    }
}      

從執行結果中,我們可以明顯看出一共列印出了 7 個降級商品。這也就是請求數超過線程池+隊列的數量而直接被 reject 的結果。

ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
調用接口查詢商品資料,productId=-2
ProductInfo(id=null, name=降級商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
{"id": -2, "name": "iphone7手機", "price": 5599, "pictureList":"a.jpg,b.jpg", "specification": "iphone7的規格", "service": "iphone7的售後服務", "color": "紅色,白色,黑色", "size": "5.5", "shopId": 1, "modifiedTime": "2017-01-01 12:00:00", "cityId": 1, "brandId": 1}
// 後面都是一些正常的商品資訊,就不貼出來了
//...      

繼續閱讀