博客 | NGINX

避免十大 NGINX 配置错误

NGINX-F5-horiz-black-type-RGB 的一部分
Timo Stark 缩略图
蒂莫·史塔克
2022 年 2 月 22 日发布

当我们帮助遇到问题的 NGINX 用户时,我们经常会看到其他用户的配置中反复出现的相同配置错误——有时甚至在 NGINX 工程师编写的配置中也会出现! 在这篇博客中,我们将讨论 10 个最常见的错误,解释错误的原因以及如何修复它们。

  1. 每个 worker 没有足够的文件描述符
  2. error_log off指令
  3. 未启用与上游服务器的保持连接
  4. 忘记指令继承的工作原理
  5. proxy_buffering off指令
  6. if指令使用不当
  7. 过度健康检查
  8. 不安全的指标访问
  9. 当所有流量来自同一个 /24 CIDR 块时使用ip_hash
  10. 没有利用上游群体

错误1: 每个 Worker 没有足够的文件描述符

worker_connections指令设置 NGINX 工作进程可以打开的最大同时连接数(默认为 512)。 所有类型的连接(例如,与代理服务器的连接)都计入最大值,而不仅仅是客户端连接。 但请务必记住,每个工作进程的同时连接数最终还有另一个限制:操作系统对分配给每个进程的最大文件描述符 (FD) 数量的限制。 在现代 UNIX 发行版中,默认限制是 1024。

除了最小的 NGINX 部署之外,每个工作进程 512 个连接的限制可能太小了。 事实上,我们随 NGINX 开源二进制文件和 NGINX Plus 分发的默认nginx.conf文件将其增加到 1024。

常见的配置错误是没有将 FD 的限制增加到worker_connections值的至少两倍。 修复方法是使用主配置上下文中的worker_rlimit_nofile指令设置该值。

需要更多 FD 的原因如下:NGINX 工作进程与客户端或上游服务器之间的每个连接都会消耗一个 FD。当 NGINX 充当 Web 服务器时,它会为客户端连接使用一个 FD,为每个服务文件使用一个 FD,每个客户端至少使用两个 FD(但大多数网页都是由多个文件构建的)。 当它充当代理服务器时,NGINX 使用一个 FD 来连接客户端和上游服务器,并且可能使用第三个 FD 来临时存储服务器的响应的文件。 作为缓存服务器,NGINX 对于缓存响应的行为就像一个 Web 服务器,如果缓存为空或已过期,则它就像一个代理服务器。

NGINX 还使用每个日志文件一个 FD 和几个 FD 与主进程进行通信,但通常这些数字与用于连接和文件的 FD 数量相比很小。

UNIX 提供了几种方法来设置每个进程的 FD 数量:

  • 如果你从 shell 启动 NGINX,则使用 ulimit命令
  • 如果您将 NGINX 作为服务启动,则 init脚本或systemd服务清单变量
  • /etc/security/limits.conf文件

但是,使用的方法取决于您如何启动 NGINX,而无论您如何启动 NGINX, worker_rlimit_nofile都可以工作。

系统范围内的 FD 数量也有限制,您可以使用操作系统的sysctl fs.file-max命令进行设置。 它通常足够大,但值得验证的是,所有 NGINX 工作进程可能使用的文件描述符的最大数量( worker_rlimit_nofile * worker_processes )是否明显小于fs.file‑max 。 如果 NGINX 以某种方式使用了所有可用的 FD(例如,在 DoS 攻击期间),则甚至不可能登录到机器来解决问题。

错误2: error_log off指令

常见的错误是认为error_log off指令会禁用日志记录。 事实上,与access_log指令不同, error_log不接受off参数。 如果在配置中包含error_log off指令,NGINX 会在 NGINX 配置文件的默认目录中创建一个名为off的错误日志文件(通常是/etc/nginx )。

我们不建议禁用错误日志,因为它是调试 NGINX 问题时的重要信息来源。但是,如果存储空间非常有限,记录的数据可能会耗尽可用的磁盘空间,则禁用错误日志记录可能是合理的。 在主配置上下文中包含此指令:

