服務端負載工具Zuul
Zuul是Netflix公司提供的服務端負載工具,Spring Cloud基於它做了一些整合。試想一下微服務場景下服務端有服務A、服務B、服務C等,每個服務對應不同的地址,作爲服務提供者,你不想直接對外暴露服務A、服務B、服務C的地址,而且每種服務又有N臺機器提供服務。使用Zuul後,可以同時聚合服務A、服務B、服務C,又可實現服務的負載均衡,即同時聚合多個服務A的提供者。Zuul是作用於服務端的,同時它在提供負載均衡時是基於Ribbon實現的。其實也很好理解,Zuul對於真正的服務提供者來說它又是作爲客戶端的,所以它使用了客戶端負載工具Ribbon。Zuul會把每個請求封裝爲Hystrix Command,所以它也可能會觸發斷路器打開。
Spring Cloud應用使用Zuul的第一步是在pom.xml中引入spring-cloud-starter-netflix-zuul
。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
然後在主程序Class上加上@EnableZuulProxy
以啓用Spring Cloud內置的Zuul反向代理。
@EnableZuulProxy
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然後可以在application.properties或application.yml中配置服務對應的路由關係。如下指定了服務hello
對應的映射路徑是/api/**
,即當接收到請求爲/api/abc/def
時會轉發到服務hello
對應的/abc/def
,因爲Zuul在轉發時默認會把前綴去掉(默認會去掉*以前的內容)。
zuul.routes.hello=/api/**
由於Zuul是基於Ribbon進行負載均衡的,所以我們可以通過Ribbon配置服務地址的方式給Zuul的某個服務配置服務地址。如下配置了服務hello
對應的服務地址是localhost:8900
和localhost:8901
。
server.port=8888
zuul.routes.hello=/api/**
hello.ribbon.listOfServers=localhost:8900,localhost:8901
當我們請求http://localhost:8888/api/hello
時就會轉發爲請求http://localhost:8900/hello
或http://localhost:8901/hello
。可以通過zuul.prefix=/api
爲所有的請求指定一個通用的前綴,而這個前綴默認也是會去掉的,比如當進行了下面的配置,在訪問http://localhost:8888/api/hello/abc
時會轉發到http://localhost:8900/abc
,因爲通用的前綴默認會去掉,特定服務的路由前綴也會去掉。
zuul.prefix=/api
zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900
如果希望通用的前綴不去掉,可以加上zuul.stripPrefix=false
,此時通過zuul.prefix
指定的前綴就不會去掉了。所以當訪問http://localhost:8888/api/hello/abc
時會轉發到http://localhost:8900/api/abc
。如果需要特定服務路由的前綴也不去掉,可以使用zuul.routes.<serviceId>.stripPrefix=false
,對於hello服務來說就是zuul.routes.hello.stripPrefix=false
。此時除了加上這個外,hello服務的路徑信息也需要通過zuul.routes.<serviceId>.path
來配置,所以此時的配置會是如下這樣。
zuul.prefix=/api
zuul.stripPrefix=false
zuul.routes.hello.path=/hello/**
zuul.routes.hello.stripPrefix=false
hello.ribbon.listOfServers=localhost:8900
@EnableZuulProxy
的自動配置由org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
負責,所有的配置信息由org.springframework.cloud.netflix.zuul.filters.ZuulProperties
負責接收。我們可以通過ZuulProperties查看可以配置哪些信息,也可以查看Zuul的一些默認配置。比如可以通過下面的方式指定最大的連接數爲100,默認是200;指定Socket的超時時間爲3秒,默認是10秒。
zuul.host.maxTotalConnections=100
zuul.host.socketTimeoutMillis=3000
之前定義的zuul.prefix
、zuul.stripPrefix
、zuul.routes.*
等都來自於ZuulProperties,更多可配置的信息請參考ZuulProperties的API文檔或源碼。
也可以不使用Ribbon,直接寫死服務轉發地址。如下配置當請求http://localhost:8888/api/hello/abc
時會轉發爲請求http://localhost:8900/api/hello/abc
。
server.port=8888
zuul.prefix=/api
zuul.stripPrefix=false
zuul.routes.hello.path=/hello/**
zuul.routes.hello.url=http://localhost:8900
zuul.routes.hello.stripPrefix=false
自定義HttpClient
Zuul默認會使用Apache的Http Client作爲向後端服務發起請求的客戶端,如果用戶想對HttpClient進行一些自定義,則可以定義自己的HttpClient類型的bean。比如下面的代碼自定義了HttpClient,其每次請求時都往Header裏面寫入名爲abc,值爲123的Header。
@Configuration
public class HttpClientConfig {
@Bean
public CloseableHttpClient httpClient() {
List<Header> defaultHeaders = new ArrayList<>();
defaultHeaders.add(new BasicHeader("abc", "123"));
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultHeaders(defaultHeaders).build();
return httpClient;
}
}
用戶也可選擇自定義自己的HttpClientBuilder類型的bean,因爲Spring Cloud在創建HttpClient時會獲取HttpClientBuilder類型的bean創建HttpClient,當我們沒有自定義HttpClientBuilder類型的bean時Spring Cloud會自動創建一個。
敏感性Header和Cookie
Zuul服務接收到的請求Header可以被轉發到負載的底層服務,但是有時候可能你不希望這些Header被轉發到底層服務,此時可以通過sensitiveHeaders指定敏感Header。比如我們的Zuul服務是直接面向瀏覽器客戶的,我們不希望瀏覽器的信息被轉發到底層服務,則可以在application.properties中添加如下配置信息,這樣就不會往底層服務傳遞user-agent和cache-control這兩個Header。
zuul.sensitiveHeaders=user-agent,cache-control
通過zuul.sensitiveHeaders
指定的配置將對所有的服務生效,如果我們只想對某個服務隱藏一些Header,則可以通過該服務的路由配置sensitiveHeaders,比如不希望user-agent和cache-control這兩個Header轉發到服務hello,則可以進行如下配置。
zuul.routes.hello.sensitiveHeaders=user-agent,cache-control
當同時配置了特定服務配置的sensitiveHeaders和通用的sensitiveHeaders時,特定服務的sensitiveHeaders將擁有更高的優先級,即特定服務的sensitiveHeaders會覆蓋通用的sensitiveHeaders。比如通用的sensitiveHeaders配置了敏感Header爲ABC,服務hello配置了sensitiveHeaders爲BCD,那麼請求轉發到服務hello時將不會轉發Header BCD,但是會繼續轉發Header ABC。
默認的敏感Header是Cookie、Set-Cookie和Authorization,當自己指定了sensitiveHeaders時,默認的sensitiveHeaders自動失效。
忽略Header
除了敏感性Header可以不轉發到底層服務外,還可以通過ignoredHeaders指定需要忽略的Header。ignoredHeaders指定的Header將不轉發到底層服務,同時將從底層服務的響應中自動移除。如下配置指定了將忽略user-agent和cache-control這兩個Header。
zuul.ignoredHeaders=user-agent,cache-control
ignoredHeaders只能指定通用的,沒有特定服務級別的。
Endpoint
當使用@EnableZuulProxy
會自動引入routes和filters這兩個Endpoint,可以單獨發佈這兩個Endpoint,也可以通過如下方式發佈所有的Endpoint。
management.endpoints.web.exposure.include=*
之後可以通過actuator/routes
查看所有的路由信息,即服務對應的映射路徑信息,類似如下這樣。
{
/api/hello/**: "hello"
}
還可以在後面加上/details
得到路由的詳細信息,即請求/actuator/routes/details
,得到的路由詳細信息是類似如下這樣的。
{
"/api/hello/**": {
"id": "hello",
"fullPath": "/api/hello/**",
"location": "hello",
"path": "/hello/**",
"prefix": "/api",
"retryable": false,
"sensitiveHeaders": [
"upgrade-insecure-requests",
"accept"
],
"customSensitiveHeaders": true,
"prefixStripped": false
}
}
可以通過actuator/filters
查看所有的com.netflix.zuul.ZuulFilter
及對應的Filter類型等信息,類似如下這樣。
{
"error": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter",
"order": 0,
"disabled": false,
"static": true
}
],
"post": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter",
"order": 1000,
"disabled": false,
"static": true
}
],
"pre": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter",
"order": 1,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter",
"order": -1,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter",
"order": -2,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter",
"order": -3,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter",
"order": 5,
"disabled": false,
"static": true
}
],
"route": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter",
"order": 100,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
"order": 10,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter",
"order": 500,
"disabled": false,
"static": true
}
]
}
本地轉發
Zuul除了把請求轉發到外部服務外,還可以把請求轉發到本地的@RequestMapping
請求。比如Zuul所在應用有如下Controller,其可以接收/local/abc
請求。
@RestController
@RequestMapping("local")
public class LocalFowardController {
@GetMapping("abc")
public String abc() {
return "ABC" + LocalDateTime.now();
}
}
然後我們配置名爲local1的路由信息,其將把/local1/**
請求轉發到本地的/local
。即當接收到請求/local1/abc
時會轉發爲請求本地的/local/abc
,即請求LocalFowardController.abc()
。
zuul.routes.local1.path=/local1/**
zuul.routes.local1.url=forward:/local
禁用ZuulFilter
Spring Cloud對Zuul的支持是由一系列的ZuulFilter來實現的,它們都定義在org.springframework.cloud.netflix.zuul.filters.xxx
下,其中xxx
指對應的ZuulFilter。每個ZuulFilter是相互獨立的,它們之間會通過com.netflix.zuul.context.RequestContext
交互數據,RequestContext還持有當前請求的HttpServletRequest和HttpServletResponse的引用。使用@EnableZuulProxy
時會自動創建這些ZuulFilter的bean。如果想禁用其中的某個ZuulFilter,則可以通過設置zuul.<SimpleClassName>.<filterType>.disable=true
來禁用它。比如想要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
,則可以設置zuul.SendResponseFilter.post.disable=true
。
這裏只是拿SendResponseFilter來舉個例,實際使用時禁用了SendResponseFilter將不會把代理的Response寫入到當前請求的Response中,所以千萬不要禁用它。
自定義ZuulFilter
如果有需要也可以定義自己的ZuulFilter,並把它加入到ZuulFilter鏈中。假設我們想從Zuul開始追蹤整個請求,我們可以定義一個ZuulFilter,往Header中寫入一個唯一的請求標識,然後在多個ZuulFilter以及後端服務之間進行共享。爲此我們定義瞭如下這樣一個ZuulFilter。只需要把它定義爲bean即可自動把它加入ZuulFilter鏈中。
@Component
public class AddRequestIdZuulFilter extends ZuulFilter {
private static final String REQUEST_ID_HEADER = "X-REQUEST-ID";
@Override
public boolean shouldFilter() {
return !RequestContext.getCurrentContext().getZuulRequestHeaders().containsKey(REQUEST_ID_HEADER);
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
context.addZuulRequestHeader(REQUEST_ID_HEADER, UUID.randomUUID().toString());
return null;
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
}
Zuul histrix
Zuul會自動把請求封裝爲一個Hystrix Command,且@EnableZuulProxy
上使用了@EnableCircuitBreaker
。可以對路由的服務使用Histrix fallback,當熔斷器打開時將調用對應的fallback。需要爲特定的路由(或serviceId)指定fallback,可以定義一個FallbackProvider類型的bean,然後通過其getRoute()
返回該fallback對應的路由(或serviceId),其fallbackResponse()
將在需要發生fallback時調用。
@Component
public class HelloFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "hello";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("hello fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
@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() {
}
};
}
}
比如上述代碼我們定義了FallbackProvider是對應於路由hello的。當該路由擁有下述配置時,如果Zuul請求http://localhost:8900/xxx
網絡不通,則會轉而返回上述的fallback的結果。
zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900
FallbackProvider也可以是作用於所有的路由的,此時只需指定FallbackProvider的getRoute()
的返回值爲*
。其作用類似於默認FallbackProvider,當同時指定了默認的FallbackProvider和作用於特定的路由的FallbackProvider時,特定路由的FallbackProvider擁有更高的優先級。
@Component
public class DefaultFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("hello fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
@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() {
}
};
}
}
指定超時時間
Zuul調用後端服務使用Ribbon時可以通過Ribbon的配置屬性來指定建立連接的超時時間和調用遠程服務的超時時間。如下配置指定了Zuul使用Ribbon進行負載,且與後端服務建立連接的超時時間是3秒,調用後端服務的接口的超時時間是2秒。
zuul.routes.hello=/hello/**
hello.ribbon.listOfServers=localhost:8900
ribbon.ReadTimeout=2000
ribbon.ConnectTimeout=3000
如果Zuul不使用Ribbon進行負載,而是直接指定路由對應的後端服務地址,則超時時間需要通過如下方式指定。
zuul.routes.hello.path=/hello/**
zuul.routes.hello.url=http://localhost:8900
zuul.host.socket-timeout-millis=2000
zuul.host.connect-timeout-millis=3000
和Eureka一起使用
Zuul底層使用Ribbon進行負載,所以Zuul和Eureka一起使用相當於Ribbon和Eureka一起使用。先在pom.xml中加上Eureka client的依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
然後在application.properties中加上Eureka的配置,Ribbon會自動使用Eureka進行服務發現。
zuul.routes.hello=/hello/**
eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8089/eureka/
Zuul默認會把serviceId映射爲/serviceId/**
,即如果有一個服務hello,其對應的映射路徑默認是/hello/**
。所以當我們的服務可以滿足這種需求時可以不通過zuul.routes.serviceId
指定服務的映射路徑。這樣的話如果你通過Eureka註冊了10個服務,那他們都會通過Zuul進行自動映射,如果你的Zuul是直接對外的,那麼可能你不希望其中的某些服務通過Zuul對外暴露。此時可以zuul.ignored-services
屬性指定需要忽略的服務id。比如下面的配置指定了將忽略服務hello1和hello2。
zuul.ignored-services=hello1,hello2
也可以像如下這樣忽略所有的服務,然後再通過routes指定需要對外暴露的映射信息,如下就指定了需要對外暴露hello服務,且對應的映射路徑是/hello/**
。
zuul.ignored-services=*
zuul.routes.hello=/hello/**
自動重試
有時候可能由於網絡波動等原因,Zuul在轉發請求到後端服務會失敗。Zuul可以設置在轉發請求到後端服務失敗時自動發起重試。使用這種自動重試機制需要先在pom.xml中引入spring retry依賴。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
然後在application.properties文件中配置zuul.retryable=true
以啓用自動重試。這樣轉發請求失敗時默認將對GET請求發起重試,且默認在同一目標機器發起的重試次數是0,最多跨域一臺目標機器。即當調用的服務S同時有機器A、B、C提供服務的時候,如果第一次調用的是機器A,失敗後不會再調用A,會轉而調用B或C一次。可以通過ribbon.OkToRetryOnAllOperations=true
指定對所有請求類型都可以進行重試,不管是GET還是POST,還是其它。可以通過ribbon.MaxAutoRetries
指定在同一機器上的最大重試次數。可以通過ribbon.MaxAutoRetriesNextServer
指定最多重試的機器數。比如當擁有下面配置時,如果我們請求的hello服務同時有機器A、B、C提供服務,第一次調用A如果失敗了,會在A繼續重試兩次,如果重試了兩次都沒成功,就會轉而重試B,B一共最多重試3次,第一次不算重試,最終如果還是失敗的,那C也是一樣的重試。還可以通過ribbon.retryableStatusCodes
來指定需要進行重試的Http狀態碼,比如只希望在狀態碼爲500或502時進行重試,則配置ribbon.retryableStatusCodes=500,502
。默認情況只要服務器通訊正常都不會重試,即狀態碼不管是404還是502等都不會發起重試,只有建立連接失敗或者請求超時會重試。所以如果我們需要在狀態碼爲502的時候也能發起重試則需要指定retryableStatusCodes。
zuul.retryable=true
ribbon.OkToRetryOnAllOperations=true
hello.ribbon.MaxAutoRetries=2
ribbon.MaxAutoRetriesNextServer=2
使用
ribbon.xxx
配置的是對所有服務都通用的配置,使用<serviceId>.ribbon.xxx
配置的是對特定服務的配置,如上面的hello.ribbon.MaxAutoRetries
。
也可以通過zuul.routes.routename.retryable
來單獨控制某個服務是否允許重試。比如單獨指定可以對hello服務進行重試則可以配置zuul.routes.hello.retryable=true
。如果全局的zuul.retryable=true
,則也可以通過zuul.routes.hello.retryable=false
指定hello服務不重試。
參考文檔
- http://cloud.spring.io/spring-cloud-static/Finchley.SR1/multi/multi__router_and_filter_zuul.html
- https://github.com/Netflix/zuul/wiki/How-it-Works
(注:本文是基於Spring cloud Finchley.SR1所寫)