避免 10 大 NGINX 配置错误(下)

语言: CN / TW / HK

原文作者:Timo Stark of F5Sergey Budnevich of F5

原文链接:避免 10 大 NGINX 配置错误

转载来源:NGINX 官方网站


在帮助 NGINX 用户解决问题时,我们经常会发现配置错误,这种配置错误也屡屡出现在其他用户的配置中,甚至有时还会出现在我们的 NGINX 工程师同事编写的配置中!本文介绍了 10 个最常见的错误,并解释了问题所在以及相应的解决方法。

  1. 每个 worker 的文件描述符不足
  2. error_log off 指令
  3. 未启用与上游服务器的 keepalive 连接
  4. 忘记指令继承的工作机制
  5. proxy_buffering off 指令
  6. if 指令使用不当
  7. 过多的健康检查
  8. 不安全地访问指标
  9. 当所有流量都来自同一个 /24 CIDR 块时使用 ip_hash
  10. 不采用上游组

错误 6:if 指令使用不当

if 指令使用起来很棘手,尤其是在 location{}块中。它通常不会按照预期执行,甚至还会导致出现段错误。

通常,在 if{} 块中,您可以一直安全使用的指令只有 return  rewrite。以下示例使用 if 来检测包含 X‑Test http消息头的请求(可以是您想要测试的任何条件)。NGINX 返回 430 (Request Header Fields Too Large) 错误,在指定的位置 @error_430 进行拦截并将请求代理到名为 b 的上游 group。

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

    proxy_pass http://a;
}

location @error_430 {
    proxy_pass b;
}

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

map $http_x_test $upstream_name {
    default "b";
    ""      "a";
}

# ...

location / {
    proxy_pass http://$upstream_name;
}

错误 7:过多的健康检查

配置多个虚拟服务器将请求代理到同一个上游组十分常见(换句话说,在多个 server{} 块中包含相同的 proxy_pass 指令)。这里的错误是在每个 server{} 块中都添加一个 health_check 指令。这样做只会增加上游服务器的负载,而不会产生任何额外信息。

显然,解决方法是每个 upstream{} 块只定义一个健康检查。此处,我们在一个指定位置为名为 b 的上游 group 定义了健康检查,并进行了适当的超时和http消息头设置。

location / {
    proxy_set_header Host $host;
    proxy_set_header "Connection" "";
    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 仪表盘分组到单个虚拟服务器中,如本例所示。

server {
	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;
	    # directives limiting access to the API (see 'Mistake 8' below)
	}
 
	location = /dashboard.html {
	    root   /usr/share/nginx/html;
	}
}

错误 8:不安全访问指标

Stub Status 模块提供了有关 NGINX 操作的基本指标。对于 NGINX Plus,您还可以使用 NGINX Plus API 收集更广泛的指标集。通过在 server{}  location{} 块中分别包含 stub_status  api 指令来启用指标收集,您随后可以通过访问 URL 来查看这些指标。

其中一些指标是敏感信息,可被用来攻击您的网站或 NGINX 代理的应用,我们有时在用户配置中看到的错误是未限制对相应 URL 的访问。此处我们将介绍一些可以保护指标的方法。在第一个示例中我们将使用 stub_status

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

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        stub_status;
    }
}

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

采用 HTTP 基本身份验证相关的方式为指标添加密码保护,包含 auth_basic  auth_basic_user_file 指令。文件(此处为 .htpasswd)列出了可以登录查看指标的客户端的用户名和密码:

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        auth_basic “closed site”;
        auth_basic_user_file conf.d/.htpasswd;
        stub_status;
    }
}

使用 allow 和 deny 指令保护指标

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

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        allow 192.168.1.0/24;
        allow 10.1.1.0/16;
        allow 2001:0db8::/32;
        allow 96.1.2.23/32;
        deny  all;
        stub_status;
    }
}

两种方法相结合

如果我们想结合使用这两种方法怎么办?我们可以允许客户端在没有密码的情况下从特定地址访问指标,但来自不同地址的客户端仍然需要登录。为此,我们使用 satisfy any 指令。它告诉 NGINX 允许使用 HTTP 基本身份验证凭证登录或使用预批准的 IP 地址登录的客户端访问。为了提高安全性,您可以将 satisfy 设置为 all,要求来自特定地址的人登录。

