避免顶级Nginx配置错误

2022-02-25 16:00:41

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

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

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

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

以下是需要更多FD的原因:从NGINX工作进程到客户端或上游服务器的每个连接都会消耗一个FD。当NGINX充当web服务器时,它使用一个FD作为客户端连接,每个服务文件使用一个FD,每个客户端至少使用两个FD(但大多数web页面是由许多文件构建的)。当它充当代理服务器时,NGINX使用一个FD分别连接到客户端和上游服务器,并可能使用第三个FD临时存储服务器响应的文件。作为缓存服务器,对于缓存响应,NGINX的行为类似于web服务器,如果缓存为空或过期,NGINX的行为类似于代理服务器。

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

如果将NGINX作为服务启动,则init脚本或systemd服务清单变量

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

FD的数量也有一个系统范围的限制,您可以使用操作系统的sysctl fs进行设置。文件最大值命令。它通常足够大,但值得验证的是,所有NGINX工作进程可能使用的最大文件描述符数量(worker_rlimit_nofile*worker_connections)明显少于fs。file‑max。如果NGINX以某种方式使用了所有可用的FD(例如,在DoS攻击期间),则即使登录到计算机也无法修复该问题。

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

我们不建议禁用错误日志,因为在调试NGINX的任何问题时,它是一个重要的信息源。但是,如果存储空间非常有限,可能需要记录足够的数据以耗尽可用的磁盘空间,那么禁用错误记录可能是有意义的。在主配置上下文中包含此指令:

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

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

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

解决方案是在NGINX和上游服务器之间启用keepalive连接,而不是在请求完成时关闭,而是保持连接打开以用于其他请求。这既降低了源端口耗尽的可能性,又提高了性能。

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

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

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

还请注意,当您在上游{}块中指定负载平衡算法时(使用哈希、ip_哈希、最小连接、最小时间或随机指令),该指令必须出现在keepalive指令的上方。这是NGINX配置中指令顺序无关紧要的一般规则的罕见例外之一。

在将请求转发到上游组的location{}块中,包括以下指令以及proxy_pass指令:

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

proxy_http_version指令告诉NGINX改用http/1.1,proxy_set_header指令从连接头中删除close值。

NGINX指令向下继承,或“外部-内部”:子上下文(嵌套在另一个上下文(其父上下文)中)继承父级包含的指令设置。例如,http{}上下文中的所有服务器{}和位置{}块继承http级别包含的指令的值,而服务器{}块中的指令由其中的所有子位置{}块继承。但是,当同一指令同时包含在父上下文及其子上下文中时,这些值不会一起添加——相反,子上下文中的值会覆盖父值。

错误在于忘记了数组指令的“覆盖规则”,它不仅可以包含在多个上下文中,而且可以在给定上下文中多次包含。例如proxy_set_header和add_header–在second的名称中加上“add”会让人特别容易忘记覆盖规则。

http{

对于侦听端口8080的服务器,在服务器{}或位置{}块中都没有add_头指令。所以继承很简单,我们可以看到http{}上下文中定义的两个头:

%curl-is localhost:8080

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

%curl-is localhost:8081

在子位置/测试块中,有一个add_header指令,它覆盖来自其父服务器{}块的头和来自http{}上下文的两个头:

%curl-is localhost:8081/test

如果我们想让位置{}块保留在其父上下文中定义的头以及本地定义的任何头,我们必须在位置{}块中重新定义父头。这就是我们在位置/正确区块中所做的:

%curl-localhost:8081/正确吗

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

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

因此,我们惊讶地发现,在配置中,代理_缓冲的频率是如此之高。也许这是为了减少客户端所经历的延迟,但其影响可以忽略,而副作用却很多:禁用代理缓冲时,即使配置了,速率限制和缓存也无法工作,性能受到影响,等等。

只有少数情况下禁用代理缓冲可能有意义(例如长轮询),因此我们强烈反对更改默认设置。有关更多信息,请参阅《NGINX Plus管理指南》。

if指令很难使用,尤其是在位置{}块中。它通常不会达到你期望的效果,甚至可能导致故障。事实上,非常棘手的是,NGINX Wiki上有一篇题为“如果是邪恶的”的文章,我们会指导您详细讨论这些问题以及如何避免它们。

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

地点/{

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

映射$http_x_test$上游_name{

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

冒着显而易见的风险,解决方法是只为每个上游{}块定义一个健康检查。在这里,我们在一个特殊的命名位置为名为b的上游组定义健康检查,并完成适当的超时和标头设置。

地点/{

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

服务器{

有关HTTP、TCP、UDP和gRPC服务器运行状况检查的更多信息,请参阅《NGINX Plus管理指南》。

有关NGINX操作的基本指标可从存根状态模块获得。对于NGINX Plus,您还可以使用NGINX Plus API收集更广泛的指标集。通过在服务器{}或位置{}块中分别包含stub_status或api指令来启用度量集合,该块将成为您随后访问以查看度量的URL。(对于NGINX Plus API,您还需要为NGINX实体(虚拟服务器、上游组、缓存等)配置共享内存区域,以便收集指标;请参阅《NGINX Plus管理指南》中的说明。)

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

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

服务器{

如果您不希望授权用户必须登录,并且您知道他们将从中访问指标的IP地址,另一个选项是allow指令。您可以指定单个IPv4和IPv6地址以及CIDR范围。deny all指令禁止从任何其他地址访问。

服务器{

如果我们想把这两种方法结合起来呢?我们可以允许客户在没有密码的情况下从特定地址访问指标,并且仍然需要来自不同地址的客户登录。为此,我们使用“满足任何指令”。它告诉NGINX允许使用HTTP基本身份验证凭据登录或使用预先批准的IP地址的客户端访问。为了获得额外的安全性,您可以将“满足所有”设置为要求来自特定地址的人登录。

服务器{

使用NGINX Plus,您可以使用相同的技术来限制对NGINX Plus API端点的访问(http://monitor.example.com:8080/api/在下面的示例中)以及http://monitor.example.com/dashboard.html.

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

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

服务器{

ip_散列算法基于客户端ip地址的散列,在上游{}块中跨服务器负载平衡流量。哈希键是IPv4地址或整个IPv6地址的前三个八位字节。该方法建立会话持久性,这意味着来自客户端的请求总是传递到同一服务器,除非该服务器不可用。

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

http{

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

解决方法是使用散列

......