本文,结合Nginx-1.10.3源码,总结一下在面试中常问的关于Nginx的面试题。
Nginx相比其他服务器,有什么优点
- 响应快。正常情况下,单次请求得到更快的响应。高峰期,比其他Web服务器更快地响应请求
- 高并发。单机支持10万以上的并发连接。
- 热部署。
说说Nginx是怎么做到响应快、高并发的呢?
- Nginx采用一个master管理进程、多个worker工作进程的设计模式。
该模式可以充分利用操作系统的多核CPU架构。master进程负责监控worker进程的状态,并管理其行为。worker进程负责accept客户端的连接请求,并处理客户端的请求。
- 采用事件驱动的架构。
对于Nginx服务器,产生的事件一般包括网卡、磁盘、定时器。事件模块负责收集事件、调用事件消费者。换句话说,事件的收集和处理是在同一进程完成。这有别于其他Web服务器,对每个请求创建一个进程或线程处理。Nginx的这种处理方式,避免了创建进程(或线程)的内存开销、进程(或线程)切换带来的CPU开销。事件驱动机制(I/O多路复用)在不同的操作系统,采用的机制不同。比如在Linux内核2.6之前,使用的poll或select,而Linux2.6之后,可以使用epoll。对于FreeBSD,可以使用kqueue。
事件驱动的缺陷
面试官:事件驱动的架构是不是万能的呢?在开发中,需要注意什么?
每个事件消费者,都不能有阻塞行为,否则将会由于长时间占用CPU进而导致其它事件得不到及时响应。 对于存在的阻塞行为,需要根据情况进行优化:
- 将阻塞调用改为非阻塞调用。 比如将socket的send、recv调用设置为非阻塞的
#define ngx_nonblocking(s) fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK)
- 将阻塞调用按照时间分解多个阶段调用。比如读取一个10M的文件发送给客户端,则可以在每次读取10k后,把10K发给客户端,在发送完成后,再读取后续内容。
- 如果还是没法避免,则应该创建新的进程处理该事件。
介绍一下master/worker的工作模式?
Nginx在启动时,首先由函数ngx_init_cycle初始化结构体ngx_cycle_t。Nginx框架是围绕着ngx_cycle_t结构体来控制进程运行的。ngx_cycle_modules函数初始化Nginx框架需要用到的所有模块,全局变量ngx_modules,保存了所有模块集合,它是在configure命令生成的,定义在objs/ngx_modules.c文件中。在初始化模块后,ngx_init_cycle函数将初始化socket的端口监听,它是由函数ngx_open_listening_sockets完成的。
int ngx_cdecl
main(int argc, char *const *argv)
{
...
cycle = ngx_init_cycle(&init_cycle);
if (ngx_process == NGX_PROCESS_SINGLE) {
ngx_single_process_cycle(cycle); // 单进程模式一般用于开发调试阶段
} else {
ngx_master_process_cycle(cycle);
}
}
ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
// 初始化module
if (ngx_cycle_modules(cycle) != NGX_OK) {
ngx_destroy_pool(pool);
return NULL;
}
// 初始化监听的端口
if (ngx_open_listening_sockets(cycle) != NGX_OK) {
goto failed;
}
}
从main函数的执行顺序可见,Nginx是先初始化模块,建立socket的监听,然后才会fork worker进程。因此worker进程共享打开的监听端口。
master进程在创建worker进程后,就开始监听系统信号、管理worker进程状态。管理worker进程,包括重启服务、平滑升级、配置文件实时生效等。它不负责接收客户端的请求。
worker进程的函数入口是ngx_worker_process_cycle,它先是通过函数ngx_worker_process_init初始化worker进程,然后不断地处理事件、定时器和来自master的信号。worker处理的信号包括QUIT(优雅地关闭进程)、TERM或者INT(强制关闭进程)、USR1(重新打开所有文件)。
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
for ( ;; ) {
ngx_process_events_and_timers(cycle); // 处理事件和定时器
}
}
在介绍ngx_process_events_and_timers处理事件前,先介绍一下worker进程是如何注册事件的、如何选择事件驱动机制。
worker进程的读事件、定时器的注册,是有ngx_event_core_module完成的。它是Nginx框架的一个重要事件模块,它在所有事件模块中的顺序是第一位(由命令configure保证)。它的职责是管理当前正在使用的事件驱动模式。例如,在Nginx启动时决定使用select还是epoll来监控网络事件。
ngx_event_core_module模块的ngx_module_t具体实现如下:
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
};
在worker进程初始化完成后,会调用ngx_event_process_init完成读事件、定时器的注册。定时器的数据结构,是通过红黑树实现的。读事件的处理函数,由函数ngx_event_accept或ngx_event_recvmsg完成。为了解决worker进程的惊群问题,采用了互斥锁。Nginx发布的1.9.1版本引入了一个新的特性:允许使用SO_REUSEPORT解决惊群问题。
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
// 初始化红黑树实现的定时器
if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
// 读事件
for (i = 0; i < cycle->listening.nelts; i++) {
c = ngx_get_connection(ls[i].fd, cycle->log);
rev = c->read;
rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
: ngx_event_recvmsg;
if (ngx_use_accept_mutex // 如果使用互斥锁,则不加入event监听
#if (NGX_HAVE_REUSEPORT)
&& !ls[i].reuseport
#endif
)
{
continue;
}
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
#endif
}
}
worker进程,处理读事件、定时器的工作,由具体的事件模块执行。可选的模块罗列如下。
extern ngx_module_t ngx_kqueue_module;
extern ngx_module_t ngx_eventport_module;
extern ngx_module_t ngx_devpoll_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_select_module;
选用哪个模块,由ngx_event_core_module在初始化模块的时候决定。ngx_event_core_module的成员ngx_event_core_module_ctx定义如下:
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 }
};
具体选用哪类事件驱动机制,在函数ngx_event_core_init_conf中完成。宏NGX_HAVE_EPOLL、NGX_HAVE_SELECT等的定义,是定义在文件objs/ngx_auto_config.h中,该文件在configure命令中产生。
static char *
ngx_event_core_init_conf(ngx_cycle_t *cycle, void *conf)
{
module = NULL;
#if (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)
module = &ngx_epoll_module;
#endif
#if (NGX_HAVE_DEVPOLL) && !(NGX_TEST_BUILD_DEVPOLL)
module = &ngx_devpoll_module;
#endif
#if (NGX_HAVE_KQUEUE)
module = &ngx_kqueue_module;
#endif
#if (NGX_HAVE_SELECT)
if (module == NULL) {
module = &ngx_select_module;
}
#endif
}
上面介绍了worker进程如何注册读事件、定时器,如何选择事件驱动机制。下面,接着介绍worker进程如何处理事件。进程处理事件和定时器的入口是ngx_process_events_and_timers。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
}
}
(void) ngx_process_events(cycle, timer, flags);
}
#define ngx_process_events ngx_event_actions.process_events
ngx_process_events_and_timers先申请互斥锁,获得接收客户端链接的权限,避免造成惊群问题。然后调用ngx_process_events处理事件。 处理事件方式,是有具体的事件机制实现的。每个机制的实现,定义在对应模块ngx_event_module_t结构体中。例如epoll事件管理机制,它的ngx_event_module_t结构体的定义如下。ngx_epoll_module_ctx的第四个成员,是结构体ngx_event_actions_t,它定义了,epoll模块如何添加一个事件、删除一个事件、如何处理一个事件。可见,处理网络事件,epoll事件机制是由函数ngx_epoll_process_events完成的。
ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
// 结构体ngx_event_actions_t
{
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 */
}
};
如何决定哪个worker接收客户端的连接请求
面试官:当一个客户端请求过来时,是如何决定由哪个worker负责accept呢?
Nginx提供了互斥锁的方式,决定由哪个worker负责accept。通过互斥锁,它规定了同一时刻只有唯一一个worker进程监听Web端口。 worker进程获取释放互斥锁,是在函数ngx_trylock_accept_mutex完成的。首先通过ngx_shmtx_trylock尝试获得锁,它是一个非阻塞的获取锁的方法。如果获得成功,则通过ngx_enable_accept_events把所有监听连接的读事件添加到当前worker的驱动模块中。如果获取失败,则判断当前worker是否占有了互斥锁,如果获得,则释放,允许其他worker进程争取。
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;
return NGX_OK;
}
if (ngx_accept_mutex_held) {
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
Nginx发布的1.9.1版本引入了一个新的特性:允许使用SO_REUSEPORT特性,交给内核决定由哪个worker完成客户端的accept。当SO_REUSEPORT选项启用是,存在对每一个IP地址和端口绑定连接的多个socket监听器,每一个工作进程都可以分配一个。系统内核决定哪一个有效的socket监听器(通过隐式的方式,给哪一个工作进程)获得连接。这可以减少工作进程之间获得新连接时的封锁竞争。 从下面的代码可以看到,在开启了SO_REUSEPORT选项后,每个worker进程在启动时,都加入了端口的accept事件。
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
// 读事件
for (i = 0; i < cycle->listening.nelts; i++) {
if (ngx_use_accept_mutex // 如果使用互斥锁,则不加入event监听
#if (NGX_HAVE_REUSEPORT)
&& !ls[i].reuseport
#endif
)
{
continue;
}
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
}
worker进程获得互斥锁后,是在等这批事件(如epoll的epoll_wait返回时,可能会有大量的就绪事件)全部执行完后释放互斥锁吗?
这当然是不可取的。因为这个worker进程上可能会有许多活跃的连接,处理这些连接上的事件会占用很时间。从而很长时间占用着锁,其他worker进程就不能得到连接的机会。号称高并发的Nginx怎么可以允许这样的短板存在。
Nginx通过引入一个标记位NGX_POST_EVENTS
- 如果flag被设置了标记位NGX_POST_EVENTS,则对就绪事件,加入队列中,将accept事件和其余事件分别加入ngx_posted_accept_events和ngx_posted_events两个队列。所有事件加入各自队列后,先处理accept事件,即处理ngx_posted_accept_events队列中的事件。处理完成后,立刻释放锁,然后再处理ngx_posted_events队列中的事件
- 如果没有设置标记位,则对就绪事件,依次执行,就没有必要加入队列的必要了。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
}
(void) ngx_process_events(cycle, timer, flags);
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
ngx_event_process_posted(cycle, &ngx_posted_events);
}
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
if (flags & NGX_POST_EVENTS) {
queue = rev->accept ? &ngx_posted_accept_events : &ngx_posted_events;
ngx_post_event(rev, queue);
} else {
rev->handler(rev);
}
}
如果既不使用互斥锁,又不开启SO_REUSEPORT选项,可以吗?
如果既不使用互斥锁,又不开启SO_REUSEPORT选项,就会造成惊群问题。何谓惊群呢?举例说,某个时刻,多个worker进程处于空闲休眠等待新连接的系统调用(如epoll的epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker进程。虽然最后已有一个worker进程才能成功accept,其他进程会accept失败。对于这些失败的worker进程,被唤醒是一种浪费。这种现象就是惊群。
worker是如何管理多个连接的呢?
首先介绍一下ngx_cycle_t结构体中,与客户端连接池相关的字段。
typedef struct ngx_cycle_s ngx_cycle_t;
struct ngx_cycle_s {
ngx_connection_t *free_connections;
ngx_connection_t *connections;
ngx_event_t *read_events;
ngx_event_t *write_events;
}
其中connections指向存放所有客户端连接的数组。对于数组中空闲的元素,以单向链表的形式管理,其头部由free_connections保存。对于每个客户端连接,Nginx自动为其创建了读事件和写事件,由read_events数组和write_events数组维护。这些字段的初始化,在ngx_event_process_init函数完成。
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
// connections、read_events、write_events大小一致,每个客户端,它的connection、read、write在数组中的索引是一致的。
cycle->connections =
ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);
c = cycle->connections;
cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
cycle->log);
cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
cycle->log);
// 构成单项链表
do {
i--;
c[i].data = next;
c[i].read = &cycle->read_events[i];
c[i].write = &cycle->write_events[i];
c[i].fd = (ngx_socket_t) -1;
next = &c[i];
} while (i);
cycle->free_connections = next;
cycle->free_connection_n = cycle->connection_n;
}
每次客户端请求建立连接时,从free_connections链表中拿出第一个元素,用于保存该客户端的连接信息。当客户端断开连接时,把该客户端的connection加入free_connections中。这样,避免了每次建立、断开连接时的内存申请和释放。
这里,不得不提一下Nginx为了节省内存,使用的一个小技巧。如下代码,ngx_connection_s结构体中的一个成员data,设置类型是void *,未使用时,data成员用于充当连接池中空闲连接池表的next指针。当连接被使用时,data的意义由使用它的Nginx模块而定。如在HTTP框架中,data指向ngx_http_request_t结构体。这样,没有必要为建立单向链表而新定义一个ngx_connection_s *字段。
struct ngx_connection_s {
void *data;
}
worker进程如何实现负载均衡
worker进程的负载均衡是通过worker一个全局变量ngx_accept_disabled实现的。它的初始化如下:
ngx_accept_disabled = ngx_cycle->connection_n / 8 -
ngx_cycle->free_connection_n;
这个值是与连接池中的已连接数量密切相关的。从下面的代码,易见,当值为负数时,尝试申请锁,接收客户端连接。当值为正值时,就减1,不申请锁。
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
}
Nginx的定时器是用红黑树实现的?
面试官:你已出色回答了上述九个关于Nginx的问题。既然Nginx的定时器是用红黑树实现的,要不你手撕写个红黑树吧? 我:........拜拜了,您嘞
本文暂时没有评论,来添加一个吧(●'◡'●)