目錄
配套資料,免費下載
鏈接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取碼:lxfx
複製這段內容後打開百度網盤手機App,操作更方便哦
第一章 Zuul介紹
1.1、什麼是Zuul
通過前面內容的學習,我們已經可以基本搭建出一套簡略版的微服務架構了,我們有註冊中心Eureka,可以將服務註冊到該註冊中心中,我們有Ribbon或Feign或OpenFegin可以實現對服務負載均衡地調用,我們有Hystrix可以實現服務的熔斷、降級以及限流以及Dashboard和Turbine來進行服務調用監控。
Zuul是Spring Cloud全家桶中的微服務API網關,所有從移動設備或網站來的請求都會先經過Zuul的API網關然後才能到達後端的Netflix應用程序,作爲一個邊界性質的應用程序,Zuul提供了動態路由、監控、彈性負載和安全控制等功能。那麼Spring Cloud這個一站式的微服務開發框架基於Netflix Zuul實現了Spring Cloud Zuul,採用Spring Cloud Zuul即可實現一套API網關服務。
1.2、爲啥用Zuul
如果我們的微服務中有很多個獨立服務都要對外提供服務,那麼我們要如何去管理這些接口?特別是當項目非常龐大的情況下要如何管理?
在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,爲了確保安全,權限管理也是一個不可迴避的問題,如果在每一個服務上都添加上相同的權限驗證代碼來確保系統不被非法訪問,那麼工作量也就太大了,而且維護也非常不方便。
爲了解決上述問題,微服務架構中提出了API網關的概念,它就像一個安檢站一樣,所有外部的請求都需要經過它的調度與過濾,然後API網關來實現請求路由、負載均衡、權限驗證等功能。
Zuul包含了對請求的路由和過濾兩個最主要的功能:其中路由功能負責將外部請求轉發到具體的微服務實例上,是實現外部訪問統一入口的基礎,過濾功能則負責對請求的處理過程進行干預,是實現請求校驗、服務聚合等功能的基礎。
Zuul底層利用各種filter實現如下功能:
- 認證和安全:識別每個需要認證的資源,拒絕不符合要求的請求。
- 性能監測:在服務邊界追蹤並統計數據,提供精確的生產視圖。
- 動態路由:根據需要將請求動態路由到後端集羣。
- 壓力測試:逐漸增加對集羣的流量以瞭解其性能。
- 負載卸載:預先爲每種類型的請求分配容量,當請求超過容量時自動丟棄。
- 靜態資源處理:直接在邊界返回某些響應。
第二章 Zuul路由功能
2.1、項目準備與啓動
我們接下來的所有操作均是在Hystrix
最後完成的工程上進行操作,相關代碼請到配套資料中尋找。
我們打開項目以後,需要對項目進行啓動,啓動的順序如下:
- eureka-server7001(啓動會報錯,暫時不用理會,因爲第二個註冊中心還沒有啓動,等第二個註冊中心啓動,過一會就恢復了)
- eureka-server7002
- service-provider8001
- service-provider8002
- service-consumer9002
- service-consumer9003
注意:有時候你確信自己的代碼沒有問題,可是效果就是出不來,很有可能是idea編譯的緩存的問題,我們目前使用的熱部署還存在一些問題,要想解決,我建議你可以先停止對應工程,刪除對應工程中的
target
目錄,然後手動啓動,這樣還不行的話,大概率是你某地方配置錯了,在檢查一下吧^__^
。
2.2、工程搭建與測試
(1)在父工程spring-cloud-study
下創建子工程gateway-zuul5001
(2)在pom.xml
中導入以下依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
(3)新建application.yaml
,在application.yaml
中加入以下配置
server:
port: 5001
spring:
application:
name: gateway-zuul5001
eureka:
client:
#是否將自己註冊到註冊中心,默認爲 true
register-with-eureka: false
#表示 Eureka Client 間隔多久去服務器拉取註冊信息,默認爲 30 秒
registry-fetch-interval-seconds: 10
#設置服務註冊中心地址
service-url:
defaultZone: http://root:123456@eureka-server7001.com:7001/eureka/,http://root:123456@eureka-server7002.com:7002/eureka/
(4)新建啓動類,在啓動類中加入以下代碼
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuul5001Application {
public static void main(String[] args) {
SpringApplication.run(GatewayZuul5001Application.class);
}
}
(5)啓動當前項目,這樣簡單的Zuul就搭建好了,我們可以通過默認的路由規則來訪問指定的服務方法,比如:
格式:“http://”+Zuul網關的域名+":"+Zuul網關的端口+"/"+微服務的名稱(一定小寫)+微服務的服務路徑(就是你Controller方法上標註的那個路徑)
例如:http://localhost:5001/service-consumer9002/consumer/product/findAll
例如:http://localhost:5001/service-consumer9003/consumer/product/findAll
2.3、配置自定義路由
現在,我們已經基本實現了Zuul默認路由的功能,但是,一般我們也可以自定義路由配置,爲什麼要自定義,細心的你會發現,現在你訪問指定的服務,必須要加註冊服務的名稱(例如:service-consumer9002
,service-consumer9003
),這個名稱可能很長,也可能會暴露你這個服務的一些性質,我們想要給這個服務起個別名,來代替當前這個默認的規則,就必須使用自定義路由配置,自定義路由配置其實很簡單,只需要在gateway-zuul5001
中的application.yaml
加上相對應的路由配置就行了,具體代碼如下所示:
zuul:
routes:
#這個屬性Key可以隨便寫(一般來說就是註冊服務的名稱,屬性spring.application.name)
SERVICE-CONSUMER9002:
#你要映射的路徑地址(/**代表後邊可以有多級路徑,/*只有一級路徑)
path: /consumer9002/**
#準備轉給哪一個服務(不知道的,可以去Eureka註冊中心找)
serviceId: SERVICE-CONSUMER9002
#這個屬性Key可以隨便寫(一般來說就是註冊服務的名稱,屬性spring.application.name)
SERVICE-CONSUMER9003:
#你要映射的路徑地址(/**代表後邊可以有多級路徑,/*只有一級路徑)
path: /consumer9003/**
#準備轉給哪一個服務(不知道的,可以去Eureka註冊中心找)
serviceId: SERVICE-CONSUMER9003
如果你還有更多服務你還可以照着上邊的規則繼續往下寫(服務提供者和服務消費者都算服務),寫完後,請重新啓動當前的項目,然後依次訪問如下地址測試:
地址1:http://localhost:5001/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/consumer9003/consumer/product/findAll
2.4、禁止默認的路由
雖然實現了自定義路由設置,但是如果你使用之前默認的路由規則,他還是可以訪問的,我們想要禁用掉,默認的那個路由規則,只需要增加一段配置,如下:
zuul:
#加入這個配置,代表忽略所有服務,也就是忽略默認的路由規則,你也可以單獨指定某個服務不能使用服務名來訪問
#這個雖然是代表禁用掉所有,但是,自定義的路由規則還是會生效的,不必擔心
ignored-services: '*'
routes:
#這個屬性Key可以隨便寫(一般來說就是註冊服務的名稱,屬性spring.application.name)
SERVICE-CONSUMER9002:
#你要映射的路徑地址(/**代表後邊可以有多級路徑,/*只有一級路徑)
path: /consumer9002/**
#準備轉給哪一個服務(不知道的,可以去Eureka註冊中心找)
serviceId: SERVICE-CONSUMER9002
#這個屬性Key可以隨便寫(一般來說就是註冊服務的名稱,屬性spring.application.name)
SERVICE-CONSUMER9003:
#你要映射的路徑地址(/**代表後邊可以有多級路徑,/*只有一級路徑)
path: /consumer9003/**
#準備轉給哪一個服務(不知道的,可以去Eureka註冊中心找)
serviceId: SERVICE-CONSUMER9003
然後重新啓動項目即可,啓動後,輸入以下地址進行測試:
地址1:http://localhost:5001/service-consumer9002/consumer/product/findAll
地址2:http://localhost:5001/service-consumer9003/consumer/product/findAll
2.5、簡化自定義路由
雖然自定義路由功能很強大,可以映射指定路徑,但是你不覺得寫起來太麻煩了嗎?有沒有一種簡化的寫法,那自然是有的,你現在可以把之前的那一大段配置註釋掉了,我們來看看全新的精煉的配置到底怎麼配,如下:
zuul:
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
寫完後,請重新啓動當前的項目,然後依次訪問如下地址測試:
地址1:http://localhost:5001/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/consumer9003/consumer/product/findAll
2.6、統一加路徑前綴
如果你想要在所有請求前邊加一個統一前綴,比如:/api
,Zuul也支持這種設置,配置如下:
zuul:
#統一添加路由前綴
prefix: /api
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
寫完後,請重新啓動當前的項目,然後依次訪問如下地址測試:
地址1:http://localhost:5001/api/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/api/consumer9003/consumer/product/findAll
2.7、路由規則通配符
通配符 | 含義 | 舉例 | 說明 |
---|---|---|---|
? | 匹配任意單個字符 | /api/consumer9003/? | /api/consumer9003/a /api/consumer9003/b /api/consumer9003/c 以上三個路由規則都可以被匹配到 |
* | 匹配任意多個字符 只能匹配一級地址 |
/api/consumer9003/* | /api/consumer9003/aaa /api/consumer9003/bbb /api/consumer9003/ccc 以上三個路由規則都可以被匹配到 /api/consumer9003/a/b/c 以上一個路由規則不可以被匹配到 |
** | 匹配任意多個字符 可以匹配多級地址 |
/api/consumer9003/** | /api/consumer9003/aaa /api/consumer9003/bbb /api/consumer9003/ccc 以上三個路由規則都可以被匹配到 /api/consumer9003/a/b/c 以上一個路由規則也可以被匹配到 |
第三章 Zuul過濾功能
3.1、過濾器介紹
過濾器 (filter) 是Zuul的核心組件,Zuul大部分功能都是通過過濾器來實現的。
3.2、過濾器類型
Zuul中定義了4種標準過濾器類型,這些過濾器類型對應於請求的典型生命週期。
- pre:這種過濾器在請求被路由之前調用。可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等。
- routing:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或 Netfilx Ribbon請求微服務。
- post:這種過濾器在路由到微服務以後執行。這種過濾器可用來爲響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
- error:在其他階段發生錯誤時執行該過濾器。
3.3、過濾器使用場景
- 請求鑑權:一般放在pre類型,如果發現沒有訪問權限,直接就攔截了。
- 異常處理:一般會在error類型和post類型過濾器中結合來處理。
- 服務調用時長統計:pre和post結合使用。
3.4、過濾器生命週期
- 正常流程:
- 請求到達首先會經過pre類型過濾器,而後到達routing類型,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
- 異常流程:
- 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會將請求交給post過濾器,最後返回給用戶。
- 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後返回。
- 如果是post過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達post過濾器了。
3.5、內置過濾器列表
3.6、自定義的過濾器
新建過濾器:com.caochenlei.filter.MyLogFilter
/**
* 這個過濾器專門用來記錄日誌的
*
* @author CaoChenLei
*/
//標註當前過濾器是一個組件,需要被Spring管理,必須有
@Component
public class MyLogFilter extends ZuulFilter {
//用來指定當前這個過濾器的執行類型,可以寫字符串也可以寫枚舉值
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
//用來指定當前這個過濾器的執行順序,可以寫數字也可以寫枚舉值
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
//用來指定當前這個過濾器是否執行,可以直接寫死true/fasle,也可以自行判斷
@Override
public boolean shouldFilter() {
return true;
}
//用來指定當前這個過濾器的執行邏輯,這個return null沒有什麼意義,但是不能省略
@Override
public Object run() throws ZuulException {
//獲取當前請求的上下文對象
RequestContext currentContext = RequestContext.getCurrentContext();
//獲取當前請求的請求對象
HttpServletRequest request = currentContext.getRequest();
//輸出自定義的語句信息,也可以直接保存到數據庫,這裏只是測試
System.out.println("===============日誌記錄開始===============");
System.out.println("訪問地址:" + request.getRequestURI());
System.out.println("===============日誌記錄結束===============");
return null;
}
}
編寫好自己的過濾器以後,重新啓動當前這個項目,然後訪問:http://localhost:5001/api/consumer9002/consumer/product/findAll
3.7、禁用指定過濾器
我們想要禁用某一個過濾器,只需要按照配置規則進行配置就好了,這裏我們就禁用掉自己編寫的過濾器,配置如下:
zuul:
#統一添加路由前綴
prefix: /api
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的過濾器
MyLogFilter:
route: #代表過濾器類型
disable: true
編寫好自己的過濾器以後,重新啓動當前這個項目,然後訪問:http://localhost:5001/api/consumer9002/consumer/product/findAll
我們發現控制檯並沒有打印我們自己定義的日誌信息。
3.8、處理全局的異常
Spring Cloud Zuul 對異常的處理是非常方便的,我們已經學過了在pre、routing、post的任意一個階段如果拋異常了,則執行error過濾器,如果你想要統一處理Zuul內部出現的異常,Zuul內部幫我們處理了,但是它返回的是一個白頁,我們也可以自己統一處理異常,我們只需要定義一個類型爲error的過濾器替換掉默認的SendErrorFilter
就能處理異常了,在處理的時候,可以使用json來統一返回錯誤信息,這樣我們就看不到Spring Boot默認的錯誤白頁了。
(1)禁用Zuul自帶的異常過濾器,配置如下:
zuul:
#統一添加路由前綴
prefix: /api
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的過濾器
MyLogFilter:
route: #代表過濾器類型
disable: true
#禁用Zuul自帶的異常過濾器
SendErrorFilter:
error: #代表過濾器類型
disable: true
(2)新建過濾器:com.caochenlei.filter.MyErrorFilter,然後替換掉自帶的過濾器
@Component
public class MyErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException) context.getThrowable();
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = response.getWriter();
writer.print("{\"code\":" + exception.nStatusCode + ",\"message\":\"" + exception.getMessage() + "\"}");
writer.close();
} catch (Exception e) {
e.printStackTrace(); }
return null;
}
}
(3)再次新建一個過濾器(com.caochenlei.filter.MyThrowExceptionFilter),這個過濾器不做別的事情,就是在run中拋出一個異常,看看,咱們定義的全局過濾器能不能攔截到他:
@Component
public class MyThrowExceptionFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
//模擬出現異常
int i = 1 / 0;
return null;
}
}
(4)編寫好自己的過濾器以後,重新啓動當前這個項目,然後訪問:http://localhost:5001/api/consumer9002/consumer/product/findAll
(5)既然可以處理自己定義過濾器的異常,那我們就沒必要在留着MyThrowExceptionFilter
過濾器了,將網關恢復正常,也就是禁止掉MyThrowExceptionFilter
,具體的配置如下:
zuul:
#統一添加路由前綴
prefix: /api
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的過濾器
MyLogFilter:
route: #代表過濾器類型
disable: true
#禁用Zuul自帶的異常過濾器
SendErrorFilter:
error: #代表過濾器類型
disable: true
#禁用掉自定義的異常過濾器
MyThrowExceptionFilter:
pre:
disable: true
(6)編寫好自己的過濾器以後,重新啓動當前這個項目,然後訪問:http://localhost:5001/api/consumer9002/consumer/product/findAll
好了,到這裏,過濾器這部分相信你已經學會了,開始學習其他部分了。
第四章 Zuul其他功能
4.1、負載均衡超時
Zuul中默認就已經集成了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的默認值,比如熔斷超時時間只有1S,很容易就觸發了。
因此建議我們手動進行配置:
zuul:
#開啓重試功能
retryable: true
#統一添加路由前綴
prefix: /api
ignored-services: '*'
routes:
#配置路由規則,key代表服務名稱,value代表映射規則
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的過濾器
MyLogFilter:
route: #代表過濾器類型
disable: true
#禁用Zuul自帶的異常過濾器
SendErrorFilter:
error: #代表過濾器類型
disable: true
#禁用掉自定義的異常過濾器
MyThrowExceptionFilter:
pre:
disable: true
ribbon:
ConnectTimeout: 500 # 連接超時時間(ms)
ReadTimeout: 2000 # 通信超時時間(ms),超時時長不能超過hystrix的熔斷時長
OkToRetryOnAllOperations: true # 是否對所有操作重試
MaxAutoRetriesNextServer: 2 # 同一服務不同實例的重試次數
MaxAutoRetries: 1 # 同一實例的重試次數
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔斷超時時長:6000ms
ConnectTimeout(連接超時時長) + ReadTimeout(讀取超時時長) 是2500,但是ribbon會進行一次重試,那麼默認就是(ConnectTimeout+ReadTimeout) * 2 是真正的總超時時長,所以timeoutInMillisecond熔斷的時長必須要超過這個時間,否則會報錯或者把ribbon的MaxAutoRetries設爲0,那麼就不進行重試了。
4.2、服務降級熔斷
Zuul本身就是一個代理服務,但如果被代理的服務突然斷了,這個時候Zuul上面會有出錯信息,例如,停止了被調用的微服務。一般服務方自己會進行服務的熔斷降級,但對於Zuul本身,也應該進行Zuul的降級處理,我們需要有一個Zuul的降級,實現如下:
com.caochenlei.fallback.ZullFallback
@Component
public class ZullFallback 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() {
//此處可以不用處理...
}
};
}
}
重新啓動當前這個項目,然後我們關閉service-consumer9002
服務,然後訪問:http://localhost:5001/api/consumer9002/consumer/product/findAll
4.3、網關緩存問題
默認情況下,所有的請求經過zuul網關的代理,默認會通過SpringMVC預先對請求進行處理緩存,普通請求並不會有什麼影響,但是對於文件上傳,就會造成不必要的網絡負擔,在高併發時,可能導致網絡阻塞,Zuul網關不可用,這樣我們的整個系統就癱瘓了。所以,我們上傳文件的請求需要繞過請求的緩存,直接通過路由到達目標微服務,簡單的意思就是在url前面加上 “/zuul” ,那麼就會跳過緩存。如果,你上傳文件的地址還用到了ribbon的負載均衡器,那麼,你應該調大超時時間,否則也會出問題。
ribbon:
ConnectTimeout: 300
ReadTimeout: 60000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 60000
我們需要在當前訪問路徑api前面加上/zuul,例如:http://localhost:5001/zuul/api/upload-service/upload/image
如果你使用了nginx對zuul網關進行了負載均衡,你可以對路徑進行重寫處理,我們需要修改到以/zuul爲前綴,可以通過nginx的rewrite指令實現這一需求。
location /api/upload-service/ {
rewrite "^/(.*)$" /zuul/$1 ;
}