API Gateway

API Gateway是微服务架构中不可或缺的部分:http://dockone.io/article/482

API Gateway是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的Facade模式很像。API Gateway封装内部系统的架构,并且提供API给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等。

API Gateway负责请求转发、合成和协议转换。所有来自客户端的请求都要先经过API Gateway,然后路由这些请求到对应的微服务。API Gateway将经常通过调用多个微服务来处理一个请求以及聚合多个服务的结果。它可以在web协议与内部使用的非Web友好型协议间进行转换,如HTTP协议、WebSocket协议。

API Gateway可以提供给客户端一个定制化的API。它暴露一个粗粒度API给移动客户端。以产品最终页这个使用场景为例。API Gateway提供一个服务提供点(/productdetails?productid=xxx)使得移动客户端可以在一个请求中检索到产品最终页的全部数据。API Gateway通过调用多个服务来处理这一个请求并返回结果,涉及产品信息、推荐、评论等。

一个很好的API Gateway例子是Netfix API Gateway。Netflix流服务提供数百个不同的微服务,包括电视、机顶盒、智能手机、游戏系统、平板电脑等。起初,Netflix视图提供一个适用全场景的API。但是,他们发现这种形式不好用,因为涉及到各式各样的设备以及它们独特的需求。现在,他们采用一个API Gateway来提供容错性高的API,针对不同类型设备有相应代码。事实上,一个适配器处理一个请求平均要调用6到8个后端服务。Netflix API Gateway每天处理数十亿的请求。

API Gateway的优点和缺点

如你所料,采用API Gateway也是优缺点并存的。API Gateway的一个最大好处是封装应用内部结构。相比起来调用指定的服务,客户端直接跟gatway交互更简单点。API Gateway提供给每一个客户端一个特定API,这样减少了客户端与服务器端的通信次数,也简化了客户端代码。

API Gateway也有一些缺点。它是一个高可用的组件,必须要开发、部署和管理。还有一个问题,它可能成为开发的一个瓶颈。开发者必须更新API Gateway来提供新服务提供点来支持新暴露的微服务。更新API Gateway时必须越轻量级越好。否则,开发者将因为更新Gateway而排队列。但是,除了这些缺点,对于大部分的应用,采用API Gateway的方式都是有效的。

使用API Gateway后,客户端和微服务之间的网络图变成下图:

API Gateway
API Gateway

通过API Gateway,可以统一向外部系统提供REST API。Spring Cloud中使用Zuul作为API Gateway。Zuul提供了动态路由、监控、回退、安全等功能。

Zuul

Netflix开源的微服务网关,核心是一系列过滤器:

  • 身份认证安全
  • 审查与监控
  • 动态路由
  • 压力测试
  • 附再分配
  • 静态响应处理
  • 多区域弹性

Zuul过滤器

Spring Cloud中使用Zuul作为API Gateway。Zuul提供了动态路由、监控、回退、安全等功能。主要为4种标准类型:

  • PRE:在请求被路由之前调用
  • ROUTING:这种过滤器将请求路由到微服务
  • POST:在路由到微服务以后执行
  • ERROR:在其他阶段发生错误时自信该过滤器

Spring Cloud Zuul

Spring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。ZuulNetflix出品的一个基于JVM路由和服务端的负载均衡器。

准备工作

新建一个Spring Boot项目,命名为:api-gateway

POM配置

pom.xml中引入以下依赖:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cayzlh</groupId>
<artifactId>spring-cloud-demos-parent</artifactId>
<version>0.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-gateway</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<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>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

配置文件

在配置文件 application.yml 中加入服务名、端口号、Eureka 注册中心的地址:

1
2
3
4
5
6
7
8
9
spring:
application:
name: api-gateway
server:
port: 28091
eureka:
client:
service-url:
defaultZone: http://localhost:28081/eureka/

启动类

使用@EnableZuulProxy注解开启Zuul功能:

1
2
3
4
5
6
7
8
9
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

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

}

至此,一个基于Spring Cloud Zuul的服务网关就已经构建完成。分别启动注册中心、服务生产者、服务消费者、API-GATEWAY

api-gateway
api-gateway

启动完成后,访问http://localhost:208081,可以看到上面的结果。

测试

由于 Spring Cloud Zuul 在整合了 Eureka 之后,具备默认的服务路由功能,即:当我们这里构建的api-gateway应用启动并注册到 Eureka 之后,服务网关会发现上面我们启动的两个服务producerconsumer,这时候 Zuul 就会创建两个路由规则。每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务 ID针对当前示例的情况,Zuul 会创建下面的两个路由规则:

  • 转发到eureka-producer服务的请求规则为:/eureka-producer/**
  • 转发到eureka-consumer服务的请求规则为:/eureka-consumer/**

最后,我们可以通过访问28091端口的服务网关来验证上述路由的正确性:

以上结果说明zuul已经开始生效了。

过滤器

Zuul还有更多的应用场景,比如:鉴权、流量转发、请求统计等等,这些功能都可以使用Zuul来实现。

Zuul的核心

FilterZuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是PREROUTINGPOSTERROR,整个生命周期可以用下图来表示:

Filter生命周期
Filter生命周期

Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClientNetfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

Zuul中默认实现的Filter

类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 标记处理Servlet的类型
pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求
pre -1 FormBodyWrapperFilter 包装请求体
route 1 DebugFilter 标记调试标志
route 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
post 0 SendErrorFilter 处理有错误的请求响应
post 1000 SendResponseFilter 处理正常的请求响应

-

禁用指定的Filter

可以在application.xml中配置需要禁用的filter,以zuul.<SimpleClassName>.<filterType>.disable=true这样的格式配置,比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter就设置:

1
2
3
4
zuul:
SendResponseFilter:
post:
disable: true

自定义Filter

我们假设有这样一个场景,因为服务网关应对的是外部的所有请求,为了避免产生安全隐患,我们需要对请求做一定的限制,比如请求中含有 Token 便让请求继续往下走,如果请求不带 Token 就直接返回并给出提示。

首先自定义一个 Filter,继承 ZuulFilter 抽象类,在 run() 方法中验证参数是否含有 Token,具体如下:

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
public class TokenFilter extends ZuulFilter {

/**
* 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。
* 这里定义为pre,代表会在请求被路由之前执行。
*
* @return
*/
@Override
public String filterType() {
return "pre";
}

/**
* filter执行顺序,通过数字指定。
* 数字越大,优先级越低。
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}

/**
* 判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。
* 实际运用中我们可以利用该函数来指定过滤器的有效范围。
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}

/**
* 过滤器的具体逻辑
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();

String token = request.getParameter("token");
if (token == null || token.isEmpty()) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("token is empty");
}
return null;
}
}

在上面实现的过滤器代码中,我们通过继承ZuulFilter抽象类并重写了下面的四个方法来实现自定义的过滤器。这四个方法分别定义了:

  • filterType():过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
  • filterOrder():过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过数字指定,数字越大,优先级越低。
  • shouldFilter():判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
  • run():过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)令 Zuul 过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回 body 内容进行编辑等。

在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的 Bean 才能启动该过滤器,比如,在应用主类中增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

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

@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}

}

在对api-gateway服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面定义的过滤器做一个验证:

路由熔断

当我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行一降级。Zuul给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。

我们通过自定义的fallback方法,并且将其指定给某个route来实现该route访问出问题的熔断处理。主要继承ZuulFallbackProvider接口来实现,ZuulFallbackProvider默认有两个方法,一个用来指明熔断拦截哪个服务,一个定制返回内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 断路出现时,它会提供一个什么返回值来处理请求。

后来Spring又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,因此最新版本建议直接继承类FallbackProvider

路由重试

有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现。需要添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

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

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

-

Zuul高可用

不同的客户端使用不同的负载将请求分发到后端的ZuulZuul在通过Eureka调用后端服务,最后对外输出。因此为了保证Zuul的高可用性,前端可以同时启动多个Zuul实例进行负载,在Zuul的前端使用Nginx或者F5进行负载转发以达到高可用性。

参考


隐藏关卡