API网关技术总结

近段时间,我和项目组的几个小伙伴一起完成了一个API网关的项目。在完成这个项目的过程中遇到了一些曲折也收获了许多。本文将对网关中使用的技术做一个总结。

背景

随着公司各个项目的扩展,不同的项目之间和第三方出现了大量调用项目API的需求。此时就面临了一系列问题,例如:如何让各个项目安全地对外开放接口、如何让调用方快速接入、如何保证接口的安全等等。最初的时候,这些工作是各个项目自己做的,这段时期的接口对接是一个极其痛苦的过程:各个项目的权限控制不一样、文档不全,接口提供方和调用方都需要经过大量重复的沟通。也是我们需要一个隔离接口提供方和调用方的中间层——API网关,它负责在抽象出各个业务需要的通用功能,例如:权限验证、限流、超时控制、熔断降级。

技术选型

用一句话来说,API网关的功能就是接收并转发请求到实际的接口,然后将实际接口返回的数据返回给调用方。

这样一个功能其实可以使用最基本的http访问函数来完成,但是为了稳定性一开始我们还是调研了目前存在的开源网关项目。

因为整个项目使用了Spring Cloud框架,因此很自然地将目光放在了Spring Cloud组件Zuul上面。我对Zuul的代码进行了一番研究,觉得对其进行简单的改造可以满足我们的需求。但是进一步研究发现Zuul存在以下几个问题:

  • Zuul对http的处理基于ServletServlet是一个面向业务的实现,它对http请求经过了一番包装,导致我们获取的http参数等数据存在问题。举个例子,当请求的Content-Typeapplication/x-www-form-urlencoded时,无法在HttpServletRequest中获取到请求体,反而能在请求参数中获取到请求体数据,并且请求体数据和url中的参数混在一起。
  • Zuul对http的请求的处理基于同步模型,针对每个请求是用一个线程来处理。通常情况下,所有请求会被放到处理队列中,从线程池中选取空闲线程来处理该请求。整个处理线程在线程内是阻塞的,因此其支持的并发量有限。

基于上面的问题,我果断放弃Zuul。经过调研,我转而使用Netty来实现网关的核心功能,Netty是一个异步的高性能网络框架,使用它可以简单地实现一个稳定的高性能网关。

架构

整个API网关可以拆分为3个子系统:

  1. 核心系统:负责请求的接收、验证、路由、返回结果等等
  2. 管理后台:负责接口的配置与管理、权限管理
  3. 监控系统:负责展示各个监控指标

架构图如下:

API网关架构

核心系统

核心系统是整个API网关的核心,它主要有以下几个模块:

  1. 限流模块
  2. 验证验证模块
  3. 接口路由模块
  4. 接口修改模块
  5. 缓存模块
  6. 后端调用模块,超时、熔断控制
  7. 响应结果处理模块
  8. 日志模块

架构如图所示:

核心系统框架

异步请求

前面我们说到Zuul采用了同步模型,因此性能不足,所以我们使用Netty重写了网关的核心业务。网关是一种IO密集型的应用,使用Netty的异步读写特性能较大程度地提示网关整体并发能力及吞吐量。

二级缓存

权限验证、接口配置信息等数据是通过服务的方式从后台管理系统中请求的。如果每次请求都调用管理系统中的服务势必会导致性能降低,为了提升网关的性能,我们增加了一层分布式缓存(使用Redis),将权限验证、接口配置等不经常变更的数据缓存下来,大大减少服务的调用。

考虑到Redis的读取仍然有网络开销,虽然这样的开销绝对值并不大,但是如果网关的并发量大那么总的开销还是可观的。于是我们在网关程序中做了本地缓存,这样读取数据的时间可以从ms级别降低到ns级别,进一步提升网关的并发性能。有关本地缓存和Redis的读取开销详见:Guava Cache与Redis的性能对比

为了保证管理系统中权限、接口等信息的修改能即使同步到各个网关节点上,我们使用消息队列来广播修改信息,接受到消息后异步地刷新本地以及Redis缓存。

链式处理

考虑到请求处理的可扩展性,我们采用了责任链的模式来实现网关的核心处理流程,将每个处理步骤实现为一个Filter,每个Filter按照预先设定的顺序先后执行。我们借鉴了Zuul的实现,采用了PRPE模式(PreRoutingPostError)。各个步骤包含的处理逻辑如下:

  • Pre:权限验证、限流
  • Routing:路由选择、请求修改
  • Post:响应结果处理
  • Error:请求错误处理

线程池隔离

我们在Netty线程模型中介绍过Netty分为请求接收线程池和工作线程池。工作线程池是真正负责IO读写操作的线程组,因为工程线程池是有限的,所以对于Netty的开发建议将复杂度较高的工作投递到后端业务线程池中统一处理。

在网关的开发中,我们将PreRoutingPostError的都进行放在了独立的线程池中进行处理,因为通常Pre处理比较快,而Routing处理比较耗时,放在独立的线程池中避免相互干扰。

限流

限流是网关比较重要的一个功能,缺少这个功能的话有可能在高并发情形下超出接口的承载能力,甚至影响其他接口的调用。

经过调研,我们采用了最终选择了令牌桶限流,令牌桶限流相比于漏桶限流能适应闲置较长时段后的尖峰调用,同时消除了简单计数器线程带来的短时间内流量不均的问题。

关于限流技术详见:限流技术总结基于redis的分布式限流方案

熔断降级

由于后端的接口可能会因为网络或者其他原因导致调用超时或者异常,后端服务无法立即恢复,这种情况下再将请求发送到后端没有意义。于是我们采用Hystrix进行超时、熔断降级处理。Hystrix支持线程池和信号量两种模式的隔离方案,网关的场景下如果我们对API做线程池隔离就会产生大量的线程,所以我们选择了信号量做隔离。

开源实现版本见:api-gateway-core

https://tech.youzan.com/api-gateway-in-practice/
https://tech.youzan.com/gateway/