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 …..