server {
    listen 80;
    server_name monitor.example.com;

    location = /basic_status {
        satisfy any;

        auth_basic “closed site”;
        auth_basic_user_file conf.d/.htpasswd;
        allow 192.168.1.0/24;
        allow 10.1.1.0/16;
        allow 2001:0db8::/32;
        allow 96.1.2.23/32;
        deny  all;
        stub_status;
    }
}

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

在没有密码的情况下,此配置只允许来自 96.1.2.23/32 网络或本地主机的客户端访问。由于指令是在 server{} 级别定义的,因此相同的限制同时应用于 API 和仪表板。附带说明一下,apiwrite=on 参数意味着这些客户端也可以使用 API 进行配置更改。

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

server {
    listen 8080;
    server_name monitor.example.com;
 
    satisfy any;
    auth_basic “closed site”;
    auth_basic_user_file conf.d/.htpasswd;
    allow 127.0.0.1/32;
    allow 96.1.2.23/32;
    deny  all;

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

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

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

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

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

http {

    upstream {
        ip_hash;
        server 10.10.20.105:8080;
        server 10.10.20.106:8080;
        server 10.10.20.108:8080;
    }
 
    server {# …}
}

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

解决方法是在哈希算法中使用 $binary_remote_addr 变量作为哈希键。该变量捕获完整的客户端地址,将其转换为二进制表示,IPv4 地址为 4 个字节,IPv6 地址为 16 个字节。现在,每个拦截设备的哈希值都不同,负载均衡可正常进行。

我们还添加了 consistent 参数以使用 ketama 哈希方法而不是默认值。这大大减少了在服务器集更改时重新映射到不同上游服务器的键的数量,为缓存服务器带来了更高的缓存命中率。

http {
    upstream {
        hash $binary_remote_addr consistent;
        server 10.10.20.105:8080;
        server 10.10.20.106:8080;
        server 10.10.20.108:8080;
    }

    server {# …}
}

错误 10:未采用上游组

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

http {

    server {
        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 {

    upstream node_backend {
        zone upstreams 64K;
        server 127.0.0.1:3000 max_fails=1 fail_timeout=2s;
        keepalive 2;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_set_header Host $host;
            proxy_pass http://node_backend/;
            proxy_next_upstream error timeout http_500;

        }
    }
}

zone 指令建立一个共享内存区,主机上的所有 NGINX worker 进程都可以访问有关上游服务器的配置和状态信息。几个上游组 可以共享该内存区。对于 NGINX Plus,该区域还支持您使用 NGINX Plus API 更改上游组中的服务器和单个服务器的设置,而无需重启 NGINX。

server 指令有几个参数可用来调整服务器行为。在本示例中,我们改变了 NGINX 用以确定服务器不健康,因此没有资格接受请求的条件。此处,只要通信尝试在每个 2 秒期间失败一次(而不是默认的在 10 秒期间失败一次),就会认为服务器不健康。

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

keepalive 指令设置每个 worker 进程缓存中保留的上游服务器的空闲 keepalive 连接的数量。我们已经在“错误 3:未启用与上游服务器的 keepalive 连接”中讨论了这样做的好处。

在 NGINX Plus 中,您还可以配置与上游组 有关的其他功能:

  • 上文提到了 NGINX 开源版仅在启动时将服务器主机名解析为 IP 地址一次。server 指令的 resolve参数能够支持 NGINX Plus 监控与上游服务器的域名对应的 IP 地址的变化,并自动修改上游配置而无需重新启动。

    service 参数进一步支持 NGINX Plus 使用 DNS SRV 记录,其中包括有关端口号、权重和优先级的信息。这对于通常动态分配服务端口号的微服务环境非常重要。

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

  • server 指令的 slow_start 参数支持 NGINX Plus 逐渐增加发送到新近被认为是健康且可用于接受请求的服务器的请求量。这可以防止请求激增,避免服务器不堪重负,进而导致再次失败。

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


更多资源

想要更及时全面地获取 NGINX 相关的技术干货、互动问答、系列课程、活动资源?

请前往 NGINX 开源社区: