【问题标题】:Regex causing high CPU load, causing Rails to not respond正则表达式导致高 CPU 负载,导致 Rails 无响应
【发布时间】:2014-11-21 21:35:25
【问题描述】:

我有一个 Ruby 1.8.7 脚本来解析 iOS 本地化文件:

singleline_comment = /\/\/(.*)$/
multiline_comment = /\/\*(.*?)\*\//m
string_line = /\s*"(.*?)"\s*=\s*"(.*?)"\s*\;\s*/xm

out = decoded_src.scan(/(?:#{singleline_comment}|#{multiline_comment})?\s*?#{string_line}/)

它以前可以正常工作,但今天我们用一个 800Kb 的文件对其进行了测试,并且每行末尾没有;。结果是高 CPU 负载并且 Rails 服务器没有响应。我的假设是它将整个文件作为捕获组中的单个字符串并阻塞了服务器。

解决方案是将?(正则表达式量词,0 或 1 次)添加到 ; 文字字符:

  /\s*"(.*?)"\s*=\s*"(.*?)"\s*\;?\s*/xm

现在即使使用旧 iOS 格式的文件,它也能正常工作,但我现在担心的是,如果用户提交格式错误的文件,比如没有结尾的文件",该怎么办。我的服务器会再次被阻止吗?

我该如何防止这种情况发生?有什么方法可以尝试只运行五秒钟吗?我可以做些什么来避免停止我的整个 Rails 应用程序?

【问题讨论】:

  • 您可以在尝试匹配正则表达式之前检查文件是否格式错误。一个简单的检查是计算" 字符的数量,看看它是偶数还是奇数。
  • 关于超时,看一下Ruby的Timeout库:ruby-doc.org/stdlib-2.1.3/libdoc/timeout/rdoc/Timeout.html
  • 看起来你有catastrophic backtracking 的变体(因为你有惰性量词,所以组每次都在扩大而不是变小)。很难推荐一个通用的修复方法,这取决于你的数据格式。一般来说,您最好使用正则表达式尽可能具体,在这种情况下,使用[^"]* 而不是.*?(甚至[^"]*+)在减少回溯方面会更安全。
  • 计数" 不一定能说明任何有用的信息。 '"1\""'.count('"') # => 3 会产生误导。

标签: ruby regex ruby-1.8.7


【解决方案1】:

看起来您正在尝试解析整个配置,就好像它是一个字符串一样。虽然这是可行的,但它很容易出错。正则表达式引擎必须做很多向前和向后的查找,而写得不好的模式最终会浪费大量的 CPU 时间。有时稍加调整就能解决问题,但处理的文本越多,表达式越复杂,发生让你搞砸的事情的可能性就越大。

通过对我自己的工作获取数据的不同方式进行基准测试,我了解到锚定正则表达式模式可以在速度上产生巨大差异。如果您无法以某种方式锚定模式,那么您将遭受模式的回溯和贪婪,除非您可以限制引擎默认执行的操作。

我必须解析很多设备配置,但​​我没有尝试将它们视为单个字符串,而是将它们分解为由行数组组成的逻辑块,然后我可以提供从这些块中提取数据的逻辑基于块包含某些类型信息的知识。小块的搜索速度更快,编写可锚定的模式要容易得多,从而提供巨大的加速。

此外,不要犹豫使用 Ruby 的 String 方法,例如 split 来分割行,并使用子字符串匹配来查找包含您想要的内容的行。它们速度非常快,不太可能导致减速。

如果我有这样的字符串:

config = "name:\n foo\ntype:\n thingie\nlast update:\n tomorrow\n"
chunks = config.split("\n").slice_before(/^\w/).to_a
# => [["name:", " foo"], ["type:", " thingie"], ["last update:", " tomorrow"]]

command_blocks = chunks.map{ |k, v| [k[0..-2], v.strip] }.to_h

command_blocks['name'] # => "foo"
command_blocks['last update'] # => "tomorrow"

slice_before 对于这类任务来说是一种非常有用的方法,因为它允许我们定义一个模式,然后用于测试主数组中的中断,并按这些模式分组。 Enumerable 模块中有很多有用的方法,所以一定要仔细看。

可以解析相同的数据。

当然,如果没有您正在尝试做的事情的样本数据,就很难提出更好的建议,但我们的想法是,将您的输入分解成易于管理的小块,然后从那里开始。

作为对您如何定义模式的评论。

不要使用/\/.../(称为“leaning-toothpicks syndrome”),而是使用%r,它允许您定义不同的分隔符:

singleline_comment = /\/\/(.*)$/     # => /\/\/(.*)$/
singleline_comment = %r#//(.*)$#     # => /\/\/(.*)$/

multiline_comment = /\/\*(.*?)\*\//m # => /\/\*(.*?)\*\//m
multiline_comment = %r#/\*(.*?)\*/#m # => /\/\*(.*?)\*\//m

上面每个示例中的第一行是您的操作方式,第二行是我的操作方式。它们产生相同的正则表达式对象,但第二个更容易理解。

你甚至可以让 Regexp 帮你转义:

NONGREEDY_CAPTURE_NONE_TO_ALL_CHARS = '(.*?)'
GREEDY_CAPTURE_NONE_TO_ALL_CHARS = '(.*)'
EOL = '$'

Regexp.new(Regexp.escape('//') + GREEDY_CAPTURE_NONE_TO_ALL_CHARS + EOL) # => /\/\/(.*)$/
Regexp.new(Regexp.escape('/*') + NONGREEDY_CAPTURE_NONE_TO_ALL_CHARS + Regexp.escape('*/'), Regexp::MULTILINE) # => /\/\*(.*?)\*\//m

这样做,您可以迭代地构建极其复杂的表达式,同时保持它们相对易于维护。

就停止 Rails 应用程序而言,不要尝试在同一个 Ruby 进程中处理文件。运行一个单独的作业来监视文件并处理它们并存储您要查找的任何内容,以便以后根据需要访问。这样,您的服务器将继续响应而不是锁定。我不会在线程中执行此操作,而是会编写一个单独的 Ruby 脚本来查找传入数据,如果没有找到,则休眠一段时间然后再查找。 Ruby 的 sleep 方法将对此有所帮助,或者您可以使用操作系统的 cron 功能。

【讨论】:

  • 这没有回答问题,而且是一个很长的评论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-24
  • 1970-01-01
  • 2013-03-14
  • 2017-07-30
  • 2013-07-05
  • 2019-11-04
相关资源
最近更新 更多