SpringCloud系列教程 | 第十篇:服務閘道器Zuul高階篇
Springboot: 2.1.6.RELEASE
SpringCloud: Greenwich.SR1
如無特殊說明,本系列教程全採用以上版本
上一篇我們主要聊到了Zuul的使用方式,以及自動轉發機制,其實Zuul還有更多的使用姿勢,比如:鑑權、流量轉發、請求統計等。
1. Zuul的核心
Zuul的核心是Filter,用來實現對外服務的控制。分別是“PRE”、“ROUTING”、“POST”、“ERROR”,整個生命週期可以用下圖來表示。
Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標準過濾器型別,這些過濾器型別對應於請求的典型生命週期。
PRE: 這種過濾器在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。
ROUTING: 這種過濾器將請求路由到微服務。這種過濾器用於構建傳送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
OST: 這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等。
ERROR: 在其他階段發生錯誤時執行該過濾器。
2. 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 | 處理正常的請求響應 |
2.1 禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式:
zuul:
FormBodyWrapperFilter:
pre:
disable: true
3. 自定義Filter
實現自定義Filter,需要繼承ZuulFilter的類,並覆蓋其中的4個方法。
package com.springcloud.zuulsimple.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
/**
* Created with IntelliJ IDEA.
*
* @User: weishiyao
* @Date: 2019/7/6
* @Time: 16:10
* @email: inwsy@hotmail.com
* Description:
*/
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return null;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() throws ZuulException {
return null;
}
}
4. 自定義Filter示例
我們假設有這樣一個場景,因為服務閘道器應對的是外部的所有請求,為了避免產生安全隱患,我們需要對請求做一定的限制,比如請求中含有Token便讓請求繼續往下走,如果請求不帶Token就直接返回並給出提示。
4.1 zuul-simple修改
首先,將上一篇的zuul-simple copy到一個新的資料夾中,自定義一個Filter,在run()方法中驗證引數是否含有Token。
package com.springcloud.zuulsimple.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* Created with IntelliJ IDEA.
*
* @User: weishiyao
* @Date: 2019/7/6
* @Time: 16:11
* @email: inwsy@hotmail.com
* Description:
*/
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() throws ZuulException {
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加入到了請求攔截中。
4.2 測試
將上一篇的Eureka和producer都CV到新的資料夾下面,依次啟動。
開啟瀏覽器,我們訪問:http://localhost:8080/spring-cloud-producer/hello?name=spring, 返回:token is empty ,請求被攔截返回。
訪問地址:http://localhost:8080/spring-cloud-producer/hello?name=spring&token=123,返回:hello spring,producer is ready,說明請求正常響應。
通過上面這例子我們可以看出,我們可以使用“PRE”型別的Filter做很多的驗證工作,在實際使用中我們可以結合shiro、oauth2.0等技術去做鑑權、驗證。
5. 路由熔斷
當我們的後端服務出現異常的時候,我們不希望將異常丟擲給最外層,期望服務可以自動進行降級處理。Zuul給我們提供了這樣的支援。當某個服務出現異常時,直接返回我們預設的資訊。
我們通過自定義的fallback方法,並且將其指定給某個route來實現該route訪問出問題的熔斷處理。主要繼承FallbackProvider介面來實現,FallbackProvider預設有兩個方法,一個用來指明熔斷攔截哪個服務,一個定製返回內容。
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.netflix.zuul.filters.route;
import org.springframework.http.client.ClientHttpResponse;
/**
* Provides fallback when a failure occurs on a route.
*
* @author Ryan Baxter
* @author Dominik Mostek
*/
public interface FallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
String getRoute();
/**
* Provides a fallback response based on the cause of the failed execution.
* @param route The route the fallback is for
* @param cause cause of the main method failure, may be <code>null</code>
* @return the fallback response
*/
ClientHttpResponse fallbackResponse(String route, Throwable cause);
}
實現類通過實現getRoute方法,告訴Zuul它是負責哪個route定義的熔斷。而fallbackResponse方法則是告訴 Zuul 斷路出現時,它會提供一個什麼返回值來處理請求。
我們以上面的spring-cloud-producer服務為例,定製它的熔斷返回內容。
package com.springcloud.zuulsimple.component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Created with IntelliJ IDEA.
*
* @User: weishiyao
* @Date: 2019/7/6
* @Time: 16:25
* @email: inwsy@hotmail.com
* Description:
*/
@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(String route, Throwable cause) {
if (cause != null && cause.getCause() != null) {
String reason = cause.getCause().getMessage();
logger.info("Excption {}",reason);
}
return fallbackResponse();
}
}
當服務出現異常時,列印相關異常資訊,並返回”The service is unavailable.”。
需要注意點,這裡我們需要將Eureka的配置檔案修改一下:
server:
port: 8761
spring:
application:
name: eureka-serve
eureka:
# server:
# enable-self-preservation: false
client:
register-with-eureka: false
service-url:
defaultZone: http://localhost:8761/eureka/
將Eureka的自我保護模式開啟,如果這裡不開啟自我保護模式,producer一停止服務,這個服務直接在Eureka下線,Zuul會直接報錯找不到對應的producer服務。
我們順次啟動這三個服務。
現在開啟瀏覽器,訪問連結:http://localhost:8080/spring-cloud-producer/hello?name=spring&token=123, 可以看到頁面正常返回:hello spring,producer is ready,現在我們把producer這個服務停下,再重新整理下頁面,可以看到頁面返回:The service is unavailable.。這樣我們熔斷也測試成功。
6. Zuul高可用
我們實際使用Zuul的方式如上圖,不同的客戶端使用不同的負載將請求分發到後端的Zuul,Zuul在通過Eureka呼叫後端服務,最後對外輸出。因此為了保證Zuul的高可用性,前端可以同時啟動多個Zuul例項進行負載,在Zuul的前端使用Nginx或者F5進行負載轉發以達到高可用性。
參考:
http://www.ityouknow.com/springcloud/2018/01/20/spring-cloud-zuul.html