【得物技术】初探 OpenResty
简介
Nginx 的高性能是业界公认的,近年来在全球服务器市场上的占比份额也在逐年增加,在国内知名互联网公司也有广泛的应用,阿里还基于 Nginx 进行扩展打造了著名的 Tengine。而 OpenResty 是由国人章亦春基于 Nginx 和 LuaJIT 打造的动态 web 平台,LuaJIT 是 Lua 编程语言的即时编译器。Lua 是一种强大、动态、轻量级的编程语言。该语言的设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能,OpenResty 就是通过使用 Lua 来扩展 Nginx 来实现的可扩展 Web 平台。目前 OpenResty 大多用在 API 网关的开发中,当然也可以用来替代 Nginx,用于反向代理和负载均衡的场景。
OpenResty 的架构组成
![](https://static001.geekbang.org/infoq/e3/e32f8ec9252563c044f5e55efdfb72ac.png)
如前所述,OpenResty 底层是基于 Nginx 和 LuaJIT 的,所以 OpenResty 继承了 Nginx 的多进程架构, 每一个 Worker 进程都是 fork Master 进程而得到的, 其实, Master 进程中的 LuaJIT 虚拟机也会一起 fork 过来。在同一个 Worker 内的所有协程,都会共享这个 LuaJIT 虚拟机,Lua 代码的执行也是在这个虚拟机中完成的。而在同一个时间点上,每个 Worker 进程只能处理一个用户的请求,也就是只有一个协程在运行。
Nginx
由于 Nginx 处理请求采用的是事件驱动模型,所以每一个 Worker 进程最好独占一个 CPU。实践中我们往往把 Worker 进程的数量配置成与 CPU 核数相同,此外把每一个 Worker 进程与某一个 CPU 核绑定在一起,这样可以更好的使用每一个 CPU 核上的 CPU 缓存,减少缓存失效的命中率,进而提高请求处理的性能。
LuaJIT
其实 OpenResty 最初默认使用的是标准 Lua,从 1.5.8.1 版本开始才默认使用 LuaJIT,背后的原因是因为 LuaJIT 相比标准 Lua 有很大的性能优势。
首先,LuaJIT 的运行时环境除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。开始的时候,LuaJIT 和标准 Lua 一样,Lua 代码被编译为字节码,字节码被 LuaJIT 的解释器解释执行。但不同的是,LuaJIT 的解释器会在执行字节码的同时,记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。当这些次数超过某个随机的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够热,这时便会触发 JIT 编译器开始工作。JIT 编译器会从热函数的入口或者热循环的某个位置开始,尝试编译对应的 Lua 代码路径。编译的过程,是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成目标机器的机器码。这个过程跟 Java 中 JIT 编译器工作原理类似,其实它们都是为了提高程序运行效率而采取的同一类优化手段,正所谓底层技术都是相通的,可以类比学习。
其次,LuaJIT 还紧密结合了 FFI(Foreign Function Interface,它不能作为单独的模块使用),可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。FFI 通过解析普通的 C 声明,就完成 Lua/C 的绑定工作。JIT 编译器从 Lua 代码访问 C 数据结构而生成的代码与 C 编译器生成的代码相同。与通过经典 Lua/C API 绑定的函数调用不同,对 C 函数的调用可以内联在 JIT 编译的代码中,所以 FFI 方式不仅简单,而且比传统的 Lua/C API 方式的性能更优。
下面是一个简单的调用示例:
短短这几行代码,就可以直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。
OpenResty 的工作原理
OpenResty 是基于 Nginx 的高性能 Web 平台,所以其高效运行与 Nginx 密不可分。
Nginx 处理 HTTP 请求有 11 个执行阶段,我们可以从 ngx_http_core_module.h 的源码中看到:
巧合的是,OpenResty 也有 11 个 *_by_lua 指令,它们和 NGINX 的 11 个执行阶段有很大的关联性。指令是使用 Lua 编写 Nginx 脚本的基本构建块,用于指定用户编写的 Lua 代码何时运行以及运行结果如何使用等。下图显示了不同指令的执行顺序,这张图可以帮助理清我们编写的脚本是按照怎样的逻辑运行的。
![](https://static001.geekbang.org/infoq/e0/e09998810a7f8c118ca8bb790f0d7d5e.png)
其中, init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。
下面对每一个 OpenResty 指令的执行时机和使用进行说明。
在 Nginx 启动过程中嵌入 Lua 代码
init_by_lua* :在 Nginx 解析配置文件(Master 进程)时在 Lua VM 层面立即调用的 Lua 代码。一般在 init_by_lua* 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。不过,init_by_lua 阶段无法执行 http 请求获取远程配置信息,对初始化工作多少有些不便。
init_worker_by_lua*:在 Nginx Worker 进程启动时调用,一般在 init_worker_by_lua*阶段,我们会执行一些定时任务,比如上游服务节点扩所容动态感知和健康检查等,对于 init_by_lua*阶段无法执行 http 请求的问题,也可以在此阶段的定时任务中进行。
在 OpenSSL 处理 SSL 协议时嵌入 Lua 代码
ssl_certificate_by_lua* :利用 OpenSSL 库(要求 1.0.2e 版本以上)的 SSL_CTX_set_cert_cb 特性,将 Lua 代码添加到验证下游客户端 SSL 证书的代码前,可用于为每个请求设置 SSL 证书链和相应的私钥以及在这种上下文中无阻塞地进行 SSL 握手流量控制。
在 11 个 HTTP 阶段中嵌入 Lua 代码
set_by_lua* :将 Lua 代码添加到 Nginx 官方 ngx_http_rewrite_module 模块中的脚本指令中执行,因为 ngx_http_rewrite_module 在它的指令中不支持非阻塞 I/O,所以需要生成当前 Lua "light threads" 的 Lua API 不能在这个阶段中工作。由于 Nginx 事件循环在此阶段代码执行过程中将被阻塞,故需要避免在此阶段中执行耗时操作,一般用于执行比较快和少的代码来设置变量。
rewrite_by_lua* :将 Lua 代码添加到 11 个阶段中的 rewrite 阶段中,作为独立模块为每个请求执行相应的 Lua 代码。此阶段的 Lua 代码可以进行 API 调用,并在独立的全局环境(即沙箱)中作为一个新生成的协程执行。此阶段可以实现很多功能,比如调用外部服务、转发和重定向处理等。
access_by_lua* :将 Lua 代码添加到 11 个阶段中的 access 阶段中执行,与 rewrite_by_lua*类似,也是作为独立模块为每个请求执行相应的 Lua 代码。此阶段的 Lua 代码可以进行 API 调用,并在独立的全局环境(即沙箱)中作为一个新生成的协程执行。一般用于访问控制、权限校验等。
content_by_lua* :在 11 个阶段的 content 阶段以独占方式为每个请求执行相应的 Lua 代码,用于生成返回内容。需要注意的是,不要在同一 location 中使用此指令和其他内容处理指令。例如,这个指令和 proxy_pass 指令不应该在同一个 location 中使用。
log_by_lua* :将 Lua 代码添加到 11 个阶段中的 log 阶段中执行,它不会替换当前请求的 access 日志,但会在其之前运行,一般用于请求的统计及日志记录。
在负载均衡时嵌入 Lua 代码
balance_by_lua* :将 Lua 代码添加到反向代理模块、生成上游服务地址的 init_upstream 回调方法中,用于 upstream 负载均衡控制。这个 Lua 代码执行上下文不支持 yield,因此在这个上下文中禁用可能 yield 的 Lua API (比如 cosockets 和 "light threads")。不过我们一般可以通过在早期的处理阶段(如 access_by_lua* )中执行这样的操作,并通过 ngx.ctx 将结果传递到这个上下文中来绕过这个限制。
在过滤响应时嵌入 Lua 代码
header_filter_by_lua* :将 Lua 代码嵌入到响应头部过滤阶段中,用于应答头过滤处理。
body_filter_by_lua* :将 Lua 代码嵌入到响应包体过滤阶段中,用于应答体过滤处理。需要注意的是,此阶段可能在一个请求中被调用多次,因为响应体可能以块的形式传递。因此,该指令中指定的 Lua 代码也可以在单个 HTTP 请求的生命周期内运行多次。
OpenResty 快速体验
在了解了 OpenResty 的架构组成和基本工作原理后,我们通过一个简单的例子来上手 OpenResty,以我们工作用的 Mac 系统来进行。
安装 OpenResty
创建工作目录
创建 nginx 配置文件
在 conf 工作目录下,创建 nginx 配置文件 nginx.conf ,配置内容如下:
启动服务
没有报错的话,说明 OpenResty 已经启动成功了。可以通过浏览器或者 curl 命令发起请求:
这就是一个最简单的基于 OpenResty 的服务开发过程,只在 Nginx HTTP 请求的 11 个阶段中的 content 阶段嵌入了 Lua 代码,直接生成了请求响应体。
OpenResty 在得物的应用
当前基础架构团队基于 OpenResty 开发了流量路由组件(API-ROUTE)用于异地多活和小得物项目,该组件主要通过识别请求中的用户 ID,根据路由规则进行动态路由,也实现了基于客户端 IP 和用户 ID 的灰度导流,后续根据规划将承担更多角色。
上面那个简单的 Demo 是不是挺简单,有没有想起编程语言入门 Demo Hello World?Hello World 看似简单,但其隐藏在背后的执行过程可没那么简单!同样的,OpenResty 也没我们看到的那么单纯!它的背后隐藏了非常多的文化和技术细节。。懂得都懂。。
最后欢迎对 OpenResty 有兴趣的同学一起交流学习进步。
参考及学习列表
文/郭先生
关注得物技术,携手走向技术的云端
版权声明: 本文为 InfoQ 作者【得物技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/6239da28226043f3322aed832】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论