俗话说,一个谎言,要用一百个谎言去圆。

在代码的世界里,同样如此。当然,这里只是借用“谎言”这个概念,并不带有任何贬义色彩。“谎言”用来指代那些由于设计或理解上的小疏漏,因为这些小疏漏,导致后续的代码里使用很多迂回的策略来达到目的。

但是代码世界里的“谎言”,又与真实世界的“谎言”截然不同,因为我们有版本管理系统在背后默默的记录着一切。通过它,我们可以对“谎言”进行追根溯源,找到最初的疏漏。

今天我们就来以 Nginx-RTMP 这个项目为例,来“拆穿”它的一个设计精巧的“谎言”。

在 ngx_rtmp.c 中,存在一个全局变量 ngx_rtmp_init_queue,通过阅读它的相关使用场景,我们可以知道这是一个队列性质的全局变量,与 ngx_posted_accept_eventsngx_posted_events 这两个变量类似。它们的用法是,将一类事件通过调用 ngx_post_event 注册到与其对应的队列中,当然这个注册是可以发生在不同的时间和场景下,然后在将来某个时刻,通过调用 ngx_event_process_posted 来耗尽这个任务队列,即遍历该任务队列中的每个事件,并执行事件的 handler 回调函数。

我们继续来看 ngx_rtmp_init_queue 这个事件队列的具体情况。

(1) ngx_rtmp_init_queue 的初始化

ngx_rtmp_init_queue 的初始化发生在 ngx_rtmp_module 模块的 init_process 中,通过调用 ngx_rtmp_init_queue 来完成。

(2) ngx_rtmp_init_queue 的事件注册

在 Nginx-RTMP 中,只有两个地方往 ngx_rtmp_init_queue 中注册了事件,他们分别是ngx_rtmp_exec_module 模块和 ngx_rtmp_relay_module 模块的 init_process 钩子。注册的事件具有相同的属性:希望在进程启动的时候就执行的事件。例如在 ngx_rtmp_relay_module 模块中,正常的 pull 回源处理逻辑,是当有播放器接入进来之后才会被触发。而配置了属性为 static 的 pull,当 worker 进程刚启动时就会去上上层拉取指定的流,而不会等播放器播放行为来触发。ngx_rtmp_exec_module 模块中的使用也是如此。

(3) ngx_rtmp_init_queue的消耗

上面第二点说明了在什么场景、什么时候,程序会往 ngx_rtmp_init_queue 这个全局队列中注册事件。我们接下来应该关心的是,这个事件队列中的事件,什么时候会得到执行。

通过查阅代码,我们会发现在 ngx_rtmp_stat_module 模块的 init_process 钩子中,ngx_rtmp_init_queue 队列中的事件会通过调用 ngx_event_process_posted 被耗尽。

至此,从逻辑上来说,我们已经完全清楚了 ngx_rtmp_init_queue 这个全局变量存在的意义。

但是这还不够,我们深入思考一下,就会有如下的疑问:

(1) 为什么在 RTMP 模块中注册的事件,要在一个 HTTP 模块(ngx_rtmp_stat_module 其实是一个HTTP 模块)中执行?ngx_rtmp_stat_module 模块的作用是统计流的信息,从逻辑上来说,我们可以选择不编译这个模块,而由于上述 ngx_rtmp_init_queue 的使用逻辑,却不能这样做,因为这些静态功能(static_pull、static_exec)在依赖它。即使模块之间有时候不能完全避免耦合,但 RTMP 里的功能却要依赖一个 HTTP 模块,这说不过去。

(2) 我们知道任务队列的作用主要是延迟事件的执行。但是这里如此使用的意义在哪里?通过前面的分析,我们知道无论是往 ngx_rtmp_init_queue 中注册事件,还是消耗 ngx_rtmp_init_queue 里的事件,都是发生在相关模块的 init_process 钩子中。而当一个 worker 进程启动后,马上就会通过一个简单的 for 循环来遍历执行所有模块的 init_process 钩子,这显然并没有起到任何延时的意义。

既然不应该让 RTMP 模块去依赖一个 HTTP 模块,也不用考虑事件的延迟执行,我们为什么不去掉 ngx_rtmp_init_queue,改为直接在各涉事 RTMP 模块的 init_process 钩子中直接去执行自己对应的事件呢?这样程序逻辑更清楚且不会有额外的损失。

答案是因为定时器

这些在 init_process 中注册到 ngx_rtmp_init_queue 中的事件,都需要用到定时器来检查这些事件是否执行正常,进而决定是否再次执行相应的逻辑。

而在 RTMP 模块的 init_process 钩子中是不能使用定时器的,严格的说,是使用定时器不会达到定时器的效果。

所以作者才会创造这么一个 ngx_rtmp_init_queue,让本应在相关 RTMP 模块的 init_process 钩子中自顾自执行的逻辑,放到统计模块这个 HTTP 模块的 init_process 中一股脑执行,因为在 HTTP 模块的 init_process 钩子中,可以使用定时器。

为什么?

因为定时器的初始化是在 ngx_event_core_module 模块的 init_process 钩子中进行的,ngx_event_core_module 是一个 NGX_EVENT_MODULE 类型的模块。而由于 Nginx-RTMP 项目 config 文件的写法,会导致所有 RTMP 类型的模块在全局模块数组 ngx_modules 中的位置,处于 ngx_event_core_module 模块的前面。从当前最新的 config 版本,我们能清楚的看到这一事实:config。所以,ngx_event_core_moduleinit_process 会在各 RTMP 模块的 init_process 之后执行,那么在 RTMP 模块的 init_process 钩子中添加的定时器,会在 ngx_event_core_moduleinit_process 中被毫不留情的初始化掉。

错误其实在一开始就铸成,从一开始作者就在 config 文件中将 RTMP 模块放在了 ngx_event_core_module 模块之前。我们可以查看 config 文件的第二次提交来确认这一点。

弄清楚了整个来龙去脉之后,我们当然就知道该如何去掉 ngx_rtmp_init_queue,这也是应该的,既然问题都不存在了,为了解决这个问题的奇技淫巧当然也不应该存在。

开源软件的魅力就在于此,你仿佛能穿越时空,通过代码与作者对话,感受他的聪明、狡黠与纠结。