nginx的正则回溯和灾难性回溯

本文探讨了正则表达式中的回溯现象,详细解释了回溯的原理,并通过实例展示了如何导致性能损耗。在线上环境中,如nginx使用不当的正则可能导致灾难性回溯,消耗大量CPU资源。提出了避免回溯的建议,如限制使用量词嵌套,减少使用'.'等。同时列举了一些不推荐的正则写法,并提供了防止回溯的解决方案。
摘要由CSDN通过智能技术生成

正则的回溯

我们先了解一下什么是回溯

https://zhuanlan.zhihu.com/p/27417442

这篇文章将的比较详细,我选取其中一个例子给大家简单介绍一下

当用 /".*"/  匹配字符串 "acd"ef 时,下面是匹配过程

简单总结一下

贪婪匹配 (.*) 后面紧跟字符 g,(.*)的匹配范围是 g匹配范围的父集,这种写法必定会导致字符 g 的匹配失败而产生回溯

字符串中 g 到 (.*) 所匹配的字符结尾间的字符越多,回溯次数越多。如果字符串中没有g,则会全部回溯

我们线上nginx 都采用的 正则匹配,平时我们提单新增location的时候如果正则写地很随意就很容易写回溯的正则影响nginx匹配的效率。下面我举一个例子

location ~ ^/re/(.*)/g 

这个正则就是一个不好的写法

    用 .* 匹配范围太广,不够明确。这个正则可以匹配多种 url 比如 /re/aaa/g/eee   /re/aaa/bbb/g/eee /re/aaa/geee  等

    此外使用 .* 会引起回溯, g 后面的字符越多,回溯越严重,造成性能损耗

假如业务的url是  /re/aaa/g/eee   /re/aaa/bbb/g/eee /re/aaa/geee 这三种都有,那实际更好的写法是

location ~ ^/re/[^g]+/g 

我在nginx上分别压测了一下上面两种配置,url 为 /re/aa/gaaaaaaaaa ,g后面有10个字符,第一种写法54336 第二种是 55964 ,第二比第一种性能提高了3%,如果url更长性能差距会更大

灾难性回溯

如果说上面的回溯造成的一点性能上的损失还可以接受的话,那灾难性回溯造成的影响就很难忽略了

灾难性回溯简单来说就是在正则表达式中过于粗暴得使用了 *、+ 和 ?等量词限定符的组合和嵌套,导致在匹配某些特定类型的字符串的时候,回溯次数随着字符串长度的增加而指数级上升。这会造成服务的cpu资源占用很高而影响正常的业务逻辑执行

下面举两个的例子

regex = /^(\w+s?)*$/
test_str = "An input string that takes a long time or even makes this regexp to hang!"

regex = /a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/
test_str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

大家可以用自己常用的语言试一下上面两个例子,看看自己所用语言的正则引擎有没有回溯的问题

https://zh.javascript.info/regexp-catastrophic-backtracking

这篇文章介绍了灾难性回溯的过程,以 (\d+)*$ 匹配 12345! 为例,由于 * 的存在,正则引擎匹配到!失败后,会尝试

\d+\d+
\d+\d+\d+

......

多种方式排列组组合来匹配字符串,随着数字长度的增加,回溯次数会成指数级增长

https://regex101.com/r/LbX3JI/1

大家可以在这里开启debug 工具来直观的感受一下回溯过程

之前说到nginx 使用的是pcre库,这就会存在灾难性回溯的问题

好消息是在测试中发现nginx 对回溯次数是有限制的

虽然没有查到具体的值是多少,翻了nginx源码,nginx在调用pcre_exec() 的是时候貌似没有特别设置限制次数,根据pcre 文章的介绍,默认值是 1000万,经过测试差不多是这个值,回溯次数超过1000万的时候,nginx 会中断请求 返回500,error log 中报错pcre_exec() failed: -8 

nginx lua 模块则可以通过设置 lua_regex_match_limit 100000; 来手动限制lua函数中的回溯次数

坏消息是这并不能根本性的解决回溯的问题, nginx 默认1000万的回溯次数仍会消耗大量cpu资源。

