如何应对 OpenResty 为支持 ARM64 引入的 break change

长安长夜Saint 2019-07-01

本文不是关于新版 OpenResty 如何支持 ARM64 的,而是关于如何应对这一过程引入的 break change。

另外,如果你没用 OpenResty 自己的 LuaJIT 分支,那么可以直接关掉这个页面了,因为这些 break change 只有在使用了 OpenResty 自己的 LuaJIT 分支才会出现。

一切的根源在于,新版本的 OpenResty 把当前请求的 ngx_http_request_t 放到了 luaStateexdata 属性里面,不再使用 getfenv(0).__ngx_req 这种方式了。exdata 是 OpenResty 自己的 LuaJIT 分支加的属性,所以如果不用 OpenResty 自己的 LuaJIT 分支,依旧还是得走 getfenv(0).__ngx_req这种方式。同样被移除的还有 getfenv(0).__ngx_cycle 和给每个 main thread 准备的全局环境表。接下来我们谈谈如何应对这几个变化。

getfenv(0).__ngx_req

新版 OpenResty 使用 resty.core.base 里面的 get_request() 代替了 getfenv(0).__ngx_req。在你的代码里,可以这么写:

local base = require "resty.core.base"
local get_request = base.get_request
if not get_request then
    get_request = function()
        return getfenv(0).__ngx_req
    end
end

...
local r = get_request()

但是! get_request() 并非 100% 兼容 getfenv(0).__ngx_req。前者返回的是一个 cdata,而后者返回的是一个 lightuserdata。cdata 和 lightuserdata 在语义上有些微妙的不同。当你用 lightuserdata 作为 table 的 hash key 时,如果 lightuserdata 指向的地址相同,那么 hash 值会相同。但如果是 cdata,即使是指向同一地址的指针类型的 cdata,由于计算 hash 时用的是 cdata 的地址,而非其内部的值,所以不同的 cdata 的 hash 值会不一样。举个例子:

local r = get_request()
local h = {}
h[r] = 1
ngx.say(h[get_request()])

在之前的版本里,两次 get_request() 会返回同一个地址(都是同一个请求嘛),所以会输出 1。而新的 OpenResty 里,你会发现输出结果是 nil。这是因为两次 get_request() 会创造两个 cdata 对象,这两个对象虽然值一样,但是内存地址不一样,所以 hash 值不一样。

那怎么解决呢?我们可以实现一个转换函数,把 cdata 的值变成某种可用作 hash key 的类型。一个简单的解决方法是加上 tostring。tostring(cdata) 的输出中会包含 cdata 指向的地址,这样同一个请求对应的 key 就会相同。

考虑到 LuaJIT 创建字符串的开销比较大,作为一种优化手段,在某些架构下我们可以用 tonumber(ffi_cast("intptr_t", cdata)) 代替。之所以限定在某些架构,是因为 LuaJIT 的 Number 其实是 double,而不是 int64,所以对于某些 64 位的架构,不一定能得到正确的输出。好在 x64 的用户态空间地址不会超过 48 位,所以我们可以在主流的 x64 服务器上采用该优化。当然前提是你没有启用 5 级页表。考虑到只有数百 TB 内存的机器才会有开启 5 级页表的需要,大体上你可以放心地认为你的 x64 环境不会遇到这样的问题。即使开启了 5 级页表,现阶段 48 位以上的内存地址也不是默认可用的。关于 5 级页表的更多上下文,可以看下这两个链接:

https://lwn.net/Articles/717293/
https://www.kernel.org/doc/Do...

getfenv(0).__ngx_cycle

有些 Lua 代码会通过 getfenv(0).__ngx_cycle 获取 ngx_cycle, 然后通过 FFI 调用传给 C 函数。其实直接在 C 函数里面访问 ngx_cycle 就可以了,不需要经过 Lua 这一层。

你可能会问,reload 的时候,init 阶段下 ngx_cylce 应该会指向旧的 ngx_cycle 吧?这里 OpenResty 做了点手脚。它会把旧的 ngx_cycle 放到 saved_ngx_cycle 里面来,让 ngx_cycle 指向新构建的 ngx_cycle_t *cycle。所以并不需要特殊的对待。

每个 main thread 准备的全局环境表

为了放下 getfenv(0).__ngx_req,过去的 OpenResty 需要给每个 main thread 准备独立的全局环境表,这样每个请求的 getfenv(0) 才会返回不同的 table。既然新的 OpenResty 已经不需要 getfenv(0).__ngx_req,这些全局环境表就能干掉了。

不过让它们下岗,还有点副作用。过去在 rewrite/access/content 等阶段里定义了全局变量(通常是手误引入的),不会污染到其他的请求。那是因为 OpenResty 设置了全局环境表,这些全局变量只会影响到它们所在的全局环境表。但是移除了全局环境表的保护后,这些全局变量就能肆无忌惮地跑来跑去。为此新版 OpenResty 加了个 guard,如果在这些阶段里遇到全局变量的定义,会打印这样的错误信息:

2019/04/30 11:01:18 [warn] 26843#26843: *240 [lua] _G write guard:12: __newindex(): writing a global lua variable ('xxx') which may lead to race conditions between concurrent requests, so prefer the use of 'local' variables
stack traceback:
...

这种错误信息对程序的流程没有影响,但对性能有影响。解决办法?把全局变量一个个都揪出来解决掉。

当然如果你是在 initinit_worker 阶段定义全局变量,并不会触发这个 guard。毕竟这么做的人一般是故意的,在过去的 OpenResty 里这也是标准的“使用”全局变量的方式。虽然我个人不推荐这么做。用全局变量,迟早都要还的。

相关推荐