通過之前幾篇Spring Cloud中幾個核心元件的介紹,我們已經可以建構一個簡略的(不夠完善)微服務架構了。比如下圖所示:

我們使用Spring Cloud Netflix中的Eureka實作了服務注冊中心以及服務注冊與發現;而服務間通過Ribbon或Feign實作服務的消費以及均衡負載;通過Spring Cloud Config實作了應用多環境的外部化配置以及版本管理。為了使得服務叢集更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個别服務出現異常時引起的故障蔓延。
在該架構中,我們的服務叢集包含:内部服務Service A和Service B,他們都會注冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務調用方。本文我們把焦點聚集在對外服務這塊,這樣的實作是否合理,或者是否有更好的實作方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
- 首先,破壞了服務無狀态特點。為了保證對外服務的安全性,我們需要實作對服務通路的權限控制,而開放服務的權限控制機制将會貫穿并污染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀态的特點。從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外可續對接口通路的控制處理。
- 其次,無法直接複用既有接口。當我們需要對一個即有的叢集内通路接口,實作外部服務通路時,我們不得不通過在原有接口上增加校驗邏輯,或增加一個代理調用來實作權限控制,無法直接複用原有的接口。
讓用戶端直接與各個微服務通訊,會有以下的問題:
- 用戶端會多次請求不同的微服務,增加了用戶端的複雜性。
- 存在跨域請求,在一定場景下處理相對複雜。
- 認證複雜,每個服務都需要獨立認證。
- 難以重構,随着項目的疊代,可能需要重新劃分微服務。例如,可能将多個服務合并成一個或者将一個服務拆分成多個。如果用戶端直接與微服務通訊,那麼重構将會很難實施。
- 某些微服務可能使用了防火牆/浏覽器不友好的協定,直接通路會有一定困難。
面對類似上面的問題,我們要如何解決呢?下面進入本文的正題:服務網關!
使用網關優點:
- 易于監控。可在微服務網關收集監控資料并将其推送到外部系統進行分析。
- 易于認證。可在微服務網關上進行認證。然後再将請求轉發到後端的微服務,而無須在每個微服務中進行認證。
- 減少了用戶端與各個微服務之間的互動次數。
為了解決上面這些問題,我們需要将權限控制這樣的東西從我們的服務單元中抽離出去,而最适合這些邏輯的地方就是處于對外通路最前端的地方,我們需要一個更強大一些的均衡負載器,它就是本文将來介紹的:服務網關。
服務網關是微服務架構中一個不可或缺的部分。通過服務網關統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時将權限控制這些較重的非業務邏輯内容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
下面我們通過執行個體例子來使用一下Zuul來作為服務的路有功能。
準備工作
在使用Zuul之前,我們先建構一個服務注冊中心、以及兩個簡單的服務,比如:我建構了一個compute-service,一個compute-service-B。然後啟動eureka-server和這兩個服務。通過通路eureka-server,我們可以看到compute-service和compute-service-B已經注冊到了服務中心。
compute-service:見《服務注冊發現Eureka之一:Spring Cloud Eureka的服務注冊與發現》
compute-service-B:将compute-service拷貝一份,修改下項目名和服務名
如果您還不熟悉如何建構服務中心和注冊服務,請先閱讀見《服務注冊發現Eureka之一:Spring Cloud Eureka的服務注冊與發現》。
開始使用Zuul
- 引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對服務叢集細節的透明性,還是用serviceId來避免直接引用url的方式吧。
傳統路由方式::通過url直接映射,我們可以如下配置:
如果是多執行個體的話,用逗号分隔
<?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>
<groupId>com.dxz.zuul</groupId>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>api-gateway</name>
<description>zuul project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version> <!--配合spring cloud版本 -->
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<!--設定字元編碼及java版本 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--增加zuul的依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
</dependency>
<!--用于測試的,本例可省略 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!--依賴管理,用于管理spring-cloud的依賴 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Brixton.SR3</version> <!--官網為Angel.SR4版本,但是我使用的時候總是報錯 -->
<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>
啟動類:
package com.dxz.zuul;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ApiGatewayApplication.class).web(true).run(args);
}
}
配置:
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/,http://localhost:2221/
測試:
啟動相關服務:
浏覽器通路,結果如下:
面向服務的路由方式::通過url映射的方式對于Zuul來說,并不是特别友好,Zuul需要知道我們所有為服務的位址,才能完成所有的映射配置。而實際上,我們在實作微服務架構時,服務名與服務執行個體位址的關系在eureka server中已經存在了,是以隻需要将Zuul注冊到eureka server上去發現其他服務,我們就可以實作對serviceId的映射:
pom中增加:
<!--增加eureka-server的依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
路由配置如下:
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=COMPUTE-SERVICE
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=COMPUTE-SERVICE-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
結果:
Zuul配置
完成上面的工作後,Zuul已經可以運作了,但是如何讓它為我們的微服務叢集服務,還需要我們另行配置,下面詳細的介紹一些常用配置内容。
服務路由
通過服務路由的功能,我們在對外提供服務的時候,隻需要通過暴露Zuul中配置的調用位址就可以讓調用方統一的來通路我們的服務,而不需要了解具體提供服務的主機資訊了。
在Zuul中提供了兩種映射方式:
- 傳統路由方式:通過url直接映射,我們可以如下配置:
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/
其中,配置屬性zuul.routes.api-a-url.path中的api-a-url部分為路由的名字,可以任意定義,但是一組映射關系的path和url要相同,下面講serviceId時候也是如此。該配置,定義了,所有到Zuul的中規則為:
/api-a-url/**
的通路都映射到
http://localhost:2222/
上,也就是說當我們通路
http://localhost:5555/api-a-url/add?a=1&b=2
的時候,Zuul會将該請求路由到:
http://localhost:2222/add?a=1&b=2
上。
- 面向服務路由方式:通過url映射的方式對于Zuul來說,并不是特别友好,Zuul需要知道我們所有為服務的位址,才能完成所有的映射配置。而實際上,我們在實作微服務架構時,服務名與服務執行個體位址的關系在eureka server中已經存在了,是以隻需要将Zuul注冊到eureka server上去發現其他服務,我們就可以實作對serviceId的映射。例如,我們可以如下配置:
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=COMPUTE-SERVICE
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=COMPUTE-SERVICE-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
推薦使用serviceId的映射方式,除了對Zuul維護上更加友好之外,serviceId映射方式還支援了斷路器,對于服務故障的情況下,可以有效的防止故障蔓延到服務網關上而影響整個系統的對外服務
服務過濾
在完成了服務路由之後,我們對外開放服務還需要一些安全措施來保護用戶端隻能通路它應該通路到的資源。是以我們需要利用Zuul的過濾器來實作我們對外服務的安全控制。
在服務網關中定義過濾器隻需要繼承
ZuulFilter
抽象類實作其定義的四個抽象函數就可對請求進行攔截與過濾。
比如下面的例子,定義了一個Zuul過濾器,實作了在請求被路由之前檢查請求中是否有
accessToken
參數,若有就進行路由,若沒有就拒絕通路,傳回
401 Unauthorized
錯誤。
package com.dxz;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class AccessFilter extends ZuulFilter {
private static Logger log = Logger.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}
-
:傳回一個字元串代表過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型,具體如下:自定義過濾器的實作,需要繼承filterType
,需要重寫實作下面四個方法:ZuulFilter
-
:可以在請求被路由之前調用pre
-
:在路由請求時候被調用routing
-
:在routing和error過濾器之後被調用post
-
:處理請求時發生錯誤時被調用error
-
:通過int值來定義過濾器的執行順序filterOrder
-
:傳回一個boolean類型來判斷該過濾器是否要執行,是以通過此函數可實作過濾器的開關。在上例中,我們直接傳回true,是以該過濾器總是生效。shouldFilter
-
:過濾器的具體邏輯。需要注意,這裡我們通過run
令zuul過濾該請求,不對其進行路由,然後通過ctx.setSendZuulResponse(false)
設定了其傳回的錯誤碼,當然我們也可以進一步優化我們的傳回,比如,通過ctx.setResponseStatusCode(401)
對傳回body内容進行編輯等。ctx.setResponseBody(body)
在實作了自定義過濾器之後,還需要執行個體化該過濾器才能生效,我們隻需要在應用主類中增加如下内容:
package com.dxz;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@EnableZuulProxy
@SpringCloudApplication
public class SCzullApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(SCzullApplication.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
啟動該服務網關後,通路:
-
:傳回401錯誤http://localhost:5555/api-a/add?a=1&b=2
-
:正确路由到server-A,并傳回計算内容http://localhost:5555/api-a/add?a=1&b=2&accessToken=token
對于其他一些過濾類型,這裡就不一一展開了,根據之前對
filterType
生命周期介紹,可以參考下圖去了解,并根據自己的需要在不同的生命周期中去實作不同類型的過濾器。
- 不僅僅實作了路由功能來屏蔽諸多服務細節,更實作了服務級别、均衡負載的路由。
- 實作了接口權限校驗與微服務業務邏輯的解耦。通過服務網關中的過濾器,在各生命周期中去校驗請求的内容,将原本在對外服務層做的校驗前移,保證了微服務的無狀态性,同時降低了微服務的測試難度,讓服務本身更集中關注業務邏輯的處理。
- 實作了斷路器,不會因為具體微服務的故障而導緻服務網關的阻塞,依然可以對外服務。