SpringCloud系列第06節之斷路器Hystrix
前言
微服務架構中,一般都存在著很多的服務單元
這樣就有可能出現一個單元因為網路原因或自身問題而出現故障或延遲,導致呼叫方的對外服務也出現延遲
如果此時呼叫方的請求不斷增加,時間一長就會出現由於等待故障方響應而形成任務積壓,最終導致呼叫方自身服務的癱瘓
為了解決這種問題:便出現了斷路器(或者叫熔斷器,Cricuit Breaker)模式
斷路器模式源於 Martin Fowler 的 Circuit Breaker 一文
我們日常生活中的斷路器,本身是一種開關裝置,用於在電路上保護線路過載
當線路中有電器發生短路時,它能夠及時切斷故障電路,防止發生過載、發熱、甚至起火等嚴重後果
而微服務架構中的斷路器,其作用是:當某個服務單元發生故障(類似用電器短路)之後
通過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個錯誤響應,而不是長時間的等待
這就不會使得執行緒被故障服務長時間佔用而不釋放,避免了故障在分散式系統中的蔓延
Hystrix的介紹
Hystrix 正是 Netflix 開源的
它是由 Java 實現的,用來處理分散式系統發生故障或延遲時的容錯庫
它提供了 斷路器、資源隔離、自我修復 三大功能
- 斷路器
實際可初步理解為快速失敗,快速失敗是防止資源耗盡的關鍵點
當 Hystrix 發現在過去某段時間內對服務 AA 的調用出錯率達到閥值時,它就會“熔斷”該服務
後續任何向服務 AA 的請求都會快速失敗,而不是白白讓呼叫執行緒去等待 - 資源隔離
首先,Hystrix 對每一個依賴服務都配置了一個執行緒池,對依賴服務的呼叫會線上程池中執行
比如,服務 AA 的執行緒池大小為20,那麼 Hystrix 會最多允許有20個容器執行緒呼叫服務 AA(超出20,它會拒絕並快速失敗)
這樣即使服務 AA 長時間未響應,容器最多也只能堵塞20個執行緒,剩餘的執行緒仍然可以處理使用者請求 - 自我修復
處於熔斷狀態的服務,在經過一段時間後,Hystrix 會讓其進入“半關閉”狀態(即允許少量請求通過)
然後統計呼叫的成功率,若每個請求都能成功,Hystrix 會恢復該服務,從而達到自我修復的效果
其中:在服務被熔斷到進入“半關閉”狀態之間的時間,就是留給開發人員排查錯誤並恢復故障的時間
Hystrix的隔離策略
Hystrix 基於命令模式 HystrixCommand 來包裝依賴呼叫邏輯,其每個命令在單獨執行緒中或訊號授權下執行
(Command 是在 Receiver 和 Invoker 之間新增的中間層,Command 實現了對 Receiver 的封裝)
Hystrix 支援兩種隔離策略:執行緒池隔離和訊號量隔離(都是限制對共享資源的併發訪問量)
- ThreadPool
根據配置把不同命令分配到不同的執行緒池中,這是比較常用的隔離策略,其優點是隔離性好,並且可以配置斷路
某個依賴被設定斷路之後,系統不會再嘗試新起執行緒執行它,而是直接提示失敗,或返回fallback值
它的缺點是新起執行緒執行命令,在執行時必然涉及上下文的切換,這會造成一定的效能消耗
但是 Netflix 做過實驗,這種消耗對比其帶來的價值是完全可以接受的,具體的資料參見 Hystrix-Wiki - Semaphores
顧名思義就是使用一個訊號量來做隔離
開發者可以限制系統對某一個依賴的最高併發數,這個基本上就是一個限流的策略
每次呼叫依賴時都會檢查一下是否到達訊號量的限制值,如達到,則拒絕
該策略的優點是不新起執行緒執行命令,減少上下文切換,缺點是無法配置斷路,每次都一定會去嘗試獲取訊號量
Hystrix的配置引數
Hystrix 的大部分配置都是 hystrix.command.[HystrixCommandKey] 開頭
其中 [HystrixCommandKey] 是可變的,預設是 default,即:hystrix.command.default(對於 Zuul 而言,CommandKey 就是 service id)
它常見的有以下幾個配置
-
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
用來設定 thread 和 semaphore 兩種隔離策略的超時時間,預設值是1000
建議設定這個引數,在 Hystrix-1.4.0 之前,semaphore-isolated 隔離策略是不能超時的,1.4.0 開始 semaphore-isolated 也支援超時時間了 -
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests
此值並非 TPS、QPS、RPS 等都是相對值,它指的是 1 秒時間視窗內的事務 / 查詢 / 請求,它是一個絕對值,無時間視窗
相當於亞毫秒級的,指任意時間點允許的併發數,當請求達到或超過該設定值後,其其餘就會被拒絕,預設值是100 -
hystrix.command.default.execution.timeout.enabled
是否開啟超時,預設為true -
hystrix.command.default.execution.isolation.thread.interruptOnTimeout
發生超時是是否中斷執行緒,預設是true -
hystrix.command.default.execution.isolation.thread.interruptOnCancel
取消時是否中斷執行緒,預設是false -
hystrix.command.default.circuitBreaker.requestVolumeThreshold
當在配置時間視窗內達到此數量的失敗後,進行短路,預設20個 -
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds
短路多久以後開始嘗試是否恢復,預設5s -
hystrix.command.default.circuitBreaker.errorThresholdPercentage
出錯百分比閾值,當達到此閾值後,開始短路,預設50% -
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests
呼叫執行緒允許請求 HystrixCommand.GetFallback() 的最大數量,預設10,超出時將會有異常丟擲
注意:該項配置對於 thread 隔離模式也起作用
以上就是列舉的一些常見配置,更多內容可參考:https://github.com/Netflix/Hystrix/wiki/Configuration
示例程式碼
示例程式碼如下(也可以直接從 Github 下載:https://github.com/v5java/demo-cloud-06-hystrix)
它是由四個模組組成的 Maven 工程,其中包含一個註冊中心、一個服務提供者、兩個服務消費者
這是公共的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jadyer.demo</groupId>
<artifactId>demo-cloud-06-hystrix</artifactId>
<version>1.1</version>
<packaging>pom</packaging>
<modules>
<module>service-client-01</module>
<module>service-client-02</module>
<module>service-discovery</module>
<module>service-server</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.5.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
註冊中心
這是註冊中心的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jadyer.demo</groupId>
<artifactId>demo-cloud-06-hystrix</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-discovery</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
這是註冊中心的配置檔案 /src/main/resources/application.yml
server:
port: 1100
eureka:
server:
enable-self-preservation: false # 關閉自我保護模式(預設為開啟)
eviction-interval-timer-in-ms: 1000 # 續期時間,即掃描失效服務的間隔時間(預設為60*1000ms)
client:
# 設定是否從註冊中心獲取註冊資訊(預設true)
# 因為這是一個單點的EurekaServer,不需要同步其它EurekaServer節點的資料,故設為false
fetch-registry: false
# 設定是否將自己作為客戶端註冊到註冊中心(預設true)
# 這裡為不需要(檢視@EnableEurekaServer註解的原始碼,會發現它間接用到了@EnableDiscoveryClient)
register-with-eureka: false
# 在未設定defaultZone的情況下,註冊中心在本例中的預設地址就是http://127.0.0.1:1100/eureka/
# 但奇怪的是,啟動註冊中心時,控制檯還是會列印這個地址的節點:http://localhost:8761/eureka/
# 而實際服務端註冊時,要使用1100埠的才能註冊成功,8761埠的會註冊失敗並報告異常
serviceUrl:
# 實際測試:若修改尾部的eureka為其它的,比如/myeureka,註冊中心啟動沒問題,但服務端在註冊時會失敗
# 報告異常:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
defaultZone: http://127.0.0.1:${server.port}/eureka/
這是註冊中心的 SpringBoot 啟動類 ServiceDiscoveryBootStrap.java
package com.jadyer.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
//建立服務註冊中心
@EnableEurekaServer
@SpringBootApplication
public class ServiceDiscoveryBootStrap {
public static void main(String[] args) {
SpringApplication.run(ServiceDiscoveryBootStrap.class, args);
}
}
服務提供方
這是服務提供方的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jadyer.demo</groupId>
<artifactId>demo-cloud-06-hystrix</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-server</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
</project>
這是服務提供方的配置檔案 /src/main/resources/application.yml
server:
port: 2100
spring:
application:
name: CalculatorServer # 指定釋出的微服務名(以後呼叫時,只需該名稱即可訪問該服務)
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true # 設定微服務呼叫地址為IP優先(預設為false)
lease-renewal-interval-in-seconds: 5 # 心跳時間,即服務續約間隔時間(預設為30s)
lease-expiration-duration-in-seconds: 15 # 發呆時間,即服務續約到期時間(預設為90s)
client:
healthcheck:
enabled: true # 開啟健康檢查(依賴spring-boot-starter-actuator)
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/ # 指定服務註冊中心的地址
這是服務提供方的 SpringBoot 啟動類 ServiceServerBootStarp.java
package com.jadyer.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 通過 @EnableEurekaClient 註解,為服務提供方賦予註冊和發現服務的能力
* ------------------------------------------------------------------------------------------------------------------
* 也可以使用org.springframework.cloud.client.discovery.@EnableDiscoveryClient註解
* 詳見以下兩篇文章的介紹
* http://cloud.spring.io/spring-cloud-static/Camden.SR3/#_registering_with_eureka
* https://spring.io/blog/2015/01/20/microservice-registration-and-discovery-with-spring-cloud-and-netflix-s-eureka
* ------------------------------------------------------------------------------------------------------------------
* Created by 玄玉<https://jadyer.cn/> on 2017/1/9 16:00.
*/
@EnableEurekaClient
@SpringBootApplication
public class ServiceServerBootStarp {
public static void main(String[] args) {
SpringApplication.run(ServiceServerBootStarp.class, args);
}
}
這是服務提供方暴露的數學運算服務 CalculatorController.java
package com.jadyer.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服務提供方暴露的數學運算服務
* Created by 玄玉<https://jadyer.cn/> on 2017/1/9 16:00.
*/
@RestController
public class CalculatorController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private DiscoveryClient client;
@RequestMapping("/add")
public int add(int a, int b){
//加運算
int result = a + b;
//輸出服務資訊
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("uri={},serviceId={},result={}", instance.getUri(), instance.getServiceId(), result);
//返回結果
return result;
}
}
服務消費方Ribbon的斷路
這是服務消費方Ribbon的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jadyer.demo</groupId>
<artifactId>demo-cloud-06-hystrix</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-client-01</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
</dependencies>
</project>
這是服務消費方Ribbon的配置檔案 /src/main/resources/application.yml
server:
port: 3100
spring:
application:
name: client-consumer-ribbon
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 15
client:
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/
這是服務消費方Ribbon的 SpringBoot 啟動類 ServiceClient01BootStarp.java
package com.jadyer.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
//@SpringCloudApplication
//開啟斷路器功能
@EnableCircuitBreaker
@EnableEurekaClient
@SpringBootApplication
public class ServiceClient01BootStarp {
//開啟軟均衡負載
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ServiceClient01BootStarp.class, args);
}
}
這是服務消費方Ribbon的,包含了斷路器配置的,遠端服務呼叫實現 CalculatorService.java
package com.jadyer.demo.ribbon;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@Service
class CalculatorService {
@Resource
private RestTemplate restTemplate;
//指定斷路後的回撥方法(回撥方法必須與原方法引數型別相同、返回值型別相同、方法名可以不同)
@HystrixCommand(fallbackMethod="addServiceToFallback")
int addService(int a, int b){
String reqURL = "http://CalculatorServer/add?a=" + a + "&b=" + b;
return restTemplate.getForEntity(reqURL, Integer.class).getBody();
}
public int addServiceToFallback(int aa, int bb){
return -999;
}
}
這是服務消費方Ribbon的呼叫示例 ConsumerController.java
package com.jadyer.demo.ribbon;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服務呼叫方
* Created by 玄玉<https://jadyer.cn/> on 2017/1/10 18:23.
*/
@RestController
@RequestMapping("/demo/ribbon")
public class ConsumerController {
@Resource
private CalculatorService calculatorService;
@RequestMapping("/toadd")
int toadd(int a, int b){
return calculatorService.addService(a, b);
}
}
服務消費方Feign的斷路
這是服務消費方Feign的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jadyer.demo</groupId>
<artifactId>demo-cloud-06-hystrix</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-client-02</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- spring-cloud-starter-feign的內部已經包含了spring-cloud-starter-ribbon和spring-cloud-starter-hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
</project>
這是服務消費方Feign的配置檔案 /src/main/resources/application.yml
server:
port: 3200
spring:
application:
name: client-consumer-feign
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 15
client:
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/
這是服務消費方Feign的 SpringBoot 啟動類 ServiceClient02BootStarp.java
package com.jadyer.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
//開啟Feign功能(無需顯式@EnableCircuitBreaker,其已含此功能)
@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication
public class ServiceClient02BootStarp {
public static void main(String[] args) {
SpringApplication.run(ServiceClient02BootStarp.class, args);
}
}
這是服務消費方Feign的,包含了斷路器配置的,遠端服務呼叫實現 CalculatorService.java
package com.jadyer.demo.feign;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
//繫結該介面到CalculatorServer服務,並通知Feign元件對該介面進行代理(不需要編寫介面實現)
@FeignClient(value="CalculatorServer", fallback=CalculatorService.HystrixCalculatorService.class)
public interface CalculatorService {
////@PathVariable這種也是支援的
//@RequestMapping(value="/add/{a}", method=RequestMethod.GET)
//int myadd(@PathVariable("a") int a, @RequestParam("b") int b);
//通過SpringMVC的註解來配置所繫結的服務下的具體實現
@RequestMapping(value="/add", method=RequestMethod.GET)
int myadd(@RequestParam("a") int a, @RequestParam("b") int b);
/**
* 這裡採用和SpringCloud官方文件相同的做法,把fallback類作為內部類放入Feign介面中
* http://cloud.spring.io/spring-cloud-static/Camden.SR6/#spring-cloud-feign-hystrix
* (也可以外面獨立定義該類,個人覺得沒必要,這種東西寫成內部類最合適)
*/
@Component
class HystrixCalculatorService implements CalculatorService {
@Override
public int myadd(@RequestParam("a") int a, @RequestParam("b") int b) {
return -999;
}
}
}
這是服務消費方Feign的呼叫示例 ConsumerController.java
package com.jadyer.demo.feign;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服務呼叫方
* Created by 玄玉<https://jadyer.cn/> on 2017/1/10 18:23.
*/
@RestController
@RequestMapping("/demo/feign")
public class ConsumerController {
@Resource
private CalculatorService calculatorService;
@RequestMapping("/toadd")
int toadd(int a, int b){
return calculatorService.myadd(a, b);
}
}
驗證
先不使用斷路器,然後啟動註冊中心、服務提供方、兩個服務消費方,然後分別訪問以下兩個介面
http://10.16.64.133:3100/demo/ribbon/toadd?a=11&b=22
http://10.16.64.133:3200/demo/feign/toadd?a=11&b=22
我們會發現都正常的返回了計算結果:33
然後停掉服務提供方,再訪問兩個介面,我們會看到下面的報警內容
# Ribbon會報告如下內容
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Apr 15 11:12:48 CST 2017
There was an unexpected error (type=Internal Server Error, status=500).
I/O error on GET request for "http://CalculatorServer/add": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect
# Feign會報告如下內容
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Apr 15 11:12:48 CST 2017
There was an unexpected error (type=Internal Server Error, status=500).
CalculatorService#myadd(int,int) timed-out and no fallback available.
然後我們再啟用斷路器,並訪問兩個介面(此時服務提供方是關閉的),都會看到該應答: