nginx 1.15.2 版本说明

0x00 Feature List

主要包含如下两点:

  • Feature: the $ssl_preread_protocol variable in the ngx_stream_ssl_preread_module.

  • Feature: now when using the “reset_timedout_connection” directive nginx will reset connections being closed with the 444 code.

这里我们重点关注第二个特性(第一个特性,后续我们专门写篇介绍Nginx和SSL应用的文章再具体分析)

0x01 Reset Timeoutd Connection

第二个机制可以直译说明如下:

如果开启reset_timedout_connection配置后,Ngnix接收HTTP 444状态码后,会重置对应的连接

可以简单拆解为如下三个问题讨论:

  • HTTP 444状态码的作用和目的
  • 指令reset_timedout_connection的机制和作用
  • 整体这个feature的作用

1.1 HTTP 444状态码

其实444是Nginx内部的状态码,从nginx return的指令,看到说明如下:

…The non-standard code 444 closes a connection without sending a response header …

同时Http Status网站也有详细说明, 如下:

A non-standard status code used to instruct nginx to close the connection without sending a response to the client, most commonly used to deny malicious or malformed requests.

这样就比较明确, HTTP 444是Nginx内部的状态码, 其主要是作用是告诉 nginx可以直接关闭对应的链接,无需发送响应头部。这可以有效处理恶意或者畸形的流量。

1.2 reset_timedout_connection指令

该指令是由ngx_http_core_module提供的,如下是官方的说明:

1
2
3
4
5
* Syntax: reset_timedout_connection on | off;
* Default: reset_timedout_connection off;
* Context: http, server, location
Enables or disables resetting timed out connections. The reset is performed as follows. Before closing a socket, the SO_LINGER option is set on it with a timeout value of 0. When the socket is closed, TCP RST is sent to the client, and all memory occupied by this socket is released. This helps avoid keeping an already closed socket with filled buffers in a FIN_WAIT1 state for a long time.

简而言之, 当开启reset_timedout_connection后,Nginx 会通过设置SO_LINGER的选项快速关闭超时的连接, 其作用是保障系统和 Nginx 能够快速释放无用的连接的资源。 具体实现上面,就是通过控制SO_LINGER选项。

关于SO_LINGER其作用,可以简述为是控制close的行为, 参考Linux Socket SO_LINGER选项 (个人感觉解释的比较清晰明了)的说明,可以简述如下:

1
2
3
4
5
//SO_LINGER选项有如下结构:
struct linger {
int l_onoff; /* 0 = off, nozero = on */
int l_linger; /* linger time */
};

当设置 l_onoff为1,l_linger为0,则连接立即终止,TCP将丢弃残留在发送缓冲区中的数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;在远端的recv()调用将以WSAECONNRESET出错。

这个也是Nginx的默认行为如下:

1
2
3
4
5
6
7
8
9
10
11
12
// file: ngx_http_request.c, 快速关闭请求
if (clcf->reset_timedout_connection) {
linger.l_onoff = 1;
linger.l_linger = 0;
if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER,
(const void *) &linger, sizeof(struct linger)) == -1)
{
ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
"setsockopt(SO_LINGER) failed");
}
}

1.3 整体 Feature 作用

如上,整体的作用其实就是让 Nginx 能够快速释放相关的资源, 如下是前后的流程对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//before
A +---------> B +--------> C
1. return 444
<-----------+
2.normal close
<------------+
//now
A +---------> B +--------> C
2. reset socket 1. return 444
<-----------+ <-----------+

0x02 Nginx 模块的执行顺序

算是这次的补充说明, 是好久之前追查的一个问题,才发现之前对 Nginx 模块的执行顺序一直理解有误,这边记录说明下。

我们知道nginx的一个状态机驱动的模型, 那么在同一个阶段(状态)下面, 模块的执行顺序如何呢? 具体结论如下:

在非 filter 模块情况下, 在configure的时候, 越晚添加的模块就会越先执行(挂载在同一阶段的模块哈), 几个注意点如下:

  1. 上面描述的是在configure阶段越晚添加的越先执行, 指的是 Nginx 默认的configure,由于一些产品线的nginx 都会包一层自己的编译处理逻辑,这个时候就需要关注其是如何新增模块的(也就是新增模块和真正 configure)的关系

  2. Nginx扩展的自己config也会影响或者控制模块的执行顺序,默认的config如下:

1
HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_baidu_problem_tracing_module"

但是如果改写成

1
HTTP_AUX_FILTER_MODULES="ngx_http_baidu_problem_tracing_module $HTTP_AUX_FILTER_MODULES "

原因具体下面说明;

现在我们来看一个模块/扩展是如何接入 Nginx 的状态机的;

1. configure阶段, 这个过程,Nginx 的脚本通过我们的编译控制,收集相关的信息,最终会生成一个 Shell 变量,这个变量包含了我们要编译进去的所有模块名称,如下:

1
2
3
4
5
6
7
8
9
10
if [ $HTTP = YES ]; then
modules="$modules $HTTP_MODULES $HTTP_FILTER_MODULES \
$HTTP_HEADERS_FILTER_MODULE \
$HTTP_AUX_FILTER_MODULES \
$HTTP_COPY_FILTER_MODULE \
$HTTP_RANGE_BODY_FILTER_MODULE \
$HTTP_NOT_MODIFIED_FILTER_MODULE"
NGX_ADDON_DEPS="$NGX_ADDON_DEPS \$(HTTP_DEPS)"
fi

这个里面需要说明的一点是module是直接引用了$HTTP_AUX_FILTER_MODULES之类的变量,而这些变量是每个扩展的config文件自己控制的,也就是说你把变量名字写在前后,直接影响你在modules里面的顺序

  1. 代码生成阶段,通过如上的环境变量, Nginx会生成类似于如下的文件,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ngx_module_t *ngx_modules[] = {
&ngx_core_module,
&ngx_errlog_module,
&ngx_conf_module,
&ngx_events_module,
&ngx_event_core_module,
&ngx_epoll_module,
&ngx_openssl_module,
&ngx_regex_module,
&ngx_http_module,
&ngx_http_core_module,
&ngx_http_log_module,
&ngx_http_upstream_module,
&ngx_http_static_module,
&ngx_http_autoindex_module,
&ngx_http_index_module,
/* 这里说明 access 是 auth basic 在 access 阶段先执行的 */
&ngx_http_auth_basic_module,
&ngx_http_access_module,
...
}
  1. 启动初始化阶段, 其在http/ngx_http.c会有大概如下的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//file: http/ngx_http.c
n += cmcf->phases[i].handlers.nelts;
// 从后面开始,追加到对于ph里面; 也就是越早在ngx modules里面被引用的,在会放在ph的越前面
for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j--) {
ph->checker = checker;
ph->handler = h[j];
ph->next = n;
ph++;
}
}
return NGX_OK;
}
  1. 执行阶段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ngx_http_core_run_phases(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_http_phase_handler_t *ph;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ph = cmcf->phase_engine.handlers;
// 顺序执行
while (ph[r->phase_handler].checker) {
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
if (rc == NGX_OK) {
return;
}
}
}

如上,可以明确:

在非filter 模块情况下, 在configure的时候, 越晚添加的模块就会越先执行(挂载在同一阶段的模块哈)

0x03 Reference