错误日志/dev/null emerg;

请注意,只有 NGINX 读取并验证配置后,此指令才适用。 因此,每次 NGINX 启动或重新加载配置时,它可能会记录到默认错误日志位置(通常是/var/log/nginx/error.log ),直到配置得到验证。 要更改日志目录,请包括 -e <错误日志位置> 参数 nginx 命令。

错误3: 未启用与上游服务器的保持连接

默认情况下,NGINX 为每个新的传入请求打开与上游(后端)服务器的新连接。 这种方式安全但效率低下,因为 NGINX 和服务器必须交换三个数据包才能建立连接,并且必须交换三到四个数据包才能终止连接。

在流量很大的时候,为每个请求打开一个新连接会耗尽系统资源,甚至无法打开连接。 原因如下:对于每个连接,源地址、源端口、目标地址和目标端口的四元组必须是唯一的。 对于从 NGINX 到上游服务器的连接,其中三个元素(第一、第三和第四个)是固定的,只留下源端口作为变量。 当连接关闭时,Linux 套接字将处于TIME‑WAIT状态两分钟,这在高流量时会增加耗尽可用源端口池的可能性。 如果发生这种情况,NGINX 将无法打开与上游服务器的新连接。

修复方法是启用 NGINX 和上游服务器之间的保持连接- 当请求完成时,连接不会关闭,而是保持打开状态以用于其他请求。 这既降低了源端口耗尽的可能性,又提高了性能

要启用保持连接:

  • 在每个upstream{}块中包含keepalive指令,以设置每个工作进程的缓存中保存的与上游服务器的空闲keepalive连接的数量。

    请注意, keepalive指令不会限制 NGINX 工作进程可以打开的与上游服务器的连接总数 - 这是一个常见的误解。 因此keepalive的参数不需要像你想象的那么大。

    我们建议将该参数设置为upstream{}块中列出的服务器数量的两倍。 这个数字足够大,使得 NGINX 能够与所有服务器维持保持连接,但又足够小,使得上游服务器也可以处理新的传入连接。

    还要注意,当您在upper{}块中指定负载平衡算法时 - 使用haship_hashleast_connleast_timerandom指令 - 该指令必须出现在keepalive指令上方。 这是 NGINX 配置中指令的顺序无关紧要的一般规则的罕见例外之一。

  • 在将请求转发到上游组的location{}块中,将以下指令与proxy_pass指令一起包含:

    proxy_http_version 1.1;
    proxy_set_header "连接" "";
    

    默认情况下,NGINX 使用 HTTP/1.0 连接上游服务器,并相应地将Connection: close标头添加到转发到服务器的请求中。 结果是,尽管在upstream{}块中存在keepalive指令,但每个连接都会在请求完成时关闭。

    proxy_http_version指令告诉 NGINX 改用 HTTP/1.1,而proxy_set_header指令从Connection标头中删除close值。

错误4: 忘记指令继承的工作原理

NGINX 指令是向下继承的,或者说是“从外向内”:上下文(嵌套在另一个上下文(其父)中的子上下文)继承了父级包含的指令的设置。 例如, http{}上下文中的所有server{}location{}块都会继承http级别包含的指令的值,而server{}块中的指令会被其中的所有子location{}块继承。 但是,当相同的指令同时包含在父上下文及其子上下文中时,它们的值不会相加 - 相反,子上下文中的值会覆盖父上下文的值。

错误在于忘记了数组指令的这个“覆盖规则”,它不仅可以在多个上下文中包含,还可以在给定上下文中包含多次。 示例包括proxy_set_headeradd_header - 在第二个名称中包含“add”使得人们很容易忘记覆盖规则。

我们可以通过add_header这个例子来说明继承是如何工作的:

http { 
add_header X-HTTP-LEVEL-HEADER 1;
add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;

服务器 {
listen 8080;
location / {
return 200 "OK";
} 
}

服务器 {
listen 8081;
add_header X-SERVER-LEVEL-HEADER 1;

location / {
return 200 "OK";
}

location /test {
add_header X-LOCATION-LEVEL-HEADER 1;
return 200 "OK";
}

location /correct {
add_header X-HTTP-LEVEL-HEADER 1;
add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;

add_header X-SERVER-LEVEL-HEADER 1;
add_header X-LOCATION-LEVEL-HEADER 1;
return 200 "OK";
} 
}
}

对于监听端口 8080 的服务器, server{}location{}块中都没有add_header指令。 因此继承很简单,我们看到在http{}上下文中定义的两个标头:

curl -i localhost:8080 HTTP / 1.1 200 OK服务器:nginx / 1.21.5日期: 2022 年 2 月 21 日星期一 10:12:15 GMT 内容类型:纯文本 内容长度: 2 连接:保持活动X-HTTP-LEVEL-HEADER: 1 X-另一个-HTTP级别标头: 1确定

对于侦听端口 8081 的服务器, server{}块中有一个add_header指令,但在其子位置/块中没有。 server{}块中定义的标头会覆盖http{}上下文中定义的两个标头:

curl -i localhost:8081 HTTP / 1.1 200 OK服务器:nginx / 1.21.5日期: 2022 年 2 月 21 日星期一 10:12:20 GMT 内容类型:纯文本 内容长度: 2 连接:保持活动X-SERVER-LEVEL-HEADER: 1确定

在子location /test块中,有一个add_header指令,它覆盖了其父server{}块的标头和http{}上下文的两个标头:

curl -i localhost:8081 / test HTTP / 1.1 200 OK服务器:nginx / 1.21.5日期: 2022 年 2 月 21 日星期一 10:12:25 GMT 内容类型:纯文本 内容长度: 2 连接:保持活动X-LOCATION-LEVEL-HEADER: 1确定

如果我们希望location{}块保留其父上下文中定义的标题以及本地定义的任何标题,则我们必须在location{}块内重新定义父标题。 这就是我们在location /correct块中所做的:

curl -i localhost:8081 / correct HTTP / 1.1 200 OK服务器:nginx / 1.21.5日期: 2022 年 2 月 21 日星期一 10:12:30 GMT 内容类型:纯文本 内容长度: 2 连接:保持活动X-HTTP-LEVEL-HEADER: 1 X-另一个-HTTP级别标头: 1 X服务器级别标头: 1 X-位置-级别-标题: 1确定

错误5: proxy_buffering off指令

NGINX 中默认启用代理缓冲( proxy_buffering指令设置为on )。 代理缓冲意味着 NGINX 将其从服务器接收到的响应存储在内部缓冲区中,并且直到整个响应缓冲完毕后才开始向客户端发送数据。 缓冲有助于优化慢速客户端的性能——因为 NGINX 会在客户端检索所有响应所需的时间内缓冲响应,所以代理服务器可以尽快返回其响应并恢复为可服务于其他请求。

当代理缓冲被禁用时,NGINX 只会在开始将服务器响应发送到客户端之前缓冲其第一部分,缓冲区默认大小为一个内存页面(4 KB或 8 KB,具体取决于操作系统)。 通常,这刚好够容纳响应头的空间。 然后,NGINX 在收到响应后将其同步发送到客户端,迫使服务器处于空闲状态,等待直到 NGINX 可以接受下一个响应段。

因此,我们对在配置中看到proxy_buffering关闭的频率感到惊讶。 也许它旨在减少客户端遇到的延迟,但效果微乎其微,而副作用却很多:禁用代理缓冲后,即使配置了速率限制和缓存也不起作用,性能会受到影响,等等。

仅在少数用例中禁用代理缓冲可能是有意义的(例如长轮询),因此我们强烈建议不要更改默认设置。 有关更多信息,请参阅NGINX Plus 管理指南

错误6: if指令的不当使用

if指令使用起来比较棘手,特别是在location{}块中。 它通常不会按照您期望的方式运行,甚至会导致段错误。 事实上,它非常棘手,以至于 NGINX Wiki 中有一篇名为“If is Evil”的文章,我们会引导您到那里详细讨论这些问题以及如何避免它们。

