Spring Cloud - 使用 Hystrix 實現斷路器



簡介

在分散式環境中,服務需要相互通訊。通訊可以是同步的,也可以是非同步的。當服務同步通訊時,可能有多種原因導致通訊中斷。例如:

  • 被呼叫服務不可用 - 被呼叫的服務由於某種原因(例如:bug、部署等)而宕機。

  • 被呼叫服務響應時間過長 - 被呼叫的服務由於負載過高、資源消耗過大或正在初始化服務而導致響應緩慢。

在任何一種情況下,對於呼叫方來說,等待被呼叫方響應都是浪費時間和網路資源的。更有意義的做法是讓服務退避,並在一段時間後再次呼叫被呼叫服務,或者共享預設響應。

Netflix Hystrix 和 Resilence4j 是兩種著名的斷路器,用於處理此類情況。在本教程中,我們將使用 Hystrix。

Hystrix - 依賴設定

讓我們使用之前用過的餐廳案例。讓我們向呼叫客戶服務的餐廳服務新增hystrix 依賴。首先,讓我們使用以下依賴更新服務的pom.xml

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
   <version>2.7.0.RELEASE</version>
</dependency>

然後,使用正確的註解,即 @EnableHystrix,註解我們的 Spring 應用程式類。

package com.tutorialspoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@EnableHystrix
public class RestaurantService{
   public static void main(String[] args) {
      SpringApplication.run(RestaurantService.class, args);
   }
}

注意事項

  • @ EnableDiscoveryClient@EnableFeignCLient - 我們已經在上一章中瞭解了這些註解。

  • @EnableHystrix - 此註解掃描我們的包,並查詢使用 @HystrixCommand 註解的方法。

Hystrix 命令註解

完成後,我們將重用之前在餐廳服務中為客戶服務類定義的 Feign 客戶端,這裡不需要做任何修改:

package com.tutorialspoint;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "customer-service")
public interface CustomerService {
   @RequestMapping("/customer/{id}")
   public Customer getCustomerById(@PathVariable("id") Long id);
}

現在,讓我們在此定義服務實現類,該類將使用 Feign 客戶端。這將是對 feign 客戶端的簡單包裝。

package com.tutorialspoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class CustomerServiceImpl implements CustomerService {
   @Autowired
   CustomerService customerService;
   @HystrixCommand(fallbackMethod="defaultCustomerWithNYCity")
   public Customer getCustomerById(Long id) {
      return customerService.getCustomerById(id);
   }
   // assume customer resides in NY city
   public Customer defaultCustomerWithNYCity(Long id) {
      return new Customer(id, null, "NY");
   }
}

現在,讓我們瞭解上面程式碼中的幾個要點:

  • HystrixCommand 註解 - 負責包裝函式呼叫(即 getCustomerById)並在其周圍提供一個代理。然後,代理透過各種鉤子提供控制我們對客戶服務呼叫的方法。例如,請求超時、請求池化、提供回退方法等。

  • 回退方法 - 當 Hystrix 確定被呼叫服務存在問題時,我們可以指定要呼叫的方法。此方法需要與被註解的方法具有相同的簽名。在我們的例子中,我們決定將資料提供給紐約市的控制器。

此註解提供的一些有用選項:

  • 錯誤閾值百分比 - 在斷路器跳閘(即呼叫回退方法)之前允許請求失敗的百分比。這可以透過使用 cicutiBreaker.errorThresholdPercentage 來控制。

  • 在超時後放棄網路請求 - 如果被呼叫服務(在本例中為客戶服務)速度緩慢,我們可以設定超時時間,在此時間之後我們將放棄請求並轉到回退方法。這可以透過設定execution.isolation.thread.timeoutInMilliseconds來控制。

最後,這是我們呼叫CustomerServiceImpl的控制器。

package com.tutorialspoint;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RestaurantController {
   @Autowired
   CustomerServiceImpl customerService;
   static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
   static{
      mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC"));
      mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO"));
      mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC"));
      mockRestaurantData.put(3L, new Restaurant(4, "Pizeeria", "NY"));
   }
   @RequestMapping("/restaurant/customer/{id}")
   public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long
id)
{
   System.out.println("Got request for customer with id: " + id);
   String customerCity = customerService.getCustomerById(id).getCity();
   return mockRestaurantData.entrySet().stream().filter(
      entry -> entry.getValue().getCity().equals(customerCity))
      .map(entry -> entry.getValue())
      .collect(Collectors.toList());
   }
}

斷路器跳閘/開啟

現在我們已經完成了設定,讓我們試一試。這裡簡單介紹一下背景,我們將執行以下操作:

  • 啟動 Eureka 伺服器

  • 啟動客戶服務

  • 啟動餐廳服務,該服務將在內部呼叫客戶服務。

  • 對餐廳服務進行 API 呼叫

  • 關閉客戶服務

  • 對餐廳服務進行 API 呼叫。由於客戶服務已關閉,這將導致失敗,最終將呼叫回退方法。

現在讓我們編譯餐廳服務程式碼並使用以下命令執行:

java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar

此外,啟動客戶服務和 Eureka 伺服器。請注意,這些服務沒有任何更改,它們與上一章中看到的一樣。

現在,讓我們嘗試查詢位於華盛頓特區的 Jane 的餐廳。

{
   "id": 1,
   "name": "Jane",
   "city": "DC"
}

為此,我們將訪問以下 URL:https://:8082/restaurant/customer/1

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

所以,這裡沒有什麼新內容,我們得到了位於華盛頓特區的餐廳。現在,讓我們進入有趣的部分,即關閉客戶服務。您可以透過按下 Ctrl+C 或簡單地殺死 shell 來執行此操作。

