一、Zuul 介紹
通過前幾篇文章的介紹,我們瞭解了Spring Cloud Eureka 如何搭建註冊中心,Spring Cloud Ribbon 如何做負載均衡,Spring Cloud Hystrix 斷路器如何保護我們的服務,以防止雪崩效應的出現,Spring Cloud Feign進行宣告式服務呼叫都有哪些應用,相比Ribbon和Hystrix都有哪些改善。可以說,以上幾個元件都是搭建一套微服務架構所必須的。通過以上思路,能夠梳理出下面這種基礎架構:
在此架構中,我們的服務叢集是內部ServiceA
和 ServiceB
,他們都會向Eureka Server叢集進行註冊與訂閱服務。而OpenService
是一個對外的Restful API 服務,它通過F5,Nginx等網路裝置或工具軟體實現對各個微服務的路由與負載,公開給外部客戶端呼叫
那麼上述的架構存在什麼問題呢?從運維
的角度來看,當客戶端單機某個功能的時候往往會發出一些請求到後端,這些請求通過F5,Nginx等設施的路由和負載均衡分配後,被轉發到各個不同的例項上,而為了讓這些設施能夠正確的路由與分發請求,運維人員需要手動維護這些例項列表
,當系統規模增大的時候,這些看似簡單的維護回變得越來越不可取。 從開發
的角度來看,為了保證服務的安全性,我們需要在呼叫內部介面的時候,加一層過濾
的功能,比如許可權
的校驗,使用者登陸狀態
的校驗等;同時為了防止客戶端在請求時被篡改等安全方面的考慮,還會有一些簽名機制的存在。
正是由於上述架構存在的問題,API閘道器
被提出,API閘道器更像是一個智慧的應用伺服器,它的定義類似於設計模式中的外觀模式,它就像是一個門面的角色,結婚時候女方親屬堵門時候的角色,我去參加婚禮當伴郎的時候去村子裡面見新娘,女方親屬會把鞋子藏起來,有可能藏在屋子裡有可能藏在身上,這得需要你自己去尋找,找到了鞋子之後,你才能夠給新娘穿上才能正式的會見家長。API閘道器真正實現的功能有請求路由
,負載均衡
,校驗過濾
,請求轉發的熔斷機制
,服務的聚合
等一系列功能。
Spring Cloud Zuul
通過與Spring Cloud Euerka
進行整合,將自身註冊為Eureka服務治理下的應用,同時從Eureka中獲得了所有的微服務的例項資訊。者可以通過使用Zuul來建立各種校驗過濾器,然後指定哪些規則的請求需要執行校驗邏輯,只有通過校驗的才會被路由到具體的微服務介面。下面我們就來搭建一下Spring Cloud Zuul服務閘道器
二、構建Spring Cloud Zuul閘道器
下面我們就來實際搭建一下Zuul閘道器,來體會一下閘道器實際的用處
構建閘道器
在實現各種API閘道器服務的高階功能之前,我們先來啟動一下前幾章搭建好的服務server-provider
,feign-consumer
,eureka-server
,雖然之前我們一直將feign-consumer視為消費者,但是在實際情況下,每個服務既時服務消費者,也是服務提供者,之前我們訪問的http://localhost:9001/feign-consumer等一系列介面就是它提供的服務。這裡就來介紹一下詳細的構建過程
- 建立一個Spring Boot功能,命名為api-gateway,並在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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.api.gateway</groupId>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
對於spring-cloud-starter-zuul 依賴,可以通過檢視依賴配置瞭解到,它不僅包含了Netflix Zuul的核心依賴zuul-core,還包括了下面這些閘道器的重要依賴
- spring-cloud-starter-hystrix: 該依賴用在閘道器服務中實現對微服務轉發時候的保護機制,通過執行緒隔離和斷路器,防止因為微服務故障引發的雪崩效應
- spring-cloud-starter-ribbon: 該依賴用在實現閘道器服務進行負載均衡和請求重試
- spring-cloud-starter-actuactor: 該依賴用來提供常規的微服務管理端點。另外,Spring Cloud Zuul 中還特別提供了/routes端點來返回當前的路由規則
- 在ApiGatewayApplication 主入口中新增
@EnableZuulProxy
註解開啟服務閘道器功能
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
- 在application.properties 中配置Zuul應用的基礎資訊,包括應用名,埠號,具體如下
spring.application.name=api-gateway
server.port=5555
請求路由
下面,我們通過一個簡單的示例來為上面構建的閘道器增加請求路由的功能,為了演示請求路由的功能,我們先將之前的Eureka服務註冊中心和微服務應用都啟動起來。觀察下面的服務列表,可以看到兩個微服務應用已經註冊成功了
傳統路由方式
使用Spring Cloud Zuul
實現路由功能非常簡單,只需要對api-gateway服務增加一些關於路由的配置規則,就能實現傳統路由方式
zuul.routes.api-a-url.path=/api-a-url/**
# 對映具體的url路徑
zuul.routes.api-a-url.url=http://localhost:8080/
該配置定義了發往API閘道器服務的請求中,所有符合/api-a-url/ 規則的訪問都將被路由轉發到 http://localhost:8080 的地址上,也就是說,當我們訪問http://localhost:5555/api-a-url/hello 的時候,API閘道器服務會將該請求路由到http://localhost:8080/hello 提供的微服務介面中。其中,配置屬性zuul.routes.api-a-url.path 中的api-a-url部分為路由的名字,可以任意定義,但是一組path和url對映關係的路由名要相同**
面向服務的路由
很顯然,傳統的配置方式對我們來說並不友好,他同樣需要運維人員花費大量的時間維護各個路由path 和url的關係。為了解決這個問題,Spring Cloud Zuul實現了與Spring Cloud Eureka的無縫銜接,我們可以讓路由的path不是對映具體的url,而是讓它對映到具體的服務
,而具體的url則交給Eureka的服務發現機制去自動維護
- 為了實現與Eureka的整合,我們需要在api-gateway的pom.xml中引入
spring-cloud-starter-eureka
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
- 在api-gateway服務中對應的application.properties檔案中加入如下程式碼
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=server-provider
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=feign-consumer
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
針對我們之前準備的兩個微服務應用
server-provider
和feign-consumer
,在上面的配置中分別定義了api-a 和 api-b 的路由來對映它們。然後這個api-gateway的預設註冊中心是預設註冊中心地址
- 完成上述配置後,我們可以將四個服務啟動起來,分別是
eureka-server
,server-provider
,feign-consumer
,api-gateway
服務,啟動完畢,會在eureka-server資訊皮膚中看到多了一個api-gateway
閘道器服務。
- http://localhost:5555/api-a/hello: 這個介面符合
/api-a/**
的規則,由api-a 路由負責轉發,該路由對映的serviceId 為server-provider
,所以最終/hello
請求會被髮送到server-provider
服務的某個例項上去 - http://localhost:9001/api-b/feign-consumer: 這個介面符合
/api-b/**
的規則,由api-b 進行路由轉發,實際的地址由Eureka負責對映,該路由的serviceId是feign-consumer
, 所以最終/feign-consumer
請求會被路由到feign-consumer
服務上。
請求過濾
在實現了請求路由功能之後,我們的微服務應用提供的介面就可以通過統一的API閘道器入口被客戶端訪問到了,但是,每個客戶端使用者請求微服務應用提供的介面時,它們的訪問許可權往往都有一定限制。為了實現客戶端請求的安全校驗和許可權控制,最簡單和粗暴的方法就是為每個微服務應用都實現一套用於校驗簽名和鑑別許可權的過濾器或攔截器。但是,這樣的方法並不可取,因為同一個系統中會有很多校驗邏輯相同的情況,最好的方法是將這些校驗邏輯剝離出去,構成一個獨立的服務。
對於上面這種問題,更好的做法是通過前置的閘道器服務來完成非業務性質的校驗。為了在API閘道器中實現對客戶端請求的校驗,我們將繼續介紹Spring Cloud Zuul的另外一個核心功能:請求過濾
,實現方法比較簡單,我們只需要繼承ZuulFilter
抽象類並實現它定義的4個抽象函式即可
下面的程式碼定義了一個簡單的Zuul過濾器,它實現了在請求被路由之前檢查HttpServletRequest
中是否帶有accessToken
引數
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
/**
* 過濾器的執行時序
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 過濾器的執行順序
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判斷過濾器是否應該執行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器的具體執行邏輯
* @return
*/
@Override
public Object run() {
RequestContext rc = RequestContext.getCurrentContext();
HttpServletRequest request = rc.getRequest();
log.info("send {} request to {}", request.getMethod(),request.getRequestURL().toString());
String accessToken = request.getParameter("accessToken");
if(null == accessToken){
log.warn("access token is null");
rc.setResponseStatusCode(401);
rc.setSendZuulResponse(false);
}
log.info("access token ok");
return null;
}
}
在上面實現的過濾器程式碼中,我們通過繼承ZuulFilter 抽象類並重寫了四個方法
- filterType : 過濾器型別,它決定過濾器的請求在哪個生命週期中執行,這裡定義為pre,意思是在請求前執行
- filterOrder : 過濾器的執行順序,當請求在一個階段存在多個過濾器時,需要根據方法的返回值來判斷過濾器的執行順序
- shouldFilter: 過濾器是否需要執行,這裡直接返回true,因為該過濾器對所有的請求都生效
- run: 過濾器的具體邏輯,這裡我們通過rc.setResponseStatusCode(401)設定失效的標誌,rc.setSendZuulResponse(false)令Zuul過濾該請求
在實現了自定義過濾器之後,它並不會直接生效,我們還需要為其建立具體的Bean才能啟動該過濾器。
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public AccessFilter filter(){
return new AccessFilter();
}
}
在對api-gateway
服務完成了上面的改造之後,我們可以重新啟動它,併發起下面的請求,對上面的過濾器做一個驗證
- 輸入 http://localhost:5555/api-a/hello : 返回 401錯誤
- 輸入 http://localhost:5555/api-a/hello?accessToken=token,正確路由到
server-provider
的/hello 介面,並返回Hello World。
到這裡,對於API閘道器的快速入門示例就搭建完成了,通過對Spring Cloud Zuul 閘道器的搭建,我們能認知到閘道器的重要性,可以總結如下:
- 它作為系統的統一入口, 遮蔽了系統內部各個微服務的細節。
- 它可以與服務治理框架結合,實現自動化的服務例項維護以及負載均衡的路由轉發。
- 它可以實現介面許可權校驗與微服務業務邏輯的解耦。
- 通過服務閘道器中的過濾器, 在各生命週期中去校驗請求的內容, 將原本在對外服務層做的校驗前移, 保證了微服務的無狀態性, 同時降低了微服務的測試難度, 讓服務本身更集中關注業務邏輯的處理。
三、路由詳解
在上面快速入門的請求路由示例中,我們對Spring Cloud zuul
中的兩類路由功能已經做了簡單的介紹,在本節中,將詳細再介紹Spring Cloud Zuul
的路由功能
傳統路由配置
所謂的傳統路由配置就是不依賴於服務發現的機制下,通過配置檔案中具體指定每個路由表示式與服務例項關係來實現API閘道器對外部請求的路由。
- 單例項配置: 通過
zuul.routes.<route>.path
與zuul.routes.<route>.url
引數對的方式進行配置,例如:
zuul.routes.api-a-url.path=/api-a-url/**
# 對映具體的url路徑
zuul.routes.api-a-url.url=http://localhost:8080/
該例項配置實現了/api-a-url/ 規則的請求路徑轉發到http://localhost:8080/ 地址的路由規則**
比如,當一個請求http://localhost:5555/api-a-url/hello 被髮送到API閘道器之後,由於/api-a-url/能夠被配置類對映到,所以API閘道器會進行轉發,轉發到http://localhost:8080/hello 上
- 多例項配置: 通過
zuul.routes.<route>.path
與zuul.routes.<route>.serviceId
引數對的方式進行配置,例如
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.service-id=api-a-url
ribbon.eureka.enabled=false
api-a-url.ribbon.listOfServers=http://localhost:8080/, http://localhost:8081
該配置實現了對符合/api-a-url/ 規則的請求路徑轉發到 http://localhost:8080/ 和 http://localhost:8081兩個例項地址的路由規則。它的配置方式與服務路由的配置方式一樣,都採用了zuul.routes.
.path 與 zuul.routes. .serviceId引數對的對映方式,只是這裡的serviceId 是由手工命名的服務名稱,配合 ribbon.listOfServers 引數實現服務與例項的維護。由於列表中有 8080 和 8081 兩個例項,所以還需要ribbon 進行負載均衡的配置,因為Zuul 預設帶有Ribbon,所以就可以直接使用**
ribbon.eureka.enabled
: 由於zuul.routes.<route>.serviceId
指的是具體的服務名稱,預設情況下ribbon 會根據服務發現機制來獲取配置服務名對應的例項清單。但是,該示例並沒有整合類似Eureka之類的服務治理框架,所以需要將該引數設定為false, 否則 配置 的serviceId獲取不到對應例項的清單
api-a-url.ribbon.listOfServers
: 該引數內容與zuul.routes.<route>.serviceid
的配置相對應, 開頭的user-service 對應了serviceId的值,這兩個引數的配置相當於在該應用內部手工維護了服務與例項的對應關係。
服務路由配置
對於服務路由,我們在上面的文字中已經討論過,Spring Cloud Zuul
通過與Spring Cloud eureka
的整合,實現了對服務例項的自動化維護,所以在使用服務路由配置的時候,不再需要像傳統的方式指定服務的具體url地址,而是可以通過指定服務的名稱來配置,比如下面的例子就能很好說明:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=server-provider
zuul.routes.
.serviceId 中的serviceId 就是需要對映的具體服務
對於面向服務的配置,除了使用path 和 serviceId 的配置外,還有一種更簡便的配置方式:
zuul.routes.api-a=/api-a/**
上面的這種配置方式使用了 zuul.routes.
= 的配置方式,與上面的 path 和 serviceId 共同使用的方式等價
預設路由規則
由於預設情況下所有Eureka上的服務都會被Zuul自動
地建立對映關係來進行路由,這會使得一些我們不希望對外開放的服務也能被外部訪問到,這個時候我們就需要遮蔽一些外部訪問的服務
zuul.ignored-services=*
使用zuul.ignored-services 引數來設定一個服務名匹配表示式來定義不自動建立路由的規則
路徑匹配
還記得我們上面配置過的 /api-a/**
嗎? 後面的 ** 代表什麼意思呢?其實這是一種路由匹配風格,路由匹配路徑表示式採用Ant風格定義,具體如下
萬用字元 | 說明 |
---|---|
? | 匹配任意單個字元 |
* | 匹配任意數量的字元 |
** | 匹配任意數量的字元,支援多級目錄 |
通過如下的示例,可以讓你參考使用
URL 路徑 | 說明 |
---|---|
/api-a/? | 它可以匹配/api-a/之後拼接的一個單個字元的路徑,比如/api-a/b, /api-a/c |
/api-a/* | 它可以匹配/api-a/之後拼接的任意字元,但是不能跨層訪問,比如可以訪問到/api-a/bbb,/api-a/ccc,但是不能訪問到/api-a/b/a |
/api-a/** | 它可以訪問任意路徑,比如 /api-a/bbb, /api-a/bbb/aaa, /api-a/ccc/bbb/aaa |
properties檔案 無法保證匹配順序
例如我因為版本上線,需要在properties檔案中重新配置一下路由的路徑,我第一個路由的路徑是/api-a/**
,對應的服務是api-a
, 我第二個路由的路徑是/api-a-pro/pro/**
,對應的服務是api-a-pro
相當於第二個路徑是第一個路徑的子集,這樣就無法保證對映的順序,也就是說 properties 配置的內容無法保證有序性,所以為了避免這種情況,採用YAML檔案來配置,能夠解決上述問題
zuul:
routes:
api-a-pro:
path: /api-a-pro/pro/**
serviceId: api-a-pro
api-a:
path: /api-a/**
serviceId: api-a
注意:
:
右邊必須要有一個空格,這個yaml檔案的書寫規範
忽略表示式
通過path引數定義的Ant表示式已經能夠完成API閘道器上的路由規則配置功能,但是為了更細粒度和更為靈活的配置路由規則,zuul還提供了一個忽略表示式引數zuul.ignored.patterns
,該引數可以用來設定不希望被API閘道器進行路由的URL表示式
比如,以上述示例為基礎,如果不希望/hello
介面被路由,那麼我們可以這樣設定
# 過濾請求
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=server-provider
那麼啟動程式,訪問http://localhost:5555/api-a/hello?accessToken=true,在api-gateway 的console 控制檯上會顯示如下資訊
WARN 28605 --- [io-5555-exec-10] o.s.c.n.z.f.pre.PreDecorationFilter : No route found for uri: /api-a/hello
路由字首
為了方便全域性地為路由規則增加字首資訊,Zuul提供了zuul.prefix
引數來進行設定。比如,希望為閘道器上的路由規則都增加/api 字首,那麼我們可以在配置檔案中增加配置: zuul.prefix=/api
。另外,對於代理字首會預設從路徑中移除,我們可以通過設定zuul.stripPrefix=false
來關閉該移除代理字首的動作,也可以通過zuul.routes.<route>.strip-prefix=true
來對指定路由關閉移除代理字首的動作。
本地跳轉
在實現的API閘道器的路由功能中,還支援forward形式的服務端跳轉配置。實現方式也比較簡單,直接在forward:/xxx
請求路徑就可以了。下述這種方式就實現了服務跳轉。
zuul.routes.api-b.path=/api/b/**
zuul.routes.api-b.url=forward:/local
經過http://localhost:5555/api/b/** 的請求會被轉發到 http://localhost:5555/local/**,下面進行簡單驗證。需要在api-gateway
中建立一個HelloController,如下:
@RestController
public class HelloController {
@RequestMapping("/local/hello")
public String hello(){
return "Hello World Local";
}
}
Cookie與頭資訊
預設情況下,Spring Cloud Zuul 在請求路由時,會過濾掉HTTP請求頭資訊中的一些敏感資訊,用來防止它們被傳遞到下游的伺服器。預設的敏感頭資訊通過zuul.sensitiveHeaders
引數定義,包括Cookie、Set-Cookie、Authorization 三個屬性。
有兩種方式,一種是對全域性的設定方式,但是這種設定方式比較暴力,不推薦
zuul.sensitiveHeaders=
還有一種方式是對指定的服務開啟自定義敏感頭,這種方式比較推薦
# 方法一: 對指定路由開啟自定義敏感頭
zuul.routes.<router>.customSensitiveHeaders=true
# 方法二: 將指定路由的敏感頭設定為空
zuul.routes.<router>.sensitiveHeaders=
Hystrix 和 Ribbon 支援
點開pom.xml看到spring-cloud-starter-zuul的起步依賴中包括spring-cloud-starter-hystrix
和 spring-cloud-starter-ribbon
的依賴,所以Zuul天生就有執行緒隔離和斷路器的自我保護功能。但是有一點需要注意:當使用path與url的對映關係來配置路由的時候,對於路由轉發請求不會採用HystrixCommand來包裝,所以沒有執行緒隔離和斷路器保護。所以我們在使用zuul的時候儘量使用path 和 service的組合來進行配置。
在使用Zuul閘道器的時候,可以通過Hystrix和Ribbon的引數來調整路由請求的各種超時時間配置
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:該引數用來設定API閘道器中轉發路由請求的HystrixCommand超時時間,單位為毫秒。當路由轉發的請求時間大於配置的時間之後。Hystrix會將該執行該命令標記為TIMEOUT並丟擲異常。
{
"timestamp": 1559269203828,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.zuul.exception.ZuulException",
"message": "TIMEOUT"
}
- ribbon.ReadTimeout 和 ribbon.SocketTimeout ,一個是ribbon的讀取超時時間,一個是ribbon的socket連線超時時間。
- ribbon.ConnectTimeout 表示用來轉發請求的時候,建立連結的超時時間。
如果ribbon.ConnectTimeout的配置時間大於hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 的時間,就表示為連線還未建立的情況下就被熔斷,不會觸發重試機制,直接返回 TIMEOUT 的超時資訊。
如果ribbon.ReadTimeout的配置時間小於hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 的超時時間,此時若路由請求的處理時間超過該配置值且依賴服務的請求還未響應的時候,會自動進行重試路由請求。如果還沒有路由到的話,會返回NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED 錯誤。
如果大於的話,會直接返回TIMEOUT超時資訊。
通過上文的描述我們知道,在ribbon.ReadTimeout 超時時間小於hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds的超時時間的話,會自動進行請求的重試,我們可以通過下面的配置,禁用請求重試機制
# 全域性關閉請求重試機制
zuul.retryable=false
# 指定路由關閉請求重試機制
zuul.routes.<route>.retryable=false
文章來源:
《Spring Cloud 微服務實戰》
https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.0.RELEASE/multi/multi__router_and_filter_zuul.html