springCloud(11):使用Hystrix實現微服務的容錯處理-簡介與實現

一、問題引入

如果服務提供者響應非常緩慢,那麼消費者對提供者的請求就會被強制等待,直到提供者響應或超時。在高負載場景下,如果不作任何處理,此類問題可能會導致服務消費者的資源耗盡甚至整個系統的崩潰。

1.1、雪崩效應

微服務架構的應用系統通常包含多個服務層。微服務之間通過網絡進行通信,從而支撐起整個應用系統,因此,微服務之間難免存在依賴關係。任何微服務都並非100%可用,網絡往往也很脆弱,因此難免有些請求會失敗。

我們通常把“基礎服務故障”導致“級聯故障”的現象稱爲雪崩效應。雪崩效應描述的是提供者不可用導致消費者不可用,並將不可用逐漸放大的過程。

1.2、如何容錯

要想防止雪崩效應,必須有一個強大的容錯機制。該容錯機制需要實現以下兩點:

a、爲網絡請求設置超時:爲每個網絡請求設置超時,讓資源儘快釋放。

b、使用斷路器模式

我們可通過以上兩點機制保護應用,從而防止雪崩效應並提升應用的可用性。

1.3、斷路器模式簡介

斷路器可理解爲對容易導致錯誤的操作的代理。這種代理能夠統計一段時間內調用失敗的次數,並決定是正常請求依賴的服務還是直接返回。

斷路器可以實現快速失敗,如果它在一段時間內檢測到許多類似的錯誤(例如超時),就會在之後的一段時間內,強迫對改服務的調用快速失敗,即不再請求所依賴的服務。這樣應用程序就無須再浪費CPU時間去等待長時間的超時。

斷路器也可自動診斷依賴的服務是否已經恢復正常,如果發現依賴的服務已經恢復正常,那麼就會恢復請求該服務,使用這種方式,就可以實現微服務的“自我修復”。

當依賴的服務不正常時打開斷路器時快速失敗,從而防止雪崩效應;當發現依賴的服務恢復正常時,又會恢復請求。

斷路器狀態轉換圖,如下:

 wKioL1ltflPyBbzUAAHdXsFZ2q0584.png

說明:

 1、正常情況下,斷路器關閉,可正常請求依賴的服務。

 2、當一段時間內,請求失敗率達到一定闊值(如錯誤率達到50%,或100次/分鐘等),斷路器就會打開。此時,不會再去請求依賴的服務。

 3、斷路器打開一段時間後,會自動進入“半開”狀態。此時,斷路器可允許一個請求訪問依賴的服務。如果該請求能夠成功調用,則關閉斷路器;否則繼續保持打開狀態。

二、Hystrix簡介

hystrix是一個實現了超時機制和斷路器模式的工具類庫。

2.1、簡介

hystrix是由Netflix開源的一個延遲和容錯庫,用於隔離訪問遠程系統、服務、或者第三方庫,防止級聯失敗,從而提升系統的可用性與容錯性。

hystrix主要通過以下幾點實現容錯和延遲:

包裹請求

 使用hystrixCommand(或hystrixObservableCommand)包裹對依賴的調用邏輯,每個命令在獨立線程中執行。這使用到了設計模式中的“命令模式”。

跳閘機制

 當某服務的錯誤率超過一定闊值時,hystrix可以自動或手動跳閘,停止請求該服務一段時間。

資料隔離

 hystrix爲每個依賴都維護了一個小型的線程池(或信號量)。如果該線程池已滿,發往該依賴的請求就會被立即拒絕,而不是排隊等待,從而加速失敗判定。

監控

 hystrix可以近乎實時的監控運行指標和配置的變化,例如成功、失敗、超時以及被拒絕的請求等

回退機制

 當請求失敗、超時、被拒絕,或當斷路器打開時,執行回退邏輯。回退邏輯可由開發人員自行提供,例如返回一個缺省值。

自我修復

 斷路器打開一段時間後,會自動進入“半開”狀態。

三、使用hystrix實現容錯

3.1、通用方式整合Hystris

複製工程spring-ribbon-eureka-client,改名爲spring-hystrix-consumer

3.1.1、添加依賴

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

3.1.2、在啓動類上加上註解@EnableHystrix

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableHystrix
@EnableEurekaClient
@SpringBootApplication
public class SpringHystrixConsumerApplication {
    @Bean
  @LoadBalanced
  public RestTemplate restTemplate(){
      return new RestTemplate();
  }
  public static void main(String[] args) {
      SpringApplication.run(SpringHystrixConsumerApplication.class, args);
  }
}

3.1.3、修改UserController.java

package com.example.demo.controller;

import com.example.demo.pojo.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * 用戶controller
 *
 * @Author: 我愛大金子
 * @Description: 用戶controller
 * @Date: Create in 11:07 2017/7/10
 */
@RestController
public class UserController {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @HystrixCommand(fallbackMethod = "findByIdFallback")    // 指定回退方法findByIdFallback
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id) throws Exception {
        ServiceInstance serviceInstance = this.loadBalancerClient.choose("spring-ribbon-eureka-client2");
        // 打印當前選擇的是哪個節點
        System.out.println("serviceId : " + serviceInstance.getServiceId());
        System.out.println("hoost : " + serviceInstance.getHost());
        System.out.println("port : " + serviceInstance.getPort());
        System.out.println("============================================================");

        if (null == id) {
            return null;
        }
        return this.restTemplate.getForObject(" + id, User.class);
    }

    /**回退方法*/
  public User findByIdFallback(Long id) {
      User user = new User();
    user.setId(-1L);
    user.setName("默認用戶");
    return user;
  }

  @GetMapping("/log-instance")
  public void serviceInstance() throws Exception {
        ServiceInstance serviceInstance = this.loadBalancerClient.choose("spring-ribbon-eureka-client2");
    // 打印當前選擇的是哪個節點
    System.out.println("serviceId : " + serviceInstance.getServiceId());
    System.out.println("hoost : " + serviceInstance.getHost());
    System.out.println("port : " + serviceInstance.getPort());
    System.out.println("============================================================");
  }
}

說明:爲findById方法編寫了一個回退方法findByIdFallback,該方法與findById方法具有相同的參數與返回值類型。

3.1.4、效果

訪問:http://localhost:8086/user/1 

 wKioL1ltsvWDjU6SAABDYPCWPy4488.jpg

關閉spring-ribbon-eureka-client2服務,再次訪問

 wKioL1lts0OAMlfgAAAVLusig6s529.jpg

3.1.5、@HystrixCommand註解說明

在findById方法上,使用註解@HystrixCommand的fallbackMethod屬性,指定回退方法是findByIdFallback。註解@HystrixCommand由名爲javanica的Hystrix contrib庫提供。javanica是一個Hystrix的子項目,用於簡化Hystrix的使用。Spring cloud自動將Spring bean與該註解封裝在一個連接到Hystrix斷路器的代理中。


@HystrixCommand的配置非常靈活,可使用註解@HystrixCommand的commandProperties屬性來配置@HystrixProperty。如:

@HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value="5000"),      // 超時時間,默認1000ms
  @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value="10000")           // 設置關於HystrixCommand執行需要的統計信息,默認10000毫秒
} )    // 指定回退方法findByIdFallback
@GetMapping("/user1/{id}")
public User findById1(@PathVariable Long id) throws Exception {
    ServiceInstance serviceInstance = this.loadBalancerClient.choose("spring-ribbon-eureka-client2");
  // 打印當前選擇的是哪個節點
  System.out.println("serviceId : " + serviceInstance.getServiceId());
  System.out.println("hoost : " + serviceInstance.getHost());
  System.out.println("port : " + serviceInstance.getPort());
  System.out.println("============================================================");

  if (null == id) {
      return null;
  }
  return this.restTemplate.getForObject(" + id, User.class);
}

Hystrix配置屬性詳情請查看Hystrix Wiki: https://github.com/Netflix/Hystrix/wiki/Configuration 

@HystrixCommand註解詳情請查看:https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica 


疑問:

 我們知道,請請求失敗,被拒絕、超時或者斷路器打開時,都會進入回退方法。但進入回退方法並不意味着斷路器已經被打開。那麼,如何才能明確瞭解短路器當前的狀態呢?

3.2、Feign使用Hystrix

前面的都是使用註解@HystrixCommand的fallbackMethod屬性實現回退的。然而,Feign是以接口的形式工作的,它沒有方法體,顯然前面的方式並不適用於Feign。那麼feign如果整合Hystrix呢?

事實上,Spring Cloud默認已爲Feign整合了Hystrix,只要Hystrix在項目的classpath中,Feign默認就會用斷路器包裹所有的方法。

3.2.1、爲feign添加回退

1、複製項目spring-feign-consumer,改名爲spring-hystrix-feign。

2、修改Feign接口

package com.example.demo.feign;

import com.example.demo.pojo.User;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Feign接口
 *
 * @Author: 我愛大金子
 * @Description: Feign接口
 * @Date: Create in 10:14 2017/7/17
 */
@FeignClient(name = "spring-ribbon-eureka-client2", fallback = FeignClientFallback.class)
public interface UserFeignClient {
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
  public User findById(@PathVariable("id") Long id) throws Exception;
}

/**
 * 回退類FeignClientFallback需要實現Feign接口
 * @Author: 我愛大金子
 * @Description: 描述
 * @Date: 17:40 2017/7/18
 */
@Component
class FeignClientFallback implements UserFeignClient {
    @Override
  public User findById(Long id) throws Exception {
      User user = new User();
    user.setId(-1L);
    user.setName("默認用戶");
    return user;
  }
}

注意:在新版中feign默認沒有開啓Hystrix的功能,開啓配置如下:

feign:
    hystrix:
        enabled: true     # 開啓Feign的Hystrix的功能


測試:

 1、啓動spring-ribbon-eureka-client2服務,訪問http://localhost:8086/user/1,結果正常,如下:

  wKioL1luyuCy1_tGAAAVvCbAZ_8746.png 

 2、關閉spring-ribbon-eureka-client2服務,訪問http://localhost:8086/user/1,結果如下:

  wKioL1luzcuzt7sIAAAUwUZIrWE706.png

3.2.2、通過FallbackFactory檢查回退原因

很多場景下,需要了解回退的原因,此時可使用註解@FeignClient的fallbackFactory屬性。

1、複製上面工程,改名爲spring-hystrix-feign-fallback-factory

2、修改feign接口

package com.example.demo.feign;

import com.example.demo.pojo.User;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Feign接口
 *
 * @Author: 我愛大金子
 * @Description: Feign接口
 * @Date: Create in 10:14 2017/7/17
 */
@FeignClient(name = "spring-ribbon-eureka-client2", fallbackFactory = FeignClientFallbackFactory.class)
public interface UserFeignClient {
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
  public User findById(@PathVariable("id") Long id) throws Exception;
}

/**
 * 回退類FeignClientFallback需要實現Feign接口
 * @Author: 我愛大金子
 * @Description: 描述
 * @Date: 17:40 2017/7/18
 */
@Component
class FeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
    private static final Logger log = LoggerFactory.getLogger(FeignClientFallbackFactory.class);
  @Override
  public UserFeignClient create(Throwable throwable) {
      return new UserFeignClient(){
            @Override
      public User findById(Long id) throws Exception {
          FeignClientFallbackFactory.log.info("fallback : ", throwable);
        User user = new User();
        user.setId(-1L);
        user.setName("默認用戶");
        return user;
      }
    };
  }
}

3、測試

wKiom1lu2K_jxG_FAACO_ZdWZIo780.png

3.2.3、爲Feign禁用Hystrix