現在讓我們再次訪問相同的 URL:https://:8082/restaurant/customer/1

{
   "id": 4,
   "name": "Pizzeria",
   "city": "NY"
}

從輸出中可以看出,我們得到了紐約的餐廳,儘管我們的客戶來自華盛頓特區。這是因為我們的回退方法返回了一個位於紐約的虛擬客戶。雖然沒有用,但以上示例顯示了回退方法按預期被呼叫。

將快取與 Hystrix 整合

為了使上述方法更有用,我們可以在使用 Hystrix 時整合快取。當底層服務不可用時,這可能是一種提供更好答案的有用模式。

首先,讓我們建立一個服務的快取版本。

package com.tutorialspoint;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class CustomerServiceCachedFallback implements CustomerService {
   Map<Long, Customer> cachedCustomer = new HashMap<>();
   @Autowired
   CustomerService customerService;
   @HystrixCommand(fallbackMethod="defaultToCachedData")
   public Customer getCustomerById(Long id) {
      Customer customer = customerService.getCustomerById(id);
      // cache value for future reference
      cachedCustomer.put(customer.getId(), customer);
      return customer;
   }
   // get customer data from local cache
   public Customer defaultToCachedData(Long id) {
      return cachedCustomer.get(id);
   }
}

我們使用 hashMap 作為儲存來快取資料。這是為了開發目的。在生產環境中,我們可能希望使用更好的快取解決方案,例如 Redis、Hazelcast 等。

現在,我們只需要更新控制器中的一行程式碼來使用上述服務:

@RestController
class RestaurantController {
   @Autowired
   CustomerServiceCachedFallback customerService;
   static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
   …
}

我們將按照與上面相同的步驟操作:

  • 啟動 Eureka 伺服器。

  • 啟動客戶服務。

  • 啟動餐廳服務,該服務在內部呼叫客戶服務。

  • 對餐廳服務進行 API 呼叫。

  • 關閉客戶服務。

  • 對餐廳服務進行 API 呼叫。由於客戶服務已關閉,但資料已快取,我們將獲得一組有效的資料。

現在,讓我們按照步驟 3 之前的相同過程操作。

現在訪問 URL:https://:8082/restaurant/customer/1

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

所以,這裡沒有什麼新內容,我們得到了位於華盛頓特區的餐廳。現在,讓我們進入有趣的部分,即關閉客戶服務。您可以透過按下 Ctrl+C 或簡單地殺死 shell 來執行此操作。

現在讓我們再次訪問相同的 URL:https://:8082/restaurant/customer/1

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

從輸出中可以看出,我們得到了華盛頓特區的餐廳,這正是我們期望的,因為我們的客戶來自華盛頓特區。這是因為我們的回退方法返回了快取的客戶資料。

將 Feign 與 Hystrix 整合

我們瞭解瞭如何使用 @HystrixCommand 註解來跳閘斷路器並提供回退。但是,我們還必須另外定義一個服務類來包裝我們的 Hystrix 客戶端。但是,我們也可以透過簡單地將正確的引數傳遞給 Feign 客戶端來實現相同的效果。讓我們嘗試這樣做。為此,首先透過新增回退類更新我們對 CustomerService 的 Feign 客戶端。

package com.tutorialspoint;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "customer-service", fallback = FallBackHystrix.class)
public interface CustomerService {
   @RequestMapping("/customer/{id}")
   public Customer getCustomerById(@PathVariable("id") Long id);
}

現在,讓我們為 Feign 客戶端添加回退類,當 Hystrix 斷路器跳閘時將呼叫該類。

package com.tutorialspoint;
import org.springframework.stereotype.Component;
@Component
public class FallBackHystrix implements CustomerService{
   @Override
   public Customer getCustomerById(Long id) {
      System.out.println("Fallback called....");
      return new Customer(0, "Temp", "NY");
   }
}

最後,我們還需要建立application-circuit.yml來啟用 hystrix。

spring:
   application:
      name: restaurant-service
server:
   port: ${app_port}
eureka:
   client:
      serviceURL:
         defaultZone: https://:8900/eureka
feign:
   circuitbreaker:
      enabled: true

現在,我們已經完成了設定,讓我們測試一下。我們將按照以下步驟操作:

  • 啟動 Eureka 伺服器。

  • 我們不啟動客戶服務。

  • 啟動餐廳服務,該服務將在內部呼叫客戶服務。

  • 對餐廳服務進行 API 呼叫。由於客戶服務已關閉,我們將注意到回退。

假設步驟 1 已完成,讓我們轉到步驟 3。讓我們編譯程式碼並執行以下命令:

java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar --
spring.config.location=classpath:application-circuit.yml

現在讓我們嘗試訪問:https://:8082/restaurant/customer/1

由於我們沒有啟動客戶服務,因此將呼叫回退,並且回退會將紐約作為城市傳送,這就是為什麼我們在以下輸出中看到紐約的餐廳。

{
   "id": 4,
   "name": "Pizzeria",
   "city": "NY"
}

此外,為了確認,在日誌中,我們將看到:

….
2021-03-13 16:27:02.887 WARN 21228 --- [reakerFactory-1]
.s.c.o.l.FeignBlockingLoadBalancerClient : Load balancer does not contain an
instance for the service customer-service
Fallback called....
2021-03-13 16:27:03.802 INFO 21228 --- [ main]
o.s.cloud.commons.util.InetUtils : Cannot determine local hostname
…..
廣告

© . All rights reserved.