关于openresty:得物技术初探OpenResty

4次阅读

共计 5186 个字符,预计需要花费 13 分钟才能阅读完成。

简介

Nginx 的高性能是业界公认的,近年来在寰球服务器市场上的占比份额也在逐年减少,在国内出名互联网公司也有宽泛的利用,阿里还基于 Nginx 进行扩大打造了驰名的 Tengine。而 OpenResty 是由国人章亦春基于 Nginx 和 LuaJIT 打造的动静 web 平台,LuaJIT 是 Lua 编程语言的即时编译器。Lua 是一种弱小、动静、轻量级的编程语言。该语言的设计目标是为了嵌入应用程序中,从而为应用程序提供灵便的扩大和定制性能,OpenResty 就是通过应用 Lua 来扩大 Nginx 来实现的可扩大 Web 平台。目前 OpenResty 大多用在 API 网关的开发中,当然也能够用来代替 Nginx,用于反向代理和负载平衡的场景。

OpenResty 的架构组成

如前所述,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 形式的性能更优。

上面是一个简略的调用示例:

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

文 / 郭先生

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

正文完
 0