為什么要使用微服務網關
- 不同的微服務一般會有不同的網絡地址,而客戶端可能需要調用多個服務接口才能完成一個業務需求
- 若讓客戶端直接與各個微服務通信,會有以下問題:
- 客戶端會多次請求不同微服務,增加了客戶端復雜性
- 存在跨域請求,處理相對復雜
- 認證復雜,每個服務都需要獨立認證
-
難以重構,多個服務可能將會合并成一個或拆分成多個
image
- 微服務網關介于服務端與客戶端的中間層,所有外部服務請求都會先經過微服務網關
- 客戶只能跟微服務網關進行交互,無需調用特定微服務接口,使得開發得到簡化
- 微服務網關還具備以下優點:
- 易于監控,微服務網關可收集監控數據并進行分析
- 易于認證,可在微服務網關上進行認證,然后在將請求轉發給微服務,無須每個微服務都進行認證
-
減少客戶端與微服務之間的交互次數
image
Zuul簡介
- Zuul 是開源的微服務網關,可與 Eureka、Ribbon、Hystrix 等組件配合使用,Zuul 它的核心是一系列過濾器,這些過濾器可完成下面功能:
- 身份認證與安全:識別每個資源的驗證要求,并拒絕那些要求不符合的請求
- 審核與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產視圖
- 動態路由:動態的將請求路由到不同的后端集群
- 壓力測試:逐漸增加指向集群的流量,以了解性能
- 負載分配:為每一種負載類型分配對應容量,并棄用超出限定值的請求
- 靜態響應處理:在邊緣位置直接建立部分響應,從而避免轉發到內部集群
- 多區域彈性:跨越 AWS Region 進行請求路由,實現 ELB 使用多樣化,讓系統邊緣更貼近使用者
- Spring Cloud 對 Zuul 進行了整合和增強,Zuul 默認使用的 HTTP 客戶端是 Apache Http Client,也可使用 RestClient 或 okHttpClient
若要使用 RestClient 可設置
ribbon.restclient.enabled = true
若要使用 okhttp3.OKHttpClient 可設置
ribbon.okhttp.enabled = true
編寫Zuul微服務網關
創建新項目,添加zuul依賴庫
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
在啟動類添加注解 @EnableZuulProxy,聲明一個 Zuul 代理,該代理用 Ribbon 來定位 Eureka Server 中的微服務,同時整合了 Hystrix,實現了容錯
編寫配置文件
server:
port: 8040
spring:
application:
name: zuul
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
測試
- 啟動 EurekaServer
- 啟動 flim-user(spring.application.name 為 flim-user) 和 flim-consumer(spring.application.name 為 flim-consumer)
- 啟動 zuul,訪問 http://localhost:8040/flim-consumer/user/1 請求會被轉發到 http://localhost:8010/user/1
- 默認情況下 Zuul 會代理所有注冊到 EurekaServer 的微服務,并且 Zuul 路由規則如下 http://ZUUL_HOST:ZUUL_PORT/微服務在Eureka上的serviceId/ 會被轉發到 serviceId 對應的微服務**
Zuul 路由端點
- 當 @EnableZuulProxy 與 Spring Boot Actuator 配合使用時,Zuul 會暴露一個路由管理端點 /routes 借助該斷點可以方便的、直觀地查看以及管理 Zuul 路由
- 通過 GET 方式訪問該端點即可返回路由列表,通過 POST 方式訪問該端點就會強制刷新 Zuul 當前映射的路由列表
路由配置
- 默認 Zuul 會代理所有注冊到 EurekaServer 的微服務,若只想代理部分服務,就需要對 URL 進行精確的控制
指定微服務訪問路徑,配置 zuul.routes.微服務的 serviceId = 指定路徑,當訪問 /user/** 時會轉發到微服務 flim-user
zuul:
routes:
flim-user: /user/**
忽略指定微服務,通過 zuul.ignored-services 配置需要忽略的微服務,多個微服務通過逗號隔開
zuul:
ignored-services: flim-user,flim-consumer
忽略所有微服務,只路由指定微服務
zuul:
ignored-services:'*' #使用'*'忽略所有微服務
routes:
flim-user: /user/**
指定微服務的 serviceId 和對應路徑
zuul:
routes:
user-route: #該配置方式中,user-route 只是給路由起的一個名稱,可以任意起名
service-id: provider-flim-user
path: /user/**
指定 path 和 URL,但會破壞 Ribbon 負載均衡和 HystrixCommand 執行
zuul:
routes:
user-route: #該配置方式中,user-route 只是給路由起的一個名稱,可以任意起名
url: http://localhost:8000/
path: /user/**
指定 path 和 URL,不會破壞 Ribbon、Hystrix 特性
zuul:
routes:
user-route: #該配置方式中,user-route 只是給路由起的一個名稱,可以任意起名
service-id: flim-user
path: /user/**
ribbon:
eureka:
enabled: false #為Ribbon禁用Eureka
flim-user:
ribbon:
listOfServers: localhost:8000,localhost:8001
借助 PatternServiceRouteMapper 使用正則表達式匹配路由
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
// 調用構造函數 PatternServiceRouteMapper(String servicePattern,String routePattern)
// servicePattern 指定微服務的正則
// routePattern 指定路由正則
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}
路由前綴
- prefix :前綴,當請求匹配前綴時會進行代理
- strip-prefix :代理前綴默認會從請求路徑中移除,通過該設置關閉移除功能,當 stripPrefix=true 的時 (http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/user/list),當stripPrefix=false的時(http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/api/user/list)
#將 /api/flim-user/1 的請求轉發到 flim-user 的 /api/1
zuul:
prefix: /api
strip-prefix: false
routes:
flim-user: /user/**
#將 /user/1 請求轉發到 flim-user 的 /user/1
zuul:
routes:
flim-user:
path: /user/**
strip-prefix: false
忽略某些路徑,通過正則匹配
zuul:
ignoredPatterns:/**/admin/** #忽略所有包含 /admin/ 的路徑
routes:
flim-user: /user/**
Zuul 安全與 Header
敏感的 Header 設置
- 一般來說,同一個系統中的服務之間共享 Header,不過應該防止一些敏感的 Header 外泄,在很多場景下,需要通過路由指定一系列敏感 Header 列表
zuul:
routes:
flim-user:
ptah: /user/**
sensitive-headers: Cookie,Set-Cookie,Authorization
url: https://downstream
- 這樣就可以為 flim-user 微服務指定敏感 Header 了,也可用 zuul.sensitive-headers 全局指定敏感 Header
zuul:
sensitive-headers: Cookie,Set-Cookie,Authorization
忽略 Header
- 可用 zuul.ignoredHeaders 屬性丟棄一些 Header,這樣設置后 Header1、Header2 將不會傳播到其它微服務中
zuul:
ignored-headers: Header1,Header2
使用 Zuul 上傳文件
- 對于小文件(1M以內)上傳無需任何處理,對于大文件(10M以上)上傳,需要為上傳路徑添加 /zuul 前綴,也可以使用 zuul.servlet-path 自定義前綴
- 假設 zuul.routes.file-upload = /file-upload/**,如果 http://{HOST}:{PORT}/uoload 是微服務 file-upload 上傳路徑,則可以使用 Zuul 的 /zuul/file-upload/upload 路徑上傳大文件
- 如果 Zuul 使用 Ribbon 做負載均衡,那么對于超大文件(例如500M)需要提升超時時間
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
編寫文件上傳微服務
創建新項目 file-upload,并添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在啟動類上添加 @EnableEurekaClient
編寫 Controller
@RestController
public class FileUploadController {
@PostMapping(name = "/upload")
public String handleFileUpLoad(@RequestParam(value = "file") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
File fileToSave = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes, fileToSave);
return fileToSave.getAbsolutePath();
}
}
編寫配置文件
server:
port: 8050
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: file-upload
http:
multipart:
max-file-size: 2000Mb
max-request-size: 2500Mb
測試
- 啟動 EurekaServer
- 啟動 file-upload、zuul
- 直接將文件通過 http://localhost:8050/upload 上傳,小文件(1M)、大文件(1G)均正常上傳(除非發生堆錯誤等問題)
- 通過 Zuul http://localhost:8040/file-upload/upload 上傳小文件,可以正常上傳
- 通過 Zuul http://localhost:8040/file-upload/upload 上傳大文件,會報異常
There was an unexpected error (type=Internal Server Error, status=500).
Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (155106628) exceeds the configured maximum (10485760)
- 添加 /zuul 前綴,通過 http://localhost:8040/zuul/file-upload/upload 此時會報新的異常,Hystrix 超時異常
There was an unexpected error (type=Internal Server Error, status=500).
TIMEOUT
- 需要在 Zuul 配置文件中添加以下內容,然后重啟微服務,可正常上傳
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
Zuul 過濾器
- Zuul 大部分功能都是通過過濾器實現的,Zuul 中定義了 4 種標準過濾器類型,對應了典型的生命周期
- PRE:在請求被路由之前調用,可用于身份驗證、集群中選擇請求的微服務、記錄調試信息等
- ROUTING:請求路由到微服務時執行,用于構建發送給微服務的請求,使用 HttpClient 或 Ribbon 請求微服務
- POST:在路由到微服務后執行,可用來為響應添加 Header,收集統計信息和指標、將相應從微服務發送給客戶端等
- ERROR:在其他階段發生錯誤時執行該過濾器
-
Zuul 還允許創建自定義過濾器類型,例如定制一種 STATIC 類型過濾器,直接在 Zuul 中生存響應,而不將請求轉發到后端的微服務
image
編寫 Zuul 過濾器
創建過濾器類,繼承抽象類 ZuulFilter 并實現抽象方法
/**
* @description: Zuul日志過濾器
* filterType:返回過濾器類型,有 pre、route、post、error 等幾種取值
* filterOrder:返回一個 int 值指定過濾器執行順序,不同過濾器允許返回相同的數字
* shouldFilter:true 表示過濾器執行、false表示不執行
* run:過濾器具體邏輯,下面代碼打印了請求方法和URL
*/
public class PreRequestLogFilter extends ZuulFilter{
private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s",request.getMethod(),request.getRequestURL().toString()));
return null;
}
}
將過濾器通過 @Bean 注入到 IOC 容器
@Bean
public PreRequestLogFilter preRequestLogFilter() {
return new PreRequestLogFilter();
}
測試
- 啟動 EurekaServer、啟動 flim-user
- 啟動 zuul 訪問 http://localhost:8040/flim-user/1 可看到如下日志
c.l.zuul.filter.PreRequestLogFilter : send GET request to http://localhost:8040/flim-user/1
禁用 Zuul 過濾器
- Spring Cloud 默認為 Zuul 啟用了一些過濾器,如 DebugFilter、FormBodyWrapperFilter、PreDecorationFilter 等,這些過濾器存放在 spring-cloud-netflix-core 這個 Jar 包的 org.springframwork.cloud.netflix.zuul.filters 包中
- 只需配置 zuul.<SimpleClassName>.<filterType>.disable = true 即可禁用 SimpleClassName 對應的過濾器,例如
zuul.SendResponseFilter.post.disable = true
為 Zuul 添加回退
- Zuul 的 Hystrix 監控粒度是微服務,而不是某個 API
- 要為 Zuul 添加回退,需要實現 ZuulFallbackProvider 接口,在實現類中指定為哪個微服務提供回退,并提供一個 ClientHttpResponse 作為回退響應
編寫 Zuul 回退類
@Component
public class UserFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
//表明為哪個微服務提供回退
return "flim-user";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//回退時的狀態碼
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
//數字類型的狀態碼,本文返回的是200
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
//狀態文本,本文返回的是OK
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
//響應體
return new ByteArrayInputStream("用戶微服務不可用".getBytes());
}
@Override
public HttpHeaders getHeaders() {
//headers設定
HttpHeaders httpHeaders = new HttpHeaders();
MediaType mt = new MediaType("application","json", Charset.forName("UTF-8"));
httpHeaders.setContentType(mt);
return httpHeaders;
}
};
}
}
測試
- 啟動 EurekaServer
- 啟動 Zuul,訪問 http://localhost:8040/flim-user/1 時會返回以下內容
用戶微服務不可用
Zuul 高可用
- 將多個 Zuul 客戶端也注冊到 Eureka Server 上,就可以實現 Zuul 高可用
- 部署多個 Zuul,Zuul 客戶端會自動從 Eureka Server 中查詢 Zuul Server 列表,并使用 Ribbon 負載均衡地請求 Zuul 集群
- 如果 Zuul 客戶端未注冊到 Eureka Server 上,可借助 Nginx 等實現負載均衡
修改 ServiceId 與路由映射規則
- 可以自定義 serviceId 和路由之間的相互映射,通過正則表達式進行匹配
- 例如將 serviceId 為 users-v1 的服務映射到路由 /v1/users/ 的路徑上,當請求 /v1/users/ 時等同于請求 /users-v1/
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>.+$)",
"${version}/${name}");
}
---- 待完善 ----