Zuul用於構建邊界服務,致力於動態路由,過濾,監控,彈性伸縮和安全等方向。
1、Zuul+Ribbon+Eureka結合,可以實現智能路由和負載均衡
2、網關將所有服務的API接口統一聚合統一暴露
3、網關統一爆率接口后,可以做身份和權限認證
4、實現監控功能,實時日志輸出
5、流量監控,實現降級和限流
6、方便測試
1、網關存在的必要性
不同的微服務有不同的請求地址,如果一個客戶端需要訪問多個接口才能完成一個業務需求的話,可能存在以下問題:
# 客戶端會多次請求不同微服務,增加客戶端的復雜性
# 存在跨域請求,在一定場景下處理相對復雜
# 認證復雜,每一個服務都需要獨立認證
# 難以重構,隨着項目的迭代,可能需要重新划分微服務,如果客戶端直接和微服務通信,那么重構會難以實施
# 某些微服務可能使用了其他協議,直接訪問有一定困難
而微服務網關可以很好的解決這個問題:
這樣客戶端只需要和網關交互,而無需直接調用特定微服務的接口,而且方便監控,易於認證,減少客戶端和各個微服務之間的交互次數。
2、主流解決方案
# Spring Cloud Gateway
# Zuul
Zuul基於 servlet 2.5(使用3.x),使用阻塞API。 它不支持任何 長連接 ,如 web sockets。而Gateway建立在Spring Framework 5,Project Reactor和Spring Boot 2之上,使用非阻塞API。 Websockets得到支持,並且由於它與Spring緊密集成,所以將會是一個更好的開發體驗。
參考:https://juejin.im/post/5aa4eacbf265da237a4ca36f
3、模擬場景
客戶端請求后端服務,網關提供后端服務的統一入口。后端的服務都注冊到Zookeeper、Consul或者Eureka (服務發現、配置管理中心服務)。網關通過負載均衡。轉發到具體的后端服務。
4、Zuul
Zuul 提供了四種過濾器的 API,動態讀取、編譯和運行這些過濾器。過濾器之間不能相互通訊,只能通過RequestContext對象共享數據。
# 前置(Pre)鑒權、請求轉發、增加請求參數等行為
一般來說整個服務的鑒權邏輯可以很復雜。
- 客戶端:App、Web、Backend
- 權限組:用戶、后台人員、其他開發者
- 實現:OAuth、JWT
- 使用方式:Token、Cookie、SSO
而對於后端應用來說,它們其實只需要知道請求屬於誰,而不需要知道為什么,所以 Gateway 可以友善的幫助后端應用完成鑒權這個行為,並將用戶的唯一標示透傳到后端,而不需要、甚至不應該將身份信息也傳遞給后端,防止某些應用利用這些敏感信息做錯誤的事情。Zuul 默認情況下在處理后會刪除請求的 Authorization
頭和 Set-Cookie
頭,也算是貫徹了這個原則。
# 后置(Post)統計返回值和調用時間、記錄日志、增加跨域頭等行為
使用 Gateway 做跨域相比應用本身或是 Nginx 的好處是規則可以配置的更加靈活。例如一個常見的規則。
-
對於任意的 AJAX 請求,返回
Access-Control-Allow-Origin
為*
,且Access-Control-Allow-Credentials
為true
,這是一個常用的允許任意源跨域的配置,但是不允許請求攜帶任何 Cookie -
如果一個被信任的請求者需要攜帶 Cookie,那么將它的
Origin
增加到白名單中。對於白名單中的請求,返回Access-Control-Allow-Origin
為該域名,且Access-Control-Allow-Credentials
為true
,這樣請求者可以正常的請求接口,同時可以在請求接口時攜帶 Cookie -
對於 302 的請求,即使在白名單內也必須要設置
Access-Control-Allow-Origin
為*
,否則重定向后的請求攜帶的Origin
會為null
,有可能會導致 iOS 低版本的某些兼容問題
Gateway 可以統一收集所有應用請求的記錄,並寫入日志文件或是發到監控系統,相比 Nginx 的 access log,好處主要也是二次開發比較方便,比如可以關注一些業務相關的 HTTP 頭,或是將請求參數和返回值都保存為日志打入消息隊列中,便於線上故障調試。也可以收集一些性能指標發送到類似 Statsd 這樣的監控平台。
# 路由(Route)一般只需要選擇 Zuul 中內置的即可
#錯誤(Error)一般只需要一個,這樣可以在 Gateway 遇到錯誤邏輯時直接拋出異常中斷流程,並直接統一處理返回結果
錯誤過濾器的主要用法就像是 Jersey 中的 ExceptionMapper
或是 Spring MVC 中的 @ExceptionHandler
一樣,在處理流程中認為有問題時,直接拋出統一的異常,錯誤過濾器捕獲到這個異常后,就可以統一的進行返回值的封裝,並直接結束該請求。
總結關鍵特性:
1、Type,規定類型
2、Execution Order,規定執行順序,Order值越小越優先
3、Criteria,規定執行所需要的條件
4、Action,如果符合條件,則執行Action
一個請求會先按順序通過所有的前置過濾器,之后在路由過濾器中轉發給后端應用,得到響應后又會通過所有的后置過濾器,最后響應給客戶端。在整個流程中如果發生了異常則會跳轉到錯誤過濾器中。
5、注解配置
/**
* 這個接口需要鑒權,鑒權方式是 OAuth
*/
@Authorization(OAuth)
@RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
public void del(@PathVariable int id) {
//...
}
/**
* 這個接口可以緩存,並且每個 IP/User 每秒最多請求 10 次
*/
@Cacheable
@RateLimiting(limit = "10/1s", scope = {IP, USER})
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
public void info(@PathVariable int id) {
//...
}
6、穩定性
# 隔離機制
在 Zuul 中,每一個后端應用都稱為一個 Route,為了避免一個 Route 搶占了太多資源影響到其他 Route 的情況出現,Zuul 使用 Hystrix 對每一個 Route 都做了隔離和限流。
Hystrix 的隔離策略有兩種,基於線程或是基於信號量。Zuul 默認的是基於線程的隔離機制,這意味着每一個 Route 的請求都會在一個固定大小且獨立的線程池中執行,這樣即使其中一個 Route 出現了問題,也只會是某一個線程池發生了阻塞,其他 Route 不會受到影響。一般使用 Hystrix 時,只有調用量巨大會受到線程開銷影響時才會使用信號量進行隔離策略,對於 Zuul 這種網絡請求的用途使用線程隔離更加穩妥。
# 重試機制
Zuul 的路由主要有 Eureka 和 Ribbon 兩種方式,簡單介紹下 Ribbon 支持哪些容錯配置。
重試的場景分為三種:
okToRetryOnConnectErrors
:只重試網絡錯誤okToRetryOnAllErrors
:重試所有錯誤OkToRetryOnAllOperations
:重試所有操作(這里不太理解,猜測是 GET/POST 等請求都會重試)
重試的次數有兩種:
MaxAutoRetries
:每個節點的最大重試次數MaxAutoRetriesNextServer
:更換節點重試的最大次數
一般來說我們希望只在網絡連接失敗時進行重試、或是對 5XX 的 GET 請求進行重試(不推薦對 POST 請求進行重試,無法保證冪等性會造成數據不一致)。單台的重試次數可以盡量小一些,重試的節點數盡量多一些,整體效果會更好。
如果有更加復雜的重試場景,例如需要對特定的某些 API、特定的返回值進行重試,那么也可以通過實現 RequestSpecificRetryHandler
定制邏輯(不建議直接使用 RetryHandler
,因為這個子類可以使用很多已有的功能)。
7、Tomcat
Tomcat的最大並發數是可以配置的,實際運用中,最大並發數與硬件性能和CPU數量都有很大關系的。更好的硬件,更多的處理器都會使Tomcat支持更多的並發。
Tomcat 默認的HTTP實現是采用阻塞式的Socket通信,每個請求都需要創建一個線程處理,當一個進程有500個線程在跑的話,那性能已經是很低很低了。Tomcat默認配置的最大請求數是150,也就是說同時支持150個並發。具體能承載多少並發,需要看硬件的配置,CPU越多性能越高,分配給JVM的內存越多性能也就越高,但也會加重GC的負擔。當某個應用擁有 250個以上並發的時候,應考慮應用服務器的集群。操作系統對於進程中的線程數有一定的限制:
Windows 每個進程中的線程數不允許超過 2000
Linux 每個進程中的線程數不允許超過 1000
在Java中每開啟一個線程需要耗用1MB的JVM內存空間用於作為線程棧之用,此處也應考慮。
8、實際應用
引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
啟動類開啟zuul代理:
@SpringBootApplication @EnableEurekaClient @EnableZuulProxy public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
配置文件配置路由信息:
server: port: 9009 spring: application: name: zuul-client eureka: client: service-url: defaultZone: http://localhost:9001/eureka/ zuul: routes: hiapi: path: /hiapi/** serviceId: hi-service
訪問:http://localhost:9009/hiapi/hi,如果hi-service部署了多個實例,那么zuul在路由轉發就做了負載均衡。
當然也可以使用url屬性代替serviceId屬性,通過指定ip+port的方式的url地址來直接訪問(當然這種情況很少出現)
如果想自己維護負載均衡的服務列表,可以使用如下方式:
zuul: routes: hiapi: path: /hiapi/** serviceId: hiapi-v1 ribbon: eureka: enabled: false hiapi-v1: ribbon: listOfServers: http://localhost:9007,http://localhost:9008,http://localhost:9009/hiapi/hi
配置API接口的版本號:
zuul: routes: hiapi: path: /hiapi/** serviceId: hi-service prefix: v1
那么訪問路徑將變為:http://localhost:9009/v1/hiapi/hi
集成Hystrix實現熔斷器:
@Component public class MyFallbackProvider implements FallbackProvider { @Override public String getRoute() { return "hi-service"; // 應用名稱或者serviceId,或者是正則表達式,如* } @Override public ClientHttpResponse fallbackResponse(String route, final Throwable cause) { if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { return response(HttpStatus.INTERNAL_SERVER_ERROR); } } private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return status; } @Override public int getRawStatusCode() throws IOException { return status.value(); } @Override public String getStatusText() throws IOException { return status.getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("fallback".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
在Zuul中使用過濾器:
@Component public class MyFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; // 前置過濾器
} @Override public int filterOrder() { return 0; // 優先級為0,數字越大,優先級越低 } @Override public boolean shouldFilter() { return true; // 是否執行該過濾器,此處為true,說明需要過濾 } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = request.getParameter("token"); if (StringUtils.isBlank(token)) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try { ctx.getResponse().getWriter().write("token is empty"); } catch (IOException e) { } } return null; } }