springcloud系列21——Zuul簡介及程式碼示例
Zuul簡介
路由是微服務架構的組成部分。 例如,/可以對映到您的Web應用程式,/ api/users對映到使用者服務,/api/shop對映到商店服務。 Zuul是Netflix基於JVM的路由器和伺服器端負載均衡器。
Netflix使用Zuul進行以下操作: 認證 洞察 壓力測試 金絲雀測試 動態路由 服務遷移 負載脫落 安全 靜態響應處理 主動/主動流量管理
Zuul的規則引擎允許任何JVM語言編寫規則和過濾器,內建支援Java和Groovy。
配置屬性zuul.max.host.connections已被兩個新屬性zuul.host.maxTotalConnections和zuul.host.maxPerRouteConnections取代,它們分別預設為200和20。
所有路由的預設Hystrix隔離模式(ExecutionIsolationStrategy)是SEMAPHORE。 如果首選此隔離模式,則可以將zuul.ribbonIsolationStrategy更改為THREAD。
程式碼示例
這裡新建一個模組microservice-gateway-zuul。
1.引入zuul和eureka client的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</ artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
spring cloud文件有說,Zuul starter沒有包含discovery client,所以我們在上面增加了eureka client的依賴。
2.在Spring boot的主類上增加註解@EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApplication.class,args);
}
}
3.application.yml配置
spring:
application:
name: microservice-gateway-zuul
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
測試
1.啟動Eureka server; 2.啟動user微服務; 3.啟動zuul模組。
在user模組,有/sample/1獲取使用者資訊的介面。
可以看到,請求成功。 我們看Zuul模組的控制檯日誌,可以看到下面的日誌:
Mapped URL path [/microservice-springcloud-user/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
自定義請求路徑
通過Eureka server中的serviceID可以請求成功,但名字太長,如何自定義?
比如我們想通過/user/sample/1訪問,如何做到?
在application.yml增加下面的配置:
zuul:
routes:
microservice-springcloud-user: /user/**
Zuul忽略某些服務
比如有user和movie2個服務,但我只想Zuul代理user服務。
zuul:
ignoredServices: '*'
routes:
microservice-springcloud-user: /user/**
ignoredServices:*忽略所有的服務,然後在routes中指定了user,所以最終就是Zuul代理user服務。
或者:
zuul:
# 多個服務id之間用逗號分隔
ignoredServices: microservice-springcloud-movie
routes:
microservice-springcloud-user: /user/**
Zuul指定path和serviceId
zuul:
routes:
# 下面的user1只是一個標識,保證唯一即可
user1:
# 對映的路徑
path: /user/**
# 服務id
serviceId: microservice-springcloud-user
上面的配置意思是:讓Zuul代理microservice-springcloud-user,路徑為/user/**,user1可以隨便寫,只要保證唯一即可。
Zuul指定path+url
除了上面說的指定path+serviceId外,還可以使用path+url的配置。
zuul:
routes:
user1:
path: /user/**
# url為user服務的url
url: http://10.41.3.149:7902
Zuul指定可用服務的負載均衡
在spring cloud的文件中有寫,如果使用上面的path+url配置,不會作為HystrixCommand執行,也不會使用Ribbon對多個URL進行負載均衡。 要實現此目的,您可以使用靜態伺服器列表指定serviceId。
zuul:
routes:
user1:
path: /user/**
serviceId: microservice-springcloud-user
# 在Ribbon中禁用eureka
ribbon:
eureka:
enabled: false
microservice-springcloud-user:
ribbon:
listOfServers: localhost:7901,localhost:7902
如上,需要在ribbon中禁用Eureka。然後指定了2個user服務,埠分別為7901,7902。
Zuul使用正則表示式指定路由規則
您可以使用regexmapper在serviceId和路由之間提供約定。 它使用名為groups的正則表示式從serviceId中提取變數並將它們注入路由模式。
將user服務id修改為
spring:
application:
name: microservice-springcloud-user-v1
zuul模組application.yml
spring:
application:
name: microservice-gateway-zuul
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
Zuul主類:
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApplication.class,args);
}
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
// 第一個引數:servicePattern,第2個引數routePattern
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
}
上面的PatternServiceRouteMapper意思是myusers-v1將對映為/v1/myusers/**。
接受任何正則表示式,但所有命名組必須同時出現在servicePattern和routePattern中。
如果servicePattern與serviceId不匹配,則使用預設行為。比如user的serviceId為microservice-springcloud-user,那麼實際上最終是通過http://localhost:zuul埠/microservice-springcloud-user/**來訪問。
在上面的示例中,serviceId“myusers”將對映到路由“/myusers/**”(在未檢測到版本時)預設情況下禁用此功能,僅適用於已發現的服務。
然後瀏覽器可以通過[http://localhost:zuul埠]/v1/microservice-springcloud-user/sample/1來訪問user服務。
為所有對映增加字首
要為所有對映新增字首,請將zuul.prefix設定為值,例如/ api。 在預設情況下轉發請求之前,會從請求中刪除代理字首(使用zuul.stripPrefix = false關閉此行為)。
在application.yml增加
zuul:
prefix: /api
然後通過http://localhost:Zuul埠/api/microservice-springcloud-user/sample/1來訪問。
上面是一種全域性的設定方式。可以通過zuul.stripPrefix=true/false來設定在請求具體的服務時是否剝離字首。比如訪問/api/sample/1,如果zuul.stripPrefix設定為true(預設為true),則實際請求使用者服務的是/sample/1,相反請求路徑是/api/sample/1.
您還可以關閉從各個路由中剝離特定於服務的字首,例如: 假定user服務中指定了context-path為/user,我們訪問/sample/1是通過/user/sample/1來訪問的。現在我想通過http://localhost:zuul埠/user/sample/1來訪問,可以這樣做:
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
或者:
zuul:
routes:
microservice-springcloud-user:
prefix: /user
path: /**
stripPrefix: false
stripPrefix是剝離字首的意思,設定為false就是不剝離字首,Zuul預設是剝離字首的。比如我們設定path=/user/**,比如訪問/user/sample/1,實際請求使用者服務的是/sample/1。
stripPrefix比較實用的場景是服務帶有context-path。
zuul.stripPrefix僅適用於zuul.prefix中設定的字首。 它對給定路由的路徑中定義的字首沒有任何影響。
Zuul忽略指定的路徑
上面說過了,通過ignoredServices可以指定忽略某些服務,這是比較粗粒度的控制。如果想細粒度的控制忽略某些路徑,可以通過下面的方式:
zuul:
ignoredPatterns: /**/admin/**
routes:
users: /myusers/**
這意味著所有諸如“/myusers/101”之類的請求將被轉發到“users”服務上的“/101”。 但包括“/admin/”在內的請求則不會處理。
Zuul指定路由的順序
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
legacy:
path: /**
上面配置的意思是/user**的請求轉發到microservice-springcloud-user去處理,其他的請求按預設的方式處理(即通過http://zuulHost:zuulPort/服務名/請求路徑)。 比如我們啟動了microservice-springcloud-user和microservice-springcloud-movie-feign-without-hystrix2個服務。
通過Zuul訪問microservice-springcloud-user,可以這樣訪問:
通過Zuul訪問microservice-springcloud-movie-feign-without-hystrix需要如下方式訪問:
如果你需要保證路由的順序,則需要使用YAML檔案,因為使用屬性檔案就會丟失順序。所以,如果你用properties檔案配置,可能會導致/user/**訪問不到microservice-springcloud-user。
Zuul Http Client
Zuul預設使用的是Apache的http client。之前使用的是RestClient。如果你還是想使用RestClient,可以設定ribbon.restclient.enabled=true;如果你想使用OkHttp3,可以設定ribbon.okhttp.enabled=true。
如果要自定義Apache HTTP客戶端或OK HTTP客戶端,請提供ClosableHttpClient或OkHttpClient型別的bean。
Cookie和敏感的Headers
在同一系統中的服務之間共享Headers是可以的,但您可能不希望敏感Headers向下遊洩漏到外部伺服器。您可以在路由配置中指定忽略的Headers列表。 Cookie起著特殊的作用,因為它們在瀏覽器中具有明確定義的語義,並且它們始終被視為敏感。如果您的代理的消費者是瀏覽器,那麼下游服務的cookie也會給使用者帶來問題,因為它們都會混亂(所有下游服務看起來都來自同一個地方)。
如果您對服務的設計非常小心,例如,如果只有一個下游服務設定了cookie,那麼您可以讓它們從後端一直流到呼叫者。此外,如果您的代理設定了cookie並且您的所有後端服務都是同一系統的一部分,那麼簡單地共享它們就很自然(例如使用Spring Session將它們連結到某個共享狀態)。除此之外,由下游服務設定的任何cookie都可能對呼叫者不是很有用,因此建議您(至少)將“Set-Cookie”和“Cookie”放入敏感的標頭中不屬於您的域名。即使對於屬於您域的路由,在允許cookie在它們與代理之間流動之前,請仔細考慮它的含義。
可以將敏感報頭配置為每個路由的逗號分隔列表,例如,
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
url: https://downstream
這是sensitiveHeaders的預設值,因此除非您希望它不同,否則無需進行設定。注: 這是Spring Cloud Netflix 1.1中的新功能(在1.0中,使用者無法控制Headers,所有Cookie都在所有方向上流動)。
sensitiveHeaders是黑名單,預設不為空,因此要使Zuul傳送所有Headers(“忽略”的Headers除外),您必須將其明確設定為空列表。 如果要將cookie或授權Headers傳遞給後端,則必須執行此操作。 例:
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream
也可以通過設定zuul.sensitiveHeaders來全域性設定敏感的Headers。 如果在路由上設定了sensitiveHeaders,則會覆蓋全域性sensitiveHeaders設定。
忽略Headers
除了每個路由規則上面的敏感頭部資訊設定,我們還可以在閘道器與外部服務互動的時候,用一個全域性的設定zuul.ignoredHeaders,去除那些我們不想要的http頭部資訊(包括請求和響應的)。在預設情況下,zuul是不會去除這些資訊的。如果Spring Security不在類路徑上的話,它們就會被初始化為一組眾所周知的“安全”頭部資訊(例如,涉及快取),這是由Spring Security指定的。在這種情況下,假設請求閘道器的服務也會新增頭部資訊,我們又要得到這些代理頭部資訊,就可以設定zuul.ignoreSecurityHeaders為false,同時保留Spring Security的安全頭部資訊和代理的頭部資訊。當然,我們也可以不設定這個值,僅僅獲取來自代理的頭部資訊。
路由端點
Actuator提供了一個可以檢視路由規則的端點/routes,我們在Zuul中引入Actuator依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
再把安全驗證關閉,讓我們可以訪問到這個端點:
management:
security:
enabled: false
這裡,遺留請求的路由規則會影響到我們訪問這個端點,先註釋掉這個路由規則:
#legacy:
#path: /**
重啟Zuul專案,我們便能看到zuul閘道器的路由規則了
如果想知道路由的詳細細節,可以增加引數?format=details
Strangulation Patterns (絞殺者模式)
遷移現有應用程式或API時的常見模式是“扼殺”舊的端點,慢慢地用不同的實現替換它們。 Zuul代理是一個有用的工具,因為可以使用它來處理來自舊端點的客戶端的所有流量,但重定向一些請求到新的端點。
zuul:
routes:
first:
path: /first/**
url: http://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd # 本地的轉發
legacy: # 老系統的請求
path: /**
url: http://legacy.example.com
在這個例子中,我們正在扼殺“遺留”應用程式,該應用程式對映到與其他模式之一不匹配的所有請求。 /first/**中的路徑已被提取到具有外部URL的新服務中。 並轉發/second/**中的路徑,以便可以在本地處理它們,例如, 使用正常的Spring @RequestMapping。 /third/**中的路徑也被轉發,但具有不同的字首(即/third/foo被轉發到/3rd/foo)。
忽略的模式不會被完全忽略,它們只是不由代理處理(因此它們也可以在本地有效轉發)。
通過Zuul上傳檔案
新建一個模組microservice-file-upload,該模組用於檔案上傳。 application.yml
server:
port: 9999
spring:
application:
name: microservice-file-upload
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
上傳檔案的Controller
@Controller
@RequestMapping("/file")
public class FileUploadController {
@RequestMapping(value = "/upload",method = RequestMethod.POST)
@ResponseBody
public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
String uploadDir = "E:/test/";
String originName = file.getOriginalFilename();
String uploadPath = uploadDir+originName;
File destDir = new File(uploadDir);
if (!destDir.exists()) {
destDir.mkdirs();
}
File dest = new File(uploadPath);
if (dest.exists()) {
dest.delete();
}
file.transferTo(dest);
System.out.println("檔案上傳路徑:" + uploadPath);
return uploadPath;
}
}
這裡將服務註冊到Eureka,是為了後面使用Zuul代理檔案上傳功能。
這裡用curl測試。
curl -F "[email protected]:/luckystar88/books/java_bloomfilter.rar" http://localhost:9999/file/upload
可以看到,請求成功。
剛剛上傳的檔案大小14Kb,我們上傳一個大點的檔案(檔案大小18.9M)。
出錯,看錯誤資訊,提示檔案大小19M,超過了配置的最大大小10M。
解決辦法:
spring:
application:
name: microservice-file-upload
http:
multipart:
# 單個檔案大小
max-file-size: 1024Mb
# 總上傳資料的大小
max-request-size: 2048Mb
配置上面2項設定檔案大小即可。
重新上傳:
現在我們使用Zuul測試。
修改Zuul的application.yml
zuul:
routes:
microservice-file-upload:
path: /upload-api/**
將/upload-api/**的請求交給microservice-file-upload處理。
啟動Eureka Server,Zuul和file-upload模組。
curl -F "[email protected]:/360極速瀏覽器下載/111.mp4" http://localhost:8808/zuul/upload-api/file/upload
可以看到上傳成功。
我們準備一個大點的檔案(175M)測試下上傳超時。
curl -F "[email protected]:\Users\Administrator\Downloads\Spring+Cloud微服務實戰.pdf" http://localhost:8808/zuul/upload-api/file/upload
可以看到Zuul報超時了。
解決辦法: 在Zuul增加配置:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
重新請求,可以看到上傳成功了(檔名亂碼就暫時不管了)。
禁用Zuul Filters
預設會使用很多filters,可採用如下方式禁止
zuul.SendResponseFilter.post.disable=true
Zuul的回退
當Zuul中給定路徑的電路跳閘時,您可以通過建立ZuulFallbackProvider型別的bean來提供回退響應。 在此bean中,您需要指定回退所針對的路由ID,並提供ClientHttpResponse作為回退返回。
我們建立一個模組microservice-gateway-zuul-fallback。 application.yml
spring:
application:
name: microservice-gateway-zuul-fallback
server:
port: 8808
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
zuul:
routes:
microservice-springcloud-user:
path: /user/**
stripPrefix: false
由於在microservice-springcloud-user服務中指定了context-path,所以這裡設定stripPrefix=false。
spring boot主類:
@SpringBootApplication
@EnableZuulProxy
public class ZuulFallbackApplication
{
public static void main( String[] args )
{
SpringApplication.run(ZuulFallbackApplication.class,args);
}
}
回退類:
@Component
public class UserFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "microservice-springcloud-user";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@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() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream((getRoute() + "==》fallback").getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
回退的類中指定了路由為microservice-springcloud-user,同時指定了響應碼,響應內容,響應型別等資訊。
測試:
啟動Eureka server,microservice-springcloud-user和microservice-gateway-zuul-fallback。
user服務正常時訪問:
關閉user服務,再次訪問:
通過/routes訪問路由資訊:
注意:FallbackProvider類中的routes必須與配置檔案中的一致。
如果想為所有的路由設定一個預設的fallback,可以建立一個ZuulFallbackProvider型別的Bean,並且getRoute返回*或null。
比如:
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
如果您想根據失敗原因選擇響應,請使用FallbackProvider,它將取代未來版本中的ZuulFallbackProvder。
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return fallbackResponse();
}
}
@Override
public ClientHttpResponse fallbackResponse() {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}