分佈式服務熔斷降級限流利器至Hystrix

全文概覽

[TOC]

爲什麼需要hystrix

hystrix官網地址github

  • Hystrix同樣是netfix公司在分佈式系統中的貢獻。同樣的也進入的不維護階段。不維護不代表被淘汰。只能說明推陳出新技術在不斷迭代。曾今的輝煌曾經的設計還是值得我們去學習的。

  • 在分佈式環境中,服務調度是特色也是頭疼的一塊。在服務治理章節我們介紹了服務治理的功能。前一課我們也介紹了ribbon、feign進行服務調用。現在自然的到了服務監控管理了。hystrix就是對服務進行隔離保護。以實現服務不會出現連帶故障。導致整個系統不可用

image-20210414145650113

  • 如上圖所示,當多個客戶端進行服務調用Aservice時,而在分佈式系統中Aservice存在三臺服務,其中Aservice某些邏輯需要Bservice處理。Bservice在分佈式系統中部署了兩臺服務。這個時候因爲網絡問題導致Aservice中有一臺和Bservice的通信異常。如果Bservice是做日誌處理的。在整個系統看來日誌丟了和系統宕機比起來應該無所謂了。但是這個時候因爲網絡通信問題導致Aservice整個服務不可用了。有點得不嘗試。

image-20210414153046542

  • 在看上圖 。 A-->B-->C-->D 。此時D服務宕機了。C因爲D宕機出現處理異常。但是C的線程卻還在爲B響應。這樣隨着併發請求進來時,C服務線程池出現爆滿導致CPU上漲。在這個時候C服務的其他業務也會受到CPU上漲的影響導致響應變慢。

特色功能

Hystrix是一個低延遲和容錯的第三方組件庫。旨在隔離遠程系統、服務和第三方庫的訪問點。官網上已經停止維護並推薦使用resilience4j。但是國內的話我們有springcloud alibaba。

Hystrix 通過隔離服務之間的訪問來實現分佈式系統中延遲及容錯機制來解決服務雪崩場景並且基於hystrix可以提供備選方案(fallback)。

  • 對網絡延遲及故障進行容錯
  • 阻斷分佈式系統雪崩
  • 快速失敗並平緩恢復
  • 服務降級
  • 實時監控、警報

$$ 99.99^{30} = 99.7% \quad uptime \ 0.3% \quad of \quad 1 \quad billion \quad requests \quad = \quad 3,000,000 \quad failures \ 2+ \quad hours \quad downtime/month \quad even \quad if \quad all \quad dependencies \quad have \quad excellent \quad uptime. $$

  • 上面試官網給出的一個統計。在30臺服務中每臺出現異常的概覽是0.01%。一億個請求就會有300000失敗。這樣換算下每個月至少有2小時停機。這對於互聯網系統來說是致命的。

image-20210414164135471

  • 上圖是官網給出的兩種情況。和我們上章節的類似。都是介紹服務雪崩的場景。

項目準備

  • 在openfeign專題中我們就探討了基於feign實現的服務熔斷當時說了內部就是基於hystrix。當時我們也看了pom內部的結構在eureka中內置ribbon的同時也內置了hystrix模塊。

image-20210414165134167

  • 雖然包裏面包含了hystrix 。我們還是引入對應的start開啓相關配置吧。這裏其實就是在openfeign專題中的列子。在那個專題我們提供了PaymentServiceFallbackImpl、PaymentServiceFallbackFactoryImpl兩個類作爲備選方案。不過當時我們只需指出openfeign支持設置兩種方式的備選方案。今天我們

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

    演示下傳統企業沒有備選方案的情況會發生什麼災難。

image-20210414171119461

image-20210414171529143

接口測試

  • 首先我們對payment#createByOrder接口進行測試。查看下響應情況

    image-20210415103254641

  • 在測試payment#getTimeout/id方法。

    image-20210415103254641

    • 現在我們用jemeter來壓測payment#getTimeOut/id這個接口。一位需要4S等待會照成資源消耗殆盡問題。這個時候我們的payment#createByOrder也會被阻塞。

    image-20210415103254641

    • spring中默認的tomcat的最大線程數是200.爲了保護我們辛苦的筆記本。這裏我們將線程數設置小點。這樣我們更容易復現線程被打滿的情況。線程滿了就會影響到payment#createByOrder接口。

    image-20210415103018785

  • 上面我們壓測的是payment的原生接口。如果壓測的是order模塊。如果沒有在openfeign中配置fallback。那麼order服務就會因爲payment#getTimeOut/id接口併發導致線程滿了從而導致order模塊響應緩慢。這就是雪崩效應。下面我們從兩個方面來解決雪崩的發生。

