目錄
- 前言
- 一、Zuul 介紹
- 二、路由功能
- 三、通配符
- 四、過濾器
- 五、zuul 過濾器的禁用
- 六、Zuul 的異常處理
- 七、Zuul 的熔斷降級
前言
通過前面内容的學習,我們已經可以基本搭建出一套簡略版的微服務架構了,我 們有注冊中心 Eureka,可以将服務注冊到該注冊中心中,我們有 Ribbon 或Feign 可以實作對服務負載均衡地調用,我們有 Hystrix 可以實作服務的熔斷。
我們來看一下下面的微服務架構圖:
在上面的架構圖中,我們的服務包括:内部服務 Service A 和内部服務 Service B,這兩個服務都是叢集部署,每個服務部署了 3 個執行個體,他們都會通過 Eureka Server 注冊中心注冊與訂閱服務,而 Open Service 是一個對外的服務,也是叢集部署,外部調用方通過負載均衡裝置調用 Open Service 服務,比如負載均衡使用 Nginx、LVS、HAProxy等。但這樣的實作是否合理,或者是否有更好的實作方式呢?接下來我們主要圍繞該問題展開讨論。
1、如果我們的微服務中有很多個獨立服務都要對外提供服務,那麼我們要如何 去管理這些接口? 特别是當項目非常龐大的情況下要如何管理 ?
2、在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,為了確定安全,權限管理也是一個不可回避的問題,如果在每一個服務上都添加上相同的權限驗 證代碼來確定系統不被非法通路,那麼工作量也就太大了,而且維護也非常不友善。
為了解決上述問題,微服務架構中提出了 API 網關的概念,它就像一個安檢站一樣,所有外部的請求都需要經過它的排程與過濾,然後 API 網關來實作請求路由、負載均衡、權限驗證等功能。
那麼 Spring Cloud 這個一站式的微服務開發架構基于 Netflix Zuul 實作了 Spring Cloud Zuul,采用 Spring Cloud Zuul 即可實作一套 API 網關服務。
一、Zuul 介紹
Zuul包含了對請求的 路由 和 過濾 兩個最主要的功能:
其中路由功能負責将外部請求轉發到具體的微服務執行個體上,是實作外部通路統一入口的基礎,過濾功能則負責對請求的處理過程進行幹預,是實作請求校驗、服務聚合等功能的基礎。
Zuul 和 Eureka 進行整合,将 Zuul 自身注冊為 Eureka 服務治理下的應用,同時從 Eureka 中獲得其他微服務的資訊,也即以後的通路微服務都是通過 Zuul 跳轉後獲得。
二、路由功能
A、項目加入依賴
<!--spring-cloud-starter-netflix-eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring-cloud-starter-netflix-zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
由于 Zuul 最終會注冊進 Eureka,是以我們此處也依賴了 Eureka。
B、配置檔案
server.port=80
#是 eureka 注冊中心首頁的Application這一欄
spring.application.name=springcloud-service-zuul
#每間隔2s,向服務端發送一次心跳,證明自己依然"存活"
eureka.instance.lease-renewal-interval-in-seconds=2
#告訴服務端,如果我10s之内沒有給你發心跳,就代表我故障了,将我踢出掉
eureka.instance.lease-expiration-duration-in-seconds=10
#告訴服務端,服務執行個體以IP作為連結,而不是取機器名
eureka.instance.prefer-ip-address=true
#告訴服務端,服務執行個體的id,id必須要唯一,是eureka注冊中心首頁的Status這一欄
eureka.instance.instance-id=34-springcloud-service-zuul
#eureka注冊中心的連接配接位址
eureka.client.service-url.defaultZone=http://192.168.160.133:8761/eureka,http://192.168.160.133:8762/eureka,http://192.168.160.133:8763/eureka
C、啟動類
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
這樣簡單的 zuul 就搭建好了, 啟動項目我們即可通過zuul 然後加上對應的微服務名字通路微服務,比如:
http://localhost:80/springcloud-service-portal/cloud/goodsFeign
http://localhost:80/ 這個是zuul本身位址
springcloud-service-portal 這個是要調用的項目名稱
/cloud/goodsFeign 這個是被調用的contrller上的接口路徑
在實際開發當中我們肯定不會通過微服務名去調用,比如我要調用消費者可能隻要一個/ cloud/goodsFeign 就好了,而不是 /springcloud-service-portal/cloud/goodsFeign
配置路由規則
zuul.routes.portal.service-id=springcloud-service-portal
zuul.routes.portal.path=/portal/**
解釋:
/** 代表是所有(多個) 層級 /cloud/goodsFeignHystrix
/* 是代表一層
如果是 /* 的話 /api/goods 就不會被路由
此時我們能通過自定義的規則進行通路,但是我們現在依然能用之前的微服務名調用,這是不合理的,第一是有多重位址了, 第二一般微服務名這種最好不要暴露在外,是以我們一般會禁用微服務名方式調用。
加入配置:
zuul.ignored-services=springcloud-service-portal
這裡能發現我們不能通過微服務名來調用了, 不過這個配置如果一個一個通過微服務名來配置難免有點複雜,是以一般這樣配置來禁用所有:
可能有時候我們的接口調用需要一定的規範,比如調用微服務的API URL字首需要加上 /api 對于這種情況,zuul 也考慮到了并給出了解決方案:
zuul.prefix=/api
比如:http://localhost/api/portal/cloud/goodsFeignHystrix
三、通配符
通配符 | 說明 |
---|---|
? | 比對任意單個字元 |
* | 比對任意數量的字元 |
** | 比對任意數量的字元 |
四、過濾器
主要功能 :限流、權限驗證、記錄日志
過濾器 (filter) 是 zuul 的核心元件,zuul 大部分功能都是通過過濾器來實作的。 zuul 中定義了 4 種标準過濾器類型,這些過濾器類型對應于請求的典型生命周期。
PRE :
這種過濾器在請求被路由之前調用。可利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等。
ROUTING :
這種過濾器将請求路由到微服務。這種過濾器用于建構發送給微服務的請求,并使用 Apache HttpClient 或 Netfilx Ribbon請求微服務
POST :
這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加标準的 HTTP
Header、收集統計資訊和名額、将響應從微服務發送給用戶端等。
ERROR :
在其他階段發生錯誤時執行該過濾器。
如果要編寫一個過濾器,則需繼承 ZuulFilter 類實作其中的方法:
@Component
public class LogFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
@Override
public boolbean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest requst = currentContext.getRequest();
String remoteAddr = request.getServerName();
System.out.println("通路位址:" + request.getRequestURI());
return null;
}
}
由代碼可知,自定義的 zuul Filter 需實作以下幾個方法:
filterType
:
傳回過濾器的類型。有 pre、 route、 post、 error 等幾種取值,分别對應上文的幾種過濾器。
詳細可以參考 com.netflix.zuul.ZuulFilter.filterType() 中的注釋。
filter0rder
:
傳回一個 int值來指定過濾器的執行順序,不同的過濾器允許傳回相同的數字。
shouldFilter
:
傳回一個 boolean 值來判斷該過濾器是否要執行, true 表示執行, false 表示不執行。
run
:
過濾器的具體邏輯。
五、zuul 過濾器的禁用
Spring Cloud 預設為 Zuul 編寫并啟用了一些過濾器,例如 DebugFilter、FormBodyWrapperFilter 等,這些過濾器都存放在 spring-cloud-netflix-zuul 這個jar 包裡,一些場景下,想要禁用掉部分過濾器,該怎麼辦呢?
隻需在 application.properties 裡設定 zuul…disable=true 例如,要禁用上面我們寫的過濾器,這樣配置就行了:
六、Zuul 的異常處理
Spring Cloud Zuul 對異常的處理是非常友善的,但是由于 Spring Cloud 處于迅速發展中,各個版本之間有所差異,本案例是以 Finchley.RELEASE 版本為例, 來說明 Spring Cloud Zuul 中的異常處理問題。
看一張官方給出的 Zuul 請求的生命周期圖:
1、正常情況下所有的請求都是按照 pre、route、post 的順序來執行,然後由 post 傳回 response 。
2、在 pre 階段,如果有自定義的過濾器則執行自定義的過濾器。
3、pre、routing、post 的任意一個階段如果抛異常了,則執行 error 過濾器
我們可以統一處理異常。
實作步驟:
禁用 zuul 預設的異常處理 SendErrorFilter 過濾器,然後自定義我們自己的Errorfilter 過濾器
@Component
public class ErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException)context.getThrowable();
System.out.println("進入系統異常攔截" + exception.getMessage());
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print("{code:" + exception.nStatusCode + ",message:\"" + exception.getMessage() +"\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer!=null) {
writer.close();
}
}
} catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
return null;
}
}
七、Zuul 的熔斷降級
zuul 是一個代理服務,但如果被代理的服務突然斷了,這個時候 zuul 上面會有出錯資訊,例如,停止了被調用的微服務。
一般服務方自己會進行服務的熔斷降級,但對于 zuul 本身,也應該進行 zuul 的降級處理。
我們需要有一個 zuul 的降級,實作如下:
@Component
public class ProviderFallback implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "text/html; charset=UTF-8");
return headers;
}
@Override
public InputStream getBody() throws IOException {
// 響應體
return new ByteArrayInputStream("服務正在維護,請稍後再試.".getBytes());
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public void close() {
}
};
}
}