Spring Cloud Zuul 构建微服务网关

为什么要有服务网关?在使用微服务架构时,一个客户端的业务需求可能会调用多个服务的接口,例如一次购物,需要调用商品服务,下单的时候要调用订单服务(服务划分更细的话还要调用更多的微服务)。客户端直接跟各个服务通信会有一些问题或者说弊端,客户端的调用会复杂,更重要的是会有跨域请求问题和复杂的权限控制认证

为了对外服务的安全性,不得不在原有的服务接口上做有关权限控制的校验逻辑,而这些权限相关的逻辑应该要把它从各个服务中抽离出来,作为外部调用和各个服务之间的负载均衡器。服务网关就是这样一个角色,对外统一 Rest API 接口,对内服务路由、负载均衡,同时还提供身份认证安全和监控功能

Zuul

Zuul 是 Netflix 开源的服务网关,它的核心就是一系类的过滤器,通过一系列的过滤器在请求的各个阶段进行处理,具体的路由器功能见 Spring Cloud Zuul 过滤器

准备

这里使用 Eureka Server 作为服务注册中心

eureka-server/.../application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

另外准备一个服务 product-service

product-service/.../application.yml
1
2
3
4
5
6
7
8
9
spring:
application:
name: product-service
server:
port: 8071
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

构建 Zuul 微服务网关

创建 Spring Boot 工程命名为 zuul-gateway 作为服务网关,添加如下依赖

pom.xml
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-netflix-eureka-client</artifactId>
</dependency>

在主类上添加注解 @EnableZuulProxy 开启 Zuul 代理,这个代理会使用 Ribbon 获取注册服务的实例,同时还整合了 Hystrix 实现容错,所有请求都会在 Hystrix 命令中执行。

GatewayZuulApplication.java
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayZuulApplication.class, args);
}
}
EnableZuulProxy.java
1
2
3
4
5
6
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

配置信息,添加 eureka 注册中心的地址

application.yml
1
2
3
4
5
6
7
8
9
spring:
application:
name: gateway-zuul
server:
port: 8090
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

到这里一个简单的服务网关就完成了,只是简单吧把网关服务注册到 Eureka,Spring Cloud Zuul 整合了 Eureka,会提供默认的服务路由功能,默认情况下,Zuul 会代理所有注册到 Eureka Server 上的微服务

gateway-zuul 的请求 /{serviceId}/** 会转发到对应注册在 Eureka Server 上对应 serviceId 的服务 /**
启动 eureka-server、product-service、zuul-gateway,然后访问 http://localhost:8090/product-service/product/1 ,该请求会被转发到 product-service 的 /product/1

另外想要跳过一些服务可以设置 zuul.ignoredServices,要获得对路由的更细粒度控制,可以单独指定路径和 serviceId

application.yml
1
2
3
4
5
6
zuul:
ignoredServices: '*'
routes:
product:
path: /product/**
serviceId: product-service

上面设置的是除 product 服务之外的服务都忽略,而访问 product-service 服务的路径为 /product/**

参考代码见:demo

Zuul 路由端点

@EnableZuulProxy 注解配合 Spring Boot Actuator,Zuul 会暴露额外的两个管理端点:RoutesFilters。分别是关于路由和过滤器的端点(过滤器的端点在这里介绍 Spring Cloud Zuul 过滤器

spring-cloud-starter-netflix-zuul 已经依赖了 spring-boot-starter-actuator,所以上面的工程已经包含了路由管理的功能。关于路由端点的路径为 /routes/routes/details

访问路径 http://localhost:8090/actuator/routes

1
2
3
{
"/product/**": "product-service"
}

访问路径 http://localhost:8090/actuator/routes/details,可以查看路由的详细信息

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

访问404是因为没有暴露端点,可以设置 management.endpoints.web.exposure.include: ‘*’

路由配置

默认情况下,Zuul 网关会代理所有注册到 Eureka Server 上的服务,但我们可以通过配置来让其只代理其中一部分的服务或者是自己控制 URL。Zuul 的路由配置非常的灵活

自定义访问路径。zuul.routes.{serviceId}={costomUrl}

1
2
3
zuul:
routes:
product-service: /product/**

忽略指定服务,多个用逗号隔开,忽略全部用 *

1
2
zuul:
ignored-services: product-service1,product-service2
1
2
3
4
5
zuul:
ignored-services: '*'
routes:
product-service: /product/**
# 忽略所有服务,只路由 product-service

同时指定微服务的 serviceId 和对应路径

1
2
3
4
5
6
zuul:
ignoredServices: '*'
routes:
product: # 只是一个路由名称
service-id: product-service
path: /product/**

同时指定地址和访问路径

1
2
3
4
5
6
zuul:
ignoredServices: '*'
routes:
product:
url: http://localhost:8081/
path: /product/**

添加路由前缀

1
2
3
4
5
6
zuul:
perfix: /api
strip-perfix: false
routes:
product-service: /product/**
# /api/product/** -> /api/**
1
2
3
4
5
zuul:
routes:
product-service: /product/**
strip-perfix: false
# /product/** -> /product/**

忽略某些路径

1
2
3
4
zuul:
ignoredPatterns: /**/admin/** #忽略所有包含 /admin/ 的路径
routes:
product-service: /product/**

Zuul 的容错和回退

在 Spring Cloud 中,Zuul 已经默认整合了 Hystrix,关于 Hystrix 介绍和监控面板可以见 Spring Cloud 断路器 Hystrix。我们使用上面的例子

  • 启动 eureka-serverproduct-servicezuul-gateway
  • 然后访问 http://localhost:8090/product/product/1
  • 访问 http://localhost:8090/actuator/hystrix.stream,可以获得 Hystrix 监控数据
  • 在 hystrix-dashboard 面板中输入地址 http://localhost:8090/actuator/hystrix.stream,可以看到如下信息

由上图可以看出,Zuul 的 Hystrix 监控粒度为服务,而不是接口。

接下来我们可以关闭服务 product-service,然后再访问接口 http://localhost:8090/product/product/1

为 Zuul 添加回退

为 Zuul 添加回退,需要实现 FallbackProvider 接口,需要指定回退用于的路由ID,并提供一个 ClientHttpResponse 作为回退返回

ProductFallbackProvider.java
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
public class ProductFallbackProvider implements FallbackProvider {

@Override
public String getRoute() {
return "product-service";
}

@Override
public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
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;
}
};
}
}

如果您想为所有路由提供默认的回退,您可以创建一个类型为 FallbackProvider 的bean,并让 getRoute 方法返回 *null

重启服务网关,访问接口 http://localhost:8090/product/product/1