基础知识
I/O 过程
- 等待数据
- 将数据从内核复制到用户空间
I/O 复用
调用 select / poll,该方法由一个用户态线程负责轮询多个 Channel,直到某个阶段1的数据就绪,再通知实际的用户线程执行阶段2的拷贝。通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现了阶段1的异步化。
Linux 网络编程
tcp 服务端的操作有,socket()、bind()、listen()、accpet()、read()、write()、close().
Linux epoll
epoll 操作有,epoll_create()、epoll_ctl()、epoll_wait().
epoll 有两种模式,边沿触发 ET 和 水平触发 LT。ET 仅当状态发生变化时才获得通知,所以要一直读写直到处理完发现错误;LT 只要有数据没有处理就会一直通知。
NGINX
事件模型
非阻塞 事件驱动
nginx 支持的 event 处理模型有:select、poll、epoll(linux)、kqueue(BSD)、/dev/poll(Solaris)、eventport(Solaris)、以及 win32 的 select 和 poll。 v1.9.0(2015)将 aio、rtsig(实时信号)移除。
以 epoll 为例,nginx 大部分事件采用 ET 模式,只有 listen 端口的读事件为 LT 模式。
- ngx_epoll_init() 中调用 epoll_create(),大小为 connection_n / 2。
- ngx_epoll_xxx() 中调用 epoll_ctl(),对事件和连接进行添加修改删除。
- ngx_process_events_and_timers() 中调用 epoll_wait()。
进程模型
nginx 为 1 master + N worker。master 负责管理 worker,worker 处理网络请求。
一般 worker 进程数与cpu内核数一致,所以不存在大量的子进程生成和管理任务,避免了大量子进程的数据IPC共享开销和切换竞争开销。各 worker 进程之间也只是重复拷贝了监听字,除了父子进程间传递控制消息,基本没有 IPC 需求。
worker 单进程内,也不需要同步操作。
图片来自 nginx.com
listening
ngx_listening,以 HTTP 为例:
-
在 ngx_http_block() 中 ngx_http_add_listening() 调用 ngx_create_listening(),指定 listening 的 handler 为 ngx_http_init_connection, 并添加 ngx_listening 到 cycle -> listening。
-
main() 中 ngx_init_cycle() 调用 ngx_open_listening_sockets(),对 cycle -> listening 中依次进行 socket()、setsockopt()、bind()、listen()。
-
worker process 中,在 ngx_worker_process_init() 中会对所有模块的 init_process() 调用, ngx_event_core_module 模块中的 init_process 方法为 ngx_event_process_init();ngx_event_process_init() 中指定每个 listening 读事件的处理函数为 ngx_event_accept()。
-
accept() / accept4() 发生在 ngx_event_accept() 处理函数。
触发读的 listening,用关联的 fd,进行 accept() / accept4() ,得到新连接的 socket 和 connection。
connection
ngx_connection,以 HTTP 为例:
-
作为 listening 的连接
main() 中 ngx_init_cycle() 调用 ngx_open_listening_sockets(),为每个 listening 绑定一个 connection。
-
作为 accept 后的连接
在 ngx_event_accept() 中,accept 后获得新连接, 使用 ngx_event_actions 的 add_conn添加到监控队列中(具体到 epoll,使用 ngx_epoll_add_event), 并调用 (ngx_listening_t) ls -> handler,即 ngx_http_init_connection()。
在 ngx_http_init_connection() 中,connection 的读事件处理函数指定为ngx_http_wait_request_handler,写事件处理函数指定为ngx_http_empty_handler。
-
作为 upstream 发起请求的连接
event
主要为网络事件和定时事件。
先看定义(忽略一部分)
struct ngx_event_s ngx_event_t;
struct ngx_event_s {
void *data;
ngx_event_handler_pt handler;
ngx_log_t *log;
};
data,通常指向 ngx_connection_t; handler,处理方法,例如 ngx_http_xxx_handler。
event module
event 模块的主要功能为,监听 accept 后建立的连接、对读写事件进行删除。
从 objs/ngx_modules.c 可以得知,关于 event 的模块加载了多个:ngx_events_module 、ngx_event_core_module、ngx_epoll_module、ngx_select_module。
先看 ngx_events_module 定义,可知 ngx_events_module 引入了 event block。
ngx_module_t ngx_events_module = {
NGX_MODULE_V1,
&ngx_events_module_ctx, /* module context */
ngx_events_commands, /* module directives */
NGX_CORE_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
static ngx_core_module_t ngx_events_module_ctx = {
ngx_string("events"),
NULL,
ngx_event_init_conf
};
static ngx_command_t ngx_events_commands[] = {
{ ngx_string("events"),
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_events_block,
0,
0,
NULL },
ngx_null_command
};
再看 ngx_event_core_module 定义,可知 ngx_event_core_module 提供了 event 的核心配置项。
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
};
static ngx_event_module_t ngx_event_core_module_ctx = {
&event_core_name,
ngx_event_core_create_conf, /* create configuration */
ngx_event_core_init_conf, /* init configuration */
{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};
static ngx_command_t ngx_event_core_commands[] = {
{ ngx_string("worker_connections"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_event_connections,
0,
0,
NULL },
{ ngx_string("use"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_event_use,
0,
0,
NULL },
{ ngx_string("multi_accept"),
NGX_EVENT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
0,
offsetof(ngx_event_conf_t, multi_accept),
NULL },
{ ngx_string("accept_mutex"),
NGX_EVENT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
0,
offsetof(ngx_event_conf_t, accept_mutex),
NULL },
{ ngx_string("accept_mutex_delay"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
0,
offsetof(ngx_event_conf_t, accept_mutex_delay),
NULL },
{ ngx_string("debug_connection"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_event_debug_connection,
0,
0,
NULL },
ngx_null_command
};
找一个具体实现的模块,ngx_epoll_module,看它的定义,可知 ngx_epoll_module 引入了 epoll 的定制配置,并终于提供了 module actions。
ngx_module_t ngx_epoll_module = {
NGX_MODULE_V1,
&ngx_epoll_module_ctx, /* module context */
ngx_epoll_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
static ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
{
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
#if (NGX_HAVE_EVENTFD)
ngx_epoll_notify, /* trigger a notify */
#else
NULL, /* trigger a notify */
#endif
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}
};
static ngx_command_t ngx_epoll_commands[] = {
{ ngx_string("epoll_events"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, events),
NULL },
{ ngx_string("worker_aio_requests"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, aio_requests),
NULL },
ngx_null_command
};
综上,ngx_events_module 引入了 event block,在 ngx_events_block() 中对所有的 NGX_EVENT_MODULE 类型的模块,进行 create_conf()、ngx_conf_parse()、init_conf(); ngx_event_core_module 提供了核心配置和流程钩子;ngx_epoll_module 提供了可操作的 actions。
关于配置,多个模块协力通过 ngx_event_init_conf()、ngx_event_core_create_conf()、ngx_event_core_init_conf()、ngx_epoll_create_conf()、ngx_epoll_init_conf() 等方法提供了配置项。
关于流程,每个模块按需提供了module 要求的 init master,init module,init process,init thread,exit thread,exit process,exit master,例如 ngx_event_module_init()、ngx_event_process_init()。 ngx_event_process_init() 中调用 NGX_EVENT_MODULE 类型的各个模块 actions 中的 init,对于 ngx_epoll_module 即 ngx_epoll_init。
event 机制核心
worker process loop
关键方法,事件的入口函数,ngx_process_events_and_timers(),在 worker process 的 loop 中出现,处理网络事件和定时事件。
以 epoll 为例,epoll_wait 就在该方法中调用。
主要操作为:
- ngx_process_events 处理网络事件
- 处理两个队列(ngx_posted_accept_events、ngx_posted_events)中的事件
- ngx_event_expire_timer 处理定时事件。
实现上,ngx_process_events 只接受而不处理事件,并加入 ngx_posted_events队列,直到 ngx_accept_mutex 全局锁去掉后才处理。 ngx_posted_accept_events 是 accept 事件延时队列,ngx_posted_events 是普通事件延时队列。
ngx_accept_mutex
负载均衡问题
ngx_accept_disabled 变量在 ngx_event_accept 函数中计算。处理 accept 事件时,每 accept 一个新的连接,就更新 ngx_accept_disabled。
如果为高负载,即大于0,就表示该进程接受的连接过多,因此放弃一次争抢 accept mutex 的机会,同时减一,然后,继续处理已有连接上的事件;如果为低负载,进程就去竞争 accept 锁,接受新的连接。
ngx_accept_disabled 的计算方法,使得 worker 处理的连接数在达到其最大处理数的 7/8 时才会触发,禁止 accept 一段时间。
ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
nginx就利用这一点实现了进程关于连接的基本负载均衡。
惊群问题
问题:当内核 accept 一个连接时,会唤醒所有等待中的进程,但其实只有一个进程能获取到连接,其他进程重新进入休眠状态。
解决:accept 锁,避免多个 process 同时调用 accept。锁的实现方式默认为 CPU 自旋锁,系统不支持的话,使用文件锁。
Syntax: accept_mutex [ on | off ]
Default: off
避免在新连接过少时,多个worker进程被唤醒,浪费系统资源。
There is no need to enable accept_mutex on systems that support the EPOLLEXCLUSIVE flag (1.11.3) or when using reuseport.
目前新版的Linux内核中增加了 EPOLLEXCLUSIVE 选项(Linux 4.5, glibc 2.24),nginx 从 1.11.3 版本(released 2016-07-26)之后也增加了 对 NGX_EXCLUSIVE_EVENT 选项的支持,这样就可以避免多 worker 的 epoll 出现的惊群效应,从此之后 accept_mutex 变成默认on。
Nginx缺省激活了accept_mutex(1.11.3之前),也就是说不会有惊群问题,但真的有那么严重么?实际上Nginx作者Igor Sysoev曾经给过相关的解释:
OS may wake all processes waiting on accept() and select(), this is called thundering herd problem. This is a problem if you have a lot of workers as in Apache (hundreds and more), but this insensible if you have just several workers as nginx usually has. Therefore turning accept_mutex off is as scheduling incoming connection by OS via select/kqueue/epoll/etc (but not accept()).
实现上,ngx_process_events_and_timers()中会获取 accept_mutex 锁,竞争成功后 ngx_enable_accept_events,同时所有事件增加一个 NGX_POST_EVENTS; 其他竞争失败的进程,ngx_disable_accept_events,下次 cycle -> listening 数组中即将有新的连接,线程也不会去响应,直到它之后主动去竞争成功。