讨论nginx location 顺序问题
目录
- 一、location 是什么
- 二、location 的选项有哪些?
- 三、location 的匹配规则
- 前缀字符串
- 正则表达式
- 总结
- 四、location 的应用规则
- 理论篇
- 实践篇
- 精准匹配优先级最高
- 正则匹配和顺序有关
- 正则、^~前缀匹配、空前缀匹配混搭
- 五、总结
网上有很多讨论 nginx location 顺序的话题,得到的结论也基本一致,总结为:
- 精准匹配
=
- 前缀匹配
^~
- 正则匹配
~
、~*
- 不带修饰符的前缀匹配
在很长的一段时间里,我对上述的结论也一直深信不疑,甚至还将这个结论分享给其他小伙伴,直到在有一次配置时发现,请求 uri 明明是符合了前缀匹配 ^~
规则,但 nginx 却没有使用,这让我对上述结论产生了疑惑。后续通过调研、实践后发现,上述结论可以说对,但也不对,是不是更疑惑了?没关系,看完这篇文章你就知道我为什么会这样说了。
本篇文章会从以下五个方面来介绍 location
顺序问题
location
是什么location
的选项有哪些location
的匹配规则是什么location
的应用规则是什么- 总结
话不多说,我们直接进入正题。
一、location 是什么
location
翻译成中文就是定位,已经描述的比较清晰了,因为它的作用就是根据请求 uri 定位到某一个规则块,然后在由该规则块决定怎么处理用户的请求。
location
模块看起来挺简单的,但真要自己写,有时候就会觉得无从下手,我应该用 location /images
、 location ^~ /images
还是 location ~ /images
呢?我应该帮这个location
规则放在什么位置呢?将规则放在最前面会影响已有的配置吗?....上面说的问题,相信大部分同学都遇到过,想要彻底解决,就必须要了解 location
的处理逻辑,当然这也是本篇文章的目的所在。
二、location 的选项有哪些?
location [ = | ~ | ~* | ^~ ] uri { ... }
按照匹配模式进行区分,location
后可以放置两种类型的匹配规则,分别是前缀字符串(prefix string)和正则表达式(regular expression)。其中前缀字符串包括 =
、^~
以及不设置(也就是空串),而正则表达式只有 ~
和 ~*
。(这五种选项是经常看到的,还有一种不常用的 location @name { ... }
,并不在今天的讨论范围)
三、location 的匹配规则
只有请求 uri 满足 location
的规则,才有可能被应用,而对于不同的匹配模式,location
的匹配规则也是不同的
前缀字符串
顾名思义,它表示从请求 uri 的开头开始进行匹配,如果用 JavaScript 来描述的话,当 uri.indexOf(locationRule.uri) === 0
时表示满足匹配规则,其中 uri
表示请求路径,而 locationRule.uri
表示 location
设置的规则。
其中 =
选项比较特殊,它又叫做精准匹配,只有匹配规则与请求 uri 完全相等时才表示满足条件,即只有当 uri === locationRule.uri
时才表示匹配成功。
正则表达式
其实就是通过正则匹配来验证是否满足规则,nginx 使用的是Perl 兼容正则表达式 (PCRE),网上可以找到很多验证正则表达式的站点,这里就不再展开了。但要注意,为了方便配置,nginx 进行了一些非标准的优化,例如,不必像在标准正则表达式中那样转义 URL 中的正斜杠(/)等等。
而对于 ~
和 ~*
唯一的区别就是:~
区分大小写,而 ~*
不区分大小写。
总结
下面,我们对 location
的选项进行一个简单的总结
选项 | 匹配规则 | 示例 |
---|---|---|
= | 精准匹配 | location = /test {...} |
^~ | 从请求 uri 的开头进行匹配 | location ^~ /test {...} |
[空串] | 从请求 uri 的开头进行匹配 | location /test {...} |
~ | 区分大小写的正则匹配 | location ~ /test {...} |
~* | 不区分大小写的正则匹配 | location ~* /test {...} |
四、location 的应用规则
理论篇
在了解完匹配规则后,我们来看下 nginx 是如何应用这些规则的,这就要说到 location
的顺序问题了。
下面是根据实践以文档总结出来的 location
应用规则的逻辑:
- 将
server
块中的location
按照匹配模式分成两个列表,分别为前缀字符串规则列表和正则匹配规则列表。 - 首先遍历前缀字符串规则列表,当满足匹配的规则是精准匹配时(即匹配选项是
=
),直接应用该规则,结束流程;否则找到匹配规则最长的那条记录(记作maxLenthStringPrefixRule
),继续执行逻辑3; - 如果
maxLenthStringPrefixRule
存在且匹配选项是^~
,应用该规则,结束流程;否则,执行逻辑4; - 遍历正则匹配规则列表,如果满足匹配规则,则直接应用,流程结束;如果直到循环结束后依然没有满足规则的
location
,则执行逻辑5; - 如果
maxLenthStringPrefixRule
存在,则应用该规则,流程结束;如果没有则返回 404,同样结束流程。
如果上述描述看着很难理解,可以尝试看下面的伪代码。
// 字符串匹配的规则集合,按照在 .conf 文件中的顺序放置到该集合中 const stringPrefixRuleList = [...] // 正则匹配的规则集合,按照在 .conf 文件中的顺序放置到该集合中 const regularExpressionRuleList = [...] // 用于存放最长字符串匹配的规则 let maxLenthStringPrefixRule; // 遍历字符串匹配的规则集合 for (let stringPrefixRule of stringPrefixRuleList) { // 符合匹配规则 if (stringPrefixRule.isMatched()) { // 匹配选项是精准匹配 if (stringPrefixRule.option === '=') { // 应用该规则,结束流程 applyRule(stringPrefixRule); return; } // 将最长匹配规则记录下来,留到后面使用 if (!maxLenthStringPrefixRule || stringPrefixRule.uri.length > maxLenthStringPrefixRule.uri.length) { maxLenthStringPrefixRule = stringPrefixRule } } } // 如果最长匹配规则的选项是 ^~, 则应用该规则,流程结束 if (maxLenthStringPrefixRule && maxLenthStringPrefixRule.option === '^~') { applyRule(maxLenthStringPrefixRule); return; } // 遍历正则匹配规则集合 for (let regularExpressionRule of regularExpressionRuleList) { // 如果有规则匹配上,则直接应用,流程结束 if (regularExpressionRule.isMatched()) { applyRule(regularExpressionRule); return; } } // 如果最长字符串匹配的规则存在,则应用该规则 if (maxLenthStringPrefixRule) { applyRule(maxLenthStringPrefixRule); return; } // 404,规则未找到 throw new Error(404)
由此,我们可以得到几个结论:
- 命中
=
匹配规则后会终止搜索,并直接使用该规则。所以,如果某个请求 url 频繁发生,例如 /,我们可以在nginx.conf
中添加location = /
规则,这会加速这些请求的处理速度,因为在命中规则后会终止搜索。 ^~
和空串的前缀匹配,区别在于,如果命中^~
的规则,并且是最长前缀匹配,就会终止搜索正则匹配规则列表。- 除了命中精准匹配外,前缀字符串匹配列表都会被遍历一遍,并且找到最长匹配的那条
location
规则,所以前缀字符串匹配和在文件中的位置无关,但是和匹配长度有关。 - 由于正则匹配的时间、资源消耗较多,所以 nginx 在对
location
规则进行正则匹配时,命中一个就直接使用了,所以正则匹配和location
规则在文件中的位置有关
实践篇
从上面的理论篇中,大家应该能大致了解到 nginx 是如何命中 location
规则的,为了加深大家记忆,同时也为了能佐证理论是对的,我们来一些实践吧。
我们的模板是这样的,后面所有的实践内容,都是放到两个 rule section 之间。
server { listen 9001; location / { default_type text/html; return 200 'hello world'; } # ===== rule section ====== # ===== rule section ======
精准匹配优先级最高
location /test { default_type text/html; return 200 '/test'; } location ~ /test { default_type text/html; return 200 '~ /test'; } location = /test { default_type text/html; return 200 '= /test'; }
我们在 nginx.conf
放置了 /test
、~ /test
、= /test
三个 location
规则,随后在浏览器输入 localhost:9001/test,发现输出内容是 = /test
,符合我们的预期,=
选项的优先级最高。
正则匹配和顺序有关
location ~ /test { default_type text/html; return 200 '~ /test'; } location ~ /test/*/demo { default_type text/html; return 200 '~ /test/*/demo'; }
随后我们将 rule section 区块替换成 ~ /test
和 ~ /test/*/demo
规则,在浏览器输入 localhost:9001/test/xyz/demo,从正则角度来说,/test/xyz/demo
既满足 /test
规则,又满足 /test/*/demo
规则,但是由于 ~ /test
在文件中位置靠前,所以优先被命中,理论上应该会输出 ~ /test
,校验后发现,确实是这样。
正则、^~前缀匹配、空前缀匹配混搭
location ^~ /test { default_type text/html; return 200 '^~ /test'; } location /test/more/andmore { default_type text/html; return 200 '/test/more/andmore'; } location ~ /test { default_type text/html; return 200 '~ /test'; }
将 section rule 区域替换成上述内容,然后在浏览器中输入 localhost:9001/test/more/andmore,猜猜会发生什么?
我们来一起分析下:
- 没有精准匹配的规则
- 找到最长的前缀字符串匹配规则是
/test/more/andmore
,它不是^~
选项,所以会遍历正则匹配规则 - 发现
~ /test
满足匹配规则,直接应用该规则,所以会输出location ~ /test
规则块的内容,也就是~ /test
我们再来试一下,在浏览器输入 localhost:9001/test/more,会显示什么呢?
- 没有精准匹配规则
- 最长前缀字符串匹配规则是
^~ /test
,是^~
选项,所以不用遍历正则匹配规则列表,所以页面会显示^~ /test
。
通过校验后发现,上述分析的结果和实际显示结果是一样的。
上面几个实践都比较简单,大家也可以尝试各种组合,然后按照上面的分析步骤来检验下自己是不是真的理解了 location
。
五、总结
看完上面的介绍,相信大家对 location
规则的处理逻辑都有一定的了解,也应该明白为什么在文章开头说曾经看到的结论对、也不对了。
如果要用对 location
顺序进行总结的话,可以在原有的基础上适当的进行一些扩展:
- 精准匹配
=
- 前缀匹配
^~
,如果该前缀匹配是最长前缀匹配规则,则应用 - 正则匹配
~
、~*
,和该规则在文件中的顺序有关,执行顺序从上到下 - 不带修饰符的前缀匹配,和匹配规则的长度有关,只会应用最长的匹配规则,与在文件中的顺序无关
到此这篇关于nginx location 顺序问题的文章就介绍到这了,更多相关nginx location 顺序内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!