業務隔離

  • 上面的場景發生是因爲payment#createByOrder 和payment#getTimeOut/id同屬於payment服務。一個payment服務實際上就是一個Tomcat服務。同一個tomcat服務是有一個線程池的。 每次請求落到該tomcat 服務裏就會去線程池中申請線程。獲取到線程了才能由線程來處理請求的業務。就是因爲tomcat內共享線程池。所以當payment#getTimeOut/id併發上來後就會搶空線程池。導致別的藉口甚至是毫不相關的接口都沒有資源可以申請。只能乾巴巴的等待資源的釋放。

  • 這就好比上班高峯期乘坐電梯因爲某一個公司集中上班導致一段時間電梯全部被使用了。這時候國家領導過來也沒辦法上電梯。

  • 我們也知道這種情況很好解決。每個園區都會有專用電梯供特殊使用。

  • 我們解決上述問題也是同樣的思路。進行隔離。不同的接口有不同的線程池。這樣就不會造成雪崩。

線程隔離

image-20210415112638886

  • 還記得我們上面爲了演示併發將order模塊的最大線程數設置爲10.這裏我們通過測試工具調用下order/getpayment/1這個接口看看日誌打印情況

    image-20210415142629374

  • 我們接口調用的地方將當前線程打印出來。我們可以看到一隻都是那10個線程在來回的使用。這也是上面爲什麼會造成雪崩現象。

	@HystrixCommand(
            groupKey = "order-service-getPaymentInfo",
            commandKey = "getPaymentInfo",
            threadPoolKey = "orderServicePaymentInfo",
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000")
            },
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize" ,value = "6"),
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")

            },
            fallbackMethod = "getPaymentInfoFallback"
    )
    @RequestMapping(value = "/getpayment/{id}",method = RequestMethod.GET)
    public ResultInfo getPaymentInfo(@PathVariable("id") Long id) {
        log.info(Thread.currentThread().getName());
        return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id, ResultInfo.class);
    }
    public ResultInfo getPaymentInfoFallback(@PathVariable("id") Long id) {
        log.info("已經進入備選方案了,下面交由自由線程執行"+Thread.currentThread().getName());
        return new ResultInfo();
    }
  @HystrixCommand(
            groupKey = "order-service-getpaymentTimeout",
            commandKey = "getpaymentTimeout",
            threadPoolKey = "orderServicegetpaymentTimeout",
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "10000")
            },
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize" ,value = "3"),
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")

            }
    )
    @RequestMapping(value = "/getpaymentTimeout/{id}",method = RequestMethod.GET)
    public ResultInfo getpaymentTimeout(@PathVariable("id") Long id) {
        log.info(Thread.currentThread().getName());
        return orderPaymentService.getTimeOut(id);
    }
  • 這裏演示效果不好展示,我就直接展示數據吧。
併發量在getpaymentTimeout getpaymentTimeout/{id} /getpayment/{id}
20 三個線程打滿後一段時間開始報錯 可以正常響應;也會慢,cpu線程切換需要時間
30 同上 同上
50 同上 也會超時,因爲order調用payment服務壓力會受影響
  • 如果我們將hystrix加載payment原生服務就不會出現上面第三條情況。爲什麼我會放在order上就是想讓大家看看雪崩的場景。在併發50的時候因爲payment設置的最大線程也是10,他本身也是有吞吐量的。在order#getpyament/id接口雖然在order模塊因爲hystrix線程隔離有自己的線程運行,但是因爲原生服務不給力導致自己調用超時從而影響運行的效果。這樣演示也是爲了後續引出fallback解決雪崩的一次場景模擬吧。
  • 我們可以在payment服務中通過hystrix設置fallback。保證payment服務低延遲從而保證order模塊不會因爲payment自己緩慢導致order#getpayment這種正常接口異常。
  • 還有一點雖然通過hystrix進行線程隔離了。但是我們在運行其他接口時響應時間也會稍長點。因爲CPU在進行線程切換的時候是有開銷的。這一點也是痛點。我們並不能隨心所欲的進行線程隔離的。這就引出我們的信號量隔離了。

