Zuul简介

spring cloud官方文档地址:http://cloud.spring.io/spring-cloud-static/Edgware.SR3/multi/multi__router_and_filter_zuul.html

路由是微服务架构的组成部分。 例如,/可以映射到您的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的依赖:

1
2
3
4
5
6
7
8
<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

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication
{
    public static void main( String[] args )
    {
        SpringApplication.run(ZuulApplication.class,args);
    }
}

3.application.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
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获取用户信息的接口。

浏览器请求http://10.41.3.149:8808/microservice-springcloud-user/sample/1

可以看到,请求成功。
我们看Zuul模块的控制台日志,可以看到下面的日志:

1
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增加下面的配置:

1
2
3
zuul:
  routes:
    microservice-springcloud-user: /user/**

Zuul忽略某些服务

比如有user和movie2个服务,但我只想Zuul代理user服务。

1
2
3
4
zuul:
  ignoredServices: '*'
  routes:
    microservice-springcloud-user: /user/**

ignoredServices:*忽略所有的服务,然后在routes中指定了user,所以最终就是Zuul代理user服务。

或者:

1
2
3
4
5
zuul:
  # 多个服务id之间用逗号分隔
  ignoredServices: microservice-springcloud-movie
  routes:
    microservice-springcloud-user: /user/**

Zuul指定path和serviceId

1
2
3
4
5
6
7
8
zuul:
  routes:
    # 下面的user1只是一个标识,保证唯一即可
    user1:
      # 映射的路径
      path: /user/**
      # 服务id
      serviceId: microservice-springcloud-user

上面的配置意思是:让Zuul代理microservice-springcloud-user,路径为/user/**,user1可以随便写,只要保证唯一即可。

然后通过http://10.41.3.149:8808/user/sample/1请求即可。

Zuul指定path+url

除了上面说的指定path+serviceId外,还可以使用path+url的配置。

1
2
3
4
5
6
zuul:
  routes:
    user1:
      path: /user/**
      # url为user服务的url
      url: http://10.41.3.149:7902

然后通过http://10.41.3.149:8808/user/sample/1请求即可。

Zuul指定可用服务的负载均衡

在spring cloud的文档中有写,如果使用上面的path+url配置,不会作为HystrixCommand执行,也不会使用Ribbon对多个URL进行负载均衡。 要实现此目的,您可以使用静态服务器列表指定serviceId。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。

仍然是通过http://localhost:8808/user/sample/1来访问。访问多次,可以在控制台看到SQL打印,是轮询调用7901和7902的。

Zuul使用正则表达式指定路由规则

您可以使用regexmapper在serviceId和路由之间提供约定。 它使用名为groups的正则表达式从serviceId中提取变量并将它们注入路由模式。

将user服务id修改为

1
2
3
spring:
  application:
    name: microservice-springcloud-user-v1

zuul模块application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
  application:
    name: microservice-gateway-zuul

server:
  port: 8808

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

Zuul主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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增加

1
2
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来访问,可以这样做:

1
2
3
4
5
zuul:
  routes:
    microservice-springcloud-user:
      path: /user/**
      stripPrefix: false

或者:

1
2
3
4
5
6
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可以指定忽略某些服务,这是比较粗粒度的控制。如果想细粒度的控制忽略某些路径,可以通过下面的方式:

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

这意味着所有诸如“/myusers/101”之类的请求将被转发到“users”服务上的“/101”。 但包括“/admin/”在内的请求则不会处理。

Zuul指定路由的顺序

1
2
3
4
5
6
7
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在它们与代理之间流动之前,请仔细考虑它的含义。

可以将敏感报头配置为每个路由的逗号分隔列表,例如,

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中,用户无法控制Headers,所有Cookie都在所有方向上流动)。

sensitiveHeaders是黑名单,默认不为空,因此要使Zuul发送所有Headers(“忽略”的Headers除外),您必须将其明确设置为空列表。 如果要将cookie或授权Headers传递给后端,则必须执行此操作。 例:

1
2
3
4
5
6
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依赖:

1
2
3
4
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-actuator</artifactId>  
</dependency>

再把安全验证关闭,让我们可以访问到这个端点:

1
2
3
management:
  security:
    enabled: false

这里,遗留请求的路由规则会影响到我们访问这个端点,先注释掉这个路由规则:

1
2
#legacy:
  #path: /**

重启Zuul项目,我们便能看到zuul网关的路由规则了

如果想知道路由的详细细节,可以增加参数?format=details

Strangulation Patterns (绞杀者模式)

迁移现有应用程序或API时的常见模式是“扼杀”旧的端点,慢慢地用不同的实现替换它们。 Zuul代理是一个有用的工具,因为可以使用它来处理来自旧端点的客户端的所有流量,但重定向一些请求到新的端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
  port: 9999

spring:
  application:
    name: microservice-file-upload

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

上传文件的Controller

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
@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测试。

1
curl -F "file=@d:/luckystar88/books/java_bloomfilter.rar" http://localhost:9999/file/upload

可以看到,请求成功。

刚刚上传的文件大小14Kb,我们上传一个大点的文件(文件大小18.9M)。

出错,看错误信息,提示文件大小19M,超过了配置的最大大小10M。

解决办法:

1
2
3
4
5
6
7
8
9
spring:
  application:
    name: microservice-file-upload
  http:
    multipart:
      # 单个文件大小
      max-file-size: 1024Mb
      # 总上传数据的大小
      max-request-size: 2048Mb

配置上面2项设置文件大小即可。

重新上传:

现在我们使用Zuul测试。

修改Zuul的application.yml

1
2
3
4
zuul:
  routes:
    microservice-file-upload:
      path: /upload-api/**

将/upload-api/**的请求交给microservice-file-upload处理。

启动Eureka Server,Zuul和file-upload模块。

1
curl -F "file=@d:/360极速浏览器下载/111.mp4" http://localhost:8808/zuul/upload-api/file/upload

可以看到上传成功。

我们准备一个大点的文件(175M)测试下上传超时。

1
curl -F "file=@C:\Users\Administrator\Downloads\Spring+Cloud微服务实战.pdf" http://localhost:8808/zuul/upload-api/file/upload

可以看到Zuul报超时了。

解决办法:
在Zuul增加配置:

1
2
3
4
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

重新请求,可以看到上传成功了(文件名乱码就暂时不管了)。

禁用Zuul Filters

默认会使用很多filters,可采用如下方式禁止

1
zuul.SendResponseFilter.post.disable=true

Zuul的回退

当Zuul中给定路径的电路跳闸时,您可以通过创建ZuulFallbackProvider类型的bean来提供回退响应。 在此bean中,您需要指定回退所针对的路由ID,并提供ClientHttpResponse作为回退返回。

我们创建一个模块microservice-gateway-zuul-fallback。
application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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主类:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableZuulProxy
public class ZuulFallbackApplication
{
    public static void main( String[] args )
    {
        SpringApplication.run(ZuulFallbackApplication.class,args);
    }
}

回退类:

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
@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。

比如:

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
@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。

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
46
47
48
49
50
51
52
53
54
55
56
57
@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;
            }
        };
    }
}