通過前面的學習,使用Spring Cloud實現微服務的架構基本成型,大致是這樣的:
我們使用Spring Cloud Netflix中的Eureka實現了服務註冊中心以及服務註冊與發現;而服務間通過Ribbon或Feign實現服務的消費以及均衡負載。爲了使得服務集羣更爲健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現異常時引起的故障蔓延。
在該架構中,我們的服務集羣包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務調用方。我們把焦點聚集在對外服務這塊,直接暴露我們的服務地址,這樣的實現是否合理,或者是否有更好的實現方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
-
破壞了服務無狀態特點。
爲了保證對外服務的安全性,我們需要實現對服務訪問的權限控制,而開放服務的權限控制機制將會貫穿並污染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務集羣中REST API無狀態的特點。
從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外考慮對接口訪問的控制處理。
-
無法直接複用既有接口。
當我們需要對一個即有的集羣內訪問接口,實現外部服務訪問時,我們不得不通過在原有接口上增加校驗邏輯,或增加一個代理調用來實現權限控制,無法直接複用原有的接口。
面對類似上面的問題,我們要如何解決呢?答案是:服務網關!
爲了解決上面這些問題,我們需要將權限控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器的 服務網關。
服務網關是微服務架構中一個不可或缺的部分。通過服務網關統一向外系統提供REST API的過程中,除了具備服務路由
、均衡負載
功能之外,它還具備了權限控制
等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,爲微服務架構提供了前門保護的作用,同時將權限控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務集羣主體能夠具備更高的可複用性和可測試性。
簡介
官網:https://github.com/Netflix/zuul
Zuul加入後的架構
快速入門
創建一個工程
編寫配置
server:
port: 10010 #服務端口
spring:
application:
name: bubbletg-zull #指定服務名
編寫引導類
@SpringBootApplication
@EnableZuulProxy // 開啓網關功能
public class BubbletgZuulApplication {
public static void main(String[] args) {
SpringApplication.run(BubbletgZuulApplication.class, args);
}
}
編寫路由規則
我們需要用Zuul來代理service-provider服務,先看一下控制面板中的服務狀態:
server:
port: 10010 #服務端口
spring:
application:
name: bubbletg-zull #指定服務名
zuul:
routes:
service-provider: # 這裏是路由id,隨意寫
path: /service-provider/** # 這裏是映射路徑
url: http://127.0.0.1:8081 # 映射路徑對應的實際url地址
我們將符合path
規則的一切請求,都代理到 url
參數指定的地址
本例中,我們將 /service-provider/**
開頭的請求,代理到http://127.0.0.1:8081
啓動測試
訪問的路徑中需要加上配置規則的映射路徑,我們訪問:http://127.0.0.1:10010/service-provider/user/2
## 面向服務的路由
在剛纔的路由規則中,我們把路徑對應的服務地址寫死了!如果同一服務有多個實例的話,這樣做顯然就不合理了。我們應該根據服務的名稱,去Eureka註冊中心查找 服務對應的所有實例列表,然後進行動態路由纔對!
對itcast-zuul工程修改優化:
添加Eureka客戶端依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加Eureka配置,獲取服務信息
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
開啓Eureka客戶端發現功能
@SpringBootApplication
@EnableZuulProxy // 開啓Zuul的網關功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
修改映射配置,通過服務名稱獲取
因爲已經有了Eureka客戶端,我們可以從Eureka獲取服務的地址信息,因此映射時無需指定IP地址,而是通過服務名稱來訪問,而且Zuul已經集成了Ribbon的負載均衡功能。
server:
port: 10010 #服務端口
spring:
application:
name: bubbletg-zull #指定服務名
#zuul:
# routes:
# service-provider: # 這裏是路由id,隨意寫
# path: /service-provider/** # 這裏是映射路徑
# url: http://127.0.0.1:8081 # 映射路徑對應的實際url地址
zuul:
routes:
service-provider: # 這裏是路由id,隨意寫
path: /service-provider/** # 這裏是映射路徑
serviceId: service-provider # 指定服務名稱
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3.4.5.啓動測試
再次啓動,這次Zuul進行代理時,會利用Ribbon進行負載均衡訪問:
簡化的路由配置
在剛纔的配置中,我們的規則是這樣的:
zuul.routes.<route>.path=/xxx/**
: 來指定映射路徑。<route>
是自定義的路由名zuul.routes.<route>.serviceId=service-provider
:來指定服務名。
而大多數情況下,我們的<route>
路由名稱往往和服務名會寫成一樣的。因此Zuul就提供了一種簡化的配置語法:zuul.routes.<serviceId>=<path>
比方說上面我們關於service-provider的配置可以簡化爲一條:
zuul:
routes:
service-provider: /service-provider/** # 這裏是映射路徑
省去了對服務名稱的配置。
默認的路由規則
在使用Zuul的過程中,上面講述的規則已經大大的簡化了配置項。但是當服務較多時,配置也是比較繁瑣的。因此Zuul就指定了默認的路由規則:
- 默認情況下,一切服務的映射路徑就是服務名本身。例如服務名爲:
service-provider
,則默認的映射路徑就 是:/service-provider/**
也就是說,剛纔的映射規則我們完全不配置也是OK的,不信就試試看。
路由前綴
配置示例:
zuul:
routes:
service-provider: /service-provider/**
service-consumer: /service-consumer/**
prefix: /api # 添加路由前綴
我們通過zuul.prefix=/api
來指定了路由的前綴,這樣在發起請求時,路徑就要以/api開頭。
過濾器
Zuul作爲網關的其中一個重要功能,就是實現請求的鑑權。而這個動作我們往往是通過Zuul提供的過濾器來實現的。
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
:請求在被路由之前執行route
:在路由請求時調用post
:在route和errror過濾器之後調用error
:處理請求時發生錯誤調用
filterOrder
:通過返回的int值來定義過濾器的執行順序,數字越小優先級越高。
過濾器執行生命週期
這張是Zuul官網提供的請求生命週期圖,清晰的表現了一個請求在各個過濾器的執行順序。
正常流程:
- 請求到達首先會經過pre類型過濾器,而後到達route類型,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
異常流程:
- 整個過程中,pre或者route過濾器出現異常,都會直接進入error過濾器,在error處理完畢後,會將請求交給POST過濾器,最後返回給用戶。
- 如果是error過濾器自己出現異常,最終也會進入POST過濾器,將最終結果返回給請求客戶端。
- 如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和route不同的是,請求不會再到達POST過濾器了。
所有內置過濾器列表:
### 使用場景
場景非常多:
- 請求鑑權:一般放在pre類型,如果發現沒有訪問權限,直接就攔截了
- 異常處理:一般會在error類型和post類型過濾器中結合來處理。
- 服務調用時長統計:pre和post結合使用。
自定義過濾器
接下來我們來自定義一個過濾器,模擬一個登錄的校驗。基本邏輯:如果請求中有access-token參數,則認爲請求有效,放行。
定義過濾器類
package cn.bubbletg.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author www.bubbletg.cn * BubbleTg
* @version 1.0
* @date 2019/8/24 11:08
*/
@Component
public class LoginFilter extends ZuulFilter {
/**
* 過濾器的類型 :pre route post error
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 執行順序,返回值越小,優先級越高
* @return
*/
@Override
public int filterOrder() {
return 10;
}
/**
* 是否執行該過濾器(fun()方法)
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 編寫過濾器的業務邏輯
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//初始化context上下文對象,
RequestContext context = RequestContext.getCurrentContext();
//獲取request 對象
HttpServletRequest request = context.getRequest();
//獲取參數
String token = request.getParameter("token");
if(StringUtils.isBlank(token)){
//爲空,攔截
//不轉發請求
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
context.setResponseBody("request error");
}
// 校驗通過,把登陸信息放入上下文信息,繼續向後執行
context.set("token", token);
//返回器爲null 表示過濾器什麼都不做
return null;
}
}
測試
沒有token參數時,訪問失敗:
添加token參數後:
負載均衡和熔斷
Zuul中默認就已經集成了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的默認值,比如熔斷超時時間只有1S,很容易就觸發了。因此建議我們手動進行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 設置hystrix的超時時間爲6000ms