返回
Featured image of post Nginx - 模块数据处理流程

Nginx - 模块数据处理流程

Nginx 模块数据处理流程

前面一篇文章中,我们解析了 Nginx 模块的初始化流程,知道了 Nginx 是如何对各个模块就行初始化的。

而接下来,这篇文章的目的是解答以下问题:

  • Nginx 是如何知道从端口接收到的数据,应该交给哪些模块进行处理的呢?

listen

我们都知道,通常 Nginx 的数据/请求是通过端口接收过来的,因此,我们来看看:

  • Nginx 是如何进行端口监听的?端口监听时做了些什么?

通过搜索源码,我们得到了相关 command:

src/http/ngx_http_core_module.c

    { ngx_string("listen"),
      NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
      ngx_http_core_listen,
      NGX_HTTP_SRV_CONF_OFFSET,
      0,
      NULL },

src/mail/ngx_mail_core_module.c

    { ngx_string("listen"),
      NGX_MAIL_SRV_CONF|NGX_CONF_1MORE,
      ngx_mail_core_listen,
      NGX_MAIL_SRV_CONF_OFFSET,
      0,
      NULL },

src/stream/ngx_stream_core_module.c

    { ngx_string("listen"),
      NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
      ngx_stream_core_listen,
      NGX_STREAM_SRV_CONF_OFFSET,
      0,
      NULL },

可以看到,各个类型,都有自己的 listen 指令,因此 nginx.conf 中,不同配置块(block)下的 listen 指令是不同。

我们以 HTTP 为例,看下执行流程。

HTTP 模块中 listen 的执行流程

为便于理解,先插播一条 listen 指令的用法:

listen

执行流程:端口监听

- ngx_init_cycle: core/ngx_cycle.c
    \ - ngx_conf_parse
        \ - ngx_http_block
            \ - ngx_conf_parse
                \ - ngx_http_core_listen: src/http/ngx_http_core_module.c
                    \ - ngx_parse_url: src/core/ngx_inet.c,解析 listen 指令后的第一部分(URL),先检查是否是 unix domain 类型,再检查是否是 IPv6 类型,都不是就调用 IPv4 的 URL 解析。
                    \ - ngx_strcmp: 解析指令的各个部分。
                    \ - ngx_inet_wildcard: src/core/ngx_inet.c,判断地址是否是通配地址(如 IPv4 的 INADDR_ANY)。
                    \ - ngx_http_add_listen:
                        \ - ngx_http_add_addresses:
                            \ - ngx_cmp_sockaddr: src/core/ngx_inet.c,对比当前列表中的地址与即将添加的地址。
                            \ - ngx_http_add_server: 将 server(虚拟主机) 添加到 address:port 对应的虚拟主机列表中(如果地址已经存在在当前列表中,则进行次操作,否则进行 ngx_http_add_address)
                            \ - ngx_http_add_address: 将 server 地址、server 名、server 对应类型的核心模块配置添加到端口列表。
                                \ - ngx_http_add_server: 将 server 对应类型的核心模块配置添加到 address:port
                        \ - ngx_http_add_address: 将 server 地址、server 名、server 对应类型的核心模块配置添加到端口列表。
                            \ - ngx_http_add_server: 将 server 对应类型的核心模块配置添加到 address:port
            \ - ngx_http_optimize_servers: 优化 ports、 addresses、 server names 列表。
                \ - ngx_http_server_names
                \ - ngx_http_init_listening
                    \ - ngx_http_add_listening
                        \ - ngx_create_listening: 把 cmcf->ports 中的 address 信息复制到 cycle->listening 中。
                            \ - ngx_array_push: 调用此函数把地址添加到列表
    \ - ngx_open_listening_sockets: 遍历 cycle->listening 进行实际的监听
        \ - setsockopt
        \ - ngx_socket
        \ - bind
        \ - listen

以上是监听的流程,相关操作是在 master 进程中进行,然后 fork worker 进程以后,worker 进程也得到了相关套接字信息。 worker 进程调用 ngx_worker_process_cycle 进入自身循环。 listen 之后,就能 accept 了,那么是如何处理 accept 的呢?

通过在源码中搜索 accept(,我们知道 accept 是在 ngx_event_accept 中进行的:

void
ngx_event_accept(ngx_event_t *ev)
{
    ...

#if (NGX_HAVE_ACCEPT4)
        if (use_accept4) {
            s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK);
        } else {
            s = accept(lc->fd, &sa.sockaddr, &socklen);
        }
#else
        s = accept(lc->fd, &sa.sockaddr, &socklen);
#endif

    ...
}

而 ngx_event_accept 是被像下面的代码设置到连接的 handler 变量中:

static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
    ...
        rev->handler = ngx_event_accept;

        ...

        rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
                                                : ngx_event_recvmsg;

    ...
}

继续搜索 ngx_event_process_init,发现:

ngx_module_t  ngx_event_core_module = {
    NGX_MODULE_V1,
    &ngx_event_core_module_ctx,            /* module context */
    ngx_event_core_commands,               /* module directives */
    NGX_EVENT_MODULE,                      /* module type */
    NULL,                                  /* init master */
    ngx_event_module_init,                 /* init module */
    ngx_event_process_init,                /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

可以看到 ngx_event_process_init 是 event 模块的进程初始化函数,

struct ngx_module_s {
    ...

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);

    ...
}

也就是初始化工作进程时,会调用到。

因此,我们可以得到 accept 相关函数被设置为 event 读事件的执行流程。

执行流程:读事件初始化

- ngx_master_process_cycle: 起工作进程、cache 管理进程等,然后进入循环
    \ - sigemptyset: 清空信号集
    \ - sigaddset: 添加信号到信号集
    \ - sigprocmask: 屏蔽(SIG_BLOCK)信号集
    \ - sigemptyset: 清空信号集——只是清空变量不会实际影响到进程信号屏蔽字
    \ - ngx_setproctitle: 设置进程标题
    \ - ngx_get_conf: 获取核心模块配置
    \ - ngx_start_worker_processes: 启动工作进程
        \ - ngx_spawn_process: 生成进程
            \ - fork
            \ - proc = ngx_worker_process_cycle: 子进程调用此函数
                \ - ngx_worker_process_init: worker 进程初始化,设置 CPU 亲缘性等
                    \ - init_process: 调用所有模块的初始化进程函数
                        \ - ngx_event_process_init: 此函数是 event 的 init_process
                            \ - rev->handler = ngx_event_accept:设置连接的读事件为 ngx_event_accept
                \ - for: 进入无限循环等待事件
                    - ngx_process_events_and_timers
        \ - ngx_pass_open_channel
    \ - ngx_start_cache_manager_processes: 启动 cache 管理进程
    \ - sigsuspend: 进入循环,开放信号,挂起进程等待信号

把连接(connection)的读事件设置为 ngx_event_accept 后,可以想象到连接有读事件时,就会调用此函数。

不过由于现在目的不是了解 event,因此像

  • 如何决定使用什么类型的 IO 多路复用?epoll、poll、select … ?
  • event 是如何工作的?

等 event 相关内容我们先跳过,继续跟踪 ngx_event_accept。

执行流程:请求处理

- ngx_event_accept
    \ - ngx_enable_accept_events: 把连接的读事件加入到事件系统中
    \ - ngx_event_get_conf: 获取事件模块的配置
    \ - accept4/accept4: accept 连接
    \ - ngx_get_connection: 获取连接
    \ - ngx_close_accepted_connection: 如果内存池之类的分配失败,则调用此函数
    \ - ls->handler: 具体的 http/mail/stream 处理函数,如 ngx_http_init_connection;也就是这里决定监听的端口是什么类型。
        \ - HTTP: ngx_http_init_connection
            \ - ngx_handle_read_event:添加到事件系统中
        \ - MAIL: ngx_mail_init_connection
        \ - STREAM: ngx_stream_init_connection

在这里,解答了我们最开始的疑问:Nginx 是如何知道从端口接收到的数据,应该交给哪些模块进行处理的呢? Nginx 通过设置不同的 handler,来处理不同类型的请求。

总结:

  • Nginx(worker 进程)为每个服务器 socket 分配了一个连接结构(ngx_connection_t),读事件设置为 ngx_event_accept;
    • 可以看到这里是每个 worker 进程都添加了 accept 事件到事件系统中,也就是一个连接请求过来时,会导致所有进程都被唤醒,这也就是所谓的惊群现象。Nginx 的解决方法是添加一个 accept 锁 ngx_accept_mutex。
  • Nginx 为每个客户端 socket 分配了一个对应类型的连接结构(ngx_http_connection_t),读事件设置为对应类型的事件。
    • 以 HTTP 为例,不同的阶段会使用不同的事件 handler,来读取 HTTP 头部或者是 body 等。

参考

相信美好的事情即将发生。
Built with Hugo
Theme Stack designed by Jimmy