第6章 使用Zuul進行服務路由
在像微服務架構這樣的分佈式架構中,有時候需要做一些統一的動作,例如日誌記錄和追蹤、記錄接口調用的時間等,爲了解決這個問題,需要將一些橫切關注點抽象成一個獨立的服務,這個服務會作爲所有微服務的過濾器和路由器,這種橫切關注點被成爲服務網關。服務客戶端不再直接調用微服務,取而代之的是,所有調用都通過服務網關進行路由,然後被路由到最終目的地。Zuul是開源的服務網關實現。
我們通過一個簡單的圖看下什麼是服務網關。
服務網關充當的是客戶端與服務端之間的中介,網關從客戶端調用中分離出路徑,此路徑用於確定客戶端真正想調用的服務,可以看到,其實網關還是所有微服務調用的入口,相當於守門人的角色,這樣橫切關注點就可以在網關實現,而不用每個服務都加上切面。服務網關中可以做的事情如下:
- 路由—網關將所有服務調用放置在URL和API路由的後面,這樣客戶端調用只需要知道一個IP和Port即可。服務網關可以根據傳入的url判斷客戶端真正請求的服務,執行智能路由
- 驗證和授權—由於所有服務調用都要經過網關進行路由,所以網關是檢查服務調用是否已經進行了驗證並被授權調用該服務
- 度量數據收集和日誌記錄—可以做些基本的統計工作,如服務調用次數,服務響應時間,日誌關聯Id的記錄。
接下來介紹SpringCloud集成Zuul。Zuul提供了較多功能,我們具體介紹:
1、將所有服務的路由映射到一個服務中,也就是提供服務的統一入口
2、構建過濾器。
下面介紹怎樣建立一個Zuul項目。和以前一樣,修改構建腳本、修改引導類、修改配置文件。
首先構建腳本添加對於zuul依賴項:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
這個依賴告訴SpringCloud框架,該服務將運行Zuul,並適當地初始化Zuul。
接下來修改引導類,爲了使服務成爲一個Zuul服務,需要添加一個@EnableZuulProxy的註解
@SpringBootApplication
//使服務成爲一個Zuul服務
@EnableZuulProxy
public class ZuulServerApplication {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
RestTemplate template = new RestTemplate();
List interceptors = template.getInterceptors();
if (interceptors == null) {
template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
} else {
interceptors.add(new UserContextInterceptor());
template.setInterceptors(interceptors);
}
return template;
}
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
還需要注意的一點是,Zuul也需要註冊到Eureka上。其將使用Eureka來通過服務ID查找服務,然後使用Ribbon對來自Zuul的請求進行客戶端負載均衡。
我們應該清楚,Zuul的核心是一個反向代理,反向代理負責捕獲客戶端的請求,然後代表客戶端調用遠程資源,Zuul必須知道如何將進來的請求映射到不同的服務中,主要有以下幾種機制
通過服務發現自動映射路由
理論上將Zuul的所有映射都是在其application.yml中定義路由來完成的。但是Zuul可以根據其服務ID自動路由請求,例如如下的URL。
http://localhost:5555/organizationservice/v1/organizations/442adb6e-fa58-47f3-9ca2-ed1fecdfe86c。其中Zuul服務器通過http://localhost:5555進行訪問。路徑的第一部分organizationservice嘗試調用在Eureka上註冊的邏輯名稱爲organizationservice的服務。
使用服務發現手動映射路由
Zuul允許開發人員更細粒度地明確定義路由映射,而不是單純地依賴服務的Eureka服務ID創建的自動路由,可以通過在Zuul服務上修改application.yml配置文件來手動定義路由映射。如果想要排除掉Eureka服務ID路由的自動映射,只提供自定義的組織服務路由,可以使用ignored-services參數,如果屏蔽掉所有的自動映射,屬性值需設爲‘*’。另外服務網關的一種常見模式是通過使用/api之類的前綴來爲所有服務添加前綴,從而區別API路由和內容路由。如下:
zuul:
prefix: /api
ignored-services: '*'
routes:
producer:
strip-prefix: false
organizationservice: /organization/**
licensingservice: /licensing/**
雖然Zuul作爲網關代理確實很靈巧,,但是Zuul的真正威力在於執行一組一致的應用程序,例如:安全性、日誌記錄、服務跟蹤等。因爲開發人員想將這些邏輯應用於所有微服務中,而不是在每個微服務中都要使用切面或者註解。那麼此時Zuul的過濾器就大殺四方了。通過Zuul的過濾器,我們可以自定義自己的邏輯,Zuul的過濾器有三種:
- 前置過濾器,在請求發送到實際目的地之前被調用,例如常用的判斷Http的請求首部
- 後置過濾器,將響應返回給客戶端時被調用
- 路由過濾器,簡單來講,就是通過路由過濾器的請求可以一部分被調用到A服務,一部分調用到B服務,服務升級時,可以使用,一小部分客戶使用升級後的服務作爲測試,大部分客戶使用原服務。
下面通過一個圖,簡單描述下:
下面,我們構建第一個過濾器
前置過濾器
功能需求:檢查所有到網關的請求,並判斷是否存在名爲tmx-correlation-id的HTTP首部。這有點類似於日誌追蹤中的GUID,用於跨多個服務跟蹤用戶請求。前置過濾器的代碼如下:
@Component
//所有Zuul過濾器必須擴展ZuulFilter,並覆蓋如下四個方法
public class TrackingFilter extends ZuulFilter{
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
//封裝的過濾器類
@Autowired
FilterUtils filterUtils;
/**
* 告訴Zuul,這是前置過濾器
* @return
*/
@Override
public String filterType() {
return FilterUtils.PRE_FILTER_TYPE;
}
/**
* 不同類型過濾器的執行順序
* @return
*/
@Override
public int filterOrder() {
return FILTER_ORDER;
}
/**
* 是否執行該過濾器
* @return
*/
public boolean shouldFilter() {
return SHOULD_FILTER;
}
private boolean isCorrelationIdPresent(){
if (filterUtils.getCorrelationId() !=null){
return true;
}
return false;
}
private String generateCorrelationId(){
return java.util.UUID.randomUUID().toString();
}
/**
* 每次服務調用通過過濾器時,都會調用。檢查首部是否存在,如果不存在,則設置一個
* @return
*/
public Object run() {
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
}
else{
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
}
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
FilterUtils類用於封裝所有過濾器使用的常用功能,如下:
@Component
public class FilterUtils {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
public static final String PRE_FILTER_TYPE = "pre";
public static final String POST_FILTER_TYPE = "post";
public static final String ROUTE_FILTER_TYPE = "route";
public String getCorrelationId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
return ctx.getRequest().getHeader(CORRELATION_ID);
}
else{
return ctx.getZuulRequestHeaders().get(CORRELATION_ID);
}
}
public void setCorrelationId(String correlationId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
}
public final String getOrgId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(ORG_ID) !=null) {
return ctx.getRequest().getHeader(ORG_ID);
}
else{
return ctx.getZuulRequestHeaders().get(ORG_ID);
}
}
public void setOrgId(String orgId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(ORG_ID, orgId);
}
public final String getUserId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(USER_ID) !=null) {
return ctx.getRequest().getHeader(USER_ID);
}
else{
return ctx.getZuulRequestHeaders().get(USER_ID);
}
}
public void setUserId(String userId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(USER_ID, userId);
}
public final String getAuthToken(){
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.getRequest().getHeader(AUTH_TOKEN);
}
public String getServiceId(){
RequestContext ctx = RequestContext.getCurrentContext();
//We might not have a service id if we are using a static, non-eureka route.
if (ctx.get("serviceId")==null) return "";
return ctx.get("serviceId").toString();
}
}
現在我們啓動網關服務和組織服務,調用下:
後置過濾器
後置過濾器是收集指標並完成與用戶相關聯的日誌記錄的理想場所,下面實現將傳遞給微服務的關聯ID注入回用戶。使用後置過濾器將關聯ID注入HTTP響應首部中。後置過濾器的代碼如下:
@Component
public class ResponseFilter extends ZuulFilter{
private static final int FILTER_ORDER=1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
public String filterType() {
return FilterUtils.POST_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());
logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
我們調用下,看下響應中是否存在對應的首部。