Spring Cloud:服務閘道器Zuul高階篇(11)
時間過的很快,寫springcloud(十):服務閘道器zuul初級篇還在半年前,現在已經是2018年了,我們繼續探討Zuul更高階的使用方式。
上篇文章主要介紹了Zuul閘道器使用模式,以及自動轉發機制,但其實Zuul還有更多的應用場景,比如:鑑權、流量轉發、請求統計等等,這些功能都可以使用Zuul來實現。
Zuul的核心
Filter是Zuul的核心,用來實現對外服務的控制。Filter的生命週期有4個,分別是“PRE”、“ROUTING”、“POST”、“ERROR”,整個生命週期可以用下圖來表示。
Zuul大部分功能都是通過過濾器來實現的,這些過濾器型別對應於請求的典型生命週期。
- PRE: 這種過濾器在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。
- ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建傳送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
- POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等。
- ERROR:在其他階段發生錯誤時執行該過濾器。 除了預設的過濾器型別,Zuul還允許我們建立自定義的過濾器型別。例如,我們可以定製一種STATIC型別的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
Zuul中預設實現的Filter
型別 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的型別 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記除錯標誌 |
route | 5 | PreDecorationFilter | 處理請求上下文供後續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式:
zuul: FormBodyWrapperFilter: pre: disable: true
自定義Filter
實現自定義Filter,需要繼承ZuulFilter的類,並覆蓋其中的4個方法。
public class MyFilter extends ZuulFilter { @Override String filterType() { return "pre"; //定義filter的型別,有pre、route、post、error四種 } @Override int filterOrder() { return 10; //定義filter的順序,數字越小表示順序越高,越先執行 } @Override boolean shouldFilter() { return true; //表示是否需要執行該filter,true表示執行,false表示不執行 } @Override Object run() { return null; //filter需要執行的具體操作 } }
自定義Filter示例
我們假設有這樣一個場景,因為服務閘道器應對的是外部的所有請求,為了避免產生安全隱患,我們需要對請求做一定的限制,比如請求中含有Token便讓請求繼續往下走,如果請求不帶Token就直接返回並給出提示。
首先自定義一個Filter,在run()方法中驗證引數是否含有Token。
public class TokenFilter extends ZuulFilter { private final Logger logger = LoggerFactory.getLogger(TokenFilter.class); @Override public String filterType() { return "pre"; // 可以在請求被路由之前呼叫 } @Override public int filterOrder() { return 0; // filter執行順序,通過數字指定 ,優先順序為0,數字越大,優先順序越低 } @Override public boolean shouldFilter() { return true;// 是否執行該過濾器,此處為true,說明需要過濾 } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString()); String token = request.getParameter("token");// 獲取請求的引數 if (StringUtils.isNotBlank(token)) { ctx.setSendZuulResponse(true); //對請求進行路由 ctx.setResponseStatusCode(200); ctx.set("isSuccess", true); return null; } else { ctx.setSendZuulResponse(false); //不對其進行路由 ctx.setResponseStatusCode(400); ctx.setResponseBody("token is empty"); ctx.set("isSuccess", false); return null; } } }
將TokenFilter加入到請求攔截佇列,在啟動類中新增以下程式碼:
@Bean public TokenFilter tokenFilter() { return new TokenFilter(); }
這樣就將我們自定義好的Filter加入到了請求攔截中。
測試
我們依次啟動示例專案:spring-cloud-eureka
、spring-cloud-producer
、spring-cloud-zuul
,這個三個專案均為上一篇示例專案,spring-cloud-zuul
稍微進行改造。
訪問地址:http://localhost:8888/spring-cloud-producer/hello?name=neo
,返回:token is empty ,請求被攔截返回。
訪問地址:http://localhost:8888/spring-cloud-producer/hello?name=neo&token=xx
,返回:hello neo,this is first messge,說明請求正常響應。
通過上面這例子我們可以看出,我們可以使用“PRE”型別的Filter做很多的驗證工作,在實際使用中我們可以結合shiro、oauth2.0等技術去做鑑權、驗證。
路由熔斷
當我們的後端服務出現異常的時候,我們不希望將異常丟擲給最外層,期望服務可以自動進行一降級。Zuul給我們提供了這樣的支援。當某個服務出現異常時,直接返回我們預設的資訊。
我們通過自定義的fallback方法,並且將其指定給某個route來實現該route訪問出問題的熔斷處理。主要繼承ZuulFallbackProvider介面來實現,ZuulFallbackProvider預設有兩個方法,一個用來指明熔斷攔截哪個服務,一個定製返回內容。
public interface ZuulFallbackProvider { /** * The route this fallback will be used for. * @return The route the fallback will be used for. */ public String getRoute(); /** * Provides a fallback response. * @return The fallback response. */ public ClientHttpResponse fallbackResponse(); }
實現類通過實現getRoute方法,告訴Zuul它是負責哪個route定義的熔斷。而fallbackResponse方法則是告訴 Zuul 斷路出現時,它會提供一個什麼返回值來處理請求。
後來Spring又擴充套件了此類,豐富了返回方式,在返回的內容中添加了異常資訊,因此最新版本建議直接繼承類FallbackProvider
。
我們以上面的spring-cloud-producer服務為例,定製它的熔斷返回內容。
@Component public class ProducerFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); //指定要處理的 service。 @Override public String getRoute() { return "spring-cloud-producer"; } public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } @Override public ClientHttpResponse fallbackResponse(Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info("Excption {}",reason); } return fallbackResponse(); } }
當服務出現異常時,列印相關異常資訊,並返回”The service is unavailable.”。
啟動專案spring-cloud-producer-2,這時候服務中心會有兩個spring-cloud-producer專案,我們重啟Zuul專案。再手動關閉spring-cloud-producer-2專案,多次訪問地址:http://localhost:8888/spring-cloud-producer/hello?name=neo&token=xx
,會交替返回:
hello neo,this is first messge
The service is unavailable.
...
根據返回結果可以看出:spring-cloud-producer-2專案已經啟用了熔斷,返回:The service is unavailable.
Zuul 目前只支援服務級別的熔斷,不支援具體到某個URL進行熔斷。
路由重試
有時候因為網路或者其它原因,服務可能會暫時的不可用,這個時候我們希望可以再次對服務進行重試,Zuul也幫我們實現了此功能,需要結合Spring Retry 一起來實現。下面我們以上面的專案為例做演示。
新增Spring Retry依賴
首先在spring-cloud-zuul專案中新增Spring Retry依賴。
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>
開啟Zuul Retry
再配置檔案中配置啟用Zuul Retry
#是否開啟重試功能 zuul.retryable=true #對當前服務的重試次數 ribbon.MaxAutoRetries=2 #切換相同Server的次數 ribbon.MaxAutoRetriesNextServer=0
這樣我們就開啟了Zuul的重試功能。
測試
我們對spring-cloud-producer-2進行改造,在hello方法中新增定時,並且在請求的一開始列印引數。
@RequestMapping("/hello") public String index(@RequestParam String name) { logger.info("request two name is "+name); try{ Thread.sleep(1000000); }catch ( Exception e){ logger.error(" hello two error",e); } return "hello "+name+",this is two messge"; }
重啟 spring-cloud-producer-2和spring-cloud-zuul專案。
訪問地址:http://localhost:8888/spring-cloud-producer/hello?name=neo&token=xx
,當頁面返回:The service is unavailable.
時檢視專案spring-cloud-producer-2後臺日誌如下:
2018-01-22 19:50:32.401 INFO 19488 --- [io-9001-exec-14] o.s.c.n.z.f.route.FallbackProvider : request two name is neo
2018-01-22 19:50:33.402 INFO 19488 --- [io-9001-exec-15] o.s.c.n.z.f.route.FallbackProvider : request two name is neo
2018-01-22 19:50:34.404 INFO 19488 --- [io-9001-exec-16] o.s.c.n.z.f.route.FallbackProvider : request two name is neo
說明進行了三次的請求,也就是進行了兩次的重試。這樣也就驗證了我們的配置資訊,完成了Zuul的重試功能。
注意
開啟重試在某些情況下是有問題的,比如當壓力過大,一個例項停止響應時,路由將流量轉到另一個例項,很有可能導致最終所有的例項全被壓垮。說到底,斷路器的其中一個作用就是防止故障或者壓力擴散。用了retry,斷路器就只有在該服務的所有例項都無法運作的情況下才能起作用。這種時候,斷路器的形式更像是提供一種友好的錯誤資訊,或者假裝服務正常執行的假象給使用者。
不用retry,僅使用負載均衡和熔斷,就必須考慮到是否能夠接受單個服務例項關閉和eureka重新整理服務列表之間帶來的短時間的熔斷。如果可以接受,就無需使用retry。
Zuul高可用
我們實際使用Zuul的方式如上圖,不同的客戶端使用不同的負載將請求分發到後端的Zuul,Zuul在通過Eureka呼叫後端服務,最後對外輸出。因此為了保證Zuul的高可用性,前端可以同時啟動多個Zuul例項進行負載,在Zuul的前端使用Nginx或者F5進行負載轉發以達到高可用性。
參考:
Spring Cloud(七)服務閘道器 Zuul Filter 使用
Spring Cloud技術分析(4)- spring cloud zuul
Zuul 路由使用