限流知識《高可用服務設計之二:Rate limiting 限流與降級》
在微服務架構中,我們將系統拆分成了一個個的服務單元,各單元間通過服務注冊與訂閱的方式互相依賴。由於每個單元都在不同的進程中運行,依賴通過遠程調用的方式執行,這樣就有可能因為網絡原因或是依賴服務自身問題出現調用故障或延遲,而這些問題會直接導致調用方的對外服務也出現延遲,若此時調用方的請求不斷增加,最后就會出現因等待出現故障的依賴方響應而形成任務積壓,最終導致自身服務的癱瘓。
舉個例子,在一個電商網站中,我們可能會將系統拆分成,用戶、訂單、庫存、積分、評論等一系列的服務單元。用戶創建一個訂單的時候,在調用訂單服務創建訂單的時候,會向庫存服務來請求出貨(判斷是否有足夠庫存來出貨)。此時若庫存服務因網絡原因無法被訪問到,導致創建訂單服務的線程進入等待庫存申請服務的響應,在漫長的等待之后用戶會因為請求庫存失敗而得到創建訂單失敗的結果。如果在高並發情況之下,因這些等待線程在等待庫存服務的響應而未能釋放,使得后續到來的創建訂單請求被阻塞,最終導致訂單服務也不可用。
在微服務架構中,存在着那么多的服務單元,若一個單元出現故障,就會因依賴關系形成故障蔓延,最終導致整個系統的癱瘓,這樣的架構相較傳統架構就更加的不穩定。為了解決這樣的問題,因此產生了斷路器模式。
什么是斷路器
斷路器模式源於Martin Fowler的Circuit Breaker一文。“斷路器”本身是一種開關裝置,用於在電路上保護線路過載,當線路中有電器發生短路時,“斷路器”能夠及時的切斷故障電路,防止發生過載、發熱、甚至起火等嚴重后果。
熔斷器設計中有三種狀態,closed(關閉狀態,流量可以正常進入)、open(即熔斷狀態,一旦錯誤達到閾值,熔斷器將打開,拒絕所有流量)和half-open(半開狀態,open狀態持續一段時間后將自動進入該狀態,重新接收流量,一旦請求失敗,重新進入open狀態,但如果成功數量達到閾值,將進入closed狀態),見下圖:
CLOSED關閉狀態:允許流量通過。
OPEN打開狀態:不允許流量通過,即處於降級狀態,走降級邏輯。
HALF_OPEN半開狀態:允許某些流量通過,並關注這些流量的結果,如果出現超時、異常等情況,將進入OPEN狀態,如果成功,那么將進入CLOSED狀態。
在分布式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間占用不釋放,避免了故障在分布式系統中的蔓延。
Netflix Hystrix
在Spring Cloud中使用了Hystrix 來實現斷路器的功能。Hystrix是Netflix開源的微服務框架套件之一,該框架目標在於通過控制那些訪問遠程系統、服務和第三方庫的節點,從而對延遲和故障提供更強大的容錯能力。Hystrix具備擁有回退機制和斷路器功能的線程和信號隔離,請求緩存和請求打包,以及監控和配置等功能。
下面我們來看看如何使用Hystrix。
Netfix Hystrix可以通過兩種方式引入:
- 從服務組件角度分:ribbon和Feign;
- 從代碼方式角度分:hystrix通過設置fallback和fallbackFactory屬性觸發請求容災降級;
准備工作
在開始加入斷路器之前,我們先拿之前構建兩個微服務為基礎進行下面的操作,主要使用下面幾個工程:
- 《服務注冊發現Eureka之一:Spring Cloud Eureka的服務注冊與發現》
- eureka-server工程:服務注冊中心,端口1111
- compute-service工程:服務單元,端口2223
在Ribbon為引入Hystirx之前
- 依次啟動eureka-server、compute-service、ribbon-consumer工程
- 訪問http://localhost:1111/可以看到注冊中心的狀態
- 訪問http://127.0.0.1:2250/add,調用ribbon-consumer的服務,該服務會去調用compute-service的服務,計算出10+20的值,頁面顯示30
- 關閉compute-service服務,訪問http://127.0.0.1:2250/add,我們獲得了下面的報錯信息
具體的操作及關鍵代碼如下:
創建一個ribbon-consumer的項目,pom為:
<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.dxz</groupId> <artifactId>ribbon-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.5.RELEASE</version> <!--配合spring cloud版本 --> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <!--設置字符編碼及java版本 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--增加eureka-server的依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <!--用於測試的,本例可省略 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <!--依賴管理,用於管理spring-cloud的依賴 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-parent</artifactId> <version>Brixton.SR3</version> <!--官網為Angel.SR4版本,但是我使用的時候總是報錯 --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <!--使用該插件打包 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
啟動類:
package com.dxz.ribbon; 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.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient public class RibbonApplication { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(RibbonApplication.class, args); } }
SpringMvc類:
package com.dxz.ribbon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class ConsumerController { @Autowired RestTemplate restTemplate; @RequestMapping(value = "/add", method = RequestMethod.GET) public String add() { return restTemplate.getForEntity("http://COMPUTE-SERVICE/add?a=10&b=20", String.class).getBody(); } }
application配置:
spring.application.name=ribbon-consumer server.port=2250 eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
啟動eureka-server,但不啟動computer-service服務,啟動ribbon-consumer服務並訪問:
測試結果:
方式一:Ribbon中引入Hystrix
1、pom.xml
中引入依賴hystrix依賴
<!--增加hystrix的依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
2、在eureka-ribbon的主類RibbonApplication
中使用@EnableCircuitBreaker
注解開啟斷路器功能:
package com.dxz.ribbon; 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.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableCircuitBreaker @SpringBootApplication @EnableDiscoveryClient public class RibbonApplication { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(RibbonApplication.class, args); } }
3、改造原來的服務消費方式,新增ComputeService
類,在使用ribbon消費服務的函數上增加@HystrixCommand
注解來指定回調方法。
package com.dxz.ribbon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; @Service public class ComputeService { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "addServiceFallback") public String addService() { return restTemplate.getForEntity("http://COMPUTE-SERVICE/add?a=10&b=20&sn=1", String.class).getBody(); } public String addServiceFallback() { return "error"; } }
4、提供rest接口的Controller從使用RestTemplate調用改為調用ComputeService的addService方法:
package com.dxz.ribbon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class ConsumerController { //@Autowired //RestTemplate restTemplate; @Autowired private ComputeService computeService; @RequestMapping(value = "/add", method = RequestMethod.GET) public String add() { //return restTemplate.getForEntity("http://COMPUTE-SERVICE/add?a=10&b=20", String.class).getBody(); return computeService.addService(); } }
- 驗證斷路器的回調
- 依次啟動eureka-server、compute-service、eureka-ribbon工程
- 訪問http://localhost:1111/可以看到注冊中心的狀態
- 訪問http://localhost:2250/add,頁面顯示:30
- 關閉compute-service服務后再訪問http://localhost:2250/add,頁面顯示:error
由於Hystrix默認超時時間為2000毫秒,所以這里采用了0至3000的隨機數以讓有一定概率發生超時來觸發斷路器。分別將服務提供方和消費方修改如下:
服務提供方,springcloud-computer:
@RequestMapping(value = "/add", method = RequestMethod.GET) public Integer add(@RequestParam Integer a, @RequestParam Integer b, @RequestParam Integer sn) { ServiceInstance instance = client.getLocalServiceInstance(); Integer r = a + b; try { int sleepTime = new Random().nextInt(3000); System.out.println("sleepTime=" + sleepTime); Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } logger.info("/add, host:" + instance.getHost() + ", service_id:" + instance.getServiceId() + ", result:" + r + ",sn="+sn +",time="+ LocalDateTime.now()); return r; }
服務消費方,ribbon-consumer:
package com.dxz.ribbon; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class ConsumerController { //@Autowired //RestTemplate restTemplate; @Autowired private ComputeService computeService; @RequestMapping(value = "/add", method = RequestMethod.GET) public String add() { //return restTemplate.getForEntity("http://COMPUTE-SERVICE/add?a=10&b=20", String.class).getBody(); Long start = System.currentTimeMillis(); String temp = computeService.addService(); Long end = System.currentTimeMillis(); System.out.println("computeService.addService()="+temp + ",user time=" +(end-start) + "毫秒"); return temp; } }
結果:
隨機出現熔斷場景
方式二:Feign使用Hystrix
注意這里說的是“使用”,沒有錯,我們不需要在Feigh工程中引入Hystix,Feign中已經依賴了Hystrix,我們可以在未做任何改造前,嘗試下面你的操作:
- 依次啟動eureka-server、compute-service、eureka-feign工程
- 訪問http://localhost:1111/可以看到注冊中心的狀態
- 訪問http://localhost:3333/add,調用eureka-feign的服務,該服務會去調用compute-service的服務,計算出10+20的值,頁面顯示30
- 關閉compute-service服務,訪問http://localhost:3333/add,我們獲得了下面的報錯信息
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Sat Jun 25 22:10:05 CST 2016 There was an unexpected error (type=Internal Server Error, status=500). add timed-out and no fallback available.
使用@FeignClient
注解中的fallback屬性指定回調類如果您夠仔細,會發現與在ribbon中的報錯是不同的,看到add timed-out and no fallback available
這句,或許您已經猜到什么,看看我們的控制台,可以看到報錯信息來自hystrix-core-1.5.2.jar
,所以在這個工程中,我們要學習的就是如何使用Feign中集成的Hystrix。
package com.dxz;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
//@FeignClient("compute-service")
@FeignClient(value = "compute-service", fallback = ComputeClientHystrix.class)
public interface ComputeClient {
@RequestMapping(method = RequestMethod.GET, value = "/add")
Integer add(@RequestParam(value = "a") Integer a, @RequestParam(value = "b") Integer b);
}
- 創建回調類
ComputeClientHystrix
,實現被@FeignClient注解
的接口,此時實現的方法就是對應@FeignClient
接口中映射的fallback函數。
package com.dxz; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestParam; @Component public class ComputeClientHystrix implements ComputeClient { @Override public Integer add(@RequestParam(value = "a") Integer a, @RequestParam(value = "b") Integer b) { return -9999; } }
- 再用之前的方法驗證一下,是否在compute-service服務不可用的情況下,頁面返回了-9999。
三、fallbackFactory
fallbackFactory與fallback不同的是可以打印詳細的錯誤信息
將原來的fallback替換成fallbackFactory
@FeignClient(value = "message-api",fallbackFactory = MessageApiFailFactory.class) public interface MessageApiFeignClient { @RequestMapping(value = "/example/hello",method = RequestMethod.GET) public String getMessage(@RequestParam("name") String name); } public interface MessageApiFeignFallBackFactoryClient extends MessageApiFeignClient{ } @Component public class MessageApiFailFactory implements FallbackFactory<MessageApiFeignClient> { public static final Logger logger = LoggerFactory.getLogger(MessageApiFailFactory.class); @Override public MessageApiFeignClient create(Throwable throwable) { logger.info("fallback; reason was: {}",throwable.getMessage()); return new MessageApiFeignFallBackFactoryClient(){ @Override public String getMessage(String name) { return "錯誤原因:"+throwable.getMessage(); } }; } }
同樣進行本地調用
messageApiFeignClient.getMessage("zhangsan");
運行后服務調用方打印:
錯誤原因:status 500 reading MessageApiFeignClient#getMessage(String); content:
{"timestamp":1508681203932,"status":500,"error":"Internal Server Error","exception":"java.lang.ArithmeticException","message":"/ by zero","path":"/example/hello"}
另外配置文件下可以設置hystrix服務超時機制
#開啟hystrix請求超時機制 也可以設置成永久不超時
hystrix.command.default.execution.timeout.enabled=false hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000