Spring Cloud - Zuul服务网关

1.为什么要使用微服务网关

在前文内容的基础上,我们已经可以通过Spring Cloud集成的一系列组件(Eureka、Ribbon、Feign、Hystrix)构建初步的微服务系统了,但目前构建的微服务系统还不够完善。

1.1 客户端直接访问多个微服务

通常客户端(尤其是外部客户端,如手机APP)需要调用多个微服务才能完成一个业务需求,客户端需要与多个微服务通信,会存在多个问题。

  • 客户端请求不同的微服务,增加复杂度;
  • 可能存在跨域请求,增加处理难度;
  • 每个微服务都需要单独认证;
  • 微服务重构(如微服务合并或拆分)困难;
  • 需要保证客户端与所有微服务的网络连通性。
客户端直接访问微服务.png

1.2 使用微服务网关

使用微服务网关封装微服务应用,客户端仅跟网关交互,无需直接请求微服务接口,开发和维护都会得到简化。

  • 易监控,可以在网关收集监控数据,还可以集中推送至外部系统进行分析;
  • 易认证,可以统一在网关实现认证,各个微服务无须单独认证;
  • 简化网络要求,仅需要保证客户端与网关的连通性,及网关与各个微服务的连通性,微服务不需要暴露在公网之下。
客户端通过网关访问微服务.png

2.Zuul

Spring Cloud Netflix中的Zuul通过服务网关统一对外提供REST API,具备服务路由、均衡负载、权限控制等功能,使得服务集群主体能够具备更高的可复用性和可测试性,并且为微服务架构提供了前置的保护和控制能力。

2.1 默认Zuul网关

只需要简单几步,zuul就能集成feign构建服务网关,无需配置就具有默认的路由功能。

Step 1:在之前的spring-cloud-hystrix项目基础上稍作修改,使用Spring Initializr创建一个新的modulezuul-server
整个项目下的module结构如下

spring-cloud-demo/
├── eureka-client-consumer
├── eureka-client-provider
├── eureka-server
├── hystrix-dashboard
└── zuul-server

Step 2:添加eureka及zuul依赖

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Finchley.RELEASE'
    }
}

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

Step 3:修改Spring Boot启动类,添加@EnableEurekaClient及@EnableZuulProxy注解

@SpringBootApplication
@EnableZuulProxy
class ZuulApplication

fun main(args: Array<String>) {
    runApplication<ZuulApplication>(*args)
}

Step 4:在application.yml中添加相关属性

server:
  port: 8080

spring:
  application:
    name: zuul-server

eureka:
  client:
    service-url:
      defaultZone: http://localhost:10001/eureka/

# 以下超时时间必须设置(默认时间太短,会导致第一次请求超时)
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000
ribbon:
  ConnectTimeout: 10000
  ReadTimeout: 10000

Step 5:依次启动以下应用

eureka-server:10001
eureka-client-provider:20001
eureka-client-provider:20002
zuul-server:8080
hystrix-dashboard:9090

Step 6:测试验证

Caused by: com.netflix.client.ClientException: Load balancer does not have available server for client: eureka-client-provider

2.2 Zuul的基本功能

  • 路由规则
    默认情况下,Zuul会代理所有注册到Eureka Server的微服务,且使用路由规则:http://ZUUL_HOST:ZUUL_PORT/serviceId/**会被转发到serviceId对应的微服务

  • 负载均衡
    Zuul整合了Ribbon,实现负载均衡。

  • Hystrix容错与监控
    Zuul整合了Hystrix,实现与监控。

2.3 Zuul路由配置

2.3.1 查看路由

当Zuul与Spring Boot Actuator结合使用时,Zuul会暴露一个路由管理endpoint(名为/routes),我们可以通过该endpoint方便地查看Zuul的路由映射。

Step 1:添加Spring Boot Actuator依赖

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
    compile('org.springframework.boot:spring-boot-starter-actuator')
}

Step 2:禁用security(否则会无法访问/routes)

说明:也可以添加Spring Security的依赖,通过账号、密码访问/routes

management:
  security:
    enabled: false

Step 3:GET方式请求http://localhost:8080/routes,会返回如下结果

{"/eureka-client-provider/**":"eureka-client-provider"}

说明:POST方式请求/routes会刷新Zuul当前映射的路由列表

2.3.2 自定义路由
2.3.2.1 指定微服务的访问路径

