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..stripPrefix=false
,对于hello服务来说就是zuul.routes.hello.stripPrefix=false
。此时除了加上这个外,hello服务的路径信息也需要通过zuul.routes..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...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服务不重试。