高性能网关设计实践( 三 )


高性能网关设计实践

文章插图
 
打个简单的比方 , 我们都有订票的经验 , 当我们委托酒店订票时 , 接待员会先把我们的电话号码和相关信息登记下来(注册事件) , 挂断电话后接待员在操作期间我们就可以去做其他事了(非阻塞) , 当接待员把手续搞好后会主动打电话给我们通知我们票订好了(回调) 。
worker 进程是从 master fork 出来的 , 这意味着 worker 进程之间是互相独立的 , 这样不同 worker 进程之间处理并发请求几乎没有同步锁的限制 , 好处就是一个 worker 进程挂了 , 不会影响其他进程 , 我们一般把 worker 数量设置成和 CPU 的个数 , 这样可以减少不必要的 CPU 切换 , 提升性能 , 每个 worker 都是单线程执行的 。那么 LuaJIT 在 OpenResty 架构中的位置是怎样的呢 。
高性能网关设计实践

文章插图
 
首先启动的 master 进程带有 LuaJIT 的机虚拟 , 而 worker 进程是从 master 进程 fork 出来的 , 在 worker 内进程的工作主要由 Lua 协程来完成 , 也就是说在同一个 worker 内的所有协程 , 都会共享这个 LuaJIT 虚拟机 , 每个 worker 进程里 lua 的执行也是在这个虚拟机中完成的 。
同一个时间点 , worker 进程只能处理一个用户请求 , 也就是说只有一个 lua 协程在运行 , 那为啥 OpenResty 能支持百万并发请求呢 , 这就需要了解 Lua 协程与 Nginx 事件机制是如何配合的了 。
高性能网关设计实践

文章插图
 
如图示 , 当用 Lua 调用查询 MySQL 或 网络 IO 时 , 虚拟机会调用 Lua 协程的 yield 把自己挂起 , 在 Nginx 中注册回调 , 此时 worker 就可以处理另外的请求了(非阻塞) , 等到 IO 事件处理完了 ,  Nginx 就会调用 resume 来唤醒 lua 协程 。
事实上 , 由 OpenResty 提供的所有 API , 都是非阻塞的 , 下文提到的与 MySQL , Redis 等交互 , 都是非阻塞的 , 所以性能很高 。
OpenResty 请求生命周期Nginx 的每个请求有 11 个阶段 , OpenResty 也有11 个 *_by_lua 的指令 , 如下图示:
高性能网关设计实践

文章插图
 
各个阶段 *_by_lua 的解释如下
set_by_lua:设置变量;rewrite_by_lua:转发、重定向等;access_by_lua:准入、权限等;content_by_lua:生成返回内容;header_filter_by_lua:应答头过滤处理;body_filter_by_lua:应答体过滤处理;log_by_lua:日志记录 。这样分阶段有啥好处呢 , 假设你原来的 API 请求都是明文的
# 明文协议版本location /request {content_by_lua '...';# 处理请求}现在需要对其加上加密和解密的机制 , 只需要在 access 阶段解密 ,  在 body filter 阶段加密即可 , 原来 content 的逻辑无需做任务改动 , 有效实现了代码的解耦 。
# 加密协议版本location /request {access_by_lua '...';# 请求体解密content_by_lua '...';# 处理请求 , 不需要关心通信协议body_filter_by_lua '...';# 应答体加密}再比如我们不是要要上文提到网关的核心功能之一不是要监控日志吗 , 就可以统一在 log_by_lua 上报日志 , 不影响其他阶段的逻辑 。
worker 间共享数据利器: shared dictworker 既然是互相独立的进程 , 就需要考虑其共享数据的问题 ,  OpenResty 提供了一种高效的数据结构: shared dict ,可以实现在 worker 间共享数据 , shared dict 对外提供了 20 多个 Lua API , 都是原子操作的 , 避免了高并发下的竞争问题 。
路由策略插件化实现有了以上 OpenResty 点的铺垫 , 来看看上文提的网关核心功能 「路由策略插件化」,「后端集群的动态变更」如何实现
首先针对某个请求的路由策略大概是这样的
高性能网关设计实践

文章插图
 
整个插件化的步骤大致如下
1、每条策略由 url ,action, cluster 等组成 , 代表请求 url 在打到后端集群过程中最终经历了哪些路由规则 , 这些规则统一在我们的路由管理平台配置 , 存在 db 里 。


推荐阅读