得物技术初探OpenResty

简介

Nginx 的高性能是业界公认的,近年来在全球服务器市场上的占比份额也在逐年增长,在国内知名互联网公司也有普遍的应用,阿里还基于Nginx进行扩展打造了著名的Tengine。而OpenResty是由国人章亦春基于Nginx和LuaJIT打造的动态web平台,LuaJIT是Lua编程语言的即时编译器。Lua是一种强大、动态、轻量级的编程语言。该语言的设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能,OpenResty就是经过使用Lua来扩展Nginx来实现的可扩展Web平台。目前OpenResty 大多用在 API 网关的开发中,固然也能够用来替代Nginx,用于反向代理和负载均衡的场景。nginx

OpenResty 的架构组成

如前所述,OpenResty 底层是基于Nginx 和 LuaJIT 的,因此 OpenResty 继承了 Nginx 的多进程架构, 每个 Worker 进程都是 fork Master 进程而获得的, 其实, Master 进程中的 LuaJIT 虚拟机也会一块儿 fork 过来。在同一个 Worker 内的全部协程,都会共享这个 LuaJIT 虚拟机,Lua 代码的执行也是在这个虚拟机中完成的。而在同一个时间点上,每一个 Worker 进程只能处理一个用户的请求,也就是只有一个协程在运行。git

Nginx

因为 Nginx 处理请求采用的是事件驱动模型,因此每个 Worker进程最好独占一个CPU。实践中咱们每每把 Worker 进程的数量配置成与CPU核数相同,此外把每个 Worker 进程与某一个CPU核绑定在一块儿,这样能够更好的使用每个CPU核上的CPU缓存,减小缓存失效的命中率,进而提升请求处理的性能。github

LuaJIT

其实 OpenResty 最初默认使用的是标准Lua,从 1.5.8.1 版本开始才默认使用 LuaJIT,背后的缘由是由于 LuaJIT 相比标准Lua有很大的性能优点。web

首先,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 方式的性能更优。segmentfault

下面是一个简单的调用示例:浏览器

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")

短短这几行代码,就能够直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。相似的,咱们能够用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。缓存

OpenResty 的工做原理

OpenResty 是基于Nginx的高性能Web平台,因此其高效运行与Nginx密不可分。服务器

Nginx 处理HTTP请求有11个执行阶段,咱们能够从 ngx_http_core_module.h 的源码中看到:数据结构

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,

    NGX_HTTP_SERVER_REWRITE_PHASE,

    NGX_HTTP_FIND_CONFIG_PHASE,
    NGX_HTTP_REWRITE_PHASE,
    NGX_HTTP_POST_REWRITE_PHASE,

    NGX_HTTP_PREACCESS_PHASE,

    NGX_HTTP_ACCESS_PHASE,
    NGX_HTTP_POST_ACCESS_PHASE,

    NGX_HTTP_PRECONTENT_PHASE,

    NGX_HTTP_CONTENT_PHASE,

    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

巧合的是,OpenResty 也有 11 个 *_by_lua 指令,它们和 NGINX 的11个执行阶段有很大的关联性。指令是使用Lua编写Nginx脚本的基本构建块,用于指定用户编写的Lua代码什么时候运行以及运行结果如何使用等。下图显示了不一样指令的执行顺序,这张图能够帮助理清咱们编写的脚本是按照怎样的逻辑运行的。

其中, 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

$ brew tap openresty/brew
$ brew install openresty

建立工做目录

$ mkdir ordemo
$ cd ordemo
$ mkdir logs/ conf/

建立nginx配置文件

在 conf 工做目录下,建立 nginx配置文件 nginx.conf ,配置内容以下:

error_log logs/error.log debug;
pid logs/nginx.pid;

events {
    worker_connections 1024;
}

http {
    access_log logs/access.log

    server {
        listen 8080;
        location / {
            content_by_lua '
                ngx.say("Welcome to OpenResty!")
            ';
        }
    }
}

启动服务

$ cd ordemo
$ openresty -p `pwd` -c conf/nginx.conf

# 中止服务
$ openresty -p `pwd` -c conf/nginx.conf -s stop

没有报错的话,说明 OpenResty 已经启动成功了。能够经过浏览器或者 curl 命令发起请求:

$ curl -i 127.0.0.1:8080
HTTP/1.1 200 OK
Server: openresty/1.19.3.1
Date: Tue, 29 Jun 2021 08:55:51 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

Welcome to OpenResty!

这就是一个最简单的基于 OpenResty 的服务开发过程,只在 Nginx HTTP 请求的11个阶段中的 content 阶段嵌入了 Lua 代码,直接生成了请求响应体。

OpenResty 在得物的应用

当前基础架构团队基于 OpenResty 开发了流量路由组件(API-ROUTE)用于异地多活和小得物项目,该组件主要经过识别请求中的用户ID,根据路由规则进行动态路由,也实现了基于客户端IP和用户ID的灰度导流,后续根据规划将承担更多角色。

上面那个简单的Demo是否是挺简单,有没有想起编程语言入门Demo Hello World?Hello World 看似简单,但其隐藏在背后的执行过程可没那么简单!一样的,OpenResty 也没咱们看到的那么单纯!它的背后隐藏了很是多的文化和技术细节。。懂得都懂。。

最后欢迎对OpenResty有兴趣的同窗一块儿交流学习进步。

参考及学习列表

Nginx核心知识150讲

OpenResty从入门到实战

OpenResty 官网

OpenResty API

awesome-resty

文/郭先生

关注得物技术,携手走向技术的云端

相关文章
相关标签/搜索