Spring Cloud微服務斷路由Hystrix與服務閘道器學習筆記
上一篇文章介紹瞭如何使用Spring Cloud與Spring Boot以及Docker容器搭建一個微服務模式的示例(https://github.com/JackmanGo/SpringCloudLearn)。接下來需要學習斷路由與服務閘道器。
斷路由
在微服務架構中,我們將系統拆分成了一個個的服務單元,各單元間通過服務註冊與訂閱的方式互相依賴。由於每個單元都在不同的程序中執行,依賴通過遠端呼叫的方式執行,這樣就有可能因為網路原因或是依賴服務自身問題出現呼叫故障或延遲,而這些問題會直接導致呼叫方的對外服務也出現延遲,若此時呼叫方的請求不斷增加,最後就會出現因等待出現故障的依賴方響應而形成任務積壓,最終導致自身服務的癱瘓。
舉個例子,在一個電商網站中,我們可能會將系統拆分成,使用者、訂單、庫存、積分、評論等一系列的服務單元。使用者建立一個訂單的時候,在呼叫訂單服務建立訂單的時候,會向庫存服務來請求出貨(判斷是否有足夠庫存來出貨)。此時若庫存服務因網路原因無法被訪問到,導致建立訂單服務的執行緒進入等待庫存申請服務的響應,在漫長的等待之後使用者會因為請求庫存失敗而得到建立訂單失敗的結果。如果在高併發情況之下,因這些等待執行緒在等待庫存服務的響應而未能釋放,使得後續到來的建立訂單請求被阻塞,最終導致訂單服務也不可用。
在微服務架構中,存在著那麼多的服務單元,若一個單元出現故障,就會因依賴關係形成故障蔓延,最終導致整個系統的癱瘓,這樣的架構相較傳統架構就更加的不穩定。為了解決這樣的問題,因此產生了斷路器模式。
斷路器本身是一種開關裝置,用於在電路上保護線路過載,當線路中有電器發生短路時,“斷路器”能夠及時的切斷故障電路,防止發生過載、發熱、甚至起火等嚴重後果。
在分散式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之後,通過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得執行緒因呼叫故障服務被長時間佔用不釋放,避免了故障在分散式系統中的蔓延。
在Spring Cloud中使用了Hystrix 來實現斷路器的功能。接著我們使用前面建立的專案分別在服務消費斷Ribbn與Feign中使用斷路由模式:
Ribbon中使用斷路由
Ribbon使用Hystrix需要匯入Hystrix 相關的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.2.6.RELEASE</version>
</dependency>
在主類中使用@EnableCircuitBreaker註解開啟斷路器功能:
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@ComponentScan("com.ribbon")
public class Main {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
在原先我們封裝的Service中添加註解:
@Service
public class RibbonService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "errServiceFallback")
public String sayHello() {
return restTemplate.getForEntity("http://SAY-HELLO/sayHello?name=jackman", String.class).getBody();
}
public String errServiceFallback() {
return "errorWithRibbon";
}
}
這樣,當Ribbon所呼叫的服務出現異常情況,Ribbon將訪問錯誤的回撥而不是服務異常訊息。
Feign中使用斷路由
Feigh工程不需要引入Hystix,因為Feign中已經依賴了Hystrix,我們可以直接使用:
@FeignClient(value = "say-hello",fallback = HystrixInFeign.class)
public interface FeignConsumer {
@RequestMapping(method = RequestMethod.GET, value = "/sayHello")
String say(@RequestParam(value = "name") String g);
}
在FeignClient註解中指定回撥,這個回撥是需要實現該介面的實現類.
@Component
public class HystrixInFeign implements FeignConsumer {
@Override
String add(@RequestParam(value = "name") String g) {
return null;
}
}
這樣當服務不可用時,則會返回null。
服務閘道器
服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
如果你熟悉Nginx的反向代理配置,那麼Zuul對你來說非常簡單。
Zuul也是一個獨立的服務,首先需要引入其依賴檔案:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
每個服務都有一個獨一無二的serviceId,在zuul中引入eureka的目的就是為了直接使用serviceId來代替url.
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}
這裡用了@SpringCloudApplication註解,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker
同樣的在配置檔案application.properties中指定Zuul應用的基礎資訊應用名、埠資訊等。
spring.application.name=api-gateway
server.port=5555
上述完成了Zuul的基礎配置,但為了使Zuul服務於系統叢集充當一個均衡負載器,還需要額外的配置,即配置服務路由:
# routes to url
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
剛剛筆者提到,引入了eureka的目的就是為了避免使用URL而使用serviceId。因為服務名與服務例項地址的關係在eureka server中已經存在了,所以只需要將Zuul註冊到eureka server上去發現其他服務,我們就可以實現對serviceId的對映。
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
嘗試通過服務閘道器來訪問service-A和service-B,根據配置的對映關係,分別訪問下面的url
http://localhost:5555/api-a/xxx?a=1&b=2:通過serviceId對映訪問service-A中的xxx服務
http://localhost:5555/api-b/xxx?a=1&b=2:通過serviceId對映訪問service-B中的xxx服務
http://localhost:5555/api-a-url/xxx?a=1&b=2:通過url對映訪問service-A中的xxx服務
推薦使用serviceId的對映方式,除了對Zuul維護上更加友好之外,serviceId對映方式還支援了斷路器,對於服務故障的情況下,可以有效的防止故障蔓延到服務閘道器上而影響整個系統的對外服務。
服務過濾
同樣的,如果熟悉Spring MVC或Struts攔截器過濾器相關的知識,那麼Zuul的服務過濾也很容易掌握。在服務閘道器中定義過濾器只需要繼承ZuulFilter抽象類實現其定義的四個抽象函式就可對請求進行攔截與過濾。
比如下面的例子,定義了一個Zuul過濾器,實現了在請求被路由之前檢查請求中是否有accessToken引數,若有就進行路由,若沒有就拒絕訪問,返回401 Unauthorized錯誤。
public class AccessFilter extends ZuulFilter {
//返回一個字串代表過濾器的型別
@Override
public String filterType() {
return "pre";
}
//通過int值來定義過濾器的執行順序
@Override
public int filterOrder() {
return 0;
}
//返回一個boolean型別來判斷該過濾器是否要執行
@Override
public boolean shouldFilter() {
return true;
}
//過濾器的具體邏輯
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object accessToken = request.getParameter("accessToken");
if(accessToken == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}
對於上述四個方法做詳細解釋:
- filterType:返回一個字串代表過濾器的型別,在zuul中定義了四種不同生命週期的過濾器型別,具體如下:pre:可以在請求被路由之前呼叫,routing:在路由請求時候被呼叫,post:在routing和error過濾器之後被呼叫,error:處理請求時發生錯誤時被呼叫
- filterOrder:通過int值來定義過濾器的執行順序
- shouldFilter:返回一個boolean型別來判斷該過濾器是否要執行,所以通過此函式可實現過濾器的開關。在上例中,我們直接返回true,所以該過濾器總是生效。
- run:過濾器的具體邏輯。需要注意,這裡我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設定了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。
在實現了自定義過濾器之後,還需要例項化該過濾器才能生效,我們只需要在應用主類中增加如下內容:
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
最後,總結一下為什麼服務閘道器是微服務架構的重要部分,是我們必須要去做的原因:
- 不僅僅實現了路由功能來遮蔽諸多服務細節,更實現了服務級別、均衡負載的路由。
- 實現了介面許可權校驗與微服務業務邏輯的解耦。通過服務閘道器中的過濾器,在各生命週期中去校驗請求的內容,將原本在對外服務層做的校驗前移,保證了微服務的無狀態性,同時降低了微服務的測試難度,讓服務本身更集中關注業務邏輯的處理。
- 實現了斷路器,不會因為具體微服務的故障而導致服務閘道器的阻塞,依然可以對外服務。