【第五章】API服務網關(Zuul) 上
微服務場景下,每一個微服務對外暴露了一組細粒度的服務。客戶端的請求可能會涉及到一串的服務調用,如果將這些微服務都暴露給客戶端,那麽客戶端需要多次請求不同的微服務才能完成一次業務處理,增加客戶端的代碼復雜度。另外,對於微服務我們可能還需要服務調用進行統一的認證和校驗等等。微服務架構雖然可以將我們的開發單元拆分的更細,降低了開發難度,但是如果不能夠有效的處理上面提到的問題,可能會造成微服務架構實施的失敗。
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服務器看到如下界面:
這裏我們啟動兩個User-Service
主要是為了後面進行負載均衡測試使用。
測試路由服務
首先,我們在瀏覽器中輸入以下地址: http://localhost:8280/product-service/products,將會顯示以下界面:
然後,我們在瀏覽器中輸入以下地址: http://localhost:8280/user-service/users/admin,將會顯示以下界面:
可見,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已經整合了Hystrix。
spring-cloud-starter-zuul
本身已經集成了hystrix和ribbon,所以Zuul天生就擁有線程隔離和斷路器的自我保護能力,以及對服務調用的客戶端負載均衡功能。但是,我們需要註意,當使用path與url的映射關系來配置路由規則時,對於路由轉發的請求則不會采用HystrixCommand
來包裝,所以這類路由請求就沒有線程隔離和斷路器保護功能,並且也不會有負載均衡的能力。因此,我們在使用Zuul的時候盡量使用path和serviceId的組合進行配置,這樣不僅可以保證API網關的健壯和穩定,也能用到Ribbon的客戶端負載均衡功能。
2. Zuul配置
2.1 路由配置詳解
或許你會覺得神奇,之前我們什麽也沒有配置,通過http://localhost:8280/product-service/products、http://localhost:8280/user-service/users/admin已經可以正確的訪問到我們的微服務了,這就是Zuul的默認路由映射功能在起作用,那麽接下來具體來看看Zuul是怎麽進行路由配置的。
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-service
所提供的服務,比如之前的訪問可以更改為: http://localhost:8280/user/users/admin。
所要配置的路徑可以指定一個正則表達式來匹配路徑,因此,/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的Hystrix監控的粒度是微服務,而不是某個API,也就是所有經過Zuul的請求都會被Hystrix保護起來。假如,我們現在把Product-Service
服務關閉,再來訪問會出現什麽結果呢?結果可能不是我們所想那樣,如下:
呃,比較郁悶是麽!那麽如何為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
,再重復上面的實驗,將會看到以下界面:
說明,回退方法已經起作用了。如果你的沒有起作用,那麽仔細檢查一下getRoute
的返回是否正確。
原文地址:http://www.jianshu.com/p/be5b26a9fa42
【第五章】API服務網關(Zuul) 上