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

有些时候,我们需要通过 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_rate 和 limit_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
标签:OpenrestyNginx 发布于:2019-11-12 21:58:02