本文主要介绍nginx中reuseport的使用,文中代码较多,阅读本文需要读者对nginx事件模块以及nginx配置过程有了解。
由于nginx比较复杂,且笔者对nginx的理解有限,文章难免存在疏忽之处,敬请指出,万分感谢!
reuseport通过listen指令来配置,配置如下:
listen 443 reuseport;
listen指令由ngx_http_core_module模块的ngx_http_core_listen() 函数进行解析。
ngx_http_core_module.c的相关解析命令如下:
{ ngx_string("listen"),
NGX_HTTP_SRV_CONF|NGX_CONF_1MORE, //listen指令只能配置在server块中,至少携带一个参数(443,reuseport等等)
ngx_http_core_listen, //解析listen指令的函数
NGX_HTTP_SRV_CONF_OFFSET,
0,
NULL },
由于篇幅限制,下面仅介绍reuseport相关的解析,不完全介绍listen指令的解析。
// 解析listen指令携带的参数,从第二个参数开始(第一个参数为ip+port)
for (n = 2; n < cf->args->nelts; n++) {
...
if (ngx_strcmp(value[n].data, "bind") == 0) {
lsopt.set = 1;
lsopt.bind = 1; //强制对此ip+port进行bind操作,如不配置bind,可能会忽略此ip+port,后续会介绍忽略的情况
continue;
}
//解析到reuseport配置,将reuseport标志置为1,同时将bind也置为1
if (ngx_strcmp(value[n].data, "reuseport") == 0) {
lsopt.reuseport = 1;
lsopt.set = 1;
lsopt.bind = 1;
continue;
}
...
}
master解析完http block之后会掉用ngx_http_optimize_servers()函数对配置的ip+port进行相应的处理,主要进行下面两个操作:
1、检查所有当前http block下的listen配置是否存在重复,即是否存在两个listen指令配置了相同的ip + port + server_name。如果存在重复的配置则会忽略靠后的ip+port+server_name的配置。检查由ngx_http_server_names()函数完成,此处并不详细介绍,有兴趣可自行查阅相应代码。
2、根据解析的配置创建listening结构,master后续监听的端口均由listening获得,每个listening结构对应一次bind + listening操作,此处重点介绍listening的创建。
首先ngx_http_optimize_servers()函数调用ngx_http_init_listening()函数,ngx_http_init_listening()代码如下:
static ngx_int_t
ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)
{
ngx_uint_t i, last, bind_wildcard;
ngx_listening_t *ls;
ngx_http_port_t *hport;
ngx_http_conf_addr_t *addr;
/*
以下是port的存储结构,一个port对应多个addrs(即port+ip),一个addr对应多个server(即server_name)
port - > addr1----|-----> server1
addr2 |-----> server2
addr3
...
*/
addr = port->addrs.elts; //取出port的所有addr,一个port可能对应多个ip+port
last = port->addrs.nelts; //port中addr的个数
//每一个port的addr都已经排序了(插入排序),ip为通配符(INADDR_ANY)的addr在最后面,配置了bind的addr在前面
if (addr[last - 1].opt.wildcard) {
addr[last - 1].opt.bind = 1;
bind_wildcard = 1;
} else {
bind_wildcard = 0;
}
i = 0;
while (i < last) {
//该port存在内容为INADDR_ANY+port的addr,若当前addr(ip+port)没有配置bind,则不需要显示地对当前ip+addr进行监听
if (bind_wildcard && !addr[i].opt.bind) {
i++;
continue;
}
ls = ngx_http_add_listening(cf, &addr[i]); //为当前ip+port创建一个listening结构,用于监听
if (ls == NULL) {
return NGX_ERROR;
}
hport = ngx_pcalloc(cf->pool, sizeof(ngx_http_port_t));
if (hport == NULL) {
return NGX_ERROR;
}
/*
以下代码将上面提到的不需要监听的ip+port的相关配置存储到最后的addr(INADDR_ANY+port)中,便于收到连接请求时,可以找到正确的配置。
但是存在一个问题,对于ip+port存在多个server的情况,在建立连接时ngx_http_init_connection()中只能取到ip+port的default_server。
*/
ls->servers = hport;
hport->naddrs = i + 1;
switch (ls->sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
if (ngx_http_add_addrs6(cf, hport, addr) != NGX_OK) {
return NGX_ERROR;
}
break;
#endif
default: /* AF_INET */
if (ngx_http_add_addrs(cf, hport, addr) != NGX_OK) {
return NGX_ERROR;
}
break;
}
//对配置了reuseport的ip+port,将对应的listening结构拷贝worker_processes - 1份,便于每个worker保存独立的fd,避免使用互斥锁
if (ngx_clone_listening(cf, ls) != NGX_OK) {
return NGX_ERROR;
}
addr++;
last--;
}
return NGX_OK;
}
ngx_clone_listening()代码如下:
ngx_clone_listening(ngx_conf_t *cf, ngx_listening_t *ls)
{
#if (NGX_HAVE_REUSEPORT)
ngx_int_t n;
ngx_core_conf_t *ccf;
ngx_listening_t ols;
if (!ls->reuseport) { //只拷贝配置了reuseport的listening
return NGX_OK;
}
ols = *ls;
ccf = (ngx_core_conf_t *) ngx_get_conf(cf->cycle->conf_ctx,
ngx_core_module);
for (n = 1; n < ccf->worker_processes; n++) {
/* create a socket for each worker process */
ls = ngx_array_push(&cf->cycle->listening);
if (ls == NULL) {
return NGX_ERROR;
}
*ls = ols;
ls->worker = n; //对应的监听套接字只能由第n个worker进程处理,避免worker间相互干扰
}
#endif
return NGX_OK;
}
master解析完配置之后并创建listening之后,开始创建监听套接字。对每一个listening结构会创建对应的监听套接字,再设置reuseport属性,最后进行bind和listen操作。具体代码如下:
首先调用ngx_open_listening_sockets()创建套接字,并进行bind和listen操作
ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
int reuseaddr;
ngx_uint_t i, tries, failed;
ngx_err_t err;
ngx_log_t *log;
ngx_socket_t s;
ngx_listening_t *ls;
reuseaddr = 1;
#if (NGX_SUPPRESS_WARN)
failed = 0;
#endif
log = cycle->log;
/* TODO: configurable try number */
for (tries = 5; tries; tries--) { //重试5次
failed = 0;
/* for each listening socket */
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
if (ls[i].ignore) {
continue;
}
#if (NGX_HAVE_REUSEPORT)
if (ls[i].add_reuseport) { //增加reuseport属性,仅用于reload和平滑升级场景
/*
* to allow transition from a socket without SO_REUSEPORT
* to multiple sockets with SO_REUSEPORT, we have to set
* SO_REUSEPORT on the old socket before opening new ones
*/
int reuseport = 1;
if (setsockopt(ls[i].fd, SOL_SOCKET, SO_REUSEPORT,
(const void *) &reuseport, sizeof(int))
== -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
"setsockopt(SO_REUSEPORT) %V failed, ignored",
&ls[i].addr_text);
}
ls[i].add_reuseport = 0;
}
#endif
if (ls[i].fd != (ngx_socket_t) -1) {
continue;
}
if (ls[i].inherited) {
/* TODO: close on exit */
/* TODO: nonblocking */
/* TODO: deferred accept */
continue;
}
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); //创建套接字
...
#if (NGX_HAVE_REUSEPORT)
if (ls[i].reuseport && !ngx_test_config) { //设置reuseport
int reuseport;
reuseport = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEPORT, (const void *) &reuseport, sizeof(int);
}
#endif
bind(s, ls[i].sockaddr, ls[i].socklen); //band操作
if (ls[i].type != SOCK_STREAM) {
ls[i].fd = s;
continue;
}
listen(s, ls[i].backlog); //listen操作
ls[i].listen = 1;
ls[i].fd = s;
}
if (!failed) {
break;
}
/* TODO: delay configurable */
ngx_log_error(NGX_LOG_NOTICE, log, 0,
"try again to bind() after 500ms");
ngx_msleep(500);
}
if (failed) {
ngx_log_error(NGX_LOG_EMERG, log, 0, "still could not bind()");
return NGX_ERROR;
}
return NGX_OK;
}
至此,对于每一个配置reuseport的ip+addr,都为其创建了worker_processes个监听套接字,接下来介绍worker进程如何使用这些套接字。
对于每一个没有配置reuseport的监听套接字,打开文件描述符表中,通过继承的方式每个worker都维护一个fd,但是内核层面仅仅维护一个表项(打开文件描述符表由所有进程共享)。正是由于这个原因导致惊群问题,早期nginx通过互斥锁(共享内存+原子操作)来避免惊群。worker调用epoll_wait之前请求锁,只有成功获得锁,才会通过epoll监听fd。由于worker进程间需要竞争锁,性能不高。
如果配置了reuseport的监听套接字,每个worker进程拥有一个独立的fd,worker进程间互不干扰,在内核层面实现负载均衡,效率更高。worker进程启动之后,会调用ngx_event_process_init()对事件模块进行初始化,代码如下:
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
ngx_uint_t m, i;
ngx_event_t *rev, *wev;
ngx_listening_t *ls;
ngx_connection_t *c, *next, *old;
ngx_core_conf_t *ccf;
ngx_event_conf_t *ecf;
ngx_event_module_t *module;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
ecf = ngx_event_get_conf(cycle->conf_ctx, ngx_event_core_module);
if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) { //worker进程判断是否使用互斥锁
ngx_use_accept_mutex = 1;
ngx_accept_mutex_held = 0;
ngx_accept_mutex_delay = ecf->accept_mutex_delay;
} else {
ngx_use_accept_mutex = 0;
}
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
#if (NGX_HAVE_REUSEPORT)
if (ls[i].reuseport && ls[i].worker != ngx_worker) { //worker仅处理属于自己的监听套接字
continue;
}
#endif
c = ngx_get_connection(ls[i].fd, cycle->log);
if (c == NULL) {
return NGX_ERROR;
}
c->type = ls[i].type;
c->log = &ls[i].log;
c->listening = &ls[i];
ls[i].connection = c;
rev = c->read;
rev->log = c->log;
rev->accept = 1;
rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
: ngx_event_recvmsg;
#if (NGX_HAVE_REUSEPORT)
if (ls[i].reuseport) {
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { //将监听套接字加入epoll,LT模式
return NGX_ERROR;
}
continue;
}
#endif
//若使用互斥锁,worker进程仅在获得互斥锁时才将监听套接字加入epoll
if (ngx_use_accept_mutex) {
continue;
}
//若不使用互斥锁,worker进程直接将监听套接字加入epoll
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
return NGX_OK;
}
若worker进程若使用互斥锁,在成功获得互斥锁时,将监听套接字加入epoll;获得锁失败时,将监听套接字从epoll中移除。具体代码如下:
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) { //请求锁
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
if (ngx_enable_accept_events(cycle) == NGX_ERROR) { //成功获得锁时,将监听套接字加入epoll
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
if (ngx_accept_mutex_held) {
//若没有获得锁,将监听套接字从epoll中移除(配置reuseport的套接字除外)
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
static ngx_int_t
ngx_enable_accept_events(ngx_cycle_t *cycle)
{
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) { //将所有监听套接字加入epoll, LT模式
c = ls[i].connection;
if (c == NULL || c->read->active) {
continue;
}
if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
return NGX_OK;
}
static ngx_int_t
ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all)
{
ngx_uint_t i;
ngx_listening_t *ls;
ngx_connection_t *c;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
c = ls[i].connection;
if (c == NULL || !c->read->active) {
continue;
}
#if (NGX_HAVE_REUSEPORT)
/*
* do not disable accept on worker's own sockets
* when disabling accept events due to accept mutex
*/
if (ls[i].reuseport && !all) { //配置reuseport的监听套接字仍旧通过epoll监听
continue;
}
#endif
if (ngx_del_event(c->read, NGX_READ_EVENT, NGX_DISABLE_EVENT)
== NGX_ERROR)
{
return NGX_ERROR;
}
}
return NGX_OK;
}
master进行reload的时候会调用ngx_init_cycle,这里仅给出ngx_init_cycle部分代码:
ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
...
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { //master重新解析配置,生成新的listening数组
environ = senv; //其中关于ip+port的配置保存在listening中
ngx_destroy_cycle_pools(&conf);
return NULL;
}
...
if (old_cycle->listening.nelts) { //如果老的listening非空,则将其中的监听套接字直接转移到新的listening里面
ls = old_cycle->listening.elts;
for (i = 0; i < old_cycle->listening.nelts; i++) {
ls[i].remain = 0;
}
nls = cycle->listening.elts;
for (n = 0; n < cycle->listening.nelts; n++) { //遍历新的listening中的每一项
for (i = 0; i < old_cycle->listening.nelts; i++) {//遍历老的listening的每一项,找到ip+port相同的项,转移监听套接字到新的listening
if (ls[i].ignore) {
continue;
}
if (ls[i].remain) {
continue;
}
if (ls[i].type != nls[n].type) {
continue;
}
if (ngx_cmp_sockaddr(nls[n].sockaddr, nls[n].socklen,
ls[i].sockaddr, ls[i].socklen, 1)
== NGX_OK)
{
/*
将监听套接字转移给新的listening,由于old worker没有退出,在某个时间段内,
新的的worker进程和老的worker进程指向相同的打开文件描述符表项,造成新老进程之间的惊群问题。
*/
nls[n].fd = ls[i].fd;
nls[n].previous = &ls[i];
ls[i].remain = 1;
if (ls[i].backlog != nls[n].backlog) { //由于listen操作的第二个参数变化,需要重新listen该监听套接字
nls[n].listen = 1;
}
#if (NGX_HAVE_REUSEPORT)
if (nls[n].reuseport && !ls[i].reuseport) { //增加reuseport属性
nls[n].add_reuseport = 1;
}
#endif
break;
}
}
if (nls[n].fd == (ngx_socket_t) -1) {
nls[n].open = 1;
}
}
} else { //老的listening为空,则解析出来的所有ip+port需要创建新的监听套接字
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
ls[i].open = 1;
}
}
if (ngx_open_listening_sockets(cycle) != NGX_OK) { //创建监听套接字,并对其进行bind+listen操作
goto failed;
}
}
只要某一次配置了reuseport,后续的reload操作即使没有配置reuseport,对应的ip+port仍然具有reuseport属性,但是解析配置是一个ip+port仅对应一个listening结构,实际上还是所有worker共用一个监听套接字,需要通过mutex来避免惊群。
平滑升级用于替换nginx可执行文件,老的master收到信号进行平滑升级时,首先fork新进程,新进程然后调用exec替换进程上下文,得到新的master进程,老的master的所有监听套接字通过环境变量传给新的master。
新的master进程起来之后,进入main函数,main会调用ngx_add_inherited_sockets来获取父进程设置的"NGINX"环境变量。该环境变量保存的是父进程的监听套接字,套接字之间通过冒号分割,最后加上一个分号。
static ngx_int_t
ngx_add_inherited_sockets(ngx_cycle_t *cycle)
{
u_char *p, *v, *inherited;
ngx_int_t s;
ngx_listening_t *ls;
inherited = (u_char *) getenv(NGINX_VAR);
if (inherited == NULL) {
return NGX_OK;
}
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
"using inherited sockets from \"%s\"", inherited);
if (ngx_array_init(&cycle->listening, cycle->pool, 10,
sizeof(ngx_listening_t))
!= NGX_OK)
{
return NGX_ERROR;
}
for (p = inherited, v = p; *p; p++) {
if (*p == ':' || *p == ';') {
s = ngx_atoi(v, p - v); //取出监听套接字的fd
if (s == NGX_ERROR) {
ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
"invalid socket number \"%s\" in " NGINX_VAR
" environment variable, ignoring the rest"
" of the variable", v);
break;
}
v = p + 1;
ls = ngx_array_push(&cycle->listening); //创建新的listening结构
if (ls == NULL) {
return NGX_ERROR;
}
ngx_memzero(ls, sizeof(ngx_listening_t));
ls->fd = (ngx_socket_t) s; //将获取的fd保存至listening结构
}
}
if (v != p) {
ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
"invalid socket number \"%s\" in " NGINX_VAR
" environment variable, ignoring", v);
}
ngx_inherited = 1;
return ngx_set_inherited_sockets(cycle); //获取监听套接字的各种属性,并将这些属性保存至对应的listening
}
ngx_int_t
ngx_set_inherited_sockets(ngx_cycle_t *cycle)
{
size_t len;
ngx_uint_t i;
ngx_listening_t *ls;
socklen_t olen;
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
/*此处会获取各种属性,篇幅问题,此处仅给出reuseport相关代码*/
#if (NGX_HAVE_REUSEPORT)
reuseport = 0;
olen = sizeof(int);
if (getsockopt(ls[i].fd, SOL_SOCKET, SO_REUSEPORT,
(void *) &reuseport, &olen) //获取reuseport属性
== -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
"getsockopt(SO_REUSEPORT) %V failed, ignored",
&ls[i].addr_text);
} else {
ls[i].reuseport = reuseport ? 1 : 0; //获取的reuseport属性保存至listening
}
#endif
}
return NGX_OK;
}
从父进程中继承得到的listening作为old_listening传至ngx_init_cycle,ngx_init_cycle函数中会重新解析配置生成新的listening数组,新的listening数组从old_listening数组中获取父进程中的监听套接字,这部分代码上面已经提到,此处省略。
再次声明:由于nginx本身较复杂,笔者可能理解有误,敬请指出,万分感谢!