spring cloud 之zuul使用详解

spring cloud 之zuul使用详解,从零开始搭建zuul,进行负载均衡与使用过滤器

Posted by yishuifengxiao on 2019-08-15

路由是微服务体系结构的一个组成部分。例如,/可以映射到您的 Web 应用程序,/api/users映射到用户服务,并将/api/shop映射到商店服务。Zuul 是 Netflix 的基于 JVM 的路由器和服务器端负载均衡器。

一 简单使用

1.1 快速启动

1 加入相关依赖

新建一个 spring cloud 工程,在项目的 pom 文件里加入以下依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2 设置配置文件

1
2
3
4
5
server:
port: 8769
spring:
application:
name: service-zuul

上述配置是将 zuul 也作为一个微服务注册到 eureka 注册中心中。

Zuul 启动器不包括发现客户端,因此对于基于服务 ID 的路由,您还需要在类路径中提供其中一个路由(例如 Eureka)。

3 新建启动类

在项目中的任意一个@Configuration 注解类的下面添加上 @EnableZuulProxy 注解即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
@EnableZuulProxy
public class ZuulApp extends SpringBootServletInitializer {
private final static Logger LOG = LoggerFactory.getLogger(ZuulApp.class);

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {

return builder.sources(ZuulApp.class);
}

public static void main(String[] args) {
try {
SpringApplication.run(ZuulApp.class, args);
} catch (Exception e) {
// e.printStackTrace();
}
LOG.debug("=================================== 启动成功 start");
}
}

在默认情况下,zuul 能够自动添加路由,启动上述程序后,zuul 即可进行请求转发了。

1.2 路由映射

1.2.1 传统路由方式

在项目的配置文件中增加以下配置:

1
2
zuul.routes.service-a.path=/service-a/**
zuul.routes.service-a.url=http://localhost:8080/

改配置定义了发往 API 网关服务的请求中,所有符合 /service-a/**规则的访问都被路由转发到http://localhost:8080/的地址上。也就是说,当我们访问 http://localhost:9000/service-a/aa的时候,请求会被转发到 http://localhost:8080/aa提供的服务上。其中,配置属性zuul.routes.service-a.path中的service-a部分为路由的名字,可以任意定义,但是一组 pathurl映射关系的路由名要相同。

1.2.2 面向服务的路由

1 增加依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>

2 增加配置属性

1
2
3
4
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

3 修改启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableFeignClients
@EnableZuulProxy
public class ZuulApp extends SpringBootServletInitializer {
private final static Logger LOG = LoggerFactory.getLogger(ZuulApp.class);

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {

return builder.sources(ZuulApp.class);
}

public static void main(String[] args) {
try {
SpringApplication.run(ZuulApp.class, args);
} catch (Exception e) {
// e.printStackTrace();
}
LOG.debug("=================================== 启动成功 start");
}
}

在默认情况下,zuul 会将 /service-a/**的请求转发到服务名为 service-a的注册服务的服务接口中,将 /service-b/**的请求转发到服务名为 service-b的注册服务的服务接口中。

要跳过自动添加的服务,请将zuul.ignored-services设置为服务 ID 模式列表。 如果服务与忽略但仍包含在显式配置的路由映射中的模式匹配,则它是不带号的,如以下示例所示:

1
2
3
4
zuul:
ignoredServices: '*'
routes:
users: /myusers/**

在此示例中,除 users之外 之外,所有服务都被忽略。

要扩充或更改代理路由,可以添加如下所示的外部配置:

1
2
3
zuul:
routes:
users: /myusers/**

这意味着对/myusers的 http 请求转发到users服务(例如/myusers/101转发到users服务中的/101)。

要获得对路由的更细粒度的控制,您可以独立地指定路径和 serviceId:

1
2
3
4
5
zuul:
routes:
users:
path: /myusers/**
serviceId: users_service

前面的示例意味着对/myusers的 HTTP 调用将转发到users_service服务。 路径必须具有可以指定为 ant 样式模式的路径,因此/myusers/*仅匹配一个级别,但/myusers/**是分层匹配的。

后端的位置可以指定为 serviceId(用于发现服务)或 url(用于物理位置),如以下示例所示:

1
2
3
4
5
zuul:
routes:
users:
path: /myusers/**
url: https://example.com/users_service

要为所有映射添加前缀,请将zuul.prefix设置为值,例如/api。 默认情况下,在转发请求之前,会从请求中删除代理前缀(您可以使用zuul.stripPrefix = false关闭此行为)。 您还可以关闭从各个路由中剥离特定于服务的前缀,如以下示例所示:

1
2
3
4
5
zuul:
routes:
users:
path: /myusers/**
stripPrefix: false

zuul.stripPrefix仅适用于zuul.prefix中设置的前缀。它对给定路由 path 中定义的前缀有影响。

在本示例中,对/myusers/101的请求将转发到users服务的/myusers/101上。

zuul.routes条目实际上绑定到类型为ZuulProperties的对象。如果您查看该对象的属性,您将看到它还具有“可重试”标志。将该标志设置为true使 Ribbon 客户端自动重试失败的请求(如果需要,可以使用 Ribbon 客户端配置修改重试操作的参数)。

默认情况下,将X-Forwarded-Host标头添加到转发的请求中。zuul.addProxyHeaders = false可关闭此行为。默认情况下,前缀路径被删除,对后端的请求会拾取一个标题X-Forwarded-Prefix(上述示例中的/myusers)。

如果您设置默认路由(/),则@EnableZuulProxy的应用程序可以作为独立服务器,例如zuul.route.home: /将路由所有流量(即/**)到home服务。

如果需要更细粒度的忽略,可以指定要忽略的特定模式。在路由位置处理开始时评估这些模式,这意味着前缀应包含在模式中以保证匹配。忽略的模式跨越所有服务,并取代任何其他路由规范。

1
2
3
4
zuul:
ignoredPatterns: /**/admin/**
routes:
users: /myusers/**

这意味着诸如/myusers/101的所有请求将被转发到users服务上的/101。但是包含/admin /的请求除外。

如果您需要路由保留配置顺序,则需要使用 YAML 文件,因为使用属性文件时排序会丢失。 以下示例显示了这样的 YAML 文件

默认情况下,Spring Cloud Zuul 在请求路由时,会过滤掉 HTTP 请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器,默认的敏感头信息通过zuul.sensitiveHeaders参数定义。包括CookieSet-CookieAuthortzation三个属性。

所以,我们在开发 Web 项目时常用的Cookie在 Spring Cloud Zuul 网关中默认是不会传递的,这就会引发一个常见的问题: 如果我们要将使用了 Spring Security、 Shiro 等安全框架构建的 Web 应用通过 Spring Cloud Zuul 构建的网关来进行路由时,由于 Cookie 信息无法传递,我们的 Web 应用将无法实现登录和签权。为了解决这个问题,配置的方法有很多。

  1. 通过设置全局参数为空来覆盖默认值,具体如下:
1
zuul.sensitiveHeaders=
  1. 通过指定路由的参数来配置
1
2
3
4
# 方法一:对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders=true
# 方法二 : 将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=

可以将敏感请求头配置为每个路由的逗号分隔列表,如以下示例所示:

1
2
3
4
5
6
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
url: https://downstream

这是sensitiveHeaders的默认值,因此您不需要设置它,除非您希望它不同。注意这是 Spring Cloud Netflix 1.1 中的新功能(1.0 中,用户无法控制标题,所有 Cookie 都在两个方向上流动)。

sensitiveHeaders是一个黑名单,默认值不为空,所以要使 Zuul 发送所有标题(“被忽略”除外),您必须将其显式设置为空列表。如果您要将 Cookie 或授权标头传递到后端,这是必要的。例:

1
2
3
4
5
6
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream

也可以通过设置zuul.sensitiveHeaders来全局设置敏感标题。如果在路由上设置sensitiveHeaders,则将覆盖全局sensitiveHeaders设置。

忽略请求头

除路由敏感标头外,您还可以为与下游服务交互期间应丢弃的值(请求和响应)设置名为zuul.ignoredHeaders的全局值。 默认情况下,如果 Spring Security 不在类路径中,则它们为空。 否则,它们被初始化为一组众所周知的“安全”头文件(例如,涉及缓存),如 Spring Security 所指定的那样。

在这种情况下的假设是下游服务也可能添加这些头,但我们想要代理的值。 要在 Spring Security 位于类路径上时不丢弃这些众所周知的安全标头,可以将zuul.ignoreSecurityHeaders设置为false。 如果您在 Spring Security 中禁用了 HTTP 安全响应标头并希望下游服务提供的值,那么这样做会非常有用。

下面是一个示例配置

1
2
3
4
5
6
7
zuul:
addHostHeader: true #zuul传递请求头
sensitive-headers: Access-Control-Allow-Origin #跨域头不传递到后面的服务
host:
connect-timeout-millis: 126000
max-per-route-connections: 126000
socket-timeout-millis: 126000

三 路线端点

默认情况下,如果将@EnableZuulProxySpring Boot Actuator一起使用,则启用另外两个端点:

路由端点

通过 GET请求访问 /routes时可以获取到所有的路由列表

1
2
3
{
/stores/**: "http://localhost:8081"
}

还可以通过将?format = details查询字符串添加到/routes来请求其他路由详细信息。 这样做会产生以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
{
"/stores/**": {
"id": "stores",
"fullPath": "/stores/**",
"location": "http://localhost:8081",
"path": "/**",
"prefix": "/stores",
"retryable": false,
"customSensitiveHeaders": false,
"prefixStripped": true
}
}

此外,/routesPOST请求强制刷新现有路由。

可以通过将endpoints.routes.enabled设置为false来禁用此端点。

过滤器端点

发送一个 GET 请求到 /filters可以获取到 zuul 中所有的过滤器列表。

如果使用@EnableZuulServer(而不是@EnableZuulProxy),您还可以运行 Zuul 服务器,而无需代理或有选择地切换代理平台的各个部分。 您添加到 ZuulFilter 类型的应用程序的任何 bean 都会自动安装(与@EnableZuulProxy一样),但不会自动添加任何代理过滤器。

Spring Cloud Netflix 安装了许多过滤器,具体取决于使用哪个注释来启用 Zuul。 @EnableZuulProxy@EnableZuulServer的超集。 换句话说,@ EnableZuulProxy包含@EnableZuulServer安装的所有过滤器。 “代理”中的其他过滤器启用路由功能。 如果你想要一个“空白”Zuul,你应该使用@EnableZuulServer

1
2
3
zuul:
routes:
api: /api/**

在这种情况下,仍然通过配置zuul.routes.*来指定进入 Zuul 服务器的路由,但是没有服务发现和代理。 因此,将忽略serviceIdurl设置。 以下示例将/ api / **中的所有路径映射到 Zuul 过滤器链

四 进阶配置

4.1 文件上传

如果您使用@EnableZuulProxy,您可以直接使用代理路径上传小文件。 对于大型文件,有一个替代路径/zuul/*绕过 Spring 中的 DispatcherServlet。 换句话说,如果你有zuul.routes.customers = /customers/**,那么你可以通过 POST 请求/zuul/customers/*上传大文件。 servlet 路径中的 /zuul可以通过zuul.servletPath进行自定义设置。

如果代理路由引导您完成功能区负载平衡器,则极大文件也需要提升超时设置,如以下示例所示:

1
2
3
4
5
# 设置API网关中路由转发请求的 HystrixCommand 执行超时时间,单位为毫秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000

4.2 全局回退

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
// 表明是为哪个微服务提供回退,*表示为所有微服务提供回退
return "*";
}

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
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;
}
};
}
}

4.3 zuul 获取上下文

Zuul 是通过 Servlet 实现的。 对于一般情况,Zuul 嵌入到 Spring Dispatch 机制中。 这让 Spring MVC 可以控制路由。 在这种情况下,Zuul 缓冲请求。 如果需要在没有缓冲请求的情况下通过 Zuul(例如,对于大型文件上载),Servlet 也会安装在 Spring Dispatcher 之外。 默认情况下,servlet 的地址为/zuul。 可以使用zuul.servlet-path属性更改此路径。

为了在过滤器之间传递信息,Zuul 使用RequestContext。 它的数据保存在特定于每个请求的ThreadLocal中。 有关在何处路由请求,错误以及实际的HttpServletRequestHttpServletResponse的信息都存储在那里。 RequestContext扩展了ConcurrentHashMap,因此任何东西都可以存储在上下文中。 FilterConstants包含 Spring Cloud Netflix 安装的过滤器使用的密钥。

例如

1
2
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();

4.4 zuul 过滤器

4.4.1 @EnableZuulServer 支持的过滤器

@EnableZuulServer用于从 Spring Boot 配置文件加载的路由定义创建一个SimpleRouteLocator

默认支持的过滤器有:

前置过滤器

  • ServletDetectionFilter:执行顺序为-3,检测当前请求是否通过 Spring 的DispatcherServlet还是通过ZuulServlert来处理运行的,它的检测结果保存在FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的参数中。

一般情况下,发送到网关的外部请求都会被DispatcherServlet处理,除了/zuul/*的请求会绕过DispatcherServlet,直接使用ZuulServlert处理,主要用于处理大文件上传的情况。可以通过 RequestUtils 工具获取访问源头

  • FormBodyWrapperFilter:执行顺序为-1,解析表单数据并为下游请求重新编码,将符合要求的请求体包装成 FormBodyWrapperWrapper 对象。
  • DebugFilter:执行顺序为 1,如果设置了debug请求参数,则将RequestContext.setDebugRouting()RequestContext.setDebugRequest()设置为true.*路由过滤器

  • SendForwardFilter:执行顺序为 500,使用Servlet RequestDispatcher转发请求。转发位置存储在RequestContext中,获取值的键为FilterConstants.FORWARD_TO_KEY

后置过滤器

  • SendResponseFilter:执行顺序为 1000 ,将代理请求的响应写入当前响应。

错误过滤器

  • SendErrorFilter:执行顺序为 0,如果RequestContext.getThrowable()不为null,则转发到/ error(默认情况下)。您可以通过设置error.path属性来更改默认转发路径(/ error)。

如果在 Zuul 过滤器生命周期的任何部分期间抛出异常,则执行错误过滤器。 仅当RequestContext.getThrowable()不为null时,才会运行SendErrorFilter。 然后,它在请求中设置特定的javax.servlet.error.*属性,并将请求转发到 Spring Boot 错误页面。

4.4.2 @EnableZuulProxy支持的过滤器

创建DiscoveryClientRouteLocator,用于从DiscoveryClient(例如Eureka)以及属性加载路径定义。 为DiscoveryClient中的每个serviceId创建一个路由。 添加新服务后,将刷新路由。

除了前面描述的过滤器之外,还支持以下过滤器(与普通的 Spring Bean 一样):

前置过滤器

  • PreDecorationFilter:根据提供的RouteLocator确定路由的位置和方式。 它还为下游请求设置各种与代理相关的请求头。

路由过滤器

  • RibbonRoutingFilter:执行顺序为 10,使用RibbonHystrix和可插入 HTTP 客户端发送请求。 服务 ID 位于RequestContext属性FilterConstants.SERVICE_ID_KEY中。 此过滤器可以使用不同的 HTTP 客户端:
  • Apache HttpClient:默认客户端。
  • Squareup OkHttpClient v3:通过在类路径上设置com.squareup.okhttp3:okhttp库并设置ribbon.okhttp.enabled = true来启用。
  • Netflix Ribbon HTTP客户端:通过设置ribbon.restclient.enabled = true启用。 此客户端具有限制,包括它不支持 PATCH 方法,但它也具有内置重试。
  • SimpleHostRoutingFilter:通过 Apache HttpClient 向预定 URL 发送请求。 URL 位于RequestContext.getRouteHost()中。
4.4.3 自定义过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class QueryParamPreFilter extends ZuulFilter {
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}

@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("sample") != null) {
// put the serviceId in `RequestContext`
ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
}
HttpServletResponse servletResponse = context.getResponse();
servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
return null;
}
}

前面的过滤器从样本请求参数填充SERVICE_ID_KEY。 在实践中,您不应该进行这种直接映射。 相反,应该从样本的值中查找服务 ID。

现在已填充SERVICE_ID_KEYPreDecorationFilter不会运行并且RibbonRoutingFilter会运行。

4.5 跨域支持

默认情况下,Zuul 将所有跨源请求(CORS)路由到服务。 如果你想要 Zuul 来处理这些请求,可以通过提供自定义WebMvcConfigurer bean 来完成:

1
2
3
4
5
6
7
8
9
10
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/path-1/**")
.allowedOrigins("https://allowed-origin.com")
.allowedMethods("GET", "POST");
}
};
}

在上面的示例中,我们允许来自https://allowed-origin.com的 GET 和 POST 方法将跨源请求发送到以 path-1 开头的端点。 您可以使用/ **映射将 CORS 配置应用于特定路径模式或全局应用于整个应用程序。 您可以通过此配置自定义属性:allowedOriginsallowedMethodsallowedHeadersexposedHeadersallowCredentialsmaxAge

注意 ,如果微服务里也设置了允许跨域,需要在 zuul 增加配置

1
zuul.sensitive-headers=Access-Control-Allow-Origin

即将跨域设置不传递到后面的微服务中。