JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

面试中关于Nginx的十问九答

wys521 2024-09-07 03:06:15 精选教程 45 ℃ 0 评论

本文,结合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进而导致其它事件得不到及时响应。 对于存在的阻塞行为,需要根据情况进行优化:

  1. 将阻塞调用改为非阻塞调用。 比如将socket的send、recv调用设置为非阻塞的
#define ngx_nonblocking(s)  fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK)
  1. 将阻塞调用按照时间分解多个阶段调用。比如读取一个10M的文件发送给客户端,则可以在每次读取10k后,把10K发给客户端,在发送完成后,再读取后续内容。
  2. 如果还是没法避免,则应该创建新的进程处理该事件。

介绍一下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的定时器是用红黑树实现的,要不你手撕写个红黑树吧? 我:........拜拜了,您嘞

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表