Nginx if 避坑指南

2024-06-15 ⏳2.7分钟(1.1千字) 🕸️

Nginx 配置以静态规则为主。现在我们可以使用 njs 或者 lua 模块实现各种动态逻辑。在这些模块出现之前,只能通过 rewrite 模块的 if 指令实现一定的动态规则。这个 if 在设计之初仅用来做 URL 重写或重定向。但发布之后很多用户尝试使用 if 实现种动态配置,这导致了很多意想不到的问题。所以有 if is evil 之说。今天就来分享如何避开 if 的各类问题。

if 指令由 rewrite 模块提供,显然它主要是用于 URL 重写领域。典型的 if 用法如下:

http {
  server {
    listen 8080;

    if ($http_user_agent ~ MSIE) {
      rewrite ^(.*)$ /msie/$1 break;
    }

    if ($request_method = POST) {
      return 405; 
    }
  }
}

上例中第一个 if 检查如果 user agent 字符串中包含 MSIE,就把 URL 重写为 /msie 开头的路径,这样就可以给微软的 IE 浏览器提供特供版本内容。

第二个 if 检查当前请求的 HTTP 方法,如果是 POST 请求则直接返回 405 状态码。

以上就是 if 最典型的用法,也是 Nginx 最初设想的用法~但很快就被用户玩坏了😂

天下苦静态配置久矣,Nginx 终于支持动态配置了👏这个 if 不就是 c 语言里的条件判断吗?大家玩起来🎢

咱们先来个条件限速吧。如果请求参数 id 的值为 124 就限速 1k~

location /api {
    if ($args ~ id=124) {
        limit_rate 1k;
    }
}

So far so good😄能不能再来点复杂的呢?比如下面这个:

location /only-one-if {
    set $true 1;

    if ($true) {
        add_header X-First 1;
    }

    if ($true) {
        add_header X-Second 2;
    }

    return 204;
}

从配置上看,先设置了变量 $true 然后是两个 if 判断,如果 $true 不为零就各添加一个头字段~逻辑很清晰嘛☺️

但是实际运行就会发现,请求 /only-one-if 时的响应中只包含 X-Second 这个头字段。这就开始出现问题了。这是为什么呢?

简单来说每一个 if 判断都可以看成是一个特殊的 location 块。在 Nginx 中,所有的模块都会为每个 location 分配单独的内存来保存该模块对于当前 location 的特定配置信息。 Nginx 在处理请求的时候,会根据实际的路径来动态确定使用哪个配置。

在上面的例子中,因为两个 if 条件都满足,所以后一个 if 的 location 配置会覆盖前一个,自然就只有 add_header X-Second 2 生效了。

基于同样的原理,就可以解释以下诡异的行为。

发给上游的请求路径不会被改写成 /

location /proxy-pass-uri {
    proxy_pass http://127.0.0.1:8080/;

    set $true 1;

    if ($true) {
        # nothing
    }
}

try_files 失效

location /if-try-files {
     try_files  /file  @fallback;

     set $true 1;

     if ($true) {
         # nothing
     }
}

nginx 工作进程报 SIGSEGV 退出

location /crash {

    set $true 1;

    if ($true) {
        # fastcgi_pass here
        fastcgi_pass  127.0.0.1:9000;
    }

    if ($true) {
        # no handler here
    }
}

if 块中创建的 location 块无法读取上面捕获的 $file 变量

location ~* ^/if-and-alias/(?<file>.*) {
    alias /tmp/$file;

    set $true 1;

    if ($true) {
        # nothing
    }
}

以上种种问题都源于 if 对应的 block 配置没有继承所有的上级 location 配置信息。但像是 proxy_pass/try_files 这种模块又依赖这些信息,所以就会出现各种诡异的行为,甚至进程还会崩溃😂

那怎样才能避免这类问题呢?上面的几个问题虽然跟 if 有关,但行为却各不相同。归根结底是因为不同的模块依赖不同的 location 配置,有的被 if 继承了,有的没继承,所以最终会产生什么结果谁也不知道。但有一点很明确,就是跟 rewrite 相关的配置肯定是没有问题的。所以说,要想避免问题,我们只能在 if 块中使用 return 和 rewrite,而且 rewrite 还得加上 last 参数。其他配置都有可能出现问题。

再一个就是避免在 if 中定义新的 location。可以使用 return + error_page 来实现跳转:

location / {
    error_page 418 = @other;
    recursive_error_pages on;

    if ($something) {
        return 418;
    }

    # some configuration
}

location @other {
    return 200 error;
}

以上就是本文全部内容了。if 虽然在一定程度上可以实现动态配置的效果,但坑还是有很多,建议大家只用它做 URL 重写。如果确实需要动态配置,可以考虑使用 njs 或者 lua 模块。 Nginx 配置的其他问题也可以参考我的这篇文章。如果有疑问或不同意见,欢迎留言讨论。

参考文章: