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,可以统一向外部系统提供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
路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul
是Netflix
出品的一个基于JVM
路由和服务端的负载均衡器。
准备工作
新建一个Spring Boot
项目,命名为:api-gateway
,
POM配置
在pom.xml
中引入以下依赖:
|
配置文件
在配置文件 application.yml
中加入服务名、端口号、Eureka
注册中心的地址:
spring: |
启动类
使用@EnableZuulProxy
注解开启Zuul
功能:
|
至此,一个基于Spring Cloud Zuul
的服务网关就已经构建完成。分别启动注册中心、服务生产者、服务消费者、API-GATEWAY
:
启动完成后,访问http://localhost:208081,可以看到上面的结果。
测试
由于 Spring Cloud Zuul
在整合了 Eureka
之后,具备默认的服务路由功能,即:当我们这里构建的api-gateway
应用启动并注册到 Eureka
之后,服务网关会发现上面我们启动的两个服务producer
和consumer
,这时候 Zuul
就会创建两个路由规则。每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务 ID
。针对当前示例的情况,Zuul
会创建下面的两个路由规则:
- 转发到
eureka-producer
服务的请求规则为:/eureka-producer/**
- 转发到
eureka-consumer
服务的请求规则为:/eureka-consumer/**
最后,我们可以通过访问28091
端口的服务网关来验证上述路由的正确性:
比如访问:http://localhost:28091/eureka-consumer/hello/zhangsan,该请求将最终被路由到
consumer
的/hello
接口上:
以上结果说明zuul已经开始生效了。
过滤器
Zuul
还有更多的应用场景,比如:鉴权、流量转发、请求统计等等,这些功能都可以使用Zuul
来实现。
Zuul的核心
Filter
是Zuul
的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是PRE
、ROUTING
、POST
、ERROR
,整个生命周期可以用下图来表示:
Zuul
大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用
Apache HttpClient
或Netfilx 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
就设置:
zuul: |
自定义Filter
我们假设有这样一个场景,因为服务网关应对的是外部的所有请求,为了避免产生安全隐患,我们需要对请求做一定的限制,比如请求中含有 Token
便让请求继续往下走,如果请求不带 Token
就直接返回并给出提示。
首先自定义一个 Filter
,继承 ZuulFilter
抽象类,在 run()
方法中验证参数是否含有 Token
,具体如下:
public class TokenFilter extends ZuulFilter { |
在上面实现的过滤器代码中,我们通过继承ZuulFilter
抽象类并重写了下面的四个方法来实现自定义的过滤器。这四个方法分别定义了:
filterType()
:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre
,代表会在请求被路由之前执行。filterOrder()
:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过数字指定,数字越大,优先级越低。shouldFilter()
:判断该过滤器是否需要被执行。这里我们直接返回了true
,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。run()
:过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)
令 Zuul 过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)
设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)
对返回 body 内容进行编辑等。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的 Bean
才能启动该过滤器,比如,在应用主类中增加如下内容:
|
在对api-gateway
服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面定义的过滤器做一个验证:
访问 http://localhost:14000/eureka-consumer/hello/zhangsan 返回 401 错误和
token is empty
访问 http://localhost:14000/eureka-consumer/hello/zhangsan?token=token 正确路由到
eureka-consumer
的/hello
接口,并返回Hello, zhangsan!, 现在时间:1546506130007
路由熔断
当我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行一降级。Zuul
给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。
我们通过自定义的fallback
方法,并且将其指定给某个route
来实现该route
访问出问题的熔断处理。主要继承ZuulFallbackProvider
接口来实现,ZuulFallbackProvider
默认有两个方法,一个用来指明熔断拦截哪个服务,一个定制返回内容。
public interface ZuulFallbackProvider { |
实现类通过实现getRoute
方法,告诉Zuul
它是负责哪个route
定义的熔断。而fallbackResponse
方法则是告诉 Zuul
断路出现时,它会提供一个什么返回值来处理请求。
后来Spring
又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,因此最新版本建议直接继承类FallbackProvider
。
路由重试
有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul
也帮我们实现了此功能,需要结合Spring Retry
一起来实现。需要添加依赖:
<dependency> |
开启重试在某些情况下是有问题的,比如当压力过大,一个实例停止响应时,路由将流量转到另一个实例,很有可能导致最终所有的实例全被压垮。说到底,断路器的其中一个作用就是防止故障或者压力扩散。用了
retry
,断路器就只有在该服务的所有实例都无法运作的情况下才能起作用。这种时候,断路器的形式更像是提供一种友好的错误信息,或者假装服务正常运行的假象给使用者。不用
retry
,仅使用负载均衡和熔断,就必须考虑到是否能够接受单个服务实例关闭和eureka
刷新服务列表之间带来的短时间的熔断。如果可以接受,就无需使用retry
。
Zuul高可用
不同的客户端使用不同的负载将请求分发到后端的Zuul
,Zuul
在通过Eureka
调用后端服务,最后对外输出。因此为了保证Zuul
的高可用性,前端可以同时启动多个Zuul
实例进行负载,在Zuul
的前端使用Nginx
或者F5
进行负载转发以达到高可用性。