一 spring cloud Zuul底層架構(gòu)
spring cloud zuul融合的是netflix 的zuul 1.x版本
1.1 netflix 的1.x的線程模型
zuul1.x采用的是線程阻塞模型穗熬,也就是我們常說的BIO,每來一個請求就會從線程池中分配一個線程去處理阔涉。這里導(dǎo)致的問題就是如果每一次請求的耗時很長,i/o操作時間很長犁享,就會導(dǎo)致大量線程被掛起,利用率不僅低,很容易耗盡容器線程池內(nèi)的線程建邓,造成容器無法接受新的請求。所以對于這種io密集型睁枕,大請求來說官边,是不合適的。他適合那種小請求外遇,cpu密集型注簿,如果每一次請求只需要0.0幾秒,一個線程1s鐘也可以處理上百次請求跳仿,再加上簡單诡渴,所以對于流量不是特別大,請求時間很短的場景是很實用的菲语。
應(yīng)用場景:
- cpu密集型任務(wù)
- 簡單操作的需求
- 開發(fā)簡單的需求
- 實時請求高的
1.2 zuul 2.x 的線程模型
可以簡單理解為有一個隊列專門負(fù)責(zé)處理用戶請求(如果連接量大妄辩,可以開個線程組來處理),后端有個隊列專門負(fù)責(zé)處理后臺服務(wù)調(diào)用(如果連接量大谨究,可以開個線程組來處理)恩袱,中間有個事件環(huán)線程(Event Loop Thread),它同時監(jiān)聽前后兩個隊列上的事件胶哲,有事件就觸發(fā)回調(diào)函數(shù)處理事件畔塔。這種模式下需要的線程比較少,基本上每個CPU核上只需要一個事件環(huán)處理線程鸯屿,前端的連接數(shù)可以很多澈吨,連接來了只需要進隊列,不需要啟動線程寄摆,事件環(huán)線程由事件觸發(fā)谅辣,沒有多線程阻塞問題。但是zuul2.x帶來的問題就是開發(fā)成本大婶恼,對于小請求來說他的性能提升不明顯桑阶。
應(yīng)用場景:
- io密集的任務(wù)
- 大請求或者大文件
- 隊列的流式數(shù)據(jù)
- 超大量的連接
1.3 Zulu 1.x 的架構(gòu)
每一次請求都會通過servlet柏副,然后進入各種過濾器,最后再返回給客戶端蚣录。而各個filters之間不會直接進行通信的割择。zuul 1.x使用RequestContext來實現(xiàn)各個filters之間共享數(shù)據(jù),而RequestContext采用ConcurrentHashMap和ThreadLocal實現(xiàn)線程安全萎河。
1.4 Zuul 1.x requestLifeCycle
一次請求的生命周期就如上圖所示荔泳。可見最重要的一環(huán)就是各種過濾器虐杯。
過濾器類型:
PRE:這種過濾器在請求被路由之前調(diào)用玛歌。我們可利用這種過濾器實現(xiàn)身份驗證、在集群中選擇請求的微服務(wù)擎椰、記錄調(diào)試信息等支子。
ROUTING:這種過濾器將請求路由到微服務(wù)。這種過濾器用于構(gòu)建發(fā)送給微服務(wù)的請求确憨,并使用Apache HttpClient或Netfilx Ribbon請求微服務(wù)译荞。
POST:這種過濾器在路由到微服務(wù)以后執(zhí)行。這種過濾器可用來為響應(yīng)添加標(biāo)準(zhǔn)的HTTP Header休弃、收集統(tǒng)計信息和指標(biāo)吞歼、將響應(yīng)從微服務(wù)發(fā)送給客戶端等你虹。
-
ERROR:在其他階段發(fā)生錯誤時執(zhí)行該過濾器
對于我們的業(yè)務(wù)來說嚼酝,我們可以結(jié)合我們的業(yè)務(wù)來定制適合自己的過濾器。
定制自己的過濾器其實也很簡單咕幻,只繼成ZuulFilter丈甸,實現(xiàn)
String filterType();
int filterOrder();
boolean shouldFilter();
-
Object run();
其中:
filterType:返回過濾器的類型糯俗。有pre、route睦擂、post得湘、error等幾種取值,分別對應(yīng)上文的幾種過濾器
filterOrder:返回一個int值來指定過濾器的執(zhí)行順序顿仇,不同的過濾器允許返回相同的數(shù)字淘正。
shouldFilter:返回一個boolean值來判斷該過濾器是否要執(zhí)行,true表示執(zhí)行臼闻,false表示不執(zhí)行鸿吆。可以在此添加一些條件
run:過濾器的具體邏輯述呐。一個對token進行check過濾器的例子:
* @author zhaokai008@ke.com
* @date 2019-05-11 21:45
*/
public class TokenFilters {
private static Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class);
?
@Override
public String filterType() {
return PRE_TYPE;
}
?
@Override
public int filterOrder() {
return 0;
}
?
@Override
public boolean shouldFilter() {
//todo 什么時候需要驗證token
return true;
}
?
@Override
public Object run() {
?
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
String token = request.getHeader("token");
checkToken(url,token);
return null;
?
}
?
private boolean checkToken(String url, String token) {
//todo dosomething
return true;
}
?
}
二 配置詳解
zuul:
SendErrorFilter:
error:
disable: true
sensitive-headers:
#ignored-services: '*'
# 限流
ratelimit:
enabled: true
fallback: true
repository: in_memory
# 全局限流
default-policy-list:
- limit: 10000 #optional - request number limit per refresh interval window
quota: 300 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- url
# 分route限流
policy-list:
test1:
- limit: 1 #optional - request number limit per refresh interval window
quota: 10 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 1 #default value (in seconds)
type: #optional
- url
routes:
test1:
path: /api/10/search/**
serviceId: common
stripPrefix: false
token: 10
test2:
path: /api/10/search/**
serviceId: on
stripPrefix: false
token: 10
test3:
path: /api/data/10/**
serviceId: DAS
stripPrefix: false
token: d10
test4:
path: /api/data/**
serviceId: no
stripPrefix: false
token: d10
test5:
path: /api/search/**
serviceId: no
stripPrefix: true
token: d10
test6:
path: /api/search/**
url: http://host/api/data/**
stripPrefix: false
token: d10
test7:
path: /*
url: http://host/api/data/**
stripPrefix: false
token: d10
# hystrix
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
host:
max-per-route-connections: 20 #默認(rèn)20
max-total-connections: 200 #默認(rèn)200
ribbon:
eager-load:
enabled: true
ConnectTimeout: 15 #默認(rèn)1000
ReadTimeout: 15 #默認(rèn)2000
2.1 請求路由
zuul.ignored-services: 默認(rèn)情況下所有Eureka上的服務(wù)都會被Zuul自動創(chuàng)建映射關(guān)系進行路由惩淳,一般情況下需要 將其 置為 * ,表示不需要為所有服務(wù)自動創(chuàng)建路規(guī)則乓搬。這個默認(rèn)情況下是需要設(shè)置為 * 思犁,因為很多時候我們不想讓我們的服務(wù)都對外暴露
2.1.1 設(shè)置路由
有兩種方式代虾,URL和serviceId.都是通過匹配到網(wǎng)關(guān)的path然后映射到對應(yīng)的服務(wù)(url或者serviceId)上去。其中test5是url式抒倚,其余的都是serviceId式褐着。但是要注意的是url方式?jīng)]有 Hystrix、Ribbon 特性托呕。
- stripPrefix: 為true時(默認(rèn)為true),會忽略path的路徑频敛,如test5项郊,當(dāng)訪問http://host/api/data路徑時,請求將會被轉(zhuǎn)發(fā)到http://host/路徑斟赚,要正常訪問的話:http://host/api/data/api/data.
2.1.2 存取路由順序
- 所有的Route保存在一個map里面着降,而map的key是path,所有如果配置有兩個path一致的話就會有一個被覆蓋
- 而對于yaml文件的來說拗军,存入的順序是按照先后順序存入的任洞。
示例:
比如上面的配置,首先存入的是test1发侵,但是后來發(fā)現(xiàn)最后test2的path和test1一致交掏,最終我們的routes里面包含的route就只有test2而沒有test1
2.1.3 讀取路由規(guī)則
1,使用路由規(guī)則匹配請求路徑的時候是通過線性遍歷的方式刃鳄,在請求路徑獲取到第一個匹配的路由規(guī)則之后就會返回并結(jié)束匹配過程盅弛。所以當(dāng)存在多個匹配的路由規(guī)則時,匹配結(jié)果完全取決于路由規(guī)則的保存順序叔锐。
2挪鹏, 在yaml存入是按照順序存入的,左邊第一個圖當(dāng)我們訪問/api/data/10的時候愉烙,因為第一個匹配到的是/api/data/10/**讨盒,所以會映射到test3的DAS,而不會映射到test4 的no服務(wù)
2.2 限流
- 分為全局限流(default-policy-list)和分route(policy-list)限流:
- Limit:單位時間內(nèi)允許訪問的次數(shù)
- Quota:單位時間內(nèi)允許訪問的總時間(統(tǒng)計每次請求的時間綜合)
- refresh-interval:單位時間設(shè)置,默認(rèn)60s
- Type限流方式:ORIGIN, USER, URL
policy-list.test1配置的意思是:在一個時間窗口 1s 內(nèi)步责,最多允許 12 次訪問返顺,或者總請求時間小于 120s
2.3 超時,熔斷
- ribbon. ConnectTimeout,ribbon.ReadTimeout
- zuul.host.connect-timeout-millis勺择, zuul.host.socket-timeout-millis
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 6000
單位:ms,區(qū)別在于创南,如果路由方式是serviceId的方式,那么ribbon的生效省核,如果是url的方式稿辙,則zuul.host開頭的生效,ribbon 和hystrix誰小,誰生效气忠。Hystrix是熔斷邻储,力度可基于全局配置和基于某個服務(wù)赋咽,但是不能基于route
2.4 線程池大小
- max-per-route-connections:表示為每一個route分配的最大線程數(shù),只是限制了線程數(shù)而不是限流吨娜,和qps沒多大關(guān)系
- max-total-connections:表示所有route的最大線程數(shù)
三 Zuul 使用的坑:
- stripPrefix:默認(rèn)為true脓匿,需要注意
- 過動態(tài)刷新的配置,對于路由規(guī)則的變動宦赠,只能新增和修改陪毡,不能刪除(所以動態(tài)刪除會刷新不生效的錯覺);
- 新增的規(guī)則在匹配順序上勾扭,位于老規(guī)則的后(如上面的test7配置生效之后毡琉,因為/*已經(jīng)表示所有的路由,導(dǎo)致后面所有新加的所以路由都會打到test7之上妙色,以后想要再添加其他的路由桅滋,是相當(dāng)于不生效的。)
- 一定要要將超時時間設(shè)置小一點身辨,負(fù)責(zé)如果一瞬間上來都是2s或者更長的請求丐谋,就會導(dǎo)致在這2s內(nèi)的這些線程得不到釋放,就不能處理更多的請求煌珊,服務(wù)器性能大幅度降低号俐。