为 OpenResty 项目编写自定义 Nginx C 模块

spacewander

有些时候,我们需要通过 Lua 代码操作 Nginx 里面的某些状态,但是想要的 API 并不存在于 OpenResty 之内。这时候,可以选择编写一个 Nginx C 模块,然后暴露出可供 Lua 调用的接口。
本文中,我们会分别探讨,如何通过 Nginx 变量或 FFI 的方式去提供 Lua 调用得到的接口。

文中的示例代码可以在 ngx_http_example_or_module 找到。

通过 Nginx 变量提供接口

ngx.var.variable= 在调用的时候,会先查找变量 variable 对应的 handler(一个在 Nginx 内注册的 C 函数),如果 handler 存在,会去调用该 handler。
这意味着,如果我们定义了一个 Nginx 变量和对应的 handler,我们就可以通过在 Lua 代码里调用 ngx.var.variable= 来触发该 handler。

空说无益,先上示例。

在 Nginx 里面我们可以通过 limit_ratelimit_rate_after 两个指令来限制响应给客户端的速率。前者决定了限速的多少,后者决定了从什么时候开始限速。当然更多的时候我们需要动态去调整这两个指标。

limit_rate 对应有一个 Nginx 内置的变量, $limit_rate,我们可以修改该变量来达到动态调整的目的。相关的 Lua 代码是 ngx.var.limit_rate = limit_rate。但是并不存在 $limit_rate_after 这样一个变量。

不用担心。因为我们可以自己加上。

// ngx_http_example_or_module.c
// 定义变量和它的 getter/setter
static ngx_http_variable_t  ngx_http_example_or_variables[] = {
    { ngx_string("limit_rate_after"), ngx_http_variable_request_set_size,
      ngx_http_variable_request_get_limit_rate_after,
      offsetof(ngx_http_request_t, limit_rate_after),
      NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_NOCACHEABLE, 0 },
    { ngx_null_string, NULL, NULL, 0, 0, 0 }
};

// getter 和 setter 的实现在 GitHub 上的示例代码里有,这里就不贴上了。

通过 FFI 提供接口

不过在大多数情况下,我们并不需要借助变量来间接调用 Nginx C 函数。我们完全可以借助 LuaJIT 的 FFI,直接调用 Nginx C 函数。

lua-resty-murmurhash2 就是一个现成的例子。

下面让我们再看另外一个例子,通过 Lua 代码来获取当前的 Nginx 错误日志等级。

在开发中,我们有时需要在测试环境中通过日志来记录某个 table 的值,比如 ngx.log(ngx.INFO, cjson.encode(res))
在生产环境里,我们会设置日志等级为 error,这样就不会输出 table 的值。但是日志等级无论是多少,cjson.encode 都是必然会被调用的。
不幸的是,这行代码所在的路径非常热,我们需要避免无谓的 json encode 操作。如果能获取实际的日志等级,判断是否为 error,来决定是否调用
cjson.encode,就能省下这一笔开销。

要实现这一功能,仅需加个获取当前配置的日志等级的 Nginx C 函数和对应的 Lua 接口。

我们可以像这样提供一个 Lua 接口:

-- lib/example_or.lua
...

if not pcall(ffi.typeof, "ngx_http_request_t") then
    ffi.cdef[[
        struct ngx_http_request_s;
        typedef struct ngx_http_request_s  ngx_http_request_t;
    ]]
end

ffi.cdef[[
int ngx_http_example_or_ffi_get_error_log_level(ngx_http_request_t *r);
]]

function _M.get_error_log_level()
    local r = getfenv(0).__ngx_req
    return tonumber(C.ngx_http_example_or_ffi_get_error_log_level(r))
end

对应的 Nginx C 函数很简单:

int
ngx_http_example_or_ffi_get_error_log_level(ngx_http_request_t *r)
{
    ngx_log_t                   *log;
    int                          log_level;

    if (r && r->connection && r->connection->log) {
        log = r->connection->log;

    } else {
        log = ngx_cycle->log;
    }

    log_level = log->log_level;
    if (log_level == NGX_LOG_DEBUG_ALL) {
        log_level = NGX_LOG_DEBUG;
    }

    return log_level;
}

使用时直接拿它跟特定的 Nginx 日志等级常量比较即可:

-- config.lua
-- 目前 Nginx 不支持动态变更日志等级,所以可以把日志等级缓存起来
local example_or = require "lib.example_or"
_M.log_leve = example_or.get_error_log_level()


-- in other file
local config = require "common.config"
local log_level = config.log_level
if log_level >= ngx.WARN then
    -- 错误日志等级是 warn 或者 info 一类
    ngx.log(ngx.WARN, "log a warning event")
else
    -- 错误日志等级是 error 一类
    ngx.log(ngx.WARN, "do not log another warning event")
end
阅读 4.3k

spacewander
这个专栏什么都有,大部分都是关于Linux或后端开发的

make building blocks that people can understand and use easily, and people will work together to ...

5.4k 声望
1.4k 粉丝
0 条评论

make building blocks that people can understand and use easily, and people will work together to ...

5.4k 声望
1.4k 粉丝
文章目录
宣传栏