看過以前SBC系列的小夥伴應該均可以搭建一個高可用、分佈式的微服務了。 目前的結構圖應該以下所示:
java
各個微服務之間都不存在單點,而且都註冊於 Eureka
,基於此進行服務的註冊於發現,再經過 Ribbon
進行服務調用,並具備客戶端負載功能。node
一切看起來都比較美好,但這裏卻忘了一個重要的細節:git
當咱們須要對外提供服務時怎麼處理?github
這固然也能實現,無非就是將咱們具體的微服務地址加端口暴露出去便可。算法
那又如何來實現負載呢?spring
簡單!能夠經過 Nginx F5
之類的工具進行負載。api
可是若是系統龐大,服務拆分的足夠多那又有誰來維護這些路由關係呢?springboot
固然這是運維的活,不過這時候運維可能就要發飆了!bash
而且還有一系列的問題:架構
針對於這一些問題 SpringCloud
全家桶天然也有對應的解決方案: Zuul
。
當咱們系統整合 Zuul 網關以後架構圖應該以下所示:
咱們在全部的請求進來以前抽出一層網關應用,將服務提供的全部細節都進行了包裝,這樣全部的客戶端都是和網關進行交互,簡化了客戶端開發。
同時具備以下功能:
Eureka
並集成了 Ribbon
因此天然也是能夠從註冊中心獲取到服務列表進行客戶端負載。基於此咱們來看看以前的架構中如何集成 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 應用上,以下圖所示:
請求結果:
一次路由就算完成了。
在上面的配置中有看到 /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 ,以下圖:
這樣雖然簡單易維護,可是有一個嚴重的缺點:那就是客戶端也得註冊到 Eureka 上才能對 Zuul 的調用作到負載,這顯然是不現實的。
因此下面這種作法更爲常見。
在調用 Zuul 以前使用 Nginx 之類的負載均衡工具進行負載,這樣 Zuul 既能註冊到 Eureka ,客戶端也能實現對 Zuul 的負載,以下圖:
這樣在原有的微服務架構的基礎上加上網關以後另整個系統更加完善了,從網關的設計來看:大多數系統架構都有分層的概念,不能解決問題那就多分幾層🤓。
博客:crossoverjie.top。