【问题标题】:Java : Matcher.find using high cpu utilizationJava:Matcher.find 使用高 CPU 利用率
【发布时间】:2013-08-26 20:10:50
【问题描述】:

我正在使用 mod 安全规则 https://github.com/SpiderLabs/owasp-modsecurity-crs 来清理用户输入数据。我在将用户输入与 mod 安全规则正则表达式匹配时面临 cpu 启动和延迟。总体而言,它包含 500 多个正则表达式来检查不同类型的攻击(xss、badrobots、generic 和 sql)。对于每个请求,我都会检查所有参数并检查所有这 500 个正则表达式。我正在使用Matcher.find 来检查参数。在这种情况下,一些参数属于无限循环,我使用以下技术解决了这个问题。

Cancelling a long running regex match?

清理用户请求大约花费了大约 500 毫秒,cpu % 迅速上升。我使用 Visualvm.java.net 和我的测试套件运行器进行了分析。

CPU 配置文件输出

请帮我降低 CPU 使用率和平均负载?

【问题讨论】:

  • 根据截图checkPattern被调用了212148825次,总计6100774ms,每次调用0.02ms。我没有看到那里的性能问题 - 并且绝对没有证明每次调用 500 毫秒。
  • 如果有特定的模式导致更长的延迟,您应该识别它们并将它们包含在您的问题中。
  • @Holger 花费的时间不是问题。我只关心负载和 CPU 使用率。我想处理并行处理参数,如果我这样做了,我的平均负载达到 > 4。我使用 jstack -l 进行线程转储并使用 thread -H -b -p <process id> 找到最大消耗线程并将 id 转换为十六进制代码,线程消耗高 cpu (50 %) 在 Matcher.find 处处于可运行状态。
  • @bharathi:这仍然是同一种问题。很可能很多模式都很便宜,但其中一些很昂贵。您必须确定昂贵的那些以集中精力优化这些特定模式。顺便说一句,Matcher.find 调用了由于分析器的默认过滤规则而未显示的其他方法。更改这些规则以允许查看 JDK 的方法可以揭示匹配的大部分时间(意味着 cpu 负载)在哪里花费。

标签: java regex performance


【解决方案1】:

我认为这是您问题的根源,而不是正则表达式本身的性能:

对于每个请求,我都会检查所有参数并检查所有这 500 个正则表达式

无论您的正则表达式有多快,这仍然是大量工作。我不知道您有多少参数,但即使只有几个参数,它仍然会检查每个请求的数千个正则表达式。那会杀死你的CPU。

除了通过预编译和/或简化它们来提高正则表达式性能等显而易见的事情之外,您还可以执行以下操作来减少正则表达式检查的数量

  1. 根据参数类型对用户输入进行肯定验证。例如。如果某些参数必须是一个简单的数字,不要浪费时间检查它是否包含恶意 XML 脚本。只需检查它是否匹配 [0-9]+ (或类似简单的东西)。如果是,那没关系 - 跳过检查所有 500 个正则表达式。

  2. 尝试找到可以消除所有攻击类别的简单正则表达式 - 在您的正则表达式中找到共同点。如果例如你有 100 个正则表达式检查某些 HTML 标签的存在,首先检查内容是否包含至少一个 HTML 标签。如果没有,您可以立即节省检查 100 个正则表达式的费用。

  3. 缓存结果。 webapps 中生成的许多参数会重复出现。不要一遍又一遍地检查相同的内容,而要记住最终的验证结果。注意限制缓存的最大大小以避免 DOS 攻击。

还要注意,否定验证通常很容易绕过。有人只是在他们的恶意代码中更改了几个字符,而您的正则表达式将不匹配。您必须扩大正则表达式的“数据库”以防止新的攻击。肯定验证(白名单)没有这个缺点,而且更有效。

【讨论】:

  • 嗨,我做了前两个步骤。现在,总执行时间比以前好。获得了大约 60 毫秒。
【解决方案2】:

我建议你看看这篇论文: "Towards Faster String Matching for Intrusion Detection or Exceeding the Speed of Snort"

有更好的方法来进行您描述的匹配。本质上,您将要匹配的 500 个模式编译成单个后缀树,该树可以非常有效地一次将输入与所有规则进行匹配。

该论文解释说,这种方法被 Dan Gusfield 描述为“Boyer-Moore Approach to Exact Set Matching”。

Boyer-Moore 是一种众所周知的字符串匹配算法。该论文描述了用于集合匹配的 Boyer-Moore 的一种变体。

【讨论】:

    【解决方案3】:

    这 500 个正则表达式中一定有一部分有问题的正则表达式。即这样的正则表达式

        String s = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB";
    
        Pattern.compile("(A+)+").matcher(s).matches();
    

    需要数年才能完成。

    因此,在您的情况下,我将记录所有有问题的正则表达式及其有问题的输入。一旦找到这些,您就可以手动重写这几个有问题的正则表达式,并与原始正则表达式进行对比测试。正则表达式总是可以用更简单、更易读的 java 函数重写。

    另一种选择,虽然它不能解决上述问题,但您也可以使用更快(在某些情况下为 x20)和更有限的正则表达式 library。它在Maven Central 中可用。

    【讨论】:

      【解决方案4】:

      如果您有这么多的正则表达式,您可以使用 trie 算法 (http://en.wikipedia.org/wiki/Trie) 对它们(至少一部分)进行分组。
      这个想法是,如果您有例如 /abc[0-9-]//abde//another example//.something else//.I run out of ideas/ 之类的正则表达式,则可以将它们组合成单个正则表达式

       /a(?:b(?:c[0-9-]|de)|nother example)|.(?:I run out of ideas|something else)/
      

      通过这种方式,匹配器只需要运行一次而不是四次,并且您避免了很多回溯,因为常见的起始部分已经写在上面的正则表达式中。

      【讨论】:

      • 嗨,戴维,我无法对其进行分组。因为,我需要获取匹配的规则(mod security rule,每个规则都有自己的属性)详细信息。
      • 原则上,如果匹配的规则只是 500 条中的一些,你可以准备 packages 个正则表达式,使用上面的过程来为每个包制作一个大的正则表达式。当大型正则表达式之一找到匹配项时,您可以检查形成包的原始规则。为了使这种方法有效,您应该将更有可能出现在一起的规则聚集在一起。我希望它是可行的。
      【解决方案5】:

      如果可能,编译一次正则表达式并保留它们,而不是重复(隐式)编译(尤其是在循环内)。
      请参阅java.util.regex - importance of Pattern.compile()? 了解更多信息。

      【讨论】:

      • 我已经这样做了。预编译所有匹配模式并将其存储为模式列表。
      【解决方案6】:

      避免使用:

      • 多行
      • 不区分大小写

      也许您可以考虑对正则表达式进行分组,并根据用户输入应用一组给定的正则表达式。

      【讨论】:

      • 这是完全错误的。为什么不区分多行/大小写? Java 中的正则表达式匹配基于 NFA + 回溯,因此区分大小写等对性能影响不大。更重要的是避免回溯,例如.* 后跟更改 (a|b|c)。
      • 如您所说,区分大小写和多行搜索对性能影响不大。这是另一种声明它们影响性能的方式。根据性能要求,它可能相关或不相关。如果需要性能,则不得使用回溯。从不。