信號量隔離

  • 關於信號量隔離這裏也就不演示了。演示的意義不是很大
   @HystrixCommand(
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
                    @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value = "SEMAPHORE"),
                    @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value = "6")
            },
            fallbackMethod = "getPaymentInfoFallback"
    )
  • 我們如上配置表示信號量最大爲6 。 表示併發6之後就會進行等待。等待超時時間未1s。
措施 優點 缺點 超時 熔斷 異步
線程隔離 一個調用一個線程池;互相不干擾;保證高可用 cpu線程切換開銷
信號量隔離 避免CPU切換。高效 在高併發場景下需要存儲信號量變大 × ×
  • 除了線程隔離、信號量隔離等隔離手段我們可以通過請求合併、接口數據緩存等手段加強穩定性。

服務降級

觸發條件

  • 程序發生除HystrixBadRequestException異常。
  • 服務調用超時
  • 服務熔斷
  • 線程池、信號量不夠

image-20210415180423428

  • 在上面我們的timeout接口。不管是線程隔離還是信號量隔離在條件滿足的時候就會直接拒絕後續請求。這樣太粗暴了。上面我們也提到了fallback。

  • 還記的上面我們order50個併發的timeout的時候會導致getpayment接口異常,當時定位了是因爲原生payment服務壓力撐不住導致的。如果我們在payment上加入fallback就能保證在資源不足的時候也能快速響應。這樣至少能保證order#getpayment方法的可用性。

    image-20210415180423428

    • 但是這種配置屬於實驗性配置。在真實生產中我們不可能在每個方法上配置fallback的。這樣愚蠢至極。

    • hystrix除了在方法上特殊定製的fallback以外,還有一個全局的fallback。只需要在類上通過@DefaultProperties(defaultFallback = "globalFallback")來實現全局的備選方案。一個方法滿足觸發降級的條件時如果該請求對應的HystrixCommand註解中沒有配置fallback則使用所在類的全局fallback。如果全局也沒有則拋出異常。

      不足

      • 雖然DefaultProperties 可以避免每個接口都配置fallback。但是這種的全局好像還不是全局的fallback。我們還是需要每個類上配置fallback。筆者查閱了資料好像也沒有
      • 但是在openfeign專題裏我們說了openfeign結合hystrix實現的服務降級功能。還記的裏面提到了一個FallbackFactory這個類嗎。這個類可以理解成spring的BeanFactory。這個類是用來產生我們所需要的FallBack的。我們在這個工廠裏可以生成一個通用類型的fallback的代理對象。代理對象可以根據代理方法的方法簽名進行入參和出參。
      • 這樣我們可以在所有的openfeign地方配置這個工廠類。這樣的話就避免的生成很多個fallback。 美中不足的還是需要每個地方都指定一下。關於FallBackFactory感興趣的可以下載源碼查看或者進主頁查看openfeign專題。

服務熔斷

  @HystrixCommand(
            commandProperties = {
                    @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),  //是否開啓斷路器
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),   //請求次數
                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),  //時間範圍
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), //失敗率達到多少後跳閘
            },
            fallbackMethod = "getInfoFallback"
    )
    @RequestMapping(value = "/get", method = RequestMethod.GET)
    public ResultInfo get(@RequestParam Long id) {
        if (id < 0) {
            int i = 1 / 0;
        }
        log.info(Thread.currentThread().getName());
        return orderPaymentService.get(id);
    }
    public ResultInfo getInfoFallback(@RequestParam Long id) {

        return new ResultInfo();
    }
  • 首先我們通過circuitBreaker.enabled=true開啓熔斷器
  • circuitBreaker.requestVolumeThreshold設置統計請求次數
  • circuitBreaker.sleepWindowInMilliseconds 設置時間滑動單位 , 在觸發熔斷後多久進行嘗試開放,及俗稱的半開狀態
  • circuitBreaker.errorThresholdPercentage 設置觸發熔斷開關的臨界條件
  • 上面的配置如果最近的10次請求錯誤率達到60% ,則觸發熔斷降級 , 在10S內都處於熔斷狀態服務進行降級。10S後半開嘗試獲取服務最新狀態
  • 下面我們通過jmeter進行接口http://localhost/order/get?id=-1進行20次測試。雖然這20次無一例額外都會報錯。但是我們會發現一開始報錯是因爲我們代碼裏的錯誤。後面的錯誤就是hystrix熔斷的錯誤了。一開始試by zero 錯誤、後面就是short-circuited and fallback failed 熔斷錯誤了

  • 正常我們在hystrix中會配置fallback , 關於fallback兩種方式我們上面降級章節已經實現了。這裏是爲了方便看到錯誤的不同特意放開了。

