鎮博圖 java
springcloud 總集:www.tapme.top/blog/detail…git
本篇中 Zuul 版本爲 1.x,目前最新的是 2.x,兩者在過濾器的使用上有較大區別github
超長警告web
項目代碼見文章結尾spring
微服務架構將一個應用拆分爲不少個微小應用,這樣會致使以前不是問題的問題出現,好比:數據庫
上面的問題在傳統的單機應用很容易解決,只須要看成一個功能實現便可。可是在微服務中就行不通了,讓每一個服務都實現一份上述功能,那是至關不現實的,費時,費力還容易出問題。json
爲了解決這個問題,須要將這些橫切關注點(分佈式系統級別的橫切關注點和 spring 中的基本一個意思)抽象成一個獨立的且做爲應用程序中全部微服務調用的過濾器和路由器的服務。這樣的服務被稱爲——服務網管(service gateway),服務客戶端再也不直接調用服務。取而代之的是,服務網關做爲單個策略執行點(Policy Enforcement Point,PEP) , 全部調用都經過服務網管進行路由,而後送到目的地。api
以前的幾節中咱們是經過 http 請求直接調用各個服務,一般在實際系統中不會直接調用。而是經過服務網關來進行服務調用。服務網關充當了服務客戶端和被調用服務間的中介。服務客戶端僅與服務網關管理的單個 url 進行對話。下圖說了服務網關在一個系統中的做用:安全
服務網關位於服務客戶端和相應的服務實例之間。全部的服務調用(內部和外部)都應流經服務網關。springboot
因爲服務網關代理了全部的服務調用,所以它還能充當服務調用的中央策略執行點(PEP),通俗的說就能可以在此實現橫切關注點,不用在各個微服務中實現。主要有如下幾個:
靜態路由——服務網關將全部的服務調用放置在單個 URL 和 API 路由後,每一個服務對應一個固定的服務端點,方便開發人員的服務調用。
動態路由——服務網關能夠檢測傳入的請求,根據請求數據和請求者執行職能路由。好比將一部分的調用路由到特定的服務實例上,好比測試版本。
驗證和受權——全部服務調用都通過服務網關,顯然能夠在此進行權限驗證,確保系統安全。
日誌記錄——當服務調用通過服務網關時,可使用服務網關來收集數據和日誌信息(好比服務調用次數,服務響應時間等)。還能確保在用戶請求上提供關鍵信息以確保日誌統計(好比給每一個用戶請求加一個 url 參數,每一個服務中可經過該參數將關鍵信息對應到某個用戶請求)。
看到這兒可能會有這樣的疑問:全部調用都經過服務網關,難道服務網關不是單點故障和潛在瓶頸嗎?
1. 在單獨的服務器前,負載均衡器是頗有用的。將負載均衡器放到多個服務網關前面是比較好的設計,確保服務網關能夠實現伸縮。可是若是將負載均衡器置於全部服務前便不是一個好主意,會形成瓶頸。
2. 服務網關的代碼應該是無狀態的。有狀態的應用實現伸縮性較爲麻煩
3. 服務網關的代碼應該輕量的。服務網關是服務調用的「阻塞點」,不易在服務網關處耽誤較長的時間,好比進行同步數據庫操做
使用 Netflix Zuul 來構建服務網關,配合以前的代碼,讓服務網關來管理服務調用。
在生產環境中不建議使用 zuul,該組件性能較弱,且已經中止更新
詳細過程不贅述,和以前同樣(注意 spring cloud 版本要和以前一致),主要 pom 依賴以下:
<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>
複製代碼
首先在啓動加入註解開啓 zuul 並註冊到 eureka 中
而後編寫配置文件:
spring:
application:
name: zuulservice
#服務發現配置
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
server:
port: 5555
複製代碼
這樣便以默認配置啓動了 zuul 服務網關。
Zuul 核心就是一個反向代理。在微服務架構下,Zuul 從客戶端接受微服務調用並將其轉發給下游服務。要和下游服務進行溝通,Zuul 必須知道如何將進來的調用映射到下游路由中。Zuul 有一如下幾種路由機制:
默認狀況下,Zuul 根據服務 ID 來進行自動路由。先將組織服務中的延時去掉
啓動以前的全部服務實例,而後經過 postman 訪問localhost:5555/organizationservice/organization/12,獲得結果以下:
說明服務網關自動路由成功。
若是要查看 Zuul 服務器管理的路由,能夠經過訪問 Zuul 服務器上的/routes,返回結果以下:
{
"/confsvr/**": "confsvr",
"/licensingservice/**": "licensingservice",
"/organizationservice/**": "organizationservice"
}
複製代碼
左邊的路由由基於 Eureka 的服務 ID 自動建立的,右邊爲路由全部映射的 Eureka 服務 ID。
若是以爲自動路由很差用,咱們還能夠更細粒度地明肯定義路由映射。例如想要縮短組織服務名稱來簡化路由,可在application.yml
配置中定義路由映射,在配置文件中加入以下配置:
zuul:
routes:
organizationservice: /org/**
複製代碼
上面的配置將org開頭的路徑映射到組織服務上了。重啓服務器,訪問localhost:5555/org/organization/12,仍然可以獲取到數據。
如今訪問/routes
端點能夠看到以下結果:
{
"/org/**": "organizationservice",
"/confsvr/**": "confsvr",
"/licensingservice/**": "licensingservice",
"/organizationservice/**": "organizationservice"
}
複製代碼
能夠看到不光有自定義的組織路由,自動映射的組織路由也存在,若是想要排除自動映射的路由可配置ignored-services
屬性,用法以下:
zuul:
routes:
organizationservice: /org/**
# 使用","分隔,「*」表示所有忽略
ignored-services: 'organizationservice'
複製代碼
服務網關有一種常見模式是經過使用/api
之類的標記來爲全部服務調用添加前綴,可經過配置prefix
屬性來支持。用法以下:
zuul:
routes:
organizationservice: /org/**
# 使用","分隔,「*」表示所有忽略
ignored-services: 'organizationservice'
prefix: /api
複製代碼
配置後再次訪問/routes
端點能夠看到路徑前都加上了/api
若是系統系統中還存在一些不受 Eureka 管理的服務,能夠創建 Zuul 直接路由到一個靜態定義的 URL。假設許可證服務是其餘語言編寫的 web 項目,而且但願經過 Zuul 來代理,可這樣配置:
zuul:
routes:
#用於內部識別關鍵字
licensestatic:
path: /licensestatic/**
url: http://localhost:8091
複製代碼
配置完成後重啓 zuul 訪問/routes
端點以下所示,靜態路由已經加入:
{
"/api/licensestatic/**": "http://localhost:8091",
"/api/org/**": "organizationservice",
"/api/confsvr/**": "confsvr",
"/api/licensingservice/**": "licensingservice",
"/api/zuulservice/**": "zuulservice"
}
複製代碼
licensestatic 端點再也不使用 Eureka,直接將請求路由到localhost:8091
。可是這裏存在一個問題,若是許可證服務有多個實例,該如何用到負載均衡?這裏只能配置一條路徑指向請求。這裏又有一個配置項來禁用 Ribbon 與 Eureka 集成,而後列出許可證服務的全部實例,配置以下:
#zuul配置
zuul:
routes:
#用於內部識別關鍵字
licensestatic:
path: /licensestatic/**
serviceId: licensestatic
organizationservice: /org/**
# 使用","分隔,「*」表示所有忽略
ignored-services: 'organizationservice'
prefix: /api
ribbon:
eureka:
#禁用Eureka支持
enabled: false
licensestatic:
ribbon:
#licensestatic服務將會路由到下列地址
listOfServers: http://localhost:10011,http://localhost:10012
複製代碼
配置完畢後,訪問/routes
端點發現licensestatic/**
映射到了 licensestatic 服務上,至關於 Zuul 模擬了一個服務出來。可是 Eureka 上是沒有這個服務的,因此須要禁用掉 Ribbon 的 Eureka 支持,否則是沒法訪問成功的(Ribbon 向 Eureka 查詢該服務不存在,報錯)。如今 x=連續訪問localhost:5555//api/licensestatic/licensing/12,能夠發現正常響應和 404 交替出現(10011 上可否訪問成功,10012 報錯 404),說明配置的多個地址生效了。
問題又來了
禁用eureka支持會致使全部服務的地址都須要手動指定,ribbon不會再從eureka中獲取服務實例信息。因此沒辦法混合使用
目前有兩種辦法來規避這個問題:
對於不能用 Eureka 管理的應用,能夠創建一個單獨的 Zuul 服務器來處理這些路由。
創建一個 Spring Cloud Sidecar 實例。Spring Cloud Sidecar 容許開發使用 Eureka 實例註冊非 JVM 服務,而後再經過 Zuul 代理,至關於曲線救國。
zuul 還有一個動態加載路由的功能,也就是在不重啓 zuul 服務的狀況下刷新路由。
直接修改application.yml
將 prefix 從/api
改成/apis
。注意這裏修改後要讓修改生效需編譯一次 application.yml 讓修改替換到 target 文件中(idea 如此,eclipse 應該相似),或者直接到編譯文件夾下修改 application.yml
而後訪問/refresh
路徑,能夠看到以下返回值:
響應代表更新 prefix。而後訪問/routes
路徑會發現前綴變成了apis
這個功能與 spring cloud config 配合,用起來就是爽。
Zuul 使用 Netflix 的 Hystrix 和 Ribbon 庫來進行 http 請求。so 也是有超時機制存在的。配置方法和前面的一篇相似。可是隻能經過配置文件來進行,沒法經過註解(這是 Zuul 管理的沒有地方給你寫註解)。經過配置hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
屬性來實現。若是要爲特定的服務配置只需將 default 替換爲服務名就好了。
注意還要只有有另外一個超時機制。雖然覆蓋了 hystrix 的超時,可是 Ribbon 也會超時任何超過 5s 的調用。so 若是超時時間大於 5s 還要配置 Ribbon 的超時,配置方式以下:
#對全部服務生效
ribbon.readTimeout: 7000
#對組織服務生效
licensingservice.ribbon.readTimeout: 7000
複製代碼
這纔是服務網關真正重要的東西。有了過濾器才能實現自定義的通用處理邏輯。可在此進行通用的安全驗證、日誌、服務跟蹤等操做。和 springboot 中的過濾器概念相似,這裏就不作說明了。
Zuul 支持如下四種過濾器:
前置過濾器——在將請求發送到目的地以前被調用。一般進行請求格式檢查、身份驗證等操做。
後置過濾器——在目標服務被調用被將響應發回調用者後被調用。一般用於記錄從目標服務返回的響應、處理錯誤或審覈敏感信息。
路由過濾器——在目標服務被調用以前攔截調用。一般用來作動態路由。
錯誤過濾器——在產生錯誤是調用,用於對錯誤進行統一處理。
下圖展現了在處理客戶端請求時,各類過濾器時如何工做的:
下面說說如何來使用這些過濾器:
這裏咱們來實現一個過濾器-IdFilter,對每一個請求檢查請求頭中是否有一個關聯 id,無 id 生成一個 id 加入到 header 中。代碼以下:
@Component
public class IdFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(IdFilter.class);
/** * 返回過濾器類型 ;pre:前置過濾器。post:後置過濾器。routing:路由過濾器。error:錯誤過濾器 */
@Override
public String filterType() {
return "pre";
}
/** * 過濾器執行順序 */
@Override
public int filterOrder() {
return 1;
}
/** * 是否啓動此過濾器 */
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String id = ctx.getRequest().getHeader("id");
//若是request找不到,再到zuul的方法中找id.request不容許直接修改response中的header,
// 因此爲了讓後續的過濾器可以獲取到id纔有下面的語法
if(id==null){
id = ctx.getZuulRequestHeaders().get("id");
}
if (id == null) {
id = UUID.randomUUID().toString();
LOGGER.info("{} 無id,生成id:{}",ctx.getRequest().getRequestURI(), id);
ctx.addZuulRequestHeader("id", id);
} else {
LOGGER.info("{}存在id:{}", ctx.getRequest().getRequestURI(), id);
}
return null;
}
}
複製代碼
要在 Zuul 中實現過濾器,必須拓展 ZuulFilter 類(2.x 版本中不是這樣的),而後覆蓋上述 4 個方法。
要給請求頭加入一個 header 須要在ctx.addZuulRequestHreader("","")
(上面代碼中的 RequestContext 是 zuul 重寫的,在其中加入了一些方法)方法中操做,zuul 會在發出請求是把 header 加到請求頭中。(由於 Zuul 本質是一個代理,它截取請求,而後本身再發送這個請求,全部不能也沒有必要在原來的 request 上加 header。
重啓項目 Zuul,訪問localhost:5555/apis/licensestatic/licensing/12
,能夠看到控制檯有以下打印:
說明前置過濾器生效。
如今從 zuul 服務網關發往許可證服務的 http 請求已經攜帶了 id。
後置過濾器一般用於進行敏感信息過濾和響應記錄。這裏咱們實現一個後置過濾器,將許可證服務請求的響應內容打印到控制檯上同時把id
header 插入到服務客戶端請求的 response 中。
@Component
public class ResponseFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(ResponseFilter.class);
/** * 返回過濾器類型 ;pre:前置過濾器。post:後置過濾器。routing:路由過濾器。error:錯誤過濾器 */
@Override
public String filterType() {
return "post";
}
/** * 過濾器執行順序 */
@Override
public int filterOrder() {
return 1;
}
/** * 是否啓動此過濾器 */
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run(){
RequestContext ctx = RequestContext.getCurrentContext();
String id = ctx.getZuulRequestHeaders().get("id");
ctx.getResponse().addHeader("id", id);
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(ctx.getResponseDataStream()));
String response = reader.readLine();
LOGGER.info("響應爲:{}", response);
//寫到輸出流中,原本能夠由zuul框架來操做,可是咱們已經讀取了輸入流,zuul讀不到數據了,因此要手動寫響應到response
ctx.getResponse().setHeader("Content-Type","application/json;charset=utf-8");
ctx.getResponse().getWriter().write(response);
} catch (Exception e) {
}
return null;
}
}
複製代碼
通過這樣一波操做,就能達到目的了。訪問:localhost:5555/apis/licensestatic/licensing/12。控制檯打印以下:
請求響應以下:
路由過濾器用起來有點複雜,這裏不寫具體的實際代碼,只是寫一個思路。具體代碼能夠參考spring 微服務
終於寫完了,微服務的基礎學習又近了一步,加油!
本篇代碼存放於:github
本篇原創發佈於: tapme.top/blog/detail…
掃碼關注微信公衆號:FleyX 學習筆記,獲取更多幹貨