如:指定通过/provider/**访问eureka-client-provider微服务下的所有请求。

zuul:
  routes:
    eureka-client-provider: /provider/**

还可以用如下形式指定(其中my-route只是一个自定义的路由名称,可以是其它名称)

zuul:
  routes:
    my-route:
      service-id: eureka-client-provider
      path: /provider/**
2.3.2.2 忽略指定微服务

如有多个微服务用,分隔,'*'可以忽略所有微服务。
如:设置Zuul网关不代理eureka-client-provider微服务。

zuul:
  ignored-services: eureka-client-provider
2.3.2.3 路由前缀

zuul.prefix可以为所有路由指定一个前缀,zuul.strip-prefix指定路由转发是是否移除前缀
如:将请求http://localhost:8080/common/provider/test路由到eureka-client-provider微服务的/test

zuul:
  routes:
    eureka-client-provider: /provider/**
  prefix: /common
  strip-prefix: true
2.3.2.4 忽略某些路径

Zuul可以忽略指定的微服务,还支持忽略指定的路径,实现更小粒度的路由控制。zuul.ignorePatterns可以以正则的形式指定忽略的路径。

zuul:
  routes:
    eureka-client-provider: /provider/**
  ignored-patterns: /**/test/**

如果希望跟踪Zuul的路由转发细节,可以将com.netflix包的日志级别设为DEBUG。

logging:
    level:
        com.netflix: debug

2.4 Zuul安全与Header

2.4.1 敏感Header

通常情况下,可以在同一个系统之间共享Header,不过应尽量防止一些敏感Header外泄。Zuul中可以设置敏感Header,这些header不会传播到下游微服务。

为指定微服务设置敏感Header

zuul:
    route:
        <指定微服务>:
            sensitive-headers: header1,header2,header3

设置全局敏感Header

zuul:
    sensitive-headers: header1,header2,header3
2.4.2 忽略Header

通过设置zuul.ignored-headers属性,可以丢弃一些header,这些header就不会传播到其它微服务。作用与上面的敏感Header差不多,实际上sensitive-headers会被添加到ignored-headers中。

zuul:
    ignored-headers: header1,header2,header3

zuul.ignored-headers属性默认值为空,但如果引入了Spring Security,zuul.ignored-headers属性默认值就是Pragma、Cache-Control、X-Frame-Options、X-Content-Type-Options、X-XSS-Protection、Expires,此时如果需要将这些Header传递到下游微服务可以设置zuul.ignoreSecurityHeaders=false

2.5 Zuul上传文件

通过Zuul上传小文件,无须任何特殊处理。而通过Zuul上传10M以上的大文件,则需要在上传路径前加上/zuul前缀,也可以使用zuul.servlet-path设置自定义前缀。

另外,spring.http.multipart.max-file-sizespring.http.multipart.max-request-size可以设置文件上传大小限制。

2.6 Zuul过滤器

2.6.1 过滤器类型

Zuul中有4种默认类型的过滤器:

  • PRE:请求路由前调用该过滤器;
  • ROUTING:该过滤器将请求路由到微服务;
  • POST:请求路由到微服务后执行该过滤器;
  • ERROR:发生错误时执行该过滤器。

除了默认的过滤类型,Zuul还允许创建自定义的过滤类型。

2.6.2 自定义过滤器

Step 1:创建自定义的过滤器类,继承ZuulFilter

public class MyFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";//可选值:pre、route、post、error
    }

    @Override
    public int filterOrder() {
        return 0;//返回int值指定过滤器的执行顺序,不同过滤器允许相同值
    }

    @Override
    public boolean shouldFilter() {
        return true;//返回boolean值判断是否执行该过滤器
    }

    @Override
    public Object run() {
        //过滤器的具体逻辑
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest httpServletRequest = requestContext.getRequest();
        System.out.println("===========Current Request:"+httpServletRequest.getMethod()+", Request Method:"+httpServletRequest.getRequestURL().toString());
        return null;
    }
}

Step 2:修改启动类,添加初始化对象

@Bean
public MyFilter myFilter(){
    return new MyFilter();
}

Step 3:重启zuul-server后,访问任意请求,都会执行run方法中的逻辑

===========Current Request:GET, Request Method:http://localhost:8080/common/provider/test
2.6.3 禁用过滤器

Spring Cloud默认为Zuul提供并启用了一些过滤器,如DebugFilter、FormBodyWrapperFilter、PreDecorationFilter等。如有特殊场景需要禁用部分过滤器,可以通过设置zuul.<ClassName>.<filterType>.disable=true禁用ClassName对应的过滤器。

如:禁用上面自定义的过滤器,仅需要设置zuul.MyFilter.pre.disable=true即可。

2.7 Zuul容错与回退

Zuul集成了Hystrix,可以实现服务容错处理。但我们还记得Spring Cloud Hystrix的回退处理是在客户端实现的,现在请求经过Zuul网关,要实现回退处理的话也需要在Zuul网关完成。

Zuul网关以微服务为粒度,为该微服务的所有请求做回退处理。

Zuul为我们提供了FallbackProvider接口,实现该接口,构建自定义的响应即可。

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public ClientHttpResponse fallbackResponse(Throwable cause) {
        return this.fallbackResponse();
    }

    @Override
    public String getRoute() {
        // 指定为哪个微服务提供回退,* 可以为所有微服务提供回退
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return this.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("{\"ret\":1,\"msg\":\"服务不可用\"}".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-8"));
                headers.setContentType(mediaType);
                return headers;
            }
        };
    }
}

重启zuul-server后,停止eureka-client-provider的所有服务,再次访问http://localhost:8080/common/provider/test,会返回我们自定义的响应结果{"ret":1,"msg":"服务不可用"}

2.8 Zuul高可用

2.8.1 客户端注册到Eureka Server

如果客户端也注册到Eureka Server上,那么请求至Zuul网关就跟请求至其它微服务一样,支持Ribbon负载均衡。

客户端注册到Eureka Server.png
2.8.2 客户端未注册到Eureka Server

实际上,更多的场景是这种情况,通常更多的是外部系统(如:手机APP、前端页面)访问微服务网关,不可能让这些系统也注册到Eureka Server。这种情况下,可以使用额外的负载均衡器(如:Nginx、HAProxy、F5等)。

客户端未注册到Eureka Server.png

3.示例代码

git clone https://github.com/yuanzicheng/spring-cloud-zuul.git

推荐阅读更多精彩内容