image-20210421163842061

  • 在HystrixCommand中配置的參數基本都是在HystrixPropertiesManager對象中。我們可以看到關於熔斷器的配置有6個參數。基本就是我們上面的四個配置

服務限流

  • 服務降級我們上面提到的兩種隔離就是實現限流的策略。

請求合併

  • 除了熔斷、降級、限流意外hystrix還爲我們提供了請求合併。顧名思義就是將多個請求合併成一個請求已達到降低併發的問題。
  • 比如說我們order有個接個是查詢當個訂單信息order/getId?id=1 突然有一萬個請求過來。爲了緩解壓力我們集中一下請求每100個請求調用一次order/getIds?ids=xxxxx 。這樣我們最終到payment模塊則是10000/100=100個請求。下面我們通過代碼配置實現下請求合併。

HystrixCollapser

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HystrixCollapser {
    String collapserKey() default "";

    String batchMethod();

    Scope scope() default Scope.REQUEST;

    HystrixProperty[] collapserProperties() default {};
}
屬性 含義
collapserKey 唯一標識
batchMethod 請求合併處理方法。即合併後需要調用的方法
scope 作用域;兩種方式[REQUEST, GLOBAL] ; <br />REQUEST : 在同一個用戶請求中達到條件將會合並<br />GLOBAL : 任何線程的請求都會加入到這個全局統計中
HystrixProperty[] 配置相關參數

image-20210422094851902

  • 在Hystrix中所有的properties配置都會在HystrixPropertiesManager.java中。我們在裏面可以找到Collapser只有兩個相關的配置。分別表示最大請求數和統計時間單元。
	@HystrixCollapser(
            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,
            batchMethod = "getIds",
            collapserProperties = {
                    @HystrixProperty(name = HystrixPropertiesManager.MAX_REQUESTS_IN_BATCH , value = "3"),
                    @HystrixProperty(name = HystrixPropertiesManager.TIMER_DELAY_IN_MILLISECONDS, value = "10")
            }
    )
    @RequestMapping(value = "/getId", method = RequestMethod.GET)
    public ResultInfo getId(@RequestParam Long id) {
        if (id < 0) {
            int i = 1 / 0;
        }
        log.info(Thread.currentThread().getName());
        return null;
    }
    @HystrixCommand
    public List<ResultInfo> getIds(List<Long> ids) {
        System.out.println(ids.size()+"@@@@@@@@@");
        return orderPaymentService.getIds(ids);
    }
  • 上面我們配置了getId會走getIds請求,最多是10S三個請求會合並在一起。然後getIds有payment服務在分別去查詢最終返回多個ResultInfo。

  • 我們通過jemeter進行getId接口壓測,日誌中ids的長度最大是3 。 驗證了我們上面getId接口的配置。這樣就能保證在出現高併發的時候會進行接口合併降低TPS。

  • 上面我們是通過請求方法註解進行接口合併處理。實際上內部hystrix是通過HystrixCommand

工作流程

image-20210421171613835

  • 官網給出的流程圖示,並配備流程說明一共是9部。下面我們就翻譯下。

  • ①、創建HystrixCommand或者HystrixObservableCommand對象

    • HystrixCommand : 用在依賴單個服務上
    • HystrixObservableCommand : 用在依賴多個服務上
  • ②、命令執行,hystrrixCommand 執行execute、queue ; hystrixObservableCommand執行observe、toObservable

