夢想起飛的地方.........
微服務場景下,每一個微服務對外暴露了一組細粒度的服務。客戶端的請求可能會涉及到一串的服務呼叫,如果將這些微服務都暴露給客戶端,那麼客戶端需要多次請求不同的微服務才能完成一次業務處理,增加客戶端的程式碼複雜度。另外,對於微服務我們可能還需要服務呼叫進行統一的認證和校驗等等。微服務架構雖然可以將我們的開發單元拆分的更細,降低了開發難度,但是如果不能夠有效的處理上面提到的問題,可能會造成微服務架構實施的失敗。
Zuul參考GOF設計模式中的Facade模式,將細粒度的服務組合起來提供一個粗粒度的服務,所有請求都匯入一個統一的入口,那麼整個服務只需要暴露一個api,對外遮蔽了服務端的實現細節,也減少了客戶端與伺服器的網路呼叫次數。這就是API服務閘道器(API Gateway)服務。我們可以把API服務閘道器理解為介於客戶端和伺服器端的中間層,所有的外部請求都會先經過API服務閘道器。因此,API服務閘道器幾乎成為實施微服務架構時必須選擇的一環。
Spring Cloud Netflix的Zuul元件可以做反向代理的功能,通過路由定址將請求轉發到後端的粗粒度服務上,並做一些通用的邏輯處理。
通過Zuul我們可以完成以下功能:
- 動態路由
- 監控與審查
- 身份認證與安全
- 壓力測試: 逐漸增加某一個服務叢集的流量,以瞭解服務效能;
- 金絲雀測試
- 服務遷移
- 負載剪裁: 為每一個負載型別分配對應的容量,對超過限定值的請求棄用;
- 靜態應答處理
1. 構建閘道器
1.1 構建Zuul-Server
編寫pom.xml檔案
Zuul-Server
是一個標準的Spring Boot應用,所以還是繼承自我們之前的parent:
<?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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>twostepsfromjava.cloud</groupId> <artifactId>twostepsfromjava-cloud-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../parent</relativePath> </parent> <artifactId>zuul-server</artifactId> <name>Spring Cloud Sample Projects: Zuul Proxy Server</name> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
這裡我們增加了spring-cloud-starter-zuul
的依賴。
編寫啟動類
/**
* TwoStepsFromJava Cloud -- Zuul Proxy 伺服器
*
* @author CD826([email protected])
* @since 1.0.0
*/
@EnableZuulProxy
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
這裡我們增加了對主應用類增加了@EnableZuulProxy
,用以啟動Zuul的路由服務。
編寫配置檔案application.properties
server.port=8280
spring.application.name=ZUUL-PROXY
eureka.client.service-url.defaultZone=http://localhost:8260/eureka
這裡定義服務名稱為: ZUUL-PROXY
,埠設為: 8280
。
1.2 構建User-Service
為了後面的則是我們再增加一個微服務: 使用者服務。
編寫pom.xml檔案
同樣繼承自我們之前的parent:
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>twostepsfromjava.cloud</groupId>
<artifactId>twostepsfromjava-cloud-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>user-service</artifactId>
<name>Spring Cloud Sample Projects: User Service Server</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>service-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
編寫啟動類
啟動類和之前的Product-Service
一樣,所以這裡不再列出來。
編寫服務介面
示例的服務介面非常簡單,就是根據給定的登入名稱查詢一個使用者資訊。如下:
/**
* User API服務
*
* @author CD826([email protected])
* @since 1.0.0
*/
@RestController
@RequestMapping("/users")
public class UserEndpoint {
protected Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
@Value("${server.port:2200}")
private int serverPort = 2200;
@RequestMapping(value = "/{loginName}", method = RequestMethod.GET)
public User detail(@PathVariable String loginName) {
String memos = "I come form " + this.serverPort;
return new User(loginName, loginName, "/avatar/default.png", memos);
}
}
其中User
類定義在之前的service-api
專案中,程式碼如下:
/**
* 使用者資訊DTO物件
*
* @author CD826([email protected])
* @since 1.0.0
*/
public class User {
private static final long serialVersionUID = 1L;
// ========================================================================
// fields =================================================================
private String loginName; // 使用者登陸名稱
private String name; // 使用者姓名
private String avatar; // 使用者頭像
private String memos; // 資訊備註
// ========================================================================
// constructor ============================================================
public User() {
}
public User(String loginName, String name, String avatar, String memos) {
this.loginName = loginName;
this.name = name;
this.avatar = avatar;
this.memos = memos;
}
// ==================================================================
// setter/getter ====================================================
// ... 省略,請自行補充 ...
}
編寫配置檔案application.properties
server.port=2200
spring.application.name=USER-SERVICE
eureka.client.service-url.defaultZone=http://localhost:8260/eureka
這裡定義服務名稱為: USER-SERVICE
,預設埠設為: 2200
。
程式碼修改,就是這麼多,下面讓我們啟動進行測試。
1.3 啟動測試
啟動各服務
請按照下面的順序啟動各伺服器:
- Service-discovery
- Product-Service
- User-Service(2200)
- User-Service(2300):
java -jar user-service-1.0.0-SNAPSHOT.jar --server.port=2300
- Zuul-Server
Ok, 服務啟動後我們可以在Eureka伺服器看到如下介面:
Zuul-proxy-010
這裡我們啟動兩個User-Service
主要是為了後面進行負載均衡測試使用。
測試路由服務
Zuul-proxy-020
Zuul-proxy-030
可見,Zuul-Server
已經幫我們路由到相應的微服務。
負載均衡測試
接下來我們測試一下負載均衡是否可以正常工作。前面我們已經啟動了兩個User-Service
微服務,埠分別為:2200和2300。我們多次在瀏覽器中輸入以下地址: http://localhost:8280/user-service/users/admin進行請求,我們將會看到以下資訊會在螢幕中交替輸出:
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2200"}
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2300"}
可見,負載均衡也是正常工作的。
Hystrix容錯與監控測試
之前我們是在Mall-Web
專案中整合Hystrix
的監控,那麼我們啟動該服務。然後在Hystrix Dashboard中輸入: http://localhost:8280/hystrix.stream,然後進行監控,那麼我們將看到如下介面:
Zuul-proxy-050
這說明,Zuul已經整合了Hystrix。
spring-cloud-starter-zuul
本身已經集成了hystrix和ribbon,所以Zuul天生就擁有執行緒隔離和斷路器的自我保護能力,以及對服務呼叫的客戶端負載均衡功能。但是,我們需要注意,當使用path與url的對映關係來配置路由規則時,對於路由轉發的請求則不會採用HystrixCommand
來包裝,所以這類路由請求就沒有執行緒隔離和斷路器保護功能,並且也不會有負載均衡的能力。因此,我們在使用Zuul的時候儘量使用path和serviceId的組合進行配置,這樣不僅可以保證API閘道器的健壯和穩定,也能用到Ribbon的客戶端負載均衡功能。
2. Zuul配置
2.1 路由配置詳解
1) 服務路由預設規則
當我們構建API服務閘道器時引入Eureka時,那麼Zuul會自動為每個服務都建立一個預設路由規則: 訪問路徑的字首為serviceId
配置的服務名稱,也就是之前為什麼我們能夠所使用:
http://localhost:8280/product-service/products
來訪問Product-Service中所提供的products服務端點的原因。
2) 自定義微服務訪問路徑
配置格式為: zuul.routes.微服務Id = 指定路徑,如:
zuul.routes.user-service = /user/**
所要配置的路徑可以指定一個正則表示式來匹配路徑,因此,/user/*
只能匹配一級路徑,但是通過/user/**
可以匹配所有以/user/
開頭的路徑。
3) 忽略指定微服務
配置格式為: zuul.ignored-services=微服務Id1,微服務Id2...,多個微服務之間使用逗號分隔。如:
zuul.ignored-services=user-service,product-service
4) 同時指定微服務Id和對應路徑
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B
5) 同時指定微服務Url和對應路徑
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8080/api-a
如之前所述,通過url配置的路由不會由HystrixCommand來執行,自然,也就得不到Ribbon的負載均衡、降級、斷路器等功能。所以在實施儘量使用serviceId進行配置,也可以採用下面的配置方式。
6) 指定多個服務例項及負載均衡
如果需要配置多個服務例項,則配置如下:
zuul.routes.user.path: /user/**
zuul.routes.user.serviceId: user
ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081
7) forward跳轉到本地url
zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user
8) 路由字首
可以通過zuul.prefix
可為所有的對映增加統一的字首。如: /api
。預設情況下,代理會在轉發前自動剝離這個字首。如果需要轉發時帶上字首,可以配置: zuul.stripPrefix=false
來關閉這個預設行為。例如:
zuul.routes.users.path=/myusers/**
zuul.routes.users.stripPrefix=false
注意:
zuul.stripPrefix
只會對zuul.prefix
的字首起作用。對於path指定的字首不會起作用。
9) 路由配置順序
如果想按照配置的順序進行路由規則控制,則需要使用YAML,如果是使用propeties檔案,則會丟失順序。例如:
zuul:
routes:
users:
path: /myusers/**
legacy:
path: /**
上例如果是使用properties檔案進行配置,則legacy
就可能會先生效,這樣users
就沒效果了。
10) 自定義轉換
我們也可以一個轉換器,讓serviceId
和路由之間使用正則表示式來自動匹配。例如:
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
這樣,serviceId為“users-v1”的服務,就會被對映到路由為“/v1/users/”的路徑上。任何正則表示式都可以,但是所有的命名組必須包括servicePattern和routePattern兩部分。如果servicePattern沒有匹配一個serviceId,那就會使用預設的。在上例中,一個serviceId為“users”的服務,將會被對映到路由“/users/”中(不帶版本資訊)。這個特性預設是關閉的,而且只適用於已經發現的服務。
2.2 Zuul的Header設定
敏感Header設定
同一個系統中各個服務之間通過Headers來共享資訊是沒啥問題的,但是如果不想Headers中的一些敏感資訊隨著HTTP轉發洩露出去話,需要在路由配置中指定一個忽略Header的清單。
預設情況下,Zuul在請求路由時,會過濾HTTP請求頭資訊中的一些敏感資訊,預設的敏感頭資訊通過zuul.sensitiveHeaders
定義,包括Cookie
、Set-Cookie
、Authorization
。配置的sensitiveHeaders
可以用逗號分割。
對指定路由的可以用下面進行配置:
# 對指定路由開啟自定義敏感頭
zuul.routes.[route].customSensitiveHeaders=true
zuul.routes.[route].sensitiveHeaders=[這裡設定要過濾的敏感頭]
設定全域性:
zuul.sensitiveHeaders=[這裡設定要過濾的敏感頭]
忽略Header設定
如果每一個路由都需要配置一些額外的敏感Header時,那你可以通過zuul.ignoredHeaders
來統一設定需要忽略的Header。如:
zuul.ignoredHeaders=[這裡設定要忽略的Header]
在預設情況下是沒有這個配置的,如果專案中引入了Spring Security
,那麼Spring Security
會自動加上這個配置,預設值為: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries
。
此時,如果還需要使用下游微服務的Spring Security的Header時,可以增加下面的設定:
zuul.ignoreSecurityHeaders=false
2.3 Zuul Http Client
Zuul的Http客戶端支援Apache Http、Ribbon的RestClient和OkHttpClient,預設使用Apache HTTP客戶端。可以通過下面的方式啟用相應的客戶端:
# 啟用Ribbon的RestClient
ribbon.restclient.enabled=true
# 啟用OkHttpClient
ribbon.okhttp.enabled=true
如果需要使用OkHttpClient需要注意在你的專案中已經包含
com.squareup.okhttp3
相關包。
3. Zuul容錯與回退
我們再來仔細看一下之前Hystrix的監控介面:
Zuul-proxy-080
請注意,Zuul的Hystrix監控的粒度是微服務,而不是某個API,也就是所有經過Zuul的請求都會被Hystrix保護起來。假如,我們現在把Product-Service
服務關閉,再來訪問會出現什麼結果呢?結果可能不是我們所想那樣,如下:
Zuul-proxy-060
呃,比較鬱悶是麼!那麼如何為Zuul實現容錯與回退呢?
Zuul提供了一個ZuulFallbackProvider
介面,通過實現該介面就可以為Zuul實現回退功能。那麼讓我們改造之前的Zuul-Server
。
3.1 實現回退方法
程式碼如下:
/**
* Product Service服務失敗回退處理
*
* @author CD826([email protected])
* @since 1.0.0
*/
@Component
public class ProductServiceFallbackProvider implements ZuulFallbackProvider {
protected Logger logger = LoggerFactory.getLogger(ProductServiceFallbackProvider.class);
@Override
public String getRoute() {
// 注意: 這裡是route的名稱,不是服務的名稱,
// 如果這裡寫成大寫PRODUCT-SERVICE將無法起到回退作用
return "product-service";
}
@Override
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("商品服務暫不可用,請稍後重試!".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
}
需要說明的是:
getRoute
方法返回了我們要為那個微服務提供回退。這裡需要注意的返回的值是route的名稱,不是服務的名稱,不能夠寫為:PRODUCT-SERVICE
,否則該回退將不起作用;fallbackResponse
方法返回ClientHttpResponse
物件,作為我們的回退響應。這裡實現非常簡單僅僅是返回:商品服務暫不可用,請稍後重試! 的提示。
3.2 重啟測試
重啟Zuul-Server
,再重複上面的實驗,將會看到以下介面:
Zuul-proxy-070
說明,回退方法已經起作用了。如果你的沒有起作用,那麼仔細檢查一下getRoute
的返回是否正確。