斷路器Hystrix
Hystrix是Netflix實現的斷路器,其github地址是https://github.com/Netflix/Hystrix。當對一個服務的調用次數超過了circuitBreaker.requestVolumeThreshold
(默認是20),且在指定的時間窗口metrics.rollingStats.timeInMilliseconds
(默認是10秒)內,失敗的比例達到了circuitBreaker.errorThresholdPercentage
(默認是50%),則斷路器會被打開,斷路器打開後接下來的請求是不會調用真實的服務的,默認的開啓時間是5秒(由參數circuitBreaker.sleepWindowInMilliseconds
控制)。在斷路器打開或者服務調用失敗時,開發者可以爲此提供一個fallbackMethod,此時將轉爲調用fallbackMethod。Spring Cloud提供了對Hystrix的支持,使用時在應用服務的pom.xml中加入spring-cloud-starter-netflix-hystrix
依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
加入了依賴後,需要通過@EnableCircuitBreaker
啓用對斷路器的支持。
@SpringBootApplication
@EnableCircuitBreaker
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
假設有如下服務,在請求/hello/error
時每次都會報錯,且每次都會成功的訪問到error()
,這是因爲此時沒有聲明需要使用斷路器。
@RestController
@RequestMapping("hello")
public class HelloController {
private int count;
@GetMapping("error")
public String error() {
System.out.println(++count + " Error. " + LocalDateTime.now());
throw new IllegalStateException();
}
}
通過在error()
添加@HystrixCommand
可以聲明該方法需要使用斷路器保護,此時在指定的時間窗口內連續訪問error()
達到一定次數後,後續的請求將不再調用error()
,因爲此時斷路器已經是打開的狀態了。
@RestController
@RequestMapping("hello")
public class HelloController {
private int count;
@HystrixCommand
@GetMapping("error")
public String error() {
System.out.println(++count + " Error. " + LocalDateTime.now());
throw new IllegalStateException();
}
}
fallback
在斷路器打開或者服務調用出錯的情況下,可以回退到調用fallbackMethod。下面的代碼指定了當error()
調用出錯時將回退到調用fallback()
方法,error()
方法將count加1,fallback()
也會將count加1,所以在調用error()
時你會看到當斷路器沒有打開時,每次返回的count是加2的,因爲error()
加了一次,fallback()
又加了一次,而當斷路器打開後,每次返回的count只會在fallback()
中加1。
private int count;
@HystrixCommand(fallbackMethod="fallback")
@GetMapping("error")
public String error() {
String result = ++count + " Error. " + LocalDateTime.now();
System.out.println(result);
if (count % 5 != 0) {
throw new IllegalStateException();
}
return result;
}
public String fallback() {
return ++count + "result from fallback.";
}
在fallbackMethod中你可以進行任何你想要的邏輯,可以是進行遠程調用、訪問數據庫等,也可以是返回一些容錯的靜態數據。
指定的fallbackMethod必須與服務方法具有同樣的簽名。同樣的簽名的意思是它們的方法返回類型和方法參數個數和類型都一致,比如下面的服務方法String error(String param)
,接收String類型的參數,返回類型也是String,它指定的fallbackMethod fallbackWithParam
也必須接收String類型的參數,返回類型也必須是String。參數值也是可以正確傳輸的,對於下面的服務當訪問error/param1
時,訪問error()
傳遞的參數是param1
,訪問fallbackWithParam()
時傳遞的參數也是param1
。
@HystrixCommand(fallbackMethod="fallbackWithParam")
@GetMapping("error/{param}")
public String error(@PathVariable("param") String param) {
throw new IllegalStateException();
}
public String fallbackWithParam(String param) {
return "fallback with param : " + param;
}
@HystrixCommand
除了指定fallbackMethod
外,還可以指定defaultFallback
。defaultFallback對應的方法必須是無參的,且它的返回類型必須和當前方法一致。它的作用與fallbackMethod是一樣的,即斷路器打開或方法調用出錯時會轉爲調用defaultFallback指定的方法。它與fallbackMethod的差別是fallbackMethod指定的方法的簽名必須與當前方法的一致,而defaultFallback的方法必須沒有入參,這樣defaultFallback對應的方法可以同時爲多個服務方法服務,即可以作爲一個通用的回退方法。當同時指定了fallbackMethod和defaultFallback時,fallbackMethod將擁有更高的優先級。
@GetMapping("error/default")
@HystrixCommand(defaultFallback="defaultFallback")
public String errorWithDefaultFallback() {
throw new IllegalStateException();
}
public String defaultFallback() {
return "default";
}
配置commandProperties
@HystrixCommand
還可以指定一些配置參數,通常是通過commandProperties來指定,而每個參數由@HystrixProperty
指定。比如下面的示例指定了斷路器打開衡量的時間窗口是30秒(metrics.rollingStats.timeInMilliseconds
),請求數量的閾值是5個(circuitBreaker.requestVolumeThreshold
),斷路器打開後停止請求真實服務方法的時間窗口是15秒(circuitBreaker.sleepWindowInMillisecond
)。
@HystrixCommand(fallbackMethod = "fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "30000"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "15000") })
@GetMapping("error")
public String error() {
String result = ++count + " Error. " + LocalDateTime.now();
System.out.println(result);
if (count % 5 != 0) {
throw new IllegalStateException();
}
return result;
}
public String fallback() {
return count++ + "result from fallback." + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS"));
}
受斷路器保護的方法,默認的超時時間是1秒,由參數execution.isolation.thread.timeoutInMilliseconds
控制,下面的代碼就配置了服務方法timeout()
的超時時間是3秒。
@GetMapping("timeout")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"))
public String timeout() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "timeout";
}
也可以通過指定參數
execution.timeout.enabled
爲false
來禁用超時,這樣就不管服務方法執行多少時間都不會超時了。
控制併發
@HystrixCommand
標註的服務方法在執行的時候有兩種執行方式,基於線程的和基於SEMAPHORE的,基於線程的執行策略每次會把服務方法丟到一個線程池中執行,即是獨立於請求線程之外的線程,而基於SEMAPHORE的會在同一個線程中執行服務方法。默認是基於線程的,默認的線程池大小是10個,且使用的緩衝隊列是SynchronousQueue,相當於沒有緩衝隊列,所以默認支持的併發數就是10,併發數超過10就會被拒絕。比如下面這段代碼,在30秒內只能成功請求10次。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"))
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
如果需要支持更多的併發,可以調整線程池的大小,線程池的大小通過threadPoolProperties指定,每個線程池參數也是通過@HystrixProperty
指定。比如下面的代碼指定了線程池的核心線程數是15,那下面的服務方法相應的就可以支持最大15個併發了。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback",
commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
threadPoolProperties = @HystrixProperty(name = "coreSize", value = "15"))
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
也可以指定緩衝隊列的大小,指定了緩存隊列的大小後將採用LinkedBlockingQueue。下面的服務方法指定了線程池的緩衝隊列是2,線程池的核心線程池默認是10,那它最多可以同時接收12個請求。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback",
commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
threadPoolProperties = @HystrixProperty(name = "maxQueueSize", value = "2"))
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
當你指定maxQueueSize
爲100時,你的服務方法也只能同時容納15個請求。這是因爲默認隊列的容量超過5後就會被拒絕,也就是說默認情況下maxQueueSize
大於5時,隊列裏面也只能容納5次請求。這是通過參數queueSizeRejectionThreshold
控制的,加上這個控制參數的原因是隊列的容量是不能動態改變的,加上這個參數後就可以通過這個參數來控制隊列的容量。下面代碼指定了隊列大小爲20,隊列拒絕添加元素的容量閾值也是20,那隊列裏面就可以最大支持20個元素,加上默認線程池裏面的10個請求,同時可以容納30個請求。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
threadPoolProperties = {
@HystrixProperty(name = "maxQueueSize", value = "20"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "20") })
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
這個線程池也是可以通過參數maximumSize
配置線程池的最大線程數的,但是單獨配置個參數時,最大線程數不會生效,需要配合參數allowMaximumSizeToDivergeFromCoreSize
一起使用。它的默認值是false,配置成true後最大線程數就會生效了。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
threadPoolProperties = {
@HystrixProperty(name = "maximumSize", value = "20"),
@HystrixProperty(name = "allowMaximumSizeToDivergeFromCoreSize", value = "true")})
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
當最大線程數生效後,超過核心線程數的線程的最大閒置時間默認是1分鐘,可以通過參數
keepAliveTimeMinutes
進行調整,單位是分鐘。
如果需要同時指定隊列大小和最大線程數,需要指定queueSizeRejectionThreshold
比maxQueueSize
大,否則最大線程數不會增長。
@GetMapping("concurrent")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
threadPoolProperties = {
@HystrixProperty(name = "maximumSize", value = "15"),
@HystrixProperty(name = "allowMaximumSizeToDivergeFromCoreSize", value = "true"),
@HystrixProperty(name = "maxQueueSize", value = "15"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "16")})
public String concurrent() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
需要服務方法的執行策略是基於SEMAPHORE的,需要指定參數execution.isolation.strategy
的值爲SEMAPHORE,其默認的併發數也是10。可以通過參數execution.isolation.semaphore.maxConcurrentRequests
調整對應的併發數。
@GetMapping("concurrent/semaphore")
@HystrixCommand(defaultFallback = "defaultFallback", commandProperties = {
@HystrixProperty(name = "execution.timeout.enabled", value = "false"),
@HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "18") })
public String concurrentSemaphore() {
System.out.println(LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "concurrent";
}
針對於fallback的併發數也是可以指定的,可以通過參數fallback.isolation.semaphore.maxConcurrentRequests
指定。雖然該參數名中包含semaphore,但是該參數對於服務方法的執行策略都是有效的,不指定時默認是10。如果你的fallback也是比較耗時的,那就需要好好考慮下默認值是否能夠滿足需求了。
上面只是列舉了一些常用的需要配置的hystrix參數,完整的配置參數可以參考https://github.com/Netflix/Hystrix/wiki/Configuration。
DefaultProperties
當Controller中有很多服務方法都需要相同的配置時,我們可以不用把這些配置都加到各自的@HystrixCommand
上,Hystrix提供了一個@DefaultProperties
用於配置相同的參數。比如下面的代碼就指定了當前Controller中所有的服務方法的超時時間是10秒,fallback的最大併發數是5。
@RestController
@RequestMapping("hystrix/properties")
@DefaultProperties(defaultFallback = "defaultFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000"),
@HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "5") })
public class DefaultPropertiesController {
@GetMapping("error")
@HystrixCommand
public String error() {
throw new IllegalStateException();
}
//...
public String defaultFallback() {
return "result from default fallback.";
}
}
在application.properties中配置參數
對於Spring Cloud應用來說,Hystrix的配置參數都可以在application.properties中配置。Hystrix配置包括默認配置和實例配置兩類。對於@HystrixCommand
的commandProperties來說,默認的配置參數是在原參數的基礎上加上hystrix.command.default.
前綴,比如控制執行的超時時間的參數是execution.isolation.thread.timeoutInMilliseconds
,那默認的配置參數就是hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
。對於@HystrixCommand
的threadPoolProperties來說,默認的配置參數是在原參數的基礎上加上hystrix.threadpool.default.
前綴,比如線程池的核心線程數由參數coreSize
控制,那默認的配置參數就是hystrix.threadpool.default.coreSize
。如果我們的application.properties中定義瞭如下配置,則表示@HystrixCommand
標註的方法默認的執行超時時間是5秒,線程池的核心線程數是5。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
hystrix.threadpool.default.coreSize=5
默認值都是可以被實例上配置的參數覆蓋的,對於上面配置的默認參數,如果擁有下面這樣的實例配置,則下面方法的執行超時時間將使用默認的5秒,而最大併發數(即核心線程數)是代碼裏面配置的6。
@GetMapping("timeout/{timeout}")
@HystrixCommand(threadPoolProperties=@HystrixProperty(name="coreSize", value="6"))
public String timeoutConfigureWithSpringCloud(@PathVariable("timeout") long timeout) {
System.out.println("Hellooooooooooooo");
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "timeoutConfigureWithSpringCloud";
}
實例的配置參數也是可以在application.properties中配置的,對於commandProperties,只需要把默認參數名稱中的default換成實例的commandKey即可,commandKey如果不特意配置時默認是取當前的方法名。所以對於上面的timeoutConfigureWithSpringCloud方法,如果需要指定該實例的超時時間爲3秒,則可以在application.properties中進行如下配置。
hystrix.command.timeoutConfigureWithSpringCloud.execution.isolation.thread.timeoutInMilliseconds=3000
方法名是很容易重複的,如果不希望使用默認的commandKey,則可以通過@HystrixCommand
的commandKey屬性進行指定。比如下面的代碼中就指定了commandKey爲springcloud。
@GetMapping("timeout/{timeout}")
@HystrixCommand(commandKey="springcloud")
public String timeoutConfigureWithSpringCloud(@PathVariable("timeout") long timeout) {
System.out.println("Hellooooooooooooo");
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "timeoutConfigureWithSpringCloud";
}
那如果需要指定該方法的超時時間爲3秒,需要調整對應的commandKey爲springcloud,即如下這樣:
hystrix.command.springcloud.execution.isolation.thread.timeoutInMilliseconds=3000
對於線程池來說,實例的配置參數需要把默認參數中的default替換爲threadPoolKey,@HystrixCommand
的threadPoolKey的默認值是當前方法所屬Class的名稱,比如當前@HystrixCommand
方法所屬Class的名稱是HelloController,可以通過如下配置指定運行該方法時使用的線程池的核心線程數是8。
hystrix.threadpool.HelloController.coreSize=8
下面代碼中手動指定了threadPoolKey爲springcloud。
@GetMapping("timeout/{timeout}")
@HystrixCommand(commandKey="springcloud", threadPoolKey="springcloud")
public String timeoutConfigureWithSpringCloud(@PathVariable("timeout") long timeout) {
System.out.println("Hellooooooooooooo");
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "timeoutConfigureWithSpringCloud";
}
如果需要指定它的線程池的核心線程數爲3,則只需要配置參數hystrix.threadpool.springcloud.coreSize
的值爲3。
hystrix.threadpool.springcloud.coreSize=3
關於Hystrix配置參數的默認參數名和實例參數名的更多介紹可以參考https://github.com/Netflix/Hystrix/wiki/Configuration。
關於Hystrix的更多介紹可以參考其GitHub的wiki地址https://github.com/netflix/hystrix/wiki。
參考文檔
https://github.com/netflix/hystrix/wiki
https://github.com/Netflix/Hystrix/wiki/Configuration
http://cloud.spring.io/spring-cloud-static/Finchley.SR1/multi/multi__circuit_breaker_hystrix_clients.html
(注:本文是基於Spring cloud Finchley.SR1所寫)