API 网关 (API Gataway)
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理相对复杂。
- 认证复杂,每个服务都需要独立认证。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。
例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助 API 网关解决。
API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
API网关介乎于客户端与服务端之间,是客户端访问业务系统的唯一入口。API网关的存在,让微服务更加专注于考虑业务逻辑,而安全、性能、监控等公共服务则交由API网关实现,例如:统一认证、负载均衡、服务熔断、流量控制、灰度发布、日志统计等。
其实,在微服务概念流行之前,API网关的实体就已经诞生了,例如,银行、证券等领域常见的前置机,它就是为了解决访问认证、报文转换、访问统计等问题而出现的。
关于API网关的职能,有人做了这样一个形象的类比。假如有一家企业,这家企业的办公场所入口有一个门卫,日常情况下,门卫会对出入企业的人员进行身份确认,有访客拜访的时候会帮助提供位置指引服务,在企业举办活动的时候协助人员限流管理等。API网关承载的角色就相当于一个门卫,商品、库存、订单、评论等服务都属于后端内部微服务,由各个业务团队独立维护,相当于企业的各个业务部门,当发生外部请求时,API网关首先对这个请求进行认证和鉴权,然后再将这个请求分发到后端对应的微服务,倘若外部请求出现流量突发,则对这些请求进行负载分担和流量控制。
认证鉴权
单体应用体系下,请求一般会通过一个权限拦截器进行权限校验,在登录时将用户信息缓存到会话Session中,后续访问则从会话中获取用户信息。
而在微服务架构下,一个应用被拆分为若干个微服务,每个微服务都需要明确当前访问用户及其权限,单体应用架构下的鉴权方式就不适合了。API网关作为服务访问的统一入口,所有用户请求都经过API网关,很适合用来做认证鉴权这类切面型服务。
API网关拦截用户请求,获取请求中附带的用户身份信息,调用认证授权中心的服务,对请求者做身份认证,即确认当前访问者确实是其所声称的身份,检查该用户是否有访问该后台服务的权限。
目前主流的认证鉴权方案有两种:
引入Redis做分布式会话。即用户登录成功后,将用户身份、权限信息存入Redis,以一个唯一ID作为Key,并设置信息在Redis里的失效时间。这个唯一ID的Key将返回给客户端,客户端可以存放在Cookie或sessionStorage等本地存储。下次访问的时候,将这个唯一ID放入请求参数中一起发送(一般放入Header)。服务端通过检查Redis里有无这个ID来判断用户是否登录,获取用户身份和权限信息。客户端如果长时间没有操作,则存储在Redis里会话信息过期自动删除。客户端每访问一次服务端,需刷新一次会话信息的过期时间,避免固定过期时间带来的低用户体验。
JWT,即Java Web Token。用户登录成功后,服务端向客户端返回的唯一ID不再是无意义的字符串,而是包含了用户身份、权限、失效时间等信息的加密字符串,并且这个字符串包含数字签名,服务端可对这个字符串做数字签名验签,确保该字符串未经篡改和伪造。相比分布式会话方案,JWT虽省去了Redis存储,但是每次访问都要做数字签名验证,增加了CPU的资源损耗。
JWT的构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload,> 类似于飞机上承载的物品),第三部分是签证(signature).
服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:
负载均衡(Load Balancer,LB)
在实际的应用部署中,当应用系统面临业务峰值,访问流量过高时,我们通常会横向扩展服务数量,使用集群负载均衡来提高系统的业务响应能力。
负载均衡分为集中式负载均衡和进程内负载均衡。
集中式负载均衡在访问者和目标服务之间架设LB硬件(例如F5)或者软件中间件(例如Ngnix)。客户端请求首先发送到负载均衡服务器,负载均衡服务器根据某种设定的均衡算法,将请求转发给后端服务器中的一个。服务端负载均衡技术成熟,效率也高,但是,后端服务器变动(扩容或缩容)需要修改负载均衡的配置,可维护性稍差,且需要对负载均衡本身实现高可用,增加了部署和维护成本。
进程内负载均衡有所不同,后端服务器的地址列表不再由负载均衡服务器存储和维护,而是将LB逻辑集成在客户端。后端服务器启动时自动向注册中心注册服务,服务下线时自动向服务注册中心注销服务。客户端建立对服务注册中心的长监听,每当后端服务发生变化,注册中心自动通知客户端更新本地缓存的服务器列表。客户端访问后端服务的时候,从服务注册中心获取哪些地址可用,然后根据负载均衡算法从这些地址中选择一个合适的服务器。进程内负载均衡的好处是方便,后端服务变化对客户端是透明的。
流量控制
流量控制的主要目的是防止短时间内的大并发请求被转发至后端导致业务过载,比较典型的就是DDoS(Distributed Denial of Service,分布式拒绝服务攻击)攻击,恶意攻击者操控大量僵尸机器,持续向目标服务器发送大量请求,让服务器忙于应付而无暇处理正常用户的请求,达到业务系统瘫痪的目的。为了防止这种情况出现,API网关可以对外部请求做限流。
常见的限流算法包括计数器算法、漏桶算法和令牌桶算法。
计数器算法是一种简单粗暴的限流算法,它的设计思想是限制一秒内允许通过的最大请求数,例如,设置限流QPS(Queries Per Second,每秒请求数)为100,那么,从第一个请求到来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到100,后续的请求就会被全部拒绝。等到1s结束后,计数器重置为0,重新开始计数。计数器算法有一个弊端:如果在单位时间1s内的前10ms,已经通过了100个请求,那么,后面的990ms将拒绝所有到来的请求,我们把这种现象称为“突刺现象”。
为了消除“突刺现象”,出现了漏桶算法。漏桶算法内部有一个“容器”,类似生活中用到的漏斗。当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变,每10ms处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。漏桶算法也存在弊端:无法应对短时间的突发流量。
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时,还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则就等待可用的令牌,或者直接拒绝。放令牌这个动作持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置QPS为100,那么限流器初始化完成1s后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
服务熔断
在实际生产环境中,一些服务很有可能因为某些原因发生故障而不可用,比如服务内部错误、网络延迟等,如果放任故障服务不管,可能会因为级联故障导致“雪崩效应”,使得整个系统瘫痪。例如,服务A调用服务B,服务B再调用服务C,如果服务C变得不可用,阻塞了大量上游请求,服务B的请求线程也将随之阻塞,紧接着,服务A也将变得不可用,整个调用链路被拖垮。
熔断机制是应对雪崩效应的一种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路中的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
对于熔断机制,一般有三种状态:
- 熔断关闭状态(Closed):服务没有故障时熔断器所处的状态,对调用方的调用不做任何限制。
- 熔断开启状态(Open):在固定时间窗口内(例如10s),接口调用出错比率达到一个阈值(例如50%),会进入熔断开启状态。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的Fallback(退路)错误方法。
- 半熔断状态(Half-Open):在进入熔断开启状态一段时间之后(例如5s),熔断器会进入半熔断状态。所谓半熔断就是尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
使用 API 网关后的优点如下
- 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数。
API 网关选型
业界的情况:
这时,我们还没有看到API Gateway。举例来说,原先IOS、Android、PC客户端调用服务的地方,需要多个URL地址,有订单的、商品的、用户的。微服务化后就必须有统一的出入口,这种情况下,API Gateway就出现了。API Gateway很好的解决了微服务下调用、统一接入等问题,如下图所示:
有了API网关之后,各个API服务提供团队可以专注于自己的业务逻辑处理,而API罔顾赞更专注于安全、流量、路由等问题。
看到上面的图示与描述,我们可能会想到另外一个与网关类似的东西——代理。网关与代理的区别:代理是纯粹的数据透传,协议不会发生变化;网关在数据透传的背景下,还会设计协议的转换,比如上图中用户请求传输到网关的协议是HTTP,通过网关透传到下游则可能已经转换成企业内部的RPC了(比如JSF、Dubbo等企业自研的RPC框架)。
API网关涵盖的基本功能
一个API网关的基本功能包含了统一接入、协议适配、流量管理与容错、以及安全防护,这四大基本功能构成了网关的核心功能。网关首要的功能是负责统一接入,然后将请求的协议转换成内部的接口协议,在调用的过程中还要有限流、降级、熔断等容错的方式来保护网关的整体稳定,同时网关还要做到基本的安全防护(防刷控制),以及黑白名单(比如IP白名单)等基本安全措施,如下图所示:
API网关的架构示例
除了基本的四大功能,网关运行良好的环境还包括注册中心(比如:ZK读取已发布的API接口的动态配置)。为了实现高性能,将数据全部异构到缓存(如:Redis)中,同时还可以配合本地缓存来进一步提高网关系统的性能。为了提高网关的吞吐率,可以使用NIO+Servlet 3 异步的方式,还可以利用Servlet 3 的异步特性将请求线程与业务线程分开,为后续的线程池隔离做好基本的支撑。访问日志的存储我们可以放到Hbase中,如果要作为开放网关使用,那么需要一个支持OAuth2.0的授权中心。还可以引入Nginx + lua的方式将一些基本的校验判断放到应用系统之上,这样可以更轻量化的处理接入的问题,整体的网关架构示例如下所示:
https://blog.joway.io/posts/kubernetes-gateway/