微服务深入浅出(7)-- 网关路由Zuul
Zuul用于構(gòu)建邊界服務(wù),致力于動態(tài)路由,過濾,監(jiān)控,彈性伸縮和安全等方向。
1、Zuul+Ribbon+Eureka結(jié)合,可以實(shí)現(xiàn)智能路由和負(fù)載均衡
2、網(wǎng)關(guān)將所有服務(wù)的API接口統(tǒng)一聚合統(tǒng)一暴露
3、網(wǎng)關(guān)統(tǒng)一爆率接口后,可以做身份和權(quán)限認(rèn)證
4、實(shí)現(xiàn)監(jiān)控功能,實(shí)時日志輸出
5、流量監(jiān)控,實(shí)現(xiàn)降級和限流
6、方便測試
1、網(wǎng)關(guān)存在的必要性
不同的微服務(wù)有不同的請求地址,如果一個客戶端需要訪問多個接口才能完成一個業(yè)務(wù)需求的話,可能存在以下問題:
# 客戶端會多次請求不同微服務(wù),增加客戶端的復(fù)雜性
# 存在跨域請求,在一定場景下處理相對復(fù)雜
# 認(rèn)證復(fù)雜,每一個服務(wù)都需要獨(dú)立認(rèn)證
# 難以重構(gòu),隨著項(xiàng)目的迭代,可能需要重新劃分微服務(wù),如果客戶端直接和微服務(wù)通信,那么重構(gòu)會難以實(shí)施
# 某些微服務(wù)可能使用了其他協(xié)議,直接訪問有一定困難
?
而微服務(wù)網(wǎng)關(guān)可以很好的解決這個問題:
這樣客戶端只需要和網(wǎng)關(guān)交互,而無需直接調(diào)用特定微服務(wù)的接口,而且方便監(jiān)控,易于認(rèn)證,減少客戶端和各個微服務(wù)之間的交互次數(shù)。
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緊密集成,所以將會是一個更好的開發(fā)體驗(yàn)。
參考:https://juejin.im/post/5aa4eacbf265da237a4ca36f
3、模擬場景
客戶端請求后端服務(wù),網(wǎng)關(guān)提供后端服務(wù)的統(tǒng)一入口。后端的服務(wù)都注冊到Zookeeper、Consul或者Eureka (服務(wù)發(fā)現(xiàn)、配置管理中心服務(wù))。網(wǎng)關(guān)通過負(fù)載均衡。轉(zhuǎn)發(fā)到具體的后端服務(wù)。
4、Zuul
Zuul 提供了四種過濾器的 API,動態(tài)讀取、編譯和運(yùn)行這些過濾器。過濾器之間不能相互通訊,只能通過RequestContext對象共享數(shù)據(jù)。
# 前置(Pre)鑒權(quán)、請求轉(zhuǎn)發(fā)、增加請求參數(shù)等行為
一般來說整個服務(wù)的鑒權(quán)邏輯可以很復(fù)雜。
- 客戶端:App、Web、Backend
- 權(quán)限組:用戶、后臺人員、其他開發(fā)者
- 實(shí)現(xiàn):OAuth、JWT
- 使用方式:Token、Cookie、SSO
而對于后端應(yīng)用來說,它們其實(shí)只需要知道請求屬于誰,而不需要知道為什么,所以 Gateway 可以友善的幫助后端應(yīng)用完成鑒權(quán)這個行為,并將用戶的唯一標(biāo)示透傳到后端,而不需要、甚至不應(yīng)該將身份信息也傳遞給后端,防止某些應(yīng)用利用這些敏感信息做錯誤的事情。Zuul 默認(rèn)情況下在處理后會刪除請求的?Authorization?頭和?Set-Cookie?頭,也算是貫徹了這個原則。
?
# 后置(Post)統(tǒng)計返回值和調(diào)用時間、記錄日志、增加跨域頭等行為
使用 Gateway 做跨域相比應(yīng)用本身或是 Nginx 的好處是規(guī)則可以配置的更加靈活。例如一個常見的規(guī)則。
對于任意的 AJAX 請求,返回?Access-Control-Allow-Origin?為?*,且?Access-Control-Allow-Credentials?為?true,這是一個常用的允許任意源跨域的配置,但是不允許請求攜帶任何 Cookie
如果一個被信任的請求者需要攜帶 Cookie,那么將它的?Origin?增加到白名單中。對于白名單中的請求,返回?Access-Control-Allow-Origin?為該域名,且?Access-Control-Allow-Credentials?為?true,這樣請求者可以正常的請求接口,同時可以在請求接口時攜帶 Cookie
對于 302 的請求,即使在白名單內(nèi)也必須要設(shè)置?Access-Control-Allow-Origin?為?*,否則重定向后的請求攜帶的?Origin?會為?null,有可能會導(dǎo)致 iOS 低版本的某些兼容問題
Gateway 可以統(tǒng)一收集所有應(yīng)用請求的記錄,并寫入日志文件或是發(fā)到監(jiān)控系統(tǒng),相比 Nginx 的 access log,好處主要也是二次開發(fā)比較方便,比如可以關(guān)注一些業(yè)務(wù)相關(guān)的 HTTP 頭,或是將請求參數(shù)和返回值都保存為日志打入消息隊(duì)列中,便于線上故障調(diào)試。也可以收集一些性能指標(biāo)發(fā)送到類似 Statsd 這樣的監(jiān)控平臺。
?
# 路由(Route)一般只需要選擇 Zuul 中內(nèi)置的即可
?
#錯誤(Error)一般只需要一個,這樣可以在 Gateway 遇到錯誤邏輯時直接拋出異常中斷流程,并直接統(tǒng)一處理返回結(jié)果
錯誤過濾器的主要用法就像是 Jersey 中的?ExceptionMapper?或是 Spring MVC 中的?@ExceptionHandler?一樣,在處理流程中認(rèn)為有問題時,直接拋出統(tǒng)一的異常,錯誤過濾器捕獲到這個異常后,就可以統(tǒng)一的進(jìn)行返回值的封裝,并直接結(jié)束該請求。
?
總結(jié)關(guān)鍵特性:
1、Type,規(guī)定類型
2、Execution Order,規(guī)定執(zhí)行順序,Order值越小越優(yōu)先
3、Criteria,規(guī)定執(zhí)行所需要的條件
4、Action,如果符合條件,則執(zhí)行Action
一個請求會先按順序通過所有的前置過濾器,之后在路由過濾器中轉(zhuǎn)發(fā)給后端應(yīng)用,得到響應(yīng)后又會通過所有的后置過濾器,最后響應(yīng)給客戶端。在整個流程中如果發(fā)生了異常則會跳轉(zhuǎn)到錯誤過濾器中。
?
5、注解配置
/*** 這個接口需要鑒權(quán),鑒權(quán)方式是 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、穩(wěn)定性
?# 隔離機(jī)制
在 Zuul 中,每一個后端應(yīng)用都稱為一個 Route,為了避免一個 Route 搶占了太多資源影響到其他 Route 的情況出現(xiàn),Zuul 使用?Hystrix 對每一個 Route 都做了隔離和限流。
Hystrix 的隔離策略有兩種,基于線程或是基于信號量。Zuul 默認(rèn)的是基于線程的隔離機(jī)制,這意味著每一個 Route 的請求都會在一個固定大小且獨(dú)立的線程池中執(zhí)行,這樣即使其中一個 Route 出現(xiàn)了問題,也只會是某一個線程池發(fā)生了阻塞,其他 Route 不會受到影響。一般使用 Hystrix 時,只有調(diào)用量巨大會受到線程開銷影響時才會使用信號量進(jìn)行隔離策略,對于 Zuul 這種網(wǎng)絡(luò)請求的用途使用線程隔離更加穩(wěn)妥。
# 重試機(jī)制
Zuul 的路由主要有 Eureka 和 Ribbon 兩種方式,簡單介紹下 Ribbon 支持哪些容錯配置。
重試的場景分為三種:
- okToRetryOnConnectErrors:只重試網(wǎng)絡(luò)錯誤
- okToRetryOnAllErrors:重試所有錯誤
- OkToRetryOnAllOperations:重試所有操作(這里不太理解,猜測是 GET/POST 等請求都會重試)
重試的次數(shù)有兩種:
- MaxAutoRetries:每個節(jié)點(diǎn)的最大重試次數(shù)
- MaxAutoRetriesNextServer:更換節(jié)點(diǎn)重試的最大次數(shù)
一般來說我們希望只在網(wǎng)絡(luò)連接失敗時進(jìn)行重試、或是對 5XX 的 GET 請求進(jìn)行重試(不推薦對 POST 請求進(jìn)行重試,無法保證冪等性會造成數(shù)據(jù)不一致)。單臺的重試次數(shù)可以盡量小一些,重試的節(jié)點(diǎn)數(shù)盡量多一些,整體效果會更好。
如果有更加復(fù)雜的重試場景,例如需要對特定的某些 API、特定的返回值進(jìn)行重試,那么也可以通過實(shí)現(xiàn)?RequestSpecificRetryHandler?定制邏輯(不建議直接使用?RetryHandler,因?yàn)檫@個子類可以使用很多已有的功能)。
?
7、Tomcat
Tomcat的最大并發(fā)數(shù)是可以配置的,實(shí)際運(yùn)用中,最大并發(fā)數(shù)與硬件性能和CPU數(shù)量都有很大關(guān)系的。更好的硬件,更多的處理器都會使Tomcat支持更多的并發(fā)。
Tomcat 默認(rèn)的HTTP實(shí)現(xiàn)是采用阻塞式的Socket通信,每個請求都需要創(chuàng)建一個線程處理,當(dāng)一個進(jìn)程有500個線程在跑的話,那性能已經(jīng)是很低很低了。Tomcat默認(rèn)配置的最大請求數(shù)是150,也就是說同時支持150個并發(fā)。具體能承載多少并發(fā),需要看硬件的配置,CPU越多性能越高,分配給JVM的內(nèi)存越多性能也就越高,但也會加重GC的負(fù)擔(dān)。當(dāng)某個應(yīng)用擁有 250個以上并發(fā)的時候,應(yīng)考慮應(yīng)用服務(wù)器的集群。操作系統(tǒng)對于進(jìn)程中的線程數(shù)有一定的限制:?
Windows 每個進(jìn)程中的線程數(shù)不允許超過 2000?
Linux 每個進(jìn)程中的線程數(shù)不允許超過 1000?
在Java中每開啟一個線程需要耗用1MB的JVM內(nèi)存空間用于作為線程棧之用,此處也應(yīng)考慮。?
8、實(shí)際應(yīng)用
引入依賴
<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部署了多個實(shí)例,那么zuul在路由轉(zhuǎn)發(fā)就做了負(fù)載均衡。
當(dāng)然也可以使用url屬性代替serviceId屬性,通過指定ip+port的方式的url地址來直接訪問(當(dāng)然這種情況很少出現(xiàn))
如果想自己維護(hù)負(fù)載均衡的服務(wù)列表,可以使用如下方式:
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那么訪問路徑將變?yōu)?#xff1a;http://localhost:9009/v1/hiapi/hi
集成Hystrix實(shí)現(xiàn)熔斷器:
@Component public class MyFallbackProvider implements FallbackProvider {@Overridepublic String getRoute() {return "hi-service"; // 應(yīng)用名稱或者serviceId,或者是正則表達(dá)式,如*}@Overridepublic 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() {@Overridepublic HttpStatus getStatusCode() throws IOException {return status;}@Overridepublic int getRawStatusCode() throws IOException {return status.value();}@Overridepublic String getStatusText() throws IOException {return status.getReasonPhrase();}@Overridepublic void close() {}@Overridepublic InputStream getBody() throws IOException {return new ByteArrayInputStream("fallback".getBytes());}@Overridepublic HttpHeaders getHeaders() {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);return headers;}};} }在Zuul中使用過濾器:
@Component public class MyFilter extends ZuulFilter {@Overridepublic String filterType() {return FilterConstants.PRE_TYPE; //?前置過濾器 }@Overridepublic int filterOrder() {return 0; //?優(yōu)先級為0,數(shù)字越大,優(yōu)先級越低}@Overridepublic boolean shouldFilter() {return true; //?是否執(zhí)行該過濾器,此處為true,說明需要過濾}@Overridepublic 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;} }?
轉(zhuǎn)載于:https://www.cnblogs.com/ijavanese/p/9198203.html
總結(jié)
以上是生活随笔為你收集整理的微服务深入浅出(7)-- 网关路由Zuul的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux(centos8):安装jav
- 下一篇: Python中_,__,__xx__方法