在Spring cloud中,只要Hystrix在項目中的classpath中,Feign就會使用斷路器包裹Feign客戶端的所有方法。這樣雖然方便,但有些場景下並不需要該功能。

爲指定的Feign客戶端禁用Hystrix

藉助Feign的自定義配置,可輕鬆爲指定名稱的Feign客戶端禁用Hystrix。如:

@Configuration
@ExcludeFromComponentScan
public class FeignConfiguration {
    @Bean
  @Scope("prototype")
  public Feign.Builder feignBuilder() {
      return Feign.builder();
  }
}

想要禁用Hystrix的@FeignClient引用改配置即可,如:

@FeignClient(name = "spring-ribbon-eureka-client2", configuration = FeignConfiguration.class)
public interface UserFeignClient {
    // ...
}


全局禁用Hystrix

只須在application.yml中配置feign.hystrix.enabled=false即可。


四、深入理解Hystrix

4.1、Hystrix斷路器的狀態監控

記得我們以前爲項目引入過Actuator:

<dependency>
    <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

斷路器的狀態也暴露在Actuator提供的/health端點中,這樣就可以直觀的瞭解斷路器的狀態

實驗:

 1、啓動spring-ribbon-eureka-client2服務,訪問http://localhost:8086/user/1,可以正常獲取結果 

 2、訪問http://localhost:8086/manage/health,結果如下:

   wKiom1ltwT3QwMMAAAAViIZ95yo551.png

   可以看到此時,Hystrix的狀態是UP,也就是一切正常,此時斷路器時關閉的。

 3、關閉spring-ribbon-eureka-client2服務,訪問http://localhost:8086/user/1,可以獲取如下結果:

   wKiom1ltwjzTZYt4AAAUlKHrFqA824.png 

 4、訪問http://localhost:8086/manage/health,結果如下:

   wKiom1ltwT3QwMMAAAAViIZ95yo551.png

  我們發現,儘管執行了回退邏輯,返回了默認用戶,但此時Hystrix的狀態依然是UP,這是因爲我們的失敗率還沒有達到闊值(默認是5秒20次失敗)。再次強調----執行回退邏輯並不代表斷路器已經打開。請求失敗、超時、被拒絕以及斷路器打開時都會執行回退邏輯。

 5、持續快速的訪問http://localhost:8086/user/1,知道http://localhost:8086/manage/health的結果如下:

  wKioL1ltw-qDs6V5AAAb5OD9I54234.png

  可以看到hystrix的狀態是CIRCUIT_OPEN,說明短路器已經打開

4.2、Hystrix線程隔離策略與傳播上下文

hystrix的隔離策略有兩種:線程隔離和信號量隔離

線程隔離(THREAD):使用該方式,HystrixCommand將會在單獨的線程上執行,併發請求受線程池中的線程數量的限制。

信號量隔離(SEMAPHORE):使用該方式,HystrixCommand將會在調用線程上執行,開銷相對較小,併發請求受信號量個數的限制。


hystrix中默認並且推薦使用線程隔離,因爲這種方式有一個除網絡超時以外的額外保護層。


一般來說,只有當調用負載非常高時(如:每個實例每秒調用數百次)才需要使用信號量隔離,因爲這種場景下使用線程隔離開銷會比較大。信號量隔離一般僅適用於非網絡調用的隔離。

可使用execution.isolation.strategy屬性指定隔離策略。


設置信號量隔離策略:

@HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties = {
    @HystrixProperty(name = "execution.isolation.strategy", value="SEMAPHORE"),      // 設置信號量隔離
})
@GetMapping("/user2/{id}")
public User findById2(@PathVariable Long id) throws Exception {
    // ...
}


總結:

 1、hystrix的隔離策略有THREAD、SEMAPHORE兩種,默認使用THREAD。

 2、正常情況下,保持默認即可。

 3、如果發生找不到上下文的運行時異常,可考慮將隔離級別設置爲SEMAPHORE


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