在用 Nginx 那么久以来,只是有一个印象就是 Nginx 的配置分离,然后可以设置不同的域名,对不同的域名提供不同的服务,但是有一个细节始终没认真得去了解过,那就是 Nginx 的配置中,server 里面的 location 是如何匹配的,匹配的规则和顺序又是怎样的,刚好手上有本关于 Nginx 的源码级解析书籍,于是乎就写了几个 Demo 尝试一下,这里就以这几个 Demo 为演示来探究一下 location 的匹配规则。

首先先说下 Nginx 的大概配置,因为 Nginx 的大多数功能都是提供 Http 服务的,所以配置也是很多的,所以这里我直接主题,那么就是 server 指令块。在 Nginx 中的 http 配置块里面,通过 server 指令可以定义一个虚拟主机(简单认为是定义了一个网站),然后在 server 块里面会定义很多其他的配置块用来指定这个虚拟主机的参数,例如默认的静态文件位置呀,监听的端口呀,域名呀等等。其中 location 也是其中改一个指令,相当于虚拟主机上的目录(想象你在一个 Linux 文件系统中找一个文件),它的定义语法为:

location [= | ~ | ~* | ^~ | @ ] uri {....}

这里中间的 '=' 和 '~' 这些都是表示可选的,如果都不填可以留空,各自代表的含义分别是:

这个可能比较明白什么意思,就是当 Nginx 收到一个请求的时候,会有对应的 uri,例如你看这篇文章:https://liqiang.io/post/location-in-nginx,那么 nginx 解析出来的 uri 就是:/post/location-in-nginx,那么如果我的 server 中的 location 有一条能够匹配上,那么这个 location 下面对应的逻辑就可能用于响应这个请求。这样子可能认为事情就很简单了,但是有个问题是在一个 server 块中可以存在多个 location,那么就可能出现多个 location 都能满足请求的 uri 的情况,那么这个时候到底交给谁来响应是个问题,所以 Nginx 就有一个判断的逻辑,我总结了一下,优先顺序应该是这样的:

  1. 如果使用 = 的精确匹配有满足的,那么就用这个了,如果没有,继续找;
  2. 匹配 ^~ 定义的规则,如果匹配成功,则使用它,如果没有,继续找;
  3. 按照定义顺序,一一匹配正则表达式,只要从前往后有一个匹配到了,那么就直接使用这个,后面的就不考虑了;
  4. 查找留空的前缀匹配,一般 nginx 都会默认带有一个 location / {},如果没有,那就只能 404 了。

ok,基于这些规则,于是我写了这个 DEMO:Github 链接,整个 DEMO 可以分为三类,分别用于鉴定不同的特性,让我一个个来描述一下:

1. 匹配顺序验证

前面说了,Nginx 收到一个请求的时候,根据请求的 URI 的匹配是按照顺序进行的,所以这里就做这么一个验证,首先我先启动了 5 个后端 Web Server,为了体现出请求是由不同的 Web Server 响应的,我做了这个一个 Go 服务器:

然后,我以这个 Go 服务端启动了 5 个服务进程:

然后就配置 Nginx 了,这里我的配置是这样的(这些在 Github 都有):

location = / {
    proxy_pass http://127.0.0.1:9090;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    expires off;
    break;
}

location / {
    proxy_pass http://127.0.0.1:9091;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    expires off;
    break;
}

location /api/ {
    proxy_pass http://127.0.0.1:9092;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    expires off;
    break;
}

location ^~ /static/ {
    proxy_pass http://127.0.0.1:9093;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    expires off;
    break;
}

location ~* \.(css|js)$ {
    proxy_pass http://127.0.0.1:9094;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    expires off;
    break;
}

然后我就尝试以不同的 URL 去请求 Nginx 了:

然后看各个请求的输出结果是不是和我预期的一样。

2. 正则验证

前面我也说了,正则表达式的匹配,是从前往后只要有一个匹配到了,那么就直接使用这个,后面的就不考虑了;所以为了验证这个,我尝试了这么一组配置:

然后尝试一下这组请求:

$ curl http://localhost:8080/api
$ curl http://localhost:8080/api/v2
$ curl http://localhost:8080/api/v3

发现和预期的一样,结果响应的是:

9090
9090
9091

如果我将两个配置调换过来,变成这样:

结果就变成全都是 9090 响应的了,和前面描述的是一致的。如果我再修改一下,使用 ^~ 符号的话,又正常了,变成了 9090 9090 9091 的响应了:

说明前面的描述都是真的,就是 Nginx 的正常表现。

3. 分离配置文件验证

前面两个验证都是在同一个配置文件里面配置的,我们知道 Nginx 是支持在配置中 #include 多个配置项的,它的效果和单个文件里面配置一堆是一样的。这里就有个问题了,我们在第 2 步验证过了,对于 ~ ,它的表现是有前后顺序的,那么如果多个配置文件有重合的,那么 Nginx 会怎么处理?其实这个问题的最直白表述就是 Nginx 对于 #include 多个配置文件是怎么处理的。

于是我也写了这么两个配置文件来验证:

然后让 nginx 来 include,重复第 2 个验证中的请求,发现响应的都是 9090,如果我将 config-03-01.conf 重命名为 config-03-03.conf 之后,再尝试一遍,就变成了 9090 9090 9091 了,所以这里我就做了一个假设,Nginx 加载多个配置文件的顺序是根据配置文件的字母序来的。

为了更加正确得认识这个,于是我就找了一波,最终在 Nginx 的 Changelog 中找到了关键字,它的描述是这样的:

和我的猜测一模一样,所以这里就有可能导致在配置 Nginx 的时候掉坑了。为了更好得处理这个问题,我不建议依赖于这个顺序,第 2 个例子也验证了,更好的处理办法是少用 ~,如果可以,更多得使用 ^~