一般来说,在if{}块中始终可以安全使用的唯一指令是returnrewrite 。 以下示例使用if来检测包含X‑Test标头的请求(但这可以是您想要测试的任何条件)。 NGINX返回430(请求字段太大错误,在命名位置@error_430处拦截它,并将请求代理到名为b的上游组。

位置 / { 
error_page 430 = @error_430;
if ($http_x_test) {
return 430; 
}

proxy_pass http://a;
}

位置 @error_430 {
proxy_pass b;
}

对于这个以及if的许多其他用途,通常可以完全避免该指令。 在以下示例中,当请求包含X-Test标头时, map{}块将$upstream_name变量设置为b ,并且请求被代理到具有该名称的上游组。

映射 $http_x_test $upstream_name { 
默认 "b";
"" "a";
}

# ...

位置 / {
proxy_pass http://$upstream_name;
}

错误7: 过度的健康检查

配置多个虚拟服务器来代理对同一个上游组的请求是很常见的(换句话说,在多个server{}块中包含相同的proxy_pass指令)。 这种情况下的错误是在每个server{}块中都包含一个health_check指令。 这只会给上游服务器带来更多负载,而不会产生任何额外的信息。

尽管风险显而易见,但修复方法是每个upstream{}块仅定义一个健康检查。 在这里,我们在一个特殊的命名位置定义名为b的上游组的健康检查,并配有适当的超时和标头设置。

location / { 
proxy_set_header Host $host;
proxy_set_header "连接" "";
proxy_http_version 1.1;
proxy_pass http://b;
}

location @health_check {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host example.com;
proxy_pass http://b;
}

在复杂的配置中,它可以进一步简化管理,将所有健康检查位置与NGINX Plus API和仪表板一起分组到单个虚拟服务器中,如本例所示。

服务器 { 
listen 8080;

location / {
# …
}

location @health_check_b {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host example.com;
proxy_pass http://b;
}

location @health_check_c {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host api.example.com;
proxy_pass http://c;
}

location /api {
api write=on;
# 限制对 API 的访问的指令(请参阅下面的“错误 8”)
}

location = /dashboard.html {
root /usr/share/nginx/html;
}
}

有关 HTTP、TCP、UDP 和 gRPC 服务器健康检查的更多信息,请参阅NGINX Plus 管理指南

错误 8: 不安全的指标访问

关于 NGINX 操作的基本指标可从Stub Status模块获取。 对于 NGINX Plus,您还可以使用NGINX Plus API收集更广泛的指标集。通过在server{}location{}块中分别包含stub_statusapi指令来启用指标收集,这些指令将成为您随后访问以查看指标的 URL。 (对于NGINX Plus API ,您还需要为想要收集指标的 NGINX 实体(虚拟服务器、上游组、缓存等)配置共享内存区域;请参阅NGINX Plus 管理指南中的说明。)

其中一些指标是敏感信息,可用于攻击您的网站或 NGINX 代理的应用程序,而我们有时在用户配置中看到的错误是未能限制对相应 URL 的访问。 下面我们来介绍一些可以确保指标安全的方法。 我们将在第一个例子中使用stub_status

通过以下配置,互联网上的任何人都可以访问http://example.com/basic_status上的指标。

服务器 { 
listen 80;
server_name example.com;

location = /basic_status {
stub_status;
}
}

使用 HTTP 基本身份验证保护指标

要使用HTTP 基本身份验证对指标进行密码保护,请包含auth_basicauth_basic_user_file指令。 该文件(此处为.htpasswd )列出了可以登录查看指标的客户端的用户名和密码:

服务器 { 
listen 80;
server_name example.com;

location = /basic_status {
auth_basic “已关闭站点”;
auth_basic_user_file conf.d/.htpasswd;
stub_status;
}
}

使用允许拒绝指令保护指标

如果您不希望授权用户登录,并且您知道他们将从哪个 IP 地址访问指标,则另一个选项是允许指令。 您可以指定单独的 IPv4 和 IPv6 地址和 CIDR 范围。 拒绝所有指令可阻止任何其他地址的访问。

服务器 { 
监听 80;
服务器名称 example.com;

位置 = /basic_status {
允许 192.168.1.0/24;
允许 10.1.1.0/16;
允许 2001:0db8::/32;
允许 96.1.2.23/32;
全部拒绝;
stub_status;
}
}

结合两种方法

如果我们想将这两种方法结合起来该怎么办? 我们可以允许客户端无需密码即可从特定地址访问指标,但仍然要求来自不同地址的客户端登录。 为此,我们使用满足任何指令。 它告诉 NGINX 允许使用 HTTP Basic 身份验证凭据登录或使用预先批准的 IP 地址的客户端访问。 为了增加安全性,您可以将满足设置为全部,以要求来自特定地址的人也登录。

服务器 { 
listen 80;
server_name monitor.example.com;

location = /basic_status {
满足任何;

auth_basic “已关闭站点”;
auth_basic_user_file conf.d/.htpasswd;
允许 192.168.1.0/24;
允许 10.1.1.0/16;
允许 2001:0db8::/32;
允许 96.1.2.23/32;
拒绝所有;
stub_status;
}
}

使用 NGINX Plus,您可以使用相同的技术来限制对NGINX Plus API端点(以下示例中为http://monitor.example.com:8080/api/ )以及http://monitor.example.com/dashboard.html上的实时活动监控仪表板的访问。

此配置仅允许来自 96.1.2.23/32 网络或本地主机的客户端无需密码进行访问。 由于指令是在服务器级别定义的,因此相同的限制适用于 API 和仪表板。 附注: apiwrite=on参数意味着这些客户端也可以使用 API 来更改配置。

有关配置 API 和仪表板的更多信息,请参阅NGINX Plus 管理指南

服务器 { 
listen 8080;
server_name monitor.example.com;

满足任何;
auth_basic “已关闭站点”;
auth_basic_user_file conf.d/.htpasswd;
允许 127.0.0.1/32;
允许 96.1.2.23/32;
全部拒绝;

location = /api/ { 
api write=on;
}

location = /dashboard.html {
root /usr/share/nginx/html;
}
}

错误9: 当所有流量来自同一个 /24 CIDR 块时使用ip_hash

ip_hash算法基于客户端 IP 地址的哈希值,在上游{}块中的服务器之间平衡流量负载。 散列密钥是 IPv4 地址的前三个八位字节或整个 IPv6 地址。 该方法建立会话持久性,这意味着除非服务器不可用,否则来自客户端的请求始终被传递到同一台服务器。

假设我们已经在配置了高可用性的虚拟专用网络中将 NGINX 部署为反向代理。 我们在 NGINX 前面放置了各种防火墙、路由器、第 4 层负载均衡器和网关,以接受来自不同来源(内部网络、合作伙伴网络、互联网等)的流量并将其传递给 NGINX 以反向代理到上游服务器。 以下是初始 NGINX 配置:

http {
上游 {
ip_hash;
服务器 10.10.20.105:8080;
服务器 10.10.20.106:8080;
服务器 10.10.20.108:8080;
}

服务器 {# …}
}

但事实证明存在一个问题:所有“拦截”设备都在同一个 10.10.0.0/24 网络上,因此对于 NGINX 来说,所有流量似乎都来自该 CIDR 范围内的地址。 请记住, ip_hash算法对 IPv4 地址的前三个八位字节进行散列。 在我们的部署中,每个客户端的前三个八位字节都是相同的 - 10.10.0 - 因此所有客户端的哈希值都是相同的,并且没有将流量分发到不同服务器的基础。

修复方法是使用哈希算法,并使用$binary_remote_addr变量作为哈希键。 该变量捕获完整的客户端地址,并将其转换为二进制表示形式,对于 IPv4 地址为 4 个字节,对于 IPv6 地址为 16 个字节。 现在,每个拦截设备的哈希值都不同,并且负载均衡按预期工作。

我们还包含一致的参数来使用ketama散列方法而不是默认方法。 这大大减少了当服务器集发生变化时重新映射到不同上游服务器的键的数量,从而为缓存服务器带来更高的缓存命中率。

http { 
上游 {
hash $binary_remote_addr 一致;
服务器 10.10.20.105:8080;
服务器 10.10.20.106:8080;
服务器 10.10.20.108:8080;
}

服务器 {# …}
}

错误10: 不利用上游群体

假设您正在使用 NGINX 作为最简单的用例之一,作为监听端口 3000 的单个基于 NodeJS 的后端应用的反向代理。 常见的配置可能如下所示:

http {
服务器 {
listen 80;
server_name example.com;

location / {
proxy_set_header Host $host;
proxy_pass http://localhost:3000/;
}
}
}

很简单,对吧? proxy_pass指令告诉 NGINX 将客户端的请求发送到哪里。 NGINX 需要做的就是将主机名解析为 IPv4 或 IPv6 地址。 一旦建立连接,NGINX 就会将请求转发到该服务器。

这里的错误是假设因为只有一台服务器 - 因此没有理由配置负载均衡 - 所以创建upstream{}块是没有意义的。 事实上, upstream{}块解锁了几个可以提高性能的功能,如以下配置所示:

http {
上游 node_backend {
区域上游 64K;
服务器 127.0.0.1:3000 max_fails=1 fail_timeout=2s;
keepalive 2;
}

服务器 {
listen 80;
server_name example.com;

location / {
proxy_set_header Host $host;
proxy_pass http://node_backend/;
proxy_next_upstream 错误超时 http_500;

}
}
}

zone指令建立一个共享内存区域,主机上的所有 NGINX 工作进程都可以访问有关上游服务器的配置和状态信息。 多个上游组可以共享该区域。 使用 NGINX Plus,该区域还允许您使用NGINX Plus API更改上游组中的服务器以及单个服务器的设置,而无需重新启动 NGINX。有关详细信息,请参阅NGINX Plus 管理指南

服务器指令有几个参数可以用来调整服务器行为。 在这个例子中,我们改变了 NGINX 使用来确定服务器不健康并因此没有资格接受请求的条件。 在这里,如果每 2 秒内通信尝试失败一次(而不是默认的10 秒内失败一次),则认为服务器不健康。

我们将此设置与proxy_next_upstream指令结合起来,以配置 NGINX 认为失败的通信尝试,在这种情况下,它会将请求传递给上游组中的下一个服务器。 在默认错误和超时条件中,我们添加了http_500 ,以便 NGINX 认为 HTTP 500(内部服务器错误)来自上游服务器的代码表示尝试失败。

keepalive指令设置每个工作进程的缓存中保存的与上游服务器的空闲保持连接的数量。 我们已经在错误 3 中讨论过其好处: 未启用与上游服务器的保持连接

使用 NGINX Plus,您可以通过上游组配置附加功能:

  • 我们上面提到,NGINX Open Source 在启动期间仅将服务器主机名解析为 IP 地址一次。 服务器指令的resolve参数使 NGINX Plus 能够监视与上游服务器域名对应的 IP 地址的变化,并自动修改上游配置,而无需重新启动。

    服务参数进一步使 NGINX Plus 能够使用 DNS SRV记录,其中包括有关端口号、权重和优先级的信息。 这在微服务环境中至关重要,因为服务的端口号通常是动态分配的。

    有关解析服务器地址的更多信息,请参阅我们博客上的使用 NGINX 和 NGINX Plus 的 DNS 进行服务发现

  • 服务器指令的slow_start参数使 NGINX Plus 能够逐渐增加它向新被视为健康且可接受请求的服务器发送的请求量。 这可以防止突然大量的请求导致服务器不堪重负并再次出现故障。

  • 当无法选择上游服务器来处理请求时,队列指令使 NGINX Plus 能够将请求放入队列,而不是立即向客户端返回错误。

资源

要试用 NGINX Plus,请立即开始30 天免费试用联系我们讨论您的用例


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”