nginx-rtmp-module 的缺陷分析(四)

乐正辰阳
2023-12-01

Nginx 一般情况下都是以多进程方式(一个 master 进程和多个 worker 进程)运行的,但是 nginx-rtmp-module 模块对多进程方式的支持很不成熟。

前面的文章提到过以多进程方式运行 Nginx 时,由于 nginx-rtmp-module 本身不支持 Vhost 功能,再加上它使用 Unix domain socket(没有端口信息)转发媒体流到其他的 worker 进程,导致其他的 worker 进程无法知道转发的媒体流需要匹配的是哪个 server 配置。

Nginx 从 1.9.11 版本开始,在类 Unix 系统上支持将第三方模块编译成动态模块,可以在运行时加载它们。这又引出多进程方式下运行 nginx-rtmp-module 的另一个问题。

这个问题产生的背景是:用户 A 开发了自己的第三方模块(不开源),用户 B 在 nginx-rtmp-module 的基础上开发了一些新的功能(也不开源),用户 A 想在最新稳定版本的 Nginx(1.18.0)中使用用户 B 开发的新功能,用户 B 告诉用户 A,你把你的第三方模块编译成动态模块,然后你告诉我除了你编译的动态模块之外的编译参数,我把我的第三方模块也编译成动态模块,这样我(用户 B)就可以在不用获得你(用户 A)自己开发的第三方模块前提下也能编译出与你(用户 A)的 Nginx 二进制兼容的第三方模块了。

用户 A 照做了,用户 B 将编译好的基于 nginx-rtmp-module 开发的模块发给用户 A,运行没有问题,直到用户 A 以多进程方式运行 Nginx,然后发现连接到中继 worker 进程(就是非接受推流请求的 worker 进程)的播放请求无法播放。

总结一下复现问题的步骤就是:

1. Nginx 的版本至少是 1.9.11,并且在类 Unix 系统上编译(Windows 到目前为止不支持将 Nginx 第三方模块编译成动态模块)。

2. 基于 nginx-rtmp-module 开发的模块(例如 nginx-http-flv-module,笑)被编译成动态模块。

3. 以多进程方式运行 Nginx。

这个问题从 nginx-http-flv-module 开源以来尚没有人反馈过,发现也有一段时间了,确认 nginx-rtmp-module 也有这个问题,最近才找到导致这个问题的原因,下面都以 nginx-http-flv-module 为例说明。

Nginx 会将模块按照一定的顺序存放在一个数组中,这个数组是在执行 configure 时生成的,位于 Nginx 源代码根目录的 objs 目录下的 ngx_modules.c 中,名为 ngx_modules。如果把 nginx-http-flv-module 编译进 Nginx 中,ngx_modules 大概是这个样子:

ngx_module_t *ngx_modules[] = {
    &ngx_core_module,
    &ngx_errlog_module,
    &ngx_conf_module,
    &ngx_rtmp_module,
    &ngx_rtmp_core_module,
    ...,
    &ngx_rtmp_notify_module,
    ...,
    &ngx_events_module,
    &ngx_event_core_module,
    ...,
    NULL
};

如果把 nginx-http-flv-module 编译成动态模块,ngx_modules 大概是这个样子:

ngx_module_t *ngx_modules[] = {
    &ngx_core_module,
    &ngx_errlog_module,
    &ngx_conf_module,
    &ngx_openssl_module,
    &ngx_regex_module,
    &ngx_events_module,
    &ngx_event_core_module,
    ...,
    NULL
};

同时还会在 objs 目录下生成一个名为 ngx_http_flv_module_modules.c 的文件,这个文件中也有一个名为 ngx_modules 的数组:

ngx_module_t *ngx_modules[] = {
    &ngx_rtmp_module,
    &ngx_rtmp_core_module,
    ...,
    NULL
};

二者的差异是将模块编译进 Nginx 时,ngx_modules.c 中的 ngx_modules 数组中已经包含了 ngx_rtmp_* 模块,而且它们都在 ngx_event_core_module 模块之前。但是将模块编译成动态模块时,ngx_modules.c 中的 ngx_modules 数组中没有包含 ngx_rtmp_* 模块,它们出现在 ngx_http_flv_module_modules.c 中的 ngx_modules 数组中。

Nginx 启动后,会首先按照 ngx_modules 数组中的模块顺序执行一些初始化动作,如果有动态模块存在,Nginx 会将动态模块添加到已经存在的数组末尾,然后执行这些模块的初始化动作。先记住它们的位置关系,下文会回到这儿。

Nginx 启动 worker 进程后,会在函数:

ngx_worker_process_init

中调用每个模块的 init_process 回调函数:

    for (i = 0; cycle->modules[i]; i++) { /* 这里的 cycle->modules 数组存放了上述的所有模块 */
        if (cycle->modules[i]->init_process) {
            if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) {
                /* fatal */
                exit(2);
            }
        }
    }

为了不陷入源代码的细节中,只简要说明一下 ngx_event_core_module 的 init_process 回调函数的功能之一:将 Nginx 创建的所有 socket 的读事件加入到读事件监听集合中。

回过头去看上文讲到的 Nginx 执行模块的初始化动作的顺序,可以发现将 nginx-http-flv-module 编译进 Nginx 时,ngx_event_core_module 是在 ngx_rtmp_*  模块之后,而将 nginx-http-flv-module 编译成动态模块时,ngx_event_core_module 是在 ngx_rtmp_* 模块之前。

上文还讲到,nginx-http-flv-module 是用 Unix domain socket 将媒体流中继到其他 worker 进程的,而 Unix domain socket 的创建是在 ngx_rtmp_auto_push_module 的 init_process 中完成的,又已知 Nginx 将创建的所有 socket 的读事件加入到读事件监听集合中是在 ngx_event_core_module 中完成的,这就造成了将 nginx-http-flv-module 编译成动态模块时,虽然各个 worker 进程都已经创建了 Unix domain socket,但是 ngx_event_core_module 的工作已经在它们之前完成,即 Nginx 不会监听 ngx_rtmp_auto_push_module 创建的 Unix domain socket 上的读事件,成了漏网之鱼。所以如果把 nginx-http-flv-module 编译成动态模块,Nginx 面对 auto push 过来的媒体流,完全是无视的,这就导致了请求非 publisher 进程上的播放请求无法播放。

空了我会修复这个 bug,欢迎关注我在 nginx-rtmp-module 的基础上开发的项目:nginx-http-flv-module。

 类似资料: