Zuul 路由使用

注意

zuul 可以实现对内的路由,即所有服务之间互相访问的时候,都通过 zuul 来做。但是这不是正确的用法。zuul 接受的请求应该全部来自于外部,主要指前端(浏览器/app)。内部服务互相调用只需要通过 Eureka Server 进行服务发现和调用即可。

启动 zuul 服务

使用 Spring Cloud 来启动 zuul 非常简单,通过引入 spring-cloud-starter-zuul 并提供注解 EnableZuulProxy 即可。

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zuul</artifactId>
		</dependency>
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulApplication.class, args);
	}

	@Bean
    public SimpleFilter simpleFilter() {
	    return new SimpleFilter();
    }
}

路由配置

zuul 的路由策略默认通过配置文件实现,也可以通过插件读取配置中心或者数据库内的配置。下面都以配置文件为实例。

不使用 Eureka

当 zuul 不配合 Eureka 服务发现服务的时候,Zuul 的路由就要基于 URL 去路由。比如下面的配置:

spring:
  application:
    name: api-gateway

server:
  port: 5555

zuul:
  routes:
    user-service:
      path: /myusers/**
      url: http://127.0.0.1:4444

每当用户访问 zuul 的/myusers 为前缀的路径时,zuul 就会将请求转至127.0.0.1:4444

使用 Eureka

基于 URL 去配置有一个问题,就是当服务本身被调度到其他节点时,Zuul 无法感知。利用 Eureka 注册服务的能力,如果 Zuul 也能去 Eureka Server 获取服务的地址并且基于所有服务的实例进行轮询/熔断/重试,那就更好了。当然可以,因为 Zuul 和 Eureka 都是 Netflix 开发的,它们之间可以配合。

首先,为 Zuul 使用 Eureka 做一些额外配置,包括添加 spring-cloud-starter-euraka 依赖,以及为 Zuul 应用配置 Eureka 相关的注解。

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-eureka</artifactId>
		</dependency>
@EnableZuulProxy
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
public class ZuulApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulApplication.class, args);
	}

	@Bean
    public SimpleFilter simpleFilter() {
	    return new SimpleFilter();
    }
}

EnableDiscoveryClient,是为了让 Zuul 能够访问 Eureka Server 并注册自身,EnableCircuitBreaker 则是提供能力,让 Zuul 的转发同时具备熔断能力。

现在我们在配置文件里面就不需要写死目标服务的地址了,而是可以用 serviceId 来代替。另外,还需要把Eureka Server 的地址告诉 Zuul。

spring:
  application:
    name: api-gateway

server:
  port: 5555

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1111/eureka/

zuul:
  routes:
    user-service: /myusers/**

路由负载均衡

Zuul 进行服务请求时,实际上是使用 Ribbon 来实现。Ribbon 我们前面说过,它就是为了服务负载均衡的。

后台起两个 user 服务。 image

再通过 Zuul 进行访问,可以看到后台两个服务被轮询调用。

image

路由熔断

试着用 Hystrix 为路由加上熔断功能。在Spring Cloud Camden.SR1版本之前(包括这个版本),Zuul 并没有提供熔断功能,在这个版本后面,我们就可以通过写自定义的 fallback 方法,并且将其指定给某个 route 来实现该 route 访问出问题的熔断处理。

所以接口应该包括两部分:

  1. 此熔断是处理哪个 route 的。
  2. 熔断的返回什么。

这个接口就是 ZuulFallbackProvider。

public interface ZuulFallbackProvider {

	/**
	 * The route this fallback will be used for.
	 * @return The route the fallback will be used for.
	 */
	public String getRoute();

	/**
	 * Provides a fallback response.
	 * @return The fallback response.
	 */
	public ClientHttpResponse fallbackResponse();
}

实现类通过实现 getRoute 方法,告诉 Zuul 它是负责哪个 route 定义的熔断。而 fallbackResponse 方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。

比如为了 user 服务,写一个如下的断路器。

@Component
public class UserServiceFallback implements ZuulFallbackProvider {

    //指定要处理的 service。
    @Override
    public String getRoute(){
        return "user-service";
    }

    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("Cached user info".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }

关闭 user 服务两个实例中的一个,就会发现熔断被触发了。 image

这里处理的粒度其实跟之前服务调用的 Feign 或者 Hystrix 是不一样的。之前处理的是某个方法,比如 getUsers,而这里处理的是 route。一般来说,一个服务一个 route(不同服务不同 context),一个服务多个方法(CRUD)。所以如果要这么用的话,其实对于一个服务来说,所有的方法都是同一个 fallback 了。如果想要实现细粒度的 fallback,就必须针对服务的方法去做 route。然而:Zuul 不支持 serviceId + URL 的混合形式。即无法为某个服务的某个接口做单独的 route。下面这种 route 配置是不生效的:

spring:
  application:
    name: api-gateway

server:
  port: 5555

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1111/eureka/

zuul:
  routes:
    user-service/api/users: /myusers

这种情况下,就只能做 url 方式的配置。或者,不要让 Zuul 自动帮你配置 Proxy,而是只启动 ZuulServer,所有的 filter 自己来实现。在这种时候,Zuul 只能帮你提供固定类型的配置,即 zuul.routes,没有 path、serviceid、url这几个参数。自定义 filter 可以读取参数来决定如何发现服务,如何组装 Ribbon client,如何选择断路器……

路由重试

Bixton 以后的 Zuul 必须要依赖 Spring Retry 来实现重试,否则就要自己实现一个带 Retry 逻辑的 HttpClient 给 Zuul 用。

开启 retry 非常简单,在依赖里面加入 spring-retry,然后配置项加入zuul.retryable=true就可以开启retry。任何一个服务实例关掉路由都会立刻转向另一个。

当然开启重试在某些情况下是有问题的,比如当压力过大,一个实例停止响应时,路由将流量转到另一个实例,很有可能导致最终所有的实例全被压垮。说到底,断路器的其中一个作用就是防止故障或者压力扩散。用了 retry,断路器就只有在该服务的所有实例都无法运作的情况下才能起作用。这种时候,断路器的形式更像是提供一种友好的错误信息,或者假装服务正常运行的假象给使用者。

不用 retry,仅使用负载均衡和熔断,就必须考虑到是否能够接受单个服务实例关闭和 eureka 刷新服务列表之间带来的短时间的熔断。如果可以接受,就无需使用 retry。

Failover

Zuul 本身并没有 Eureka Server 那样的集群模式。对于 Zuul 的高可用,需要像对待普通应用一样,进行额外的工作。分两种模式吧。

多个 Zuul 注册 Eureka

由于 Zuul 配合 Eureka 时,不支持 serviceId+URL 的拼写模式,所以对于内部的服务调用,通过 Zuul 进行路由的意义不大,同样都需要提供 ServiceId,同样需要指定对应接口的 URL,只不过 serviceId 从目标服务的,换成了 Zuul 的。并且还需要知道该服务在 Zuul 上面注册的路由的路径。

原本目标服务需要知道的信息: TargetServiceId + /ApiURI/

现在目标服务需要知道的信息: ZuulServiceId + 'RoutingContext' + /ApiURI/

Zuul 所能做的就是把你的请求转给该服务而已。当然,这种情况下 Zuul 可以统一配置断路器、负载均衡以及重试,但是这种逻辑放到调用方会更好,Zuul 做的会比较通用,特别是断路器的逻辑,粒度不如服务直接使用 hystrix 或者 feign 来的细。

多个 Zuul 配合 Haproxy 或者其他 LB

这种方式是比较推荐的,相当于 Zuul 只对外部暴露(内部没有做服务发现,也可以通过这里进来)。也就是说无论是否使用 Eureka,都推荐用 Haproxy 这种 LB 来为 Zuul 服务进行故障转移和负载均衡。

路由实现代码

调用的顺序大致是 SpringMVC -> ZuulControler -> ZuulServlet -> service(route+filter) -> ZuulRunner ,后面再具体分析。