nginx 中易出现灾难性回溯的正则表达式特征

在nginx配置中我们不太容易写出 /a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/ 这种正则,但是会比较容易写出这种类型的正则 /^(\w+s?)*$/。我简单总结一下出现灾难性回溯正则及其匹配的字符串特征

例子: /(a*)*$/

1. 正则采用了 * + 嵌套比如 (a*)*,并且可以匹配较长的目标字符串,比如 aaaaaaaaaab中,可以匹配到aaaaaaaaaa

2.  正则嵌套之后有会匹配到的字符,比如上面的$。注意$ 也可以是其他字符

3.  1、2 中的正则之间存在会匹配失败的字符串,且只有在匹配失败后才会导致回溯。比如上述正则在匹配字符串  aaaaaaaaaab 时,字符"b"匹配失败导致前面嵌套的正则发生回溯

/(a*)*$/ 这个例子比较简单,类似的还有

/(.*)*[^b]$/ 在匹配字符串 aaaaaaaaaaaaaaaaaaaab 时

/(.*)*[^b]e/ 在匹配字符串 aaaaaaaaaaaaaaaaaaaabe 时

稍微特别的,里面有类似-的分隔符

/(-a*)*[^b]e/ 在匹配字符串 -a-a-a-a-a-a-abe 时这个时候字符串中分隔符 - 的数量将会指数级影响回溯的次数

上面有字符 a* 来举例方便了解,实际上这种只有在特定字符才会异常。而实际生产中一般会用 \w* 甚至 .* 这种形式,这会极大地增加出现灾难性回溯的概率


nginx 发生灾难性回溯造成影响

会消耗大量CPU资源

经过 https://regex101.com/ 中测试

此正则 /(a*)*$/ 匹配特定字符串是所需的匹配次数

字符串  匹配次数
 ax 20    
 aax 40
 aaax 80
 aaaax 160

匹配次数为20*2^(字符a的数量)

这个例子比较极端,但是可以反映出回溯导致匹配次数的指数级增长趋势

会造成集群故障

由于灾难性回溯会消耗大量cpu资源,单个nginx worker 7qps的异常请求就能把cpu 使用到100%,对于一个10台40核服务器的nginx集群,几千qps的请求就能干崩整个集群

异常定位非常困难

灾难性回溯造成线上影响的排查很困难,故障的产生需要一些特定流量才会触发,流量的增加和配置的上线时间节点不一定一致,对于缺乏经验的同学来说想要定位到异常配置需要花费大量时间

几种nginx配置中不推荐的正则写法

下面几种配置的写法不一定会造成灾难性回溯,但是我们极不推荐这些写法,他们很有可能在某些需求下错写成易造成灾难性回溯的正则

我都以location 配置为例

lcoation ~ ^/test(/.*)*$

这个写法如果不熟悉nginx同学可能觉得没啥问题,但实际上他等价于 lcoation ~ ^/test

location ~ ^/test/(-\w+)*$ 

这种写法括号里有分隔符 - ,他会造成灾难性回溯,但是对应灾难性回溯的字符串要求稍微苛刻一些需要含有大量分隔符,但是如果改成 location ~ ^/test/(-?w+)*$  就很容易出现灾难性回溯,而写正则的同学可能完全意识不到这二者影响大小的区别

类似的还有这种写法

lcoation ~ ^/test/(\w+/?)*$

这个乍一看去好像很合理,没有用 .* ,甚至还考虑到了url随后有无斜杠两种情况,但它会造成灾难性回溯

如何解决

禁止使用 量词限定符 * + 的正则嵌套

根本的解决办法就是跟换正则引擎,但这对于我们nginx来说成本太高,并且有兼容现有正则的问题

避免 .* 的使用,减少回溯

尽量避免线上nginx中使用复杂正则

以上就是本篇文章的全部内容。正则的实现原理是很复杂的,我们平时在写正则的时候往往就是能匹配到就行而忽略了正则的匹配原理,这样时间长了就可能会踩到意想不到的坑里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值