前言
看過之前SBC系列的小夥伴應該都可以搭建一個高可用、分散式的微服務了。 目前的結構圖應該如下所示:
各個微服務之間都不存在單點,並且都註冊於 Eureka
,基於此進行服務的註冊於發現,再通過 Ribbon
進行服務呼叫,並具有客戶端負載功能。
一切看起來都比較美好,但這裡卻忘了一個重要的細節:
當我們需要對外提供服務時怎麼處理?
這當然也能實現,無非就是將我們具體的微服務地址加埠暴露出去即可。
那又如何來實現負載呢?
簡單!可以通過 Nginx F5
之類的工具進行負載。
但是如果系統龐大,服務拆分的足夠多那又有誰來維護這些路由關係呢?
當然這是運維的活,不過這時候運維可能就要發飆了!
並且還有一系列的問題:
- 服務呼叫之間的一些鑑權、簽名校驗怎麼做?
- 由於服務端地址較多,客戶端請求難以維護。
針對於這一些問題 SpringCloud
全家桶自然也有對應的解決方案: Zuul
。
當我們系統整合 Zuul 閘道器之後架構圖應該如下所示:
我們在所有的請求進來之前抽出一層閘道器應用,將服務提供的所有細節都進行了包裝,這樣所有的客戶端都是和閘道器進行互動,簡化了客戶端開發。
同時具有如下功能:
- Zuul 註冊於
Eureka
並整合了Ribbon
所以自然也是可以從註冊中心獲取到服務列表進行客戶端負載。 - 功能豐富的路由功能,解放運維。
- 具有過濾器,所以鑑權、驗籤都可以整合。
基於此我們來看看之前的架構中如何整合 Zuul
。
整合 Zuul
為此我新建了一個專案 sbc-gateway-zuul
就是一個基礎的 SpringBoot
結構。其中加入了 Zuul 的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>複製程式碼
由於需要將閘道器也註冊到 Eureka
中,所以自然也需要:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>複製程式碼
緊接著配置一些專案基本資訊:
# 專案配置
spring.application.name=sbc-gateway-zuul
server.context-path=/
server.port=8383
# eureka地址
eureka.client.serviceUrl.defaultZone=http://node1:8888/eureka/
eureka.instance.prefer-ip-address=true複製程式碼
在啟動類中加入開啟 Zuul
的註解,一個閘道器應用就算是搭好了。
@SpringBootApplication
//開啟zuul代理
@EnableZuulProxy
public class SbcGateWayZuulApplication {
}複製程式碼
啟動 Eureka
和閘道器看到已經註冊成功那就大功告成了:
服務路由
路由是閘道器的核心功能之一,可以使系統有一個統一的對外介面,下面來看看具體的應用。
傳統路由
傳統路由非常簡單,和 Nginx
類似,由開發、運維人員來維護請求地址和對應服務的對映關係,類似於:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-sercice.url=http://localhost:8080/複製程式碼
這樣當我們訪問 http://localhost:8383/user-service/getUserInfo/1
閘道器就會自動給我們路由到 http://localhost:8080/getUserInfo/1
上。
可見只要我們維護好這個對映關係即可自由的配置路由資訊(user-sercice 可自定義
),但是很明顯這種方式不管是對運維還是開發都不友好。由於實際這種方式用的不多就再過多展開。
服務路由
對此 Zuul
提供了一種基於服務的路由方式。我們只需要維護請求地址與服務 ID 之間的對映關係即可,並且由於整合了 Ribbon
, Zuul 還可以在路由的時候通過 Eureka 實現負載呼叫。
具體配置:
zuul.routes.sbc-user.path=/api/user/**
zuul.routes.sbc-user.serviceId=sbc-user複製程式碼
這樣當輸入 http://localhost:8383/api/user/getUserInfo/1
時就會路由到註冊到 Eureka
中服務 ID 為 sbc-user
的服務節點,如果有多節點就會按照 Ribbon 的負載演算法路由到其中一臺上。
以上配置還可以簡寫為:
# 服務路由 簡化配置
zuul.routes.sbc-user=/api/user/**複製程式碼
這樣讓我們訪問 http://127.0.0.1:8383/api/user/userService/getUserByHystrix
時候就會根據負載演算法幫我們路由到 sbc-user 應用上,如下圖所示:
啟動了兩個 sbc-user 服務。
請求結果:
一次路由就算完成了。
在上面的配置中有看到 /api/user/**
這樣的萬用字元配置,具體有以下三種配置需要了解:
?
只能匹配任意的單個字元,如/api/user/?
就只能匹配/api/user/x /api/user/y /api/user/z
這樣的路徑。*
只能匹配任意字元,如/api/user/*
就只能匹配/api/user/x /api/user/xy /api/user/xyz
。**
可以匹配任意字元、任意層級。結合了以上兩種萬用字元的特點,如/api/user/**
則可以匹配/api/user/x /api/user/x/y /api/user/x/y/zzz
這樣的路徑,最簡單粗暴!
談到萬用字元匹配就不得不提到一個問題,如上面的 sbc-user
服務由於後期迭代更新,將 sbc-user 中的一部分邏輯抽成了另一個服務 sbc-user-pro
。新應用的路由規則是 /api/user/pro/**
,如果我們按照:
zuul.routes.sbc-user=/api/user/**
zuul.routes.sbc-user-pro=/api/user/pro/**複製程式碼
進行配置的話,我們想通過 /api/user/pro/
來訪問 sbc-user-pro
應用,卻由於滿足第一個路由規則,所以會被 Zuul 路由到 sbc-user
這個應用上,這顯然是不對的。該怎麼解決這個問題呢?
翻看路由原始碼 org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator
中的 locateRoutes()
方法:
/**
* Compute a map of path pattern to route. The default is just a static map from the
* {@link ZuulProperties}, but subclasses can add dynamic calculations.
*/
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}複製程式碼
發現路由規則是遍歷配置檔案並放入 LinkedHashMap
中,由於 LinkedHashMap
是有序的,所以為了達到上文的效果,配置檔案的載入順序非常重要,因此我們只需要將優先匹配的路由規則放前即可解決。
過濾器
過濾器可以說是整個 Zuul 最核心的功能,包括上文提到路由功能也是由過濾器來實現的。
摘抄官方的解釋: Zuul 的核心就是一系列的過濾器,他能夠在整個 HTTP
請求、響應過程中執行各樣的操作。
其實總結下來就是四個特徵:
- 過濾型別
- 過濾順序
- 執行條件
- 具體實現
其實就是 ZuulFilter
介面中所定義的四個介面:
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();複製程式碼
官方流程圖(生命週期):
簡單理解下就是:
當一個請求進來時,首先是進入 pre
過濾器,可以做一些鑑權,記錄除錯日誌等操作。之後進入 routing
過濾器進行路由轉發,轉發可以使用 Apache HttpClient
或者是 Ribbon
。post
過濾器呢則是處理服務響應之後的資料,可以進行一些包裝來返回客戶端。 error
則是在有異常發生時才會呼叫,相當於是全域性異常攔截器。
自定義過濾器
接下來實現一個文初所提到的鑑權操作:
新建一個 RequestFilter
類繼承與 ZuulFilter
介面
/**
* Function: 請求攔截
*
* @author crossoverJie
* Date: 2017/11/20 00:33
* @since JDK 1.8
*/
public class RequestFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(RequestFilter.class) ;
/**
* 請求路由之前被攔截 實現 pre 攔截器
* @return
*/
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = request.getParameter("token");
if (StringUtil.isEmpty(token)){
logger.warn("need token");
//過濾請求
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(400);
return null ;
}
logger.info("token ={}",token) ;
return null;
}
}複製程式碼
非常 easy,就簡單校驗下請求中是否包含 token
,不包含就返回 401 code。
不但如此,還需要將該類加入到 Spring 進行管理:
新建了 FilterConf
類:
@Configuration
@Component
public class FilterConf {
@Bean
public RequestFilter filter(){
return new RequestFilter() ;
}
}複製程式碼
這樣重啟之後就可以看到效果了:
不傳 token 時:
傳入 token 時:
可見一些鑑權操作是可以放到這裡來進行統一處理的。
其餘幾個過濾器也是大同小異,可以根據實際場景來自定義。
Zuul 高可用
Zuul 現在既然作為了對外的第一入口,那肯定不能是單節點,對於 Zuul 的高可用有以下兩種方式實現。
Eureka 高可用
第一種最容易想到和實現:
我們可以部署多個 Zuul 節點,並且都註冊於 Eureka ,如下圖:
這樣雖然簡單易維護,但是有一個嚴重的缺點:那就是客戶端也得註冊到 Eureka 上才能對 Zuul 的呼叫做到負載,這顯然是不現實的。
所以下面這種做法更為常見。
基於 Nginx 高可用
在呼叫 Zuul 之前使用 Nginx 之類的負載均衡工具進行負載,這樣 Zuul 既能註冊到 Eureka ,客戶端也能實現對 Zuul 的負載,如下圖:
總結
這樣在原有的微服務架構的基礎上加上閘道器之後另整個系統更加完善了,從閘道器的設計來看:大多數系統架構都有分層的概念,不能解決問題那就多分幾層?。
部落格:crossoverjie.top。