方法 作用
execute 同步執行;返回結果對象或者異常拋出
queue 異步執行;返回Future對象
observe 返回Observable對象
toObservable 返回Observable對象
  • ③、查看緩存是否開啓及是否命中緩存,命中則返回緩存響應
  • ④、是否熔斷, 如果已經熔斷則fallback降級;如果熔斷器是關閉的則放行
  • ⑤、線程池、信號量是否有資源供使用。如果沒有足夠資源則fallback 。 有則放行
  • ⑥、執行run或者construct方法。這兩個是hystrix原生的方法,java實現hystrix會實現兩個方法的邏輯,springcloud已經幫我們封裝了。這裏就不看這兩個方法了。如果執行錯誤或者超時則fallback。在此期間會將日誌採集到監控中心。
  • ⑦、計算熔斷器數據,判斷是否需要嘗試放行;這裏統計的數據會在hystrix.stream的dashboard中查看到。方便我們定位接口健康狀態
  • ⑧、在流程圖中我們也能看到④、⑤、⑥都會指向fallback。 也是我們俗稱的服務降級。可見服務降級是hystrix熱門業務啊。
  • ⑨、返回響應

HystrixDashboard

  • hystrix 除了服務熔斷、降級、限流以外,還有一個重要的特性是實時監控。並形成報表統計接口請求信息。

  • 關於hystrix的安裝也很簡單,只需要在項目中配置actutor和hystrix-dashboard兩個模塊就行了

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
  • 啓動類上添加EnableHystrixDashboard 就引入了dashboard了。 我們不需要進行任何開發。這個和eureka一樣主需要簡單的引包就可以了。

image-20210422161743942

  • 就這樣dashboard搭建完成了。dashboard主要是用來監控hystrix的請求處理的。所以我們還需要在hystrix請求出將端點暴露出來。

  • 在使用了hystrix命令的模塊加入如下配置即可,我就在order模塊加入

@Component
public class HystrixConfig {
    @Bean
    public ServletRegistrationBean getServlet(){
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        //注意這裏配置的/hystrix.stream  最終訪問地址就是 localhost:port/hystrix.stream ; 如果在配置文件中配置在新版本中是需要
        //加上actuator  即 localhost:port/actuator
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}
  • 然後我們訪問order模塊localhost/hystrix.stream 就會出現ping的界面。表示我們order模塊安裝監控成功。當然order也需要actuator模塊
  • 下面我們通過jmeter來壓測我們的熔斷、降級、限流接口在通過dashboard來看看各個狀態吧。

  • 上面的動畫看起來我們的服務還是很忙的。想想如果是電商當你看着每個接口的折線圖像不像就是你的心跳。太高的你就擔心的。太低了就沒有成就高。下面我們看看dashboard的指標詳情

image-20210422163536850

  • 我們看看我們服務運行期間各個接口的現狀。

image-20210422161504688

聚合監控

  • 上面我們通過新建的模塊hystrix-dashboard 來對我們的order模塊進行監控。但是實際應用中我們不可能只在order中配置hystrix的。
  • 我們只是在上面爲了演示所以在order配置的。現在我們在payment中也對hystrix中配置。那麼我們就需要在dashboard中來回切換order、payment的監控數據了。
  • 所以我們的聚合監控就來了。在進行聚合監控之前我們先將payment也引入hystrix。注意上面我們是通過bean方式注入hystrix.stream 的 。 訪問前綴不需要actuator

新建hystrix-turbine

pom

<!--新增hystrix dashboard-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
        </dependency>
  • 主要就是新增turbine座標,其他的就是hystrix , dashboard等模塊,具體查看結尾處源碼

yml

spring:
  application:
    name: cloud-hystrix-turbine

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    prefer-ip-address: true

# 聚合監控

turbine:
  app-config: cloud-order-service,cloud-payment-service
  cluster-name-expression: "'default'"
  # 該處配置和url一樣。如果/actuator/hystrix.stream 的則需要配置actuator
  instanceUrlSuffix: hystrix.stream

啓動類

啓動類上添加EnableTurbine註解

image-20210423093456668




源碼

上述源碼

image-20210414153359481

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