陈云佳 2020-04-21
在 Lua 中, table 是唯一的数据结构。共享内存字典shared dict, 是在 OpenResty 编程中最为重要的数据结构。它不仅支持数据的存放和读取,还支持原子计数和队列操作。
基于 shared dict,可以实现多个 worker 之间的缓存和通信,以及限流限速、流量统计等功能。可以把 shared dict 当作简单的 Redis 来使用,只不过 shared dict 中的数据不能持久化,所以存放在其中的数据,一定要考虑到丢失的情况。
在编写OpenResty Lua 代码的过程中,会遇到,在一个请求的不同阶段、不同 worker 之间共享数据的情况,还可能需要在 Lua 和 C 代码之间共享数据。
OpenResty 中常见的几种数据共享的方法有如下几种:
它可以在 Nginx C 模块之间共享数据,也可以在 C 模块和 OpenResty 提供的 lua-nginx-module 之间共享数据
location /foo { set $my_var ‘‘; # this line is required to create $my_var at config time content_by_lua_block { ngx.var.my_var = 123; ... } }
使用 Nginx 变量这种方式来共享数据是比较慢的,因为它涉及到 hash 查找和内存分配。同时,这种方法有其局限性,只能用来存储字符串,不能支持复杂的 Lua 类型。
它其实就是一个普通的 Lua 的 table,所以速度很快,还可以存储各种 Lua 的对象。它的生命周期是请求级别的,当一个请求结束的时候,ngx.ctx 也会跟着被销毁掉。
一个典型的使用场景用 ngx.ctx 来缓存 Nginx 变量这种昂贵的调用,并在不同阶段都可以 使用到它:
location /test { rewrite_by_lua_block { ngx.ctx.host = ngx.var.host } access_by_lua_block { if (ngx.ctx.host == ‘openresty.org‘) then ngx.ctx.host = ‘test.com‘ end } content_by_lua_block { ngx.say(ngx.ctx.host) } }
如果使用 curl 访问:
curl -i 127.0.0.1:8080/test -H ‘host:openresty.org‘
就会打印出 test.com,可以表明 ngx.ctx 的确是在不同阶段共享了数据。当然,还可以自己动手修改上面的例子,保存 table 等更复杂的对象,而非简单的字符串。
需要注意的是,正因为 ngx.ctx 的生命周期是请求级别的,所以它并不能在模块级别进行缓存。比如,在 foo.lua 文件中这样使用就是错误的:
local ngx_ctx = ngx.ctx local function bar() ngx_ctx.host = ‘test.com‘ end
应该在函数级别进行调用和缓存:
local ngx = ngx local function bar() ngx_ctx.host = ‘test.com‘ end
-- mydata.lua local _M = {} local data = { dog = 3, cat = 4, pig = 5, } function _M.get_age(name) return data[name] end return _M
在 nginx.conf 的配置如下:
location /lua { content_by_lua_block { local mydata = require "mydata" ngx.say(mydata.get_age("dog")) } }
在这个示例中,mydata 就是一个模块,它只会被 worker 进程加载一次,之后,这个 worker 处理的所有 请求,都会共享 mydata 模块的代码和数据。
mydata 模块中的 data 这个变量,就是 模块级别的变量,它位于模块的 top level,也就是模块最 开始的位置,所有函数都可以访问到它。
可以把需要在请求间共享的数据,放在模块的 top level 变量中。一般我们只用这种方式来保存只读的数据。如果涉及到写操作,可能会有 race condition,这是非常难以定位的 bug。
这种方法是基于红黑树实现的,性能很好,但也有自己的局限性——你必须事先在 Nginx 的配置文件中, 声明共享内存的大小,并且这不能在运行期更改:
lua_shared_dict dogs 10m;
shared dict 同样只能缓存字符串类型的数据,不支持复杂的 Lua 数据类型。这也就意味着,当需要存放 table 等复杂的数据类型时,将不得不使用 json 或者其他的方法,来序列化和反序列化,这自然会带来不小的性能损耗。
前面三种数据共享的范围都是在请求级别,或者单个 worker 级别。所以,在当前的 OpenResty 的实现中,只有 shared dict 可以完成 worker 间的数据共享,并借此实现 worker 之间的通信,
共享字典本身,它对外提供了 20多个 Lua API,不过所有的这些 API 都是原子操作,不用担心多个 worker 和高并发的情况下的竞争问题。
字典读写类的 API,是共享字典最常用的功能。下面是一个最简单的示例:
$ resty --shdict=‘dogs 1m‘ -e ‘local dict = ngx.shared.dogs dict:set("Tom", 56) print(dict:get("Tom"))‘
除了 set 外,OpenResty 还提供了 safe_set、add、safe_add、replace 这四种写入的方法。这里 safe 前缀的含义是,在内存占满的情况下,不根据 LRU 淘汰旧的数据,而是写入失败并返回 no memory 的错误信息。
除了 get 外,OpenResty 还提供了 get_stale 的读取数据的方法,相比 get 方法,它多了一个过期数据的返回值:
value, flags, stale = ngx.shared.DICT:get_stale(key)
它是 OpenResty 后续新增的功能,提供了和 Redis 类似的接口。队列中的每一个元素, 都用 ngx_http_lua_shdict_list_node_t 来描述