本文讲述基于 OpenResty 的接口网关设计,主要谈及接口网关的请求路由与安全认证(IP 与 URI 白名单、加解密与验签名流程等)这两部分内容,其中涉及到的 Nginx、OpenResty等相关内容会作简单介绍。
笔者曾参与开发两个接口网关的项目,一个是基于 Tomcat 的应用提供的网关服务,另一个是基于 OpenResty 的 Nginx 应用提供的网关服务。经过两个网关项目的开发,笔者在接口网关开发方面稍微积累了一些经验,故在此把这些经验分享出来一起交流学习。由于基于 OpenResty 的 Nginx 网关普遍被认为是更优的方案,故本文主要针对基于 OpenResty 的 Nginx 网关进行讲述。当然,由于不同的并发数量级,不同的业务场景,接口网关的设计多种多样,本文所述其中较为简单且轻量级的一种。
1.1 定位
接口网关,顾名思义,是企业 IT 在系统边界上提供给外部访问内部接口服务的统一入口。这里的外部可以指客户端、浏览器或者第三方应用等,在这种情况下,接口网关可以有多种定位:
在笔者的工作中,同样把面向客户端的网关称作 APIGateway,把作为开放平台提供给第三方服务的网关称作 OpenApi。本文主要以 OpenApi 作为接口网关为例来讲述。
1.2 功能
作为企业 IT 系统的统一入口,接口网关可提供请求路由与组合、协议转换、安全认证、服务鉴权、流量控制与日志监控等服务。在笔者的工作中,主要在接口网关上实现了请求路由与安全认证的功能,题目中所说的“设计”,主要是指请求路由与安全认证方面,暂不涉及流量控制或日志监控等其他方面的设计。
正如上文所言,网关接口为企业应用提供了丰富的功能,而笔者在工作中开发的接口网关主要提供请求路由与安全认证的功能,那么在回答“为什么需要接口网关”的时候,需要对这两者多加阐述。
2.1 请求路由
企业提供内外两网,在没有接口网关时,提供外部服务的应用需要部署在外网。随着服务的增多,部署在外网的应用越来越多,在服务的安全压力与维护成本增大的情况下,需要一个统一的接口网关“隔离”内外服务。企业提供的服务(无论内部服务还是外部服务)均部署在内网,而由部署在外网的网关接受请求,并路由到内网服务。在这种情况下,既有利于对外屏蔽企业内部服务部署细节,提供统一的服务访问地址,又便于管理与维护内外部服务接口,便于演进与重构服务。这是接口网关提供请求路由的作用。
2.2 安全认证
在没有接口网关时,企业对外服务直接由外部访问,身份验证与数据加解密等工作都需要每一个对外服务本身去处理,增加了服务本不该有的职责,并且增加了服务开发的难度与工作量。实际在大多数情况下,可以将身份验证与数据加解密等安全工作可以从服务抽离,统一由接口网关负责处理。接口网关作为入口,对外验证调用方的 IP,身份以及接口访问权限等,并且可以解密数据后再将请求路由到服务。这是接口网关提供安全认证的功能。
以上是实际工作中涉及的为什么需要接口网关的其中两个原因,当然原因远不止此,有兴趣的读者可以阅读其他文章,比如 《谈API网关的背景、架构以及落地方案》 或者 《微服务:从设计到部署》(英文原文:Microservices: From Design to Deployment)。接下来的章节我们开始探讨如何开发接口网关。
我们先看看工作中设计的提供请求路由与安全认证功能的接口网关的架构。
不过在介绍接口网关的设计之前,我们先来了解一下关于 Nginx 与 OpenResty 的基础知识。
3.1 Nginx 与 OpenResty 简介
3.1.1 Nginx 简介
Nginx 是世界第二大 Web 服务器,仅次于 Apache,然而由于其极高的性能可处理海量的互联网请求,现在已经成为业界高性能 Web 服务器的代名词。
它的主要特征是高性能、高扩展性、高可靠性、低内存消耗、单机支持 10 万以上的并发连接,支持热部署,以及使用最自由的 BSD 许可协议。其中,Nginx 可以处理高并发压力下的并发请求的原因如下:
除了基于事件驱动的架构使其支持百万级的 TCP 连接,另外高度模块化的设计和自由的许可证使其拥有非常多扩展其功能的第三方模块,也是它的重要特性。所以,后来才会有 OpenResty 的诞生。
我们看一个 Nginx 作简单配置来提供服务的例子:
worker_processes 1;events { worker_connections 1024; } http { upstream backend { server 127.0.0.1:8080 } server { location / back { proxy_pass http://backend; } } }
上述配置文件中,分别在 event、http、server 以及 location 块配置项中做了一些简单的配置,当安装完并启动 Nginx 后(监听 80 端口),访问到 /back 路径下的请求会被转发到本地 127.0.0.1:8080 服务上。
3.1.1 OpenResty 简介
根据官网定义,OpenResty 是一个通过 Lua 扩展 Nginx 实现的可伸缩的 Web 平台。其核心是基于 Nginx 一个 C 模块将 Lua 语言嵌入到 Nginx 服务器中,对外提供一套完整的 Lua Web 的 API,并透明支持非阻塞 I/O,提供协程 —— “轻量级线程”、定时器等,从而极大地降低了高性能服务端的开发难度和开发周期。
OpenResty 将两个极为优秀的组件 Nginx 与 Lua 进行糅合,一方面保留了 Nginx 高性能 web 服务特征,另一方面有提供 Lua 特性在极少损失性能情况下便于业务功能的开发。根据官网介绍,OpenResty 非常便于用来搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
我们也是因为 OpenResty 的这些特性,特别是它对搭建动态网关的友好支持,才选择了基于 OpenResty 来开发我们的接口网关 —— APIGateway 与 OpenApi。
开发接口网关使用到的 OpenResty 一个重要知识:OpenResty 对于一个请求的处理流程。Nginx 把一个请求分为不同的阶段,从而让第三方模块通过挂载行为在不同的阶段来定制自己的行为;OpenResty 拥有同样的特性,不过在不同阶段挂载的是 Lua 脚本。下图是基于《OpenResty 最佳实践》原图重绘而来:
从上图可知,OpenResty 处理请求大致分为四个阶段:
我们看一个 OpenResty 作简单配置来提供服务的例子:
worker_processes 1;events { worker_connections 1024;}http { resolver 127.0.0.1; lua_package_path '$prefix/lua/?.lua;;'; init_by_lua_block { # ... } init_worker_by_lua_file lua/init_work_by_lua.lua; server { listen 80; location / { rewrite_by_lua_file lua/rewrite_by_lua.lua; access_by_lua_file lua/access_by_lua.lua; proxy_pass http://<url>; } }}
上述配置文件中,分别在 event、http、server 以及 location 块配置项中做了一些简单的配置,当安装完并启动 Nginx 后(监听 80 端口),首先执行 init_by_lua_block、init_worker_by_lua_file 进行初始化,接着接受请求,所有的请求都会匹配上 "/" 路径,进而执行 rewrite_by_lua_file、access_by_lua_file进行重写与访问,最后转发请求到本地 127.0.0.1 服务上。
在实际的接口网关开发中,我们主要是使用到了 OpenResty 中初始化阶段的 init_by_lua*、init_worker_by_lua*、重写与访问阶段 的 rewrite_by_lua*、access_by_lua* 以及内容生成阶段 content_by_lua* 过程。
3.2 接口网关的架构
这一节是本文的核心内容,重点讲述接口网关的架构设计。如前文所述,本文主要以 OpenApi 为例来讲述接口网关的架构设计。先看图:
下面我们来一步步来分析架构图的各个部分,首先是两层的 HAProxy 。
3.2.1 两层 HAProxy 代理
根据维基百科定义,HAProxy 是一个使用 C 语言编写的自由及开放源代码软件,其提供高可用性、负载均衡,以及基于 TCP 和 HTTP 的应用程序代理。
如图所示,隔离的内网与外网上分别提供了 HAProxy 代理, 外层暂且称为 HAProxy internet ,内层称为 HAProxy internal。外层暴露于外网中,使用统一地址如 http://openapi.company.com 来接受外部请求(这里指第三方的请求);中间是基于 OpenResty 的 Nginx 网关层,外部请求经过网关后通过 HAProxy internal 转发到内网的服务上,内网服务遵循 Restful 风格,网关转发到内网的地址由接口网关控制。
然而,目前的代理架构受到了当前整体架构的约束,实际上两层的 HAProxy 代理并不是必需的。
在我们当前的系统量级下,这两层 HAProxy 转发消耗非常小可以被接受,所以调整架构的优先级还不高,以后再慢慢演进。
3.2.2 接口网关
接下来这一节是最为重点的接口网关的设计。接口网关主要利用前文所述的 OpenResty 执行阶段对请求与响应进行流程处理,包括接口地址的重写,IP 与资源白名单的控制,请求的解密与验签,请求的路由以及响应的签名与加密等。
这里分成主流程,配置服务,安全服务三部分进行讲述。
3.2.2.1 主流程设计
主流程是网关的核心,是请求处理的控制中心;它是通过 OpenResty 的 Lua 脚本处理流程来实现对请求的处理。
A. 主流程
--openapi --lua --access_by_lua.lua --cache_management.lua --content_by_lua.lua --init_work_by_lua.lua --rewrite_by_lua.lua --security.lua --prod --Dockerfile --nginx.conf --sit --Dockerfile --nginx.conf --README.md
# Nginx worker 进程个数,直接影响性能。# 如果确认不会出现阻塞式调用,那么有多少 CPU 内核设置多少个进程# 如果有可能出现阻塞式调用,需要配置多一些进程worker_processes 1; events { worker_connections 1024;}http { # 内网地址 resolver xxx.x.x.xxx yyy.y.y.yyy; # 日志格式配置 log_format graylog2_format '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' '<msec=$msec|connection=$connection|connection_requests=$connection_requests|millis=$request_time>'; # 日志路径配置 access_log syslog:server=<host>:<port> graylog2_format; error_log syslog:server=<host>:<port> warn; # 配置 Lua 包地址 lua_package_path '$prefix/lua/?.lua;;'; init_by_lua_block { # 引入依赖(可能会污染全局环境,待研究) http = require "resty.http" cjson = require "cjson" cache_management = require "cache_management" ... } # 设置定时任务缓存配置,及上面的 cache_management 模块 init_worker_by_lua_file lua/init_work_by_lua.lua; # Nginx Web 服务配置 server { listen 80; # ngx.location.capture 子请求代理,转发原请求到接口服务 location = /ngx_proxy/ { internal; proxy_set_header Accept-Encoding ''; proxy_pass http://$context$http_host_suffix$proxy_uri; } # 匹配所有请求,进行 URL 重写、访问控制、转发请求以及响应处理(各阶段的处理在此配置)。 location / { set $context ''; ... rewrite_by_lua_file lua/rewrite_by_lua.lua; access_by_lua_file lua/access_by_lua.lua; content_by_lua_file lua/content_by_lua.lua; } }}
{ # 接口映射配置 "apiMapping":{ "$context":{ "$fromApi":"$toApi" } }, # 接口白名单配置、加解密配置 "apiConfig":{ "$channel $context":{ "$httpMethod $uri":{ "reqNeedDecrypt":false, "respNeedEncrypt":false } } }, # IP 白名单配置,验签名配置 "channelConfig":{ "$channel":{ ips:{ "$ip":1 }, "reqNeedVerifySign":false, "respNeedSign":false, "needCheckIp":false } }}