注意
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 服务。
再通过 Zuul 进行访问,可以看到后台两个服务被轮询调用。
路由熔断
试着用 Hystrix 为路由加上熔断功能。在Spring Cloud Camden.SR1版本之前(包括这个版本),Zuul 并没有提供熔断功能,在这个版本后面,我们就可以通过写自定义的 fallback 方法,并且将其指定给某个 route 来实现该 route 访问出问题的熔断处理。
所以接口应该包括两部分:
- 此熔断是处理哪个 route 的。
- 熔断的返回什么。
这个接口就是 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 服务两个实例中的一个,就会发现熔断被触发了。
这里处理的粒度其实跟之前服务调用的 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 ,后面再具体分析。