SpringCloud微服務治理一(介紹,環境搭建,Eureka)
SpringCloud微服務治理二(Robbin,Hystix,Feign)
SpringCloud微服務治理三(Zuul閘道器)
9.Zuul閘道器
通過前面的學習,使用Spring Cloud實現微服務的架構基本成型,大致是這樣的:
在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。我們把焦點聚集在對外服務這塊,直接暴露我們的服務地址,這樣的實現是否合理,或者是否有更好的實現方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
- 首先,破壞了服務無狀態特點。
- 為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。
- 從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外考慮對介面訪問的控制處理。
- 其次,無法直接複用既有介面。
- 當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。
面對類似上面的問題,我們要如何解決呢?答案是:服務閘道器!
為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器的 服務閘道器。
服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制
等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
9.1.Zuul加入後的架構
- 不管是來自於客戶端(PC或移動端)的請求,還是服務內部呼叫。一切對服務的請求都會經過Zuul這個閘道器,然後再由閘道器來實現 鑑權、動態路由等等操作。Zuul就是我們服務的統一入口。
9.2.Zuul簡介
9.3.快速入門
9.3.1.新建工程
填寫基本資訊:
新增Zuul依賴:
9.3.2.編寫啟動類
通過@EnableZuulProxy
註解開啟Zuul的功能:
@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
複製程式碼
9.3.3.編寫配置
server:
port: 10010 #服務埠
spring:
application:
name: api-gateway #指定服務名
複製程式碼
9.3.4.編寫路由規則
我們需要用Zuul來代理user-service服務,先看一下控制皮膚中的服務狀態:
- ip為:127.0.0.1
- 埠為:8081
對映規則:
zuul:
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
url: http://127.0.0.1:8081 # 對映路徑對應的實際url地址
複製程式碼
我們將符合path
規則的一切請求,都代理到 url
引數指定的地址
本例中,我們將 /user-service/**
開頭的請求,代理到http://127.0.0.1:8081
9.3.5.啟動測試:
訪問的路徑中需要加上配置規則的對映路徑,我們訪問:http://127.0.0.1:8081/user-service/user/10
9.4.面向服務的路由
在剛才的路由規則中,我們把路徑對應的服務地址寫死了!如果同一服務有多個例項的話,這樣做顯然就不合理了。
我們應該根據服務的名稱,去Eureka註冊中心查詢 服務對應的所有例項列表,然後進行動態路由才對!
9.4.1.新增Eureka客戶端依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
複製程式碼
9.4.2.開啟Eureka客戶端發現功能
@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
複製程式碼
9.4.3.新增Eureka配置,獲取服務資訊
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
複製程式碼
9.4.4.修改對映配置,通過服務名稱獲取
因為已經有了Eureka客戶端,我們可以從Eureka獲取服務的地址資訊,因此對映時無需指定IP地址,而是通過服務名稱來訪問,而且Zuul已經整合了Ribbon的負載均衡功能。
zuul:
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
serviceId: user-service # 指定服務名稱
複製程式碼
9.4.5.啟動測試
再次啟動,這次Zuul進行代理時,會利用Ribbon進行負載均衡訪問:
日誌中可以看到使用了負載均衡器:
9.5.簡化的路由配置
在剛才的配置中,我們的規則是這樣的:
zuul.routes.<route>.path=/xxx/**
: 來指定對映路徑。<route>
是自定義的路由名zuul.routes.<route>.serviceId=/user-service
:來指定服務名。
而大多數情況下,我們的<route>
路由名稱往往和 服務名會寫成一樣的。因此Zuul就提供了一種簡化的配置語法:zuul.routes.<serviceId>=<path>
比方說上面我們關於user-service的配置可以簡化為一條:
zuul:
routes:
user-service: /user-service/** # 這裡是對映路徑
複製程式碼
省去了對服務名稱的配置。
9.6.預設的路由規則
在使用Zuul的過程中,上面講述的規則已經大大的簡化了配置項。但是當服務較多時,配置也是比較繁瑣的。因此Zuul就指定了預設的路由規則:
- 預設情況下,一切服務的對映路徑就是服務名本身。
- 例如服務名為:
user-service
,則預設的對映路徑就是:/user-service/**
- 例如服務名為:
也就是說,剛才的對映規則我們完全不配置也是OK的
9.7.路由字首
配置示例:
zuul:
prefix: /api # 新增路由字首
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
service-id: user-service # 指定服務名稱
複製程式碼
我們通過zuul.prefix=/api
來指定了路由的字首,這樣在發起請求時,路徑就要以/api開頭。
路徑/api/user-service/user/1
將會被代理到/user-service/user/1
9.8.過濾器
Zuul作為閘道器的其中一個重要功能,就是實現請求的鑑權。而這個動作我們往往是通過Zuul提供的過濾器來實現的。
9.8.1.ZuulFilter
ZuulFilter是過濾器的頂級父類。在這裡我們看一下其中定義的4個最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 來自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
複製程式碼
shouldFilter
:返回一個Boolean
值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。run
:過濾器的具體業務邏輯。filterType
:返回字串,代表過濾器的型別。包含以下4種:pre
:請求在被路由之前執行routing
:在路由請求時呼叫post
:在routing和errror過濾器之後呼叫error
:處理請求時發生錯誤呼叫
filterOrder
:通過返回的int值來定義過濾器的執行順序,數字越小優先順序越高。
9.8.2.過濾器執行生命週期:
這張是Zuul官網提供的請求生命週期圖,清晰的表現了一個請求在各個過濾器的執行順序。
- 正常流程:
- 請求到達首先會經過pre型別過濾器,而後到達routing型別,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
- 異常流程:
- 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會將請求交給POST過濾器,最後返回給使用者。
- 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後返回。
- 如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。
所有內建過濾器列表:
9.8.3.使用場景
場景非常多:
- 請求鑑權:一般放在pre型別,如果發現沒有訪問許可權,直接就攔截了
- 異常處理:一般會在error型別和post型別過濾器中結合來處理。
- 服務呼叫時長統計:pre和post結合使用。
9.9.自定義過濾器
接下來我們來自定義一個過濾器,模擬一個登入的校驗。基本邏輯:如果請求中有access-token引數,則認為請求有效,放行。
9.9.1.定義過濾器類
@Component
public class LoginFilter extends ZuulFilter{
@Override
public String filterType() {
// 登入校驗,肯定是在前置攔截
return "pre";
}
@Override
public int filterOrder() {
// 順序設定為1
return 1;
}
@Override
public boolean shouldFilter() {
// 返回true,代表過濾器生效。
return true;
}
@Override
public Object run() throws ZuulException {
// 登入校驗邏輯。
// 1)獲取Zuul提供的請求上下文物件
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 從上下文中獲取request物件
HttpServletRequest req = ctx.getRequest();
// 3) 從請求中獲取token
String token = req.getParameter("access-token");
// 4) 判斷
if(token == null || "".equals(token.trim())){
// 沒有token,登入校驗失敗,攔截
ctx.setSendZuulResponse(false);
// 返回401狀態碼。也可以考慮重定向到登入頁。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校驗通過,可以考慮把使用者資訊放入上下文,繼續向後執行
return null;
}
}
複製程式碼
9.10.負載均衡和熔斷
Zuul中預設就已經整合了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的預設值,比如熔斷超時時間只有1S,很容易就觸發了。因此建議我們手動進行配置:
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 連線超時時間(ms)
ReadTimeout: 2000 # 通訊超時時間(ms)
OkToRetryOnAllOperations: true # 是否對所有操作重試
MaxAutoRetriesNextServer: 2 # 同一服務不同例項的重試次數
MaxAutoRetries: 1 # 同一例項的重試次數
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔斷超時時長:6000ms
複製程式碼