http://www.scienjus.com/api-gateway-and-netflix-zuul/git
最近参与了公司 API Gateway 的搭建工做,技术选型是 Netflix Zuul,主要聊一聊其中的一些心得和体会。github
本文主要是介绍使用 Zuul 且在不强制使用其余 Neflix OSS 组件时,如何搭建生产环境的 Gateway,以及能使用 Gateway 作哪些事。不打算介绍任何关于如何快速搭建 Zuul,或是一些轻易集成 Eureka 之类的的方法,这些在官方文档上已经介绍的很明确了。spring
API Gateway 是随着微服务(Microservice)这个概念一块儿兴起的一种架构模式,它用于解决微服务过于分散,没有一个统一的出入口进行流量管理的问题。json
用 Kong 官网的两张图来解释再合适不过。后端
当使用微服务构建整个 API 服务时,通常会有许许多多职责不一样的应用在运行着,这些应用会须要一些通用的功能,例如鉴权、流控、监控、日志统计。api
在传统的单体应用中,这些功能通常都是内嵌在应用中,做为一个组件运行。可是在微服务模式下,不一样种类且独立运行的应用可能会有数十甚至数百种,继续使用这种方式会形成很是高的管理和发布成本。因此就须要在这些应用上抽象出一个统一的流量入口,完成这些功能的实现。跨域
在我看来,API Gateway 的职责主要分为两部分:缓存
对于 API Gateway,常见的选型有基于 Openresty 的 Kong、基于 Go 的 Tyk 和基于 Java 的 Zuul。网络
这三个选型自己没有什么明显的区别,主要仍是看技术栈是否能知足快速应用和二次开发,例如我司原有的技术栈就是使用 Go/Openresty 的平台组和使用 Java 的后端组,讨论后以为 API Gateway 将来仍是处理业务功能的场景更多些,并且后端这边有不少功能能够直接移植过来,最终就选择了 Zuul。架构
关于 Zuul,大部分使用 Java 作微服务的人可能都会或多或少了解 Spring Cloud 和 Netflix 全家桶。而对于彻底不了解的人,能够暂时将它想象为一个相似于 Servlet 中过滤器(Filter)的概念。
就像上图中所描述的同样,Zuul 提供了四种过滤器的 API,分别为前置(Pre)、后置(Post)、路由(Route)和错误(Error)四种处理方式。
一个请求会先按顺序经过全部的前置过滤器,以后在路由过滤器中转发给后端应用,获得响应后又会经过全部的后置过滤器,最后响应给客户端。在整个流程中若是发生了异常则会跳转到错误过滤器中。
通常来讲,若是须要在请求到达后端应用前就进行处理的话,会选择前置过滤器,例如鉴权、请求转发、增长请求参数等行为。在请求完成后须要处理的操做放在后置过滤器中完成,例如统计返回值和调用时间、记录日志、增长跨域头等行为。路由过滤器通常只须要选择 Zuul 中内置的便可,错误过滤器通常只须要一个,这样能够在 Gateway 遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果。
如下介绍一些 Zuul 中不一样过滤器的应用场景。
通常来讲整个服务的鉴权逻辑能够很复杂。
而对于后端应用来讲,它们其实只须要知道请求属于谁,而不须要知道为何,因此 Gateway 能够友善的帮助后端应用完成鉴权这个行为,并将用户的惟一标示透传到后端,而不须要、甚至不该该将身份信息也传递给后端,防止某些应用利用这些敏感信息作错误的事情。
Zuul 默认状况下在处理后会删除请求的 Authorization
头和 Set-Cookie
头,也算是贯彻了这个原则。
流量转发的含义就是将指向 /a/xxx.json
的请求转发到指向 /b/xxx.json
的请求。这个功能可能在一些项目迁移、或是灰度发布上会有一些用处。
在 Zuul 中并无一个很好的办法去修改 Request URI。在某些 Issue 中开发者会建议设置 requestURI
这个属性,可是实际在 Zuul 自身的 PreDecorationFilter
流程中又会被覆盖一遍。
不过对于一个基于 Servlet 的应用,使用 HttpServletRequestWrapper
基本能够解决一切问题,在这个场景中只须要重写其 getRequestURI
方法便可。
class RewriteURIRequestWrapper extends HttpServletRequestWrapper { |
使用 Gateway 作跨域相比应用自己或是 Nginx 的好处是规则能够配置的更加灵活。例如一个常见的规则。
Access-Control-Allow-Origin
为 *
,且 Access-Control-Allow-Credentials
为 true
,这是一个经常使用的容许任意源跨域的配置,可是不容许请求携带任何 CookieOrigin
增长到白名单中。对于白名单中的请求,返回 Access-Control-Allow-Origin
为该域名,且 Access-Control-Allow-Credentials
为 true
,这样请求者能够正常的请求接口,同时能够在请求接口时携带 CookieAccess-Control-Allow-Origin
为 *
,不然重定向后的请求携带的 Origin
会为 null
,有可能会致使 iOS 低版本的某些兼容问题Gateway 能够统一收集全部应用请求的记录,并写入日志文件或是发到监控系统,相比 Nginx 的 access log,好处主要也是二次开发比较方便,好比能够关注一些业务相关的 HTTP 头,或是将请求参数和返回值都保存为日志打入消息队列中,便于线上故障调试。也能够收集一些性能指标发送到相似 Statsd 这样的监控平台。
错误过滤器的主要用法就像是 Jersey 中的 ExceptionMapper
或是 Spring MVC 中的 @ExceptionHandler
同样,在处理流程中认为有问题时,直接抛出统一的异常,错误过滤器捕获到这个异常后,就能够统一的进行返回值的封装,并直接结束该请求。
虽然将这些逻辑都切换到了 Gateway,省去了不少维护和迭代的成本,可是也面临着一个很大的问题,就是 Gateway 只有逻辑却没有配置,它并不知道一个请求要走哪些流程。
例如一样是后端服务 API,有的多是给网页版用的、有的是给客户端用的,亦或是有的给用户用、有的给管理人员用,那么 Gateway 如何知道到底这些 API 是否须要登陆、流控以及缓存呢?
理论上咱们能够为 Gateway 编写一个管理后台,里面有当前服务的全部 API,每个开发者均可以在里面建立新的 API,以及为它增长鉴权、缓存、跨域等功能。为了简化使用,也许咱们会额外的增长一个权限组,例如 /admin/*
下的全部 API 都应该为后台接口,它只容许内部来源的鉴权访问。
可是这样作依旧太复杂了,并且很是硬编码,当开发者开发了一个新的 API 以后,即便这个应用已经能正常接收特定 URI 的请求并处理以后,却还要经过人工的方式去一个管理后台进行额外的配置,并且可能会由于不谨慎打错了路径中的某个单词而形成没必要要的事故,这都是不合理的。
我我的推荐的作法是,在后端应用中依旧保持配置的能力,即便应用里已经没有真实处理的逻辑了。例如在 Java 中经过注解声明式的编写 API,且在应用启动时自动注册 Gateway 就是一种比较好的选择。
/** |
这样 API 的编写者就会根据业务场景考虑该 API 须要哪些功能,也减小了管理的复杂度。
除此以外还会有一些后端应用无关的配置,有些是自动化的,例如恶意请求拦截,Gateway 会将全部请求的信息经过消息队列发送给一些实时数据分析的应用,这些应用会对请求分析,发现恶意请求的特征,并经过 Gateway 提供的接口将这些特征上报给 Gateway,Gateway 就能够实时的对这些恶意请求进行拦截。
在 Nginx 和后端应用之间又创建了一个 Java 应用做为流量入口,不少人会去担忧它的稳定性,亦或是担忧它可否像 Nginx 同样和后端的多个 upstream 进行交互,如下主要介绍一下 Zuul 的隔离机制以及重试机制。
在微服务的模式下,应用之间的联系变得没那么强烈,理想中任何一个应用超过负载或是挂掉了,都不该该去影响到其余应用。可是在 Gateway 这个层面,有没有可能出现一个应用负载太重,致使将整个 Gateway 都压垮了,已致全部应用的流量入口都被切断?
这固然是有可能的,想象一个每秒会接受不少请求的应用,在正常状况下这些请求可能在 10 毫秒以内就能正常响应,可是若是有一天它出了问题,全部请求都会 Block 到 30 秒超时才会断开(例如频繁 Full GC 没法有效释放内存)。那么在这个时候,Gateway 中也会有大量的线程在等待请求的响应,最终会吃光全部线程,致使其余正常应用的请求也受到影响。
在 Zuul 中,每个后端应用都称为一个 Route,为了不一个 Route 抢占了太多资源影响到其余 Route 的状况出现,Zuul 使用 Hystrix 对每个 Route 都作了隔离和限流。
Hystrix 的隔离策略有两种,基于线程或是基于信号量。Zuul 默认的是基于线程的隔离机制,这意味着每个 Route 的请求都会在一个固定大小且独立的线程池中执行,这样即便其中一个 Route 出现了问题,也只会是某一个线程池发生了阻塞,其余 Route 不会受到影响。
通常使用 Hystrix 时,只有调用量巨大会受到线程开销影响时才会使用信号量进行隔离策略,对于 Zuul 这种网络请求的用途使用线程隔离更加稳妥。
通常来讲,后端应用的健康状态是不稳定的,应用列表随时会有修改,因此 Gateway 必须有足够好的容错机制,可以减小后端应用变动时形成的影响。
Zuul 的路由主要有 Eureka 和 Ribbon 两种方式,因为我一直使用的都是 Ribbon,因此简单介绍下 Ribbon 支持哪些容错配置。
重试的场景分为三种:
okToRetryOnConnectErrors
:只重试网络错误okToRetryOnAllErrors
:重试全部错误OkToRetryOnAllOperations
:重试全部操做(这里不太理解,猜想是 GET/POST 等请求都会重试)重试的次数有两种:
MaxAutoRetries
:每一个节点的最大重试次数MaxAutoRetriesNextServer
:更换节点重试的最大次数通常来讲咱们但愿只在网络链接失败时进行重试、或是对 5XX 的 GET 请求进行重试(不推荐对 POST 请求进行重试,没法保证幂等性会形成数据不一致)。单台的重试次数能够尽可能小一些,重试的节点数尽可能多一些,总体效果会更好。
若是有更加复杂的重试场景,例如须要对特定的某些 API、特定的返回值进行重试,那么也能够经过实现 RequestSpecificRetryHandler
定制逻辑(不建议直接使用 RetryHandler
,由于这个子类可使用不少已有的功能)。