一、為什麼需要Zuul?
Zuul 作為微服務系統的網關元件,用于建構邊界服務( Edge Service ),緻力于動态路由、過濾、監控、彈性伸縮和安全。Zuul 作為路由網關元件,在微服務架構中有着非常重要的作用,主要展現在以下6個方面。
1)Zuul Ribbon 以及 Eureka 相結合,可以實作智能路由和負載均衡的功能, Zuul 能夠将請求流量按某種政策分發到叢集狀态的多個服務執行個體。
2)網關将所有服務的 API 接口統一聚合,并統一對外暴露。外界系統調用 API 接口時,都是由網關對外暴露的 API 接口,外界系統不需要知道微服務系統中各服務互相調用的複雜性。微服務系統 保護了其内部微服務單元的 API 接口 防止其被外界直接調用,導緻服務的敏感資訊對外暴露。
3)網關服務可以做使用者身份認證和權限認證,防止非法請求操作 API 接口,對伺服器起到保護作用。
4)網關可以實作監控功能,實時日志輸出,對請求進行記錄。
5)網關可以用來實作流量監控。在高流量的情況下,對服務進行降級。
6)API 接口從内部服務分離出來 友善做測試。
二、Zuul的工作原理
Zuul 是通過 Servlet 來實作的, Zuul 通過自定義的 Zuul Servlet (類似于 Spring MVC的DispatcServlet 〕來對請求進行控制。 Zuul 的核心是一系列過濾器,可以在 Http 請求的發起和響應傳回期間執行一系列的過濾器。 Zuul 包括以下4種過濾器。
1)PRE 過濾器:它是在請求路由到具體的服務之前執行的,這種類型的過濾器可以做安全驗證,例如身份驗證、 參數驗證等。
2)ROUTING 過濾器:它用于将請求路由到具體的微服務 。在預設情況下,它使用Http Client 進行網絡請求。
3)POST 過濾器:它是在請求已被路由到微服務後執行的一般情況下,用作收集統計資訊、名額,以及将響應傳輸到用戶端。
4)ERROR 過濾器:它是在其他過濾器發生錯誤時執行的。
Zuul 采取了動态讀取、編譯和運作這些過濾器。過濾器之間不能直接通信,而是通RequestContext 對象來共享資料,每個請求都會建立一個RequestContext 對象。Zuul 過濾器具有以下關鍵特性。
1)Type (類型):Zuul 過濾器的類型,這個類型決定了過濾器在請求的哪個階段起作用,例如 Pre Post 階段等。
2)Execution Order (執行順序) :規定了過濾器的執行順序, Order 的值越小,越先執行
3)Criteria (标準):Filter 行所需的條件。
4)Action (行動):如果符合執行條件,則執行 Action (即邏輯代碼)。
Zuul的請求生命周期:

當一個用戶端 Request 請求進入 Zuul網關服務時,網關先進入“pre filter ,進行一系列的驗證、操作或者判斷。然後交給“routing filter ”進行路由轉發,轉發到具體的服務執行個體進行邏輯處理、傳回資料。當具體的服務處理完後,最後由“post filter進行處理,該類型的處理器處理完之後,将 Response 資訊傳回給用戶端。
ZuulServlet是Zuul的核心Servlet,ZuulServlet的作用是初始化ZuulFilter。并編排這些順序,具體的邏輯在service()方法中
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
...
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
三、搭建具體的Zuul應用服務。
1)加入依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2)編寫啟動程式加上@EnableZuulProxy注解。
package com.cetc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
3)加入配置(服務端口為8682):
server:
port: 8682
spring:
application:
name: zuul
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式
zuul:
routes:
test:
path: /test/**
url: http://127.0.0.1:8673/
ribbon:
path: /ribbon/**
serviceId: rest
feign:
path: /feign/**
serviceId: feign
說明:上面的為基本配置:
zuul.routes.test/ribbon/feign:中的test/ribbon/feign為自定義配置。
path:為Zuul中代理的路徑。
serviceId:為服務的名稱。
url:為具體的位址,不存在負載均衡的問題。(主要用于外部服務加入)
4)測試:啟動項目包含Eureka-Server, 2個Eureka-Client,Ribbon中的Rest,Feign和Zuul。端口分别為:8670、8673/8674、8675、8676、8682。
效果如下:
測試接口為:
http://127.0.0.1:8682/test/api/test/getPort
http://127.0.0.1:8682/ribbon/api/ribbon/getPort
http://127.0.0.1:8682/feign/api/feign/getPort
可見這裡的通路:使用url的方式預設沒有負載均衡的,是以不建議使用,使用ribbon和feign兩種方式都是可以進行負載均衡的。
5)如果想使用url的方式來做負載均衡那麼就要自己維護通路清單。配置如下:
zuul:
routes:
test-r:
path: /test-r/**
serviceId: test-r
test-r:
ribbon:
listOfServers: http://127.0.0.1:8673/, http://127.0.0.1:8674/
ribbon:
eureka:
enabled: false
說明:這裡禁用ribbon不影響前面配置好的服務。但是會影響Zuul調用ribbon的rest或者feign服務,如果這兩個服務存在負載均衡,那麼調用的時候就存在負載均衡問題。是以一般建議不要這樣使用。
測試:
6)如果想加入具體的版本号,可以加入如下配置:
zuul:
prefix: /v1
通路方式為:http://127.0.0.1:8682/v1/test/api/test/getPort。在連結中加入版本号。
四、Zuul加入熔斷器:
預設實作FallbackProvider接口,加入容器即可
package org.springframework.cloud.netflix.zuul.filters.route;
import org.springframework.http.client.ClientHttpResponse;
public interface FallbackProvider {
String getRoute();
ClientHttpResponse fallbackResponse(String var1, Throwable var2);
}
實作為:
package com.cetc.config;
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 org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@Component
public class ZuulFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
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("error".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
return httpHeaders;
}
};
}
}
測試:關閉feign測試接口為:
五、在Zuul中自定義filter:
package com.cetc.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//是否開啟過濾邏輯,開啟後運作run()方法
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
HttpServletResponse response = requestContext.getResponse();
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
try {
response.getWriter().write("token is empty");
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
是不是感覺這類有點驗證的味道了。通過自定義的過濾就能達到請求過濾的目的。實作過程如下:
六、Zuul的常見使用方式。
Zuul 是采用了類似于 Spring MVC DispatchServlet 來實作的,采用的是異步阻塞模型,是以性能比 Ngnix 差。由于 Zuul和其他 Netflix 元件可以互相配合、無縫內建 Zuul 很容易就能實作負載均衡、智能路由和熔斷器等功能。在大多數情況下 Zuul 都是以叢集的形式在的。由于Zuul的橫向擴充能力非常好,是以當負載過高時,可以通過添加執行個體來解決性能瓶頸。
1)一種常見的使用方式是對不同的管道使用不同的 Zuul 來進行路由,例如移動端共用Zuul一個網關執行個體。Web 端用另一個Zuul 網關執行個體,其他的用戶端用另外一個Zuul 執行個體進行路由。
2)另外一種常見的叢集是通過 Ngnix和Zuul 互相結合來做負載均衡。暴露在最外面的是Ngnix 主從雙熱備進行 Keepalive, Ngnix 經過某種路由政策,将請求路由轉發到 Zuul 叢集上,Zuul 最終将請求分發到具體的服務上。
七、源碼位址:https://github.com/lilin409546297/spring-cloud/tree/master/zuul