在微服務架構中,需要幾個基礎的服務治理元件,包括服務注冊與發現、服務消費、負載均衡、斷路器、智能路由、配置管理等,由這幾個基礎元件互相協作,共同組建了一個簡單的微服務系統。一個簡答的微服務系統如下圖:

在Spring Cloud微服務系統中,一種常見的負載均衡方式是,用戶端的請求首先經過負載均衡(zuul、Ngnix),再到達服務網關(zuul叢集),然後再到具體的服。,服務統一注冊到高可用的服務注冊中心叢集,服務的所有的配置檔案由配置服務管理(下一篇文章講述),配置服務的配置檔案放在git倉庫,友善開發人員随時改配置。
一、Zuul簡介
Zuul的主要功能是路由轉發和過濾器。路由功能是微服務的一部分,比如api/user轉發到到user服務,/api/order轉發到到order服務。zuul預設和Ribbon結合實作了負載均衡的功能。
- SpringCloud Zuul通過與SpringCloud Eureka進行整合,将自身注冊為Eureka服務治理下的應用,同時從Eureka中獲得了所有其他微服務的執行個體資訊。外層調用都必須通過API網關,使得将維護服務執行個體的工作交給了服務治理架構自動完成。
- 在API網關服務上進行統一調用來對微服務接口做前置過濾,以實作對微服務接口的攔截和校驗。
Zuul天生就擁有線程隔離和斷路器的自我保護功能,以及對服務調用的用戶端負載均衡功能。也就是說:Zuul也是支援Hystrix和Ribbon。
zuul有以下功能:
- Authentication
- Insights
- Stress Testing
- Canary Testing
- Dynamic Routing
- Service Migration
- Load Shedding
- Security
- Static Response handling
- Active/Active traffic management
1.1、可能對Zuul的疑問
Zuul支援Ribbon和Hystrix,也能夠實作用戶端的負載均衡。我們的Feign不也是實作用戶端的負載均衡和Hystrix的嗎?既然Zuul已經能夠實作了,那我們的Feign還有必要嗎?
或者可以這樣了解:
- zuul是對外暴露的唯一接口相當于路由的是controller的請求,而Ribbonhe和Fegin路由了service的請求
- zuul做最外層請求的負載均衡 ,而Ribbon和Fegin做的是系統内部各個微服務的service的調用的負載均衡
有了Zuul,還需要Nginx嗎?他倆可以一起使用嗎?
- 我的了解:Zuul和Nginx是可以一起使用的(畢竟我們的Zuul也是可以搭成叢集來實作高可用的),要不要一起使用得看架構的複雜度了(業務)~~~
二、實戰
2.1、建立zuul工程
其pom.xml檔案如下:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>spring-cloud-test</artifactId>
<groupId>com.dukun.study</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul</artifactId>
<dependencies>
<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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在其入口applicaton類加上注解@EnableZuulProxy,開啟zuul的功能:
package com.dukun.study.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* zuul網關
*
* @Author: dukun0210
* @Date: 2021/1/12 14:17
*/
@SpringBootApplication
@EnableZuulProxy
public class AppZuul {
public static void main(String[] args) {
SpringApplication.run(AppZuul.class);
}
}
yml檔案:
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:3000/eureka/ #eureka服務端提供的注冊位址 參考服務端配置的這個路徑
instance:
instance-id: zuul-0 #此執行個體注冊到eureka服務端的唯一的執行個體ID
prefer-ip-address: true #是否顯示IP位址
leaseRenewalIntervalInSeconds: 20 #eureka客戶需要多長時間發送心跳給eureka伺服器,表明它仍然活着,預設為30 秒 (與下面配置的機關都是秒)
leaseExpirationDurationInSeconds: 60 #Eureka伺服器在接收到執行個體的最後一次發出的心跳後,需要等待多久才可以将此執行個體删除,預設為90秒
spring:
application:
name: zuul #此執行個體注冊到eureka服務端的name
zuul:
prefix: /api
ignored-services: "*"
# stripPrefix: false
routes:
order:
serviceId: server-order
path: /order/**
user:
serviceId: client-user
path: /user/**
power:
serviceId: server-power
path: /power/**
注意/ **代表是所有層級 / * 是代表一層。 如果是/ * 的話 /power/admin/getUser.do 就不會被路由 。
這樣 簡單的zuul就搭建好了, 啟動項目 我們就可以通過zuul然後加上對應的通路微服務:
以/api/order/** 開頭的請求都轉發給server-order服務;以/api/user/**開頭的請求都轉發給client-user服務;
http://localhost:9000/api/user/getOrderAndPowerrFeign.do?name=8848 就可以通路到 user服務中的 /getOrderAndPowerrFeign.do 方法。
2.2、相關配置
2.2.1、統一字首
這個很簡單,就是我們可以在前面加一個統一的字首,這個時候我們在
yaml
配置檔案中添加如下。
zuul:
prefix: /zuul
2.2.2、路由政策配置
你會發現前面的通路方式(直接使用服務名),需要将微服務名稱暴露給使用者,會存在安全性問題。是以,可以自定義路徑來替代微服務名稱,即自定義路由政策。
zuul: prefix: /api routes: order: serviceId: server-order path: /order/**
2.2.3、服務名屏蔽
這個時候你别以為你好了,你可以試試,在你配置完路由政策之後使用微服務名稱還是可以通路的,這個時候你需要将服務名屏蔽。
zuul:
ignore-services: "*"
2.2.4、路徑屏蔽
Zuul
還可以指定屏蔽掉的路徑 URI,即隻要使用者請求中包含指定的 URI 路徑,那麼該請求将無法通路到指定的服務。通過該方式可以限制使用者的權限。
zuul:
ignore-patterns: **/auto/**
2.3、Zuul 的過濾功能
如果說,路由功能是
Zuul
的基操的話,那麼過濾器就是
Zuul
的利器了。畢竟所有請求都經過網關(Zuul),那麼我們可以進行各種過濾,這樣我們就能實作限流,灰階釋出,權限控制等等。
先解釋一下關于過濾器的一些注意點:
zuul中定義了4種标準過濾器類型,這 些過濾器類型對應于請求的典型生命周期。
過濾器類型:
- PRE:這種過濾器在請求被路由之前調用。可利用這種過濾器實作身份 驗證、在 叢集中選擇請求的微服務、記錄調試資訊等。
- ROUTING:這種過濾器将請求路由到微服務。這種過濾器 用于建構發送給微服 務的請求,并使用 Apache HttpCIient或 Netfilx Ribbon請求微服務
- POST:這種過濾器在路由 到微服務以後執行。這種過濾器可用來為響應添加标準 的 HTTP Header、收集統計資訊和名額、将響應從微服務 發送給用戶端等。
- ERROR:在其他階段發生錯誤時執行該過濾器。
2.3.1、簡單實作一個請求通路記錄列印
/**
* ZUUL 網關的過濾器
* 添加日志功能
*
* @Author: dukun0210
* @Date: 2021/1/13 10:24
*/
@Component
public class LogFilter extends ZuulFilter {
//Pre、Routing、Post。
// 前置Pre就是在請求之前進行過濾,
// Routing路由過濾器就是我們上面所講的路由政策,
// 而Post後置過濾器就是在Response之前進行過濾的過濾器。
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
// 指定過濾順序 越小越先執行,這裡第一個執行
// 當然不是隻真正第一個 在Zuul内置中有其他過濾器會先執行
// 那是寫死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
// 什麼時候該進行過濾
// 這裡我們可以進行一些判斷,這樣我們就可以過濾掉一些不符合規定的請求等等
@Override
public boolean shouldFilter() {
return true;
}
// 如果過濾器允許通過則怎麼進行處理
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String remoteAddr = request.getRemoteAddr();
System.out.println("通路者IP:"+remoteAddr+"通路位址:"+request.getRequestURI());
return null;
}
}
由代碼可知,自定義的 zuul Filter需實作以下幾個方法。
filterType:傳回過濾器的類型。有 pre、 route、 post、 error等幾種取值,分别對應上文的幾種過濾器。 詳細可以參考 com.netflix.zuul.ZuulFilter.filterType()中的注釋。
filter0rder:傳回一個 int值來指定過濾器的執行順序,不同的過濾器允許傳回相同的數字。
shouldFilter:傳回一個 boolean值來判斷該過濾器是否要執行, true表示執行, false表示不執行。
run:過濾器的具體邏輯。 禁用zuul過濾器 Spring Cloud預設為Zuul編寫并啟用了一些過濾器,例如DebugFilter、 FormBodyWrapperFilter 等,這些過濾器都存放在spring-cloud-netflix-core這個jar包 裡,一些場景下,想要禁用掉部分過濾器,該怎麼辦 呢? 隻需在application.yml裡設定zuul...disable=true 例如,要禁用上面我們寫的過濾器,這樣配置就行了: zuul.LogFilter.pre.disable=true
2.3.2、令牌桶限流
當然不僅僅是令牌桶限流方式,
Zuul
隻要是限流的活它都能幹,這裡我隻是簡單舉個例子。
我先來解釋一下什麼是令牌桶限流吧。
首先我們會有個桶,如果裡面沒有滿那麼就會以一定固定的速率會往裡面放令牌,一個請求過來首先要從桶中擷取令牌,如果沒有擷取到,那麼這個請求就拒絕,如果擷取到那麼就放行。很簡單吧,啊哈哈、
下面我們就通過
Zuul
的前置過濾器來實作一下令牌桶限流。
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定義一個令牌桶,每秒産生2個令牌,即每秒最多處理2個請求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("通路量超載");
// 指定目前請求未通過過濾
context.setSendZuulResponse(false);
// 向用戶端傳回響應碼429,請求數量過多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
這樣我們就能将請求數量控制在一秒兩個,有沒有覺得很酷?
2.3.3、zuul容錯與回退
zuul預設是整合了hystrix和ribbon的, 提供降級回退,那如何來使用hystrix呢?
我們自行寫一個類,繼承FallbackProvider 類 然後重寫裡面的方法。
package com.dukun.study.zuul.provider;
import com.netflix.hystrix.exception.HystrixTimeoutException;
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;
/**
* zuul網關的 容錯
*
* @Author: dukun0210
* @Date: 2021/1/13 11:10
*/
@Component
public class FallBackProvider implements FallbackProvider {
@Override
public String getRoute() {
//制定為哪個微服務提供回退(這裡寫微服務名 寫*代表所有微服務)
return "*";
}
//此方法需要傳回一個ClientHttpResponse對象 ClientHttpResponse是一個接口,具體的回退邏輯要實 現此接口
//route:出錯的微服務名 cause:出錯的異常對象
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
//這裡可以判斷根據不同的異常來做不同的處理, 也可以不判斷
//完了之後調用response方法并根據異常類型傳入HttpStatus
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
//這裡傳回一個ClientHttpResponse對象 并實作其中的方法,關于回退邏輯的詳細,便在下面的方法中
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
傳回一個HttpStatus對象 這個對象是個枚舉對象, 裡面包含了一個status code 和
//reasonPhrase資訊
return status;
}
@Override
public int getRawStatusCode() throws IOException {
//傳回status的code 比如 404,500等
return status.value();
}
@Override
public String getStatusText() throws IOException {
//傳回一個HttpStatus對象的reasonPhrase資訊
return status.getReasonPhrase();
}
@Override
public void close() {
//close的時候調用的方法, 講白了就是當降級資訊全部響應完了之後調用的方法
}
@Override
public InputStream getBody() throws IOException {
//吧降級資訊響應回前端
return new ByteArrayInputStream("系統繁忙,稍後再試".getBytes());
}
@Override
public HttpHeaders getHeaders() {
//需要對響應報頭設定的話可以在此設定
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
關于 Zuul 的其他
Zuul
的過濾器的功能肯定不止上面我所實作的兩種,它還可以實作權限校驗,包括我上面提到的灰階釋出等等。
當然,
Zuul
作為網關肯定也存在單點問題,如果我們要保證
Zuul
的高可用,我們就需要進行
Zuul
的叢集配置,這個時候可以借助額外的一些負載均衡器比如
Nginx
。