内容简介:OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台。我们知道开发 Nginx 的模块需要用 C 语言,同时还要熟悉它的源码,成本和门槛比较高。国人章亦春把 LuaJIT VM 嵌入到了 Nginx 中,使得可以直接通过 Lua 脚本在 Nginx 上进行编程,同时还提供了大量的类库(如:lua-resty-mysql lua-resty-redis 等),直接把一个 Nginx 这个 Web Server 扩展成了一个 Web 框架,借助于 Nginx 的高性能,能够快速地构
作者 | 黄超
杏仁运维工程师,关注容器技术和自动化运维。
OpenResty 简介
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台。我们知道开发 Nginx 的模块需要用 C 语言,同时还要熟悉它的源码,成本和门槛比较高。国人章亦春把 LuaJIT VM 嵌入到了 Nginx 中,使得可以直接通过 Lua 脚本在 Nginx 上进行编程,同时还提供了大量的类库(如:lua-resty-mysql lua-resty-redis 等),直接把一个 Nginx 这个 Web Server 扩展成了一个 Web 框架,借助于 Nginx 的高性能,能够快速地构造出一个足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
Nginx 采用的是 master-worker 模型,一个 master 进程管理多个 worker 进程,worker 真正负责对客户端的请求处理,master 仅负责一些全局初始化,以及对 worker 进行管理。在 OpenResty 中,每个 worker 中有一个 Lua VM,当一个请求被分配到 worker 时,worker 中的 Lua VM 里创建一个 coroutine(协程) 来负责处理。协程之间的数据隔离,每个协程具有独立的全局变量 _G
。
OpenResty 处理请求流程
由于 Nginx 把一个请求分成了很多阶段,第三方模块就可以根据自己的行为,挂载到不同阶段处理达到目的。OpenResty 也应用了同样的特性。不同的阶段,有不同的处理行为,这是 OpenResty 的一大特色。OpenResty 处理一个请求的流程参考下图(从 Request start 开始):
指令 | 使用范围 | 解释 |
---|---|---|
int_by_lua* init_worker_by_lua* |
http | 初始化全局配置/预加载Lua模块 |
set_by_lua* |
server,server if,location,location if | 设置nginx变量,此处是阻塞的,Lua代码要做到非常快 |
rewrite_by_lua* |
http,server,location,location if | rewrite阶段处理,可以实现复杂的转发/重定向逻辑 |
access_by_lua* |
http,server,location,location if | 请求访问阶段处理,用于访问控制 |
content_by_lua* |
location, location if | 内容处理器,接收请求处理并输出响应 |
header_filter_by_lua* |
http,server,location,location if | 设置 heade 和 cookie |
body_filter_by_lua* |
http,server,location,location if | 对响应数据进行过滤,比如截断、替换 |
log_by_lua |
http,server,location,location if | log阶段处理,比如记录访问量/统计平均响应时间 |
更多详情请参考官方文档
配置 OpenResty
OpenResty 的 Lua 代码是提现在 nginx.conf
的配置文件之中的,可以与配置文件写在一起,也可以把 Lua 脚本放在一个文件中进行加载:
内联在 nginx.conf
中:
server { ... location /lua_content { # MIME type determined by default_type: default_type 'text/plain'; content_by_lua_block { ngx.say('Hello,world!') } } .... }
通过加载 lua 脚本的方式:
server { ... location = /mixed { rewrite_by_lua_file /path/to/rewrite.lua; access_by_lua_file /path/to/access.lua; content_by_lua_file /path/to/content.lua; } .... }
OpenResty 变量的共享范围
全局变量
在 OpenResty 中,只有在 init_by_lua*
和 init_worker_by_lua*
阶段才能定义真正的全局变量。因为在其他阶段,OpenResty 会设置一个隔离的全局变量表,以免在处理过程中污染了其他请求。即使在上述两个阶段可以定义全局变量,也尽量避免这么做。全局变量能解决的问题,用模块变量也能解决,而且会更清晰,干净。
模块变量
这里将定义在 Lua 模块中的变量称为模块变量。Lua VM 会将 require
进来的模块换成到 package.loaded
table 里,模块里的变量都会被缓存起来,在同一个 Lua VM下,模块中的变量在每个请求中是共享的,这样就可以避免使用全局变量来实现共享了,看下面一个例子:
nginx.conf
worker_processes 1; ... location { ... lua_code_cache on; default_type "text/html"; content_by_lua_file 'lua/test_module_1.lua' }
lua/test_module_1.lua
local module1 = require("module1") module1.hello()
lua/module1.lua
local count = 0 local function hello() count = count + 1 ngx.say("count: ", count) end local _M = { hello = hello } return _M
当通过浏览器访问时,可以看到 count 输出是一个递增的,这也说明了在 lua/module1.lua
的模块变量在每个请求中时共享的:
count: 1 count: 2 .....
另外,如果 worker_processes
的数量大于 1 时呢,得到的结果可能就不一样了。因为每个 worker 中都有一个 Lua VM 了,模块变量仅在同一个 VM 下,所有的请求共享。如果要在多个 Worker 进程间共享请考虑使用 ngx.shared.DICT 或如 Redis 存储了。
本地变量
跟全局变量,模块变量相对,我们这里姑且把 *_by_lua*
里定义的变量称为本地变量。本地变量仅在当前阶段有效,如果需要跨阶段使用,需要借助 ngx.ctx
或者附加到模块变量里。
这里我们使用了 ngx.ctx
表在三个不同的阶段来传递使用变量 foo
:
location /test { rewrite_by_lua_block { ngx.ctx.foo = 76 } access_by_lua_block { ngx.ctx.foo = ngx.ctx.foo + 3 } content_by_lua_block { ngx.say(ngx.ctx.foo) } }
额外注意,每个请求,包括子请求,都有一份自己的 ngx.ctx 表。例如:
location /sub { content_by_lua_block { ngx.say("sub pre: ", ngx.ctx.blah) ngx.ctx.blah = 32 ngx.say("sub post: ", ngx.ctx.blah) } } location /main { content_by_lua_block { ngx.ctx.blah = 73 ngx.say("main pre: ", ngx.ctx.blah) local res = ngx.location.capture("/sub") ngx.print(res.body) ngx.say("main post: ", ngx.ctx.blah) } }
访问 GET /main 输出:
main pre: 73 sub pre: nil # 子请求中并没有获取到父请求的变量 $pre sub post: 32 main post: 73
性能开关 lua_code_cache
开启或关闭在 *_by_lua_file
(如: set_by_lua_file
, content_by_lua_file
) 指令中以及 Lua 模块中 Lua 代码的缓存。
若关闭,ngx_lua 会为每个请求创建一个独立的 Lua VM,所有 *_by_lua_file
指令中的代码将不会被缓存到内存中,并且所有的 Lua 模块每次都会从头重新加载。在开发模式下,这给我们带来了不需要 reload
nginx 就能调试的便利性, 但是在生成环境下,强烈建议开启。 若关闭,即使是一个简单的 Hello World 都会慢上一个数量级(每次 IO 读取和编译消耗很大)。
但是,那些直接写在 nginx.conf
配置文件中的 *_by_lua_block
指令下的代码不会在你编辑下实时更新,只有发送 HUP
信号给 Nginx 才能能够重新。
小案例
通过 OpenResty + Redis 实现动态路由
Nginx 经常用来作为反向代理服务器。通常情况下,我们将后端的服务配置在 Nginx 的 upstream
中,当后端服务有变更时就去修改 upstream
中的配置再通过 reload
的方式使其生效。这个操作如果在后端服务经常发生变更的情况下,操作起来就会显得有些繁琐了。现在利用 Lua + Redis 的方式将 upstream
中的配置放在 Redis 中,以实现动态配置的效果。
架构图
原理:
在求请求访问阶段处理(access_by_lua*)通过指定的规则(这个规则根据自己的需求去设计)从 Redis 中去获取相对应的后端服务地址去替换 Nginx 配置中的 proxy_pass
的地址。
流程:
-
在 Nginx 配置中创建后端服务地址的变量
$backend_server
。
server {listen 80 ; server_name app1 . example . com ; location / { ... set $ backend_server '' ; } }
同时在 Redis 中存入后端服务的地址。
set app1 10.10.10.10:8080
-
使用 ngx_redis2 模块来实现一个读取 Redis 的接口。
# GET /get?key=some_keylocation = / get { internal ; # 保护这个接口只运行内部调用 set_unescape_uri $ key $ arg_key ; # this requires ngx_set_misc redis2_query get $ key ; redis2_pass foo . com : 6379 ; # redis_server and port }
2. 在求请求访问阶段处理利用 ngx.location.capture 模块请求去上个阶段定义的 Redis 接口,并将结果替换 $backend_server
。
location / { ... access_by_lua_block { local rds_key = "app1" # 从 redis 中获取 key 为 app1 对应的 server_ip local res = ngx.location.capture('/get', { args = {key = rds_key}}) # 解析 redis 结果 local parser = require("redis.parser") local server, typ = parser.parse_reply(res.body) if typ ~= parser.BULK_REPLY or not server then ngx.log(ngx.ERR, "bad redis response: ", res.body) ngx.exit(500) end ngx.var.backend_server = server } }
3.Nginx 转发阶段将请求转发至后端服务。
location / { ... access_by_lua_block {...}; proxy_pass http://$backend_server; }
最后,推荐两个基于 OpenResty 的比较实用的两个开源项目:
-
基于动态策略的灰度发布系统 ABTestingGateway
-
基于ngx_lua的web应用防火墙 ngx_lua_waf
参考
-
OpenResty Best Practices( https://legacy.gitbook.com/book/moonbingbing/openresty-best-practices/details )
-
lua-nginx-module( https://www.nginx.com/resources/wiki/modules/lua/ )
-
Nginx Lua Directives( https://github.com/openresty/lua-nginx-module#directives )
-
Nginx API for Lua( https://github.com/openresty/lua-nginx-module#nginx-api-for-lua )
-
Lua 简明教程( https://coolshell.cn/articles/10739.html )
全文完
以下文章您可能也会感兴趣:
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。
杏仁技术站
长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript实战手册
David Sawyer McFarland / 李强 / 机械工业出版社 / 2009 / 89.00元
在《JavaScript实战手册》中,畅销书作者David McFarland教你如何以高级的方式使用JavaScript,即便你只有很少或者没有编程经验。一旦掌握了这种语言的结构和术语,你将学习如何使用高级的JavaScript工具来快速为站点添加有用的交互,而不是一切从头开始编写脚本。和其他的Missing Manuals图书不同,《JavaScript实战手册》清楚、精炼,手把手地讲解。 ......一起来看看 《JavaScript实战手册》 这本书的介绍吧!