当前位置: 首页 > news >正文

JVM 生产环境问题定位与解决实战(八):实战篇——正则表达式回溯引发的CPU 100%

在这里插入图片描述

本文已收录于《JVM生产环境问题定位与解决实战》专栏,完整系列见文末目录

1. 引言

在上一篇文章中,我们深入剖析了OSSClient泄漏引发的FullGC风暴全链路排查过程。本文聚焦另一个经典线上问题——正则表达式回溯导致的CPU 100%。在Java应用中,正则表达式使用普遍,但设计缺陷可能引发灾难性回溯,导致CPU使用率飙升至100%。本文将介绍如何使用Arthas快速定位和解决此类性能问题。

案例二:正则表达式回溯引发的CPU 100%

2. 问题现象

某日线上系统突现异常:

  • 系统响应缓慢:接口响应时间显著延长,部分请求处理超时
  • CPU资源耗尽:服务器CPU使用率持续维持在100%,Load值飙升
  • 线程阻塞:部分线程长时间处于RUNNABLE状态,但无死锁迹象
  • GC无异常:GC无明显增加,堆内存无泄漏

提示:线上问题由XSS过滤器的正则表达式引发,本文使用测试代码模拟相同场景

3. 排查过程

3.1 系统资源监控

通过top命令快速锁定异常进程:

top - 16:34:15 up 1 day,  8:46,  3 users,  load average: 0.40, 0.83, 0.78
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                            
16114 root      20   0 3529992 398948  13900 S 176.2 10.3   1:38.78 java

关键发现:

  • Java进程(PID=16114)CPU占用率接近100%,问题指向应用程序内部。
3.2 Arthas 接入

通过Arthas快速连接目标JVM,选择 PID 为 16114 的进程,进入 Arthas 命令行。

[root@k8s-node1 ~]# java -jar arthas-boot.jar
[INFO] JAVA_HOME: /opt/jdk1.8.0_371/jre
[INFO] arthas-boot version: 4.0.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 16114 boot-demo.jar
1
[INFO] arthas home: /root/.arthas/lib/4.0.5/arthas
[INFO] Try to attach process 16114
[INFO] Attach process 16114 success.
[INFO] arthas-client connect 127.0.0.1 3658,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'                          
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.                          
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |                         
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          wiki        https://arthas.aliyun.com/doc                                       
tutorials   https://arthas.aliyun.com/doc/arthas-tutorials.html                 
version     4.0.5                                                               
main_class  boot-demo.jar                                                       
pid         16114                                                               
start_time  2025-04-27 15:59:37.340                                             
currnt_time 2025-04-27 16:01:46.218  
3.3 初步分析(dashboard)

运行 dashboard 命令,每 2 秒刷新线程和资源状态:

# 输入命令
dashboard -i 2000  # 每2秒刷新一次

在这里插入图片描述
输出显示:

  • ID25、26线程 CPU 占用极高,持续处于 RUNNABLE 状态10分钟之久。
  • 无阻塞或等待迹象,确认问题与高计算任务相关。
3.4 高 CPU 线程排查(thread -n 3)

列出 CPU 占用最高的 3 个线程:

[arthas@15488]$  thread -n 3
"http-nio-8888-exec-8" Id=25 cpuUsage=96.38% deltaTime=198ms time=41076ms RUNNABLEat java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)at java.util.regex.Pattern$Curly.match(Pattern.java:4241)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)##### 省略部分日志 #####at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Slice.match(Pattern.java:3986)at java.util.regex.Pattern$Begin.match(Pattern.java:3539)at java.util.regex.Matcher.match(Matcher.java:1270)at java.util.regex.Matcher.matches(Matcher.java:604)at java.util.regex.Pattern.matches(Pattern.java:1135)at java.lang.String.matches(String.java:2121)at com.controller.TestController.isValid(TestController.java:522)at com.controller.TestController.testReg3(TestController.java:503)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)"http-nio-8888-exec-9" Id=26 cpuUsage=94.69% deltaTime=194ms time=34853ms RUNNABLEat java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)at java.util.regex.Pattern$Curly.match(Pattern.java:4241)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)##### 省略部分日志 #####at java.util.regex.Matcher.match(Matcher.java:1270)at java.util.regex.Matcher.matches(Matcher.java:604)at java.util.regex.Pattern.matches(Pattern.java:1135)at java.lang.String.matches(String.java:2121)at com.controller.TestController.isValid(TestController.java:522)at com.controller.TestController.testReg3(TestController.java:503)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)

关键信息:

  • ID25、26线程的堆栈指向 java.util.regex.Pattern 的匹配方法。
  • 调用源自 com.controller.TestController.isValid(TestController.java:522),疑似正则匹配问题。

在这里插入图片描述

3.5 输入参数分析(watch)

使用 watch 命令查看 isValid 方法的入参,确认触发 CPU 飙升的输入:

watch com.controller.TestController isValid '{params,returnObj,throwExp}' -n 5 -x 3

输出示例:

ts=2025-04-27 16:10:32.522; [cost=1.171926ms] result=@ArrayList[@Object[][@String[http://www.example.com/aaaaaaaaaab],],@Boolean[false],null,
]

发现:

  • 输入字符串如 http://www.example.com/aaaaaaaaaab 较长,且不完全匹配正则。
  • 长输入可能触发正则引擎的回溯。

使用 trace 进一步分析方法耗时:

[arthas@16114]$ trace com.controller.TestController isValid -n 5 --skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 1, method count: 1) cost in 186 ms, listenerId: 2
`---ts=2025-04-27 16:11:18.432;thread_name=http-nio-8888-exec-4;id=21;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---[0.76795ms] com.controller.TestController:isValid()`---[82.98% 0.637233ms] java.lang.String:matches() #522`---ts=2025-04-27 16:11:25.781;thread_name=http-nio-8888-exec-2;id=19;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---ts=2025-04-27 16:11:26.577;thread_name=http-nio-8888-exec-5;id=22;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---[2318.506752ms] com.controller.TestController:isValid()`---[100.00% 2318.4676ms] java.lang.String:matches() #522

结论:长输入(如 http://www.example.com/aaaaaaaaaab)导致正则匹配耗时显著增加,触发回溯。

4.正则表达式回溯详解

正则表达式引擎主要分为 DFA(确定型有穷自动机)和 NFA(非确定型有穷自动机)。Java 使用 NFA 引擎,匹配时通过回溯(Backtracking)尝试不同路径。回溯在匹配失败时会导致引擎反复尝试,时间复杂度可能从 O(n) 恶化到 O(2^n),引发 CPU 100% 的问题,称为“灾难性回溯”。

以下是DFA(确定型有穷自动机)和NFA(非确定型有穷自动机)的对比表格:

特性DFA (Deterministic Finite Automaton)NFA (Nondeterministic Finite Automaton)
状态转移每个输入符号对应唯一确定的状态转移同一输入符号可能对应多个状态转移(包括ε空转移)
回溯机制无回溯(线性时间匹配)可能触发回溯(最坏情况下指数级时间复杂度)
实现复杂度实现简单,但状态数可能较多实现复杂,但状态数通常较少
匹配速度稳定高效(O(n))通常较慢(最坏情况O(2^n))
内存消耗较高(需预计算所有可能转移)较低(动态计算转移路径)
正则引擎代表grep、awk等传统Unix工具Java(java.util.regex)、Perl、Python等现代语言正则引擎
构造难度直接构造较困难更容易从正则表达式构造
ε-转移(空转移)不允许允许
匹配方式文本主导(每一步只关注当前字符)正则主导(尝试所有可能的路径)
典型应用场景需要高性能匹配的场景(如网络协议解析)需要复杂模式匹配的场景(如文本处理)
例子匹配**a*b**的DFA只有2个状态匹配**a*b**的NFA可能有多个转移路径
4.1 回溯机制

Java 的正则引擎基于 NFA(不确定型有穷自动机),支持回溯以尝试不同匹配路径。含嵌套量词的模式在匹配失败时,可能导致指数级的路径尝试。例如:
当你写一个正则,比如:

(a+)+

去匹配一个字符串 "aaaaac" 时,正则引擎会:

  1. 首先尝试把整个 "aaaaac" 都匹配到内层的 a+
  2. 然后整个内层 a+ 被匹配为一组,外层再试图重复一次;
  3. 但由于最后有个 "c",无法匹配;
  4. 引擎就开始“回溯”——尝试将内层 a+ 分得更短一点(比如只匹配一半),再看外层能否继续匹配下去;
  5. 如果一直失败,它就会继续尝试所有可能的组合。
  6. 匹配 aaaaac 时,引擎会尝试所有可能的 a 分配组合(如 a/aaa、aa/aa、aaaa/ 等),导致时间复杂度从 O(n) 恶化到 O(2^n)。

这就导致大量的组合尝试,尤其在输入比较长、而正则又含有嵌套的可重复匹配(比如 (a+)+)时,回溯的路径会呈指数级增长。

4.2 CPU 飙升原因

因为回溯次数非常多,正则引擎会尝试各种可能的组合路径,直到找到匹配,或者穷尽所有路径确认不匹配。

  • 对某些输入,回溯路径可能达到 数百万甚至数十亿次
  • 每次尝试都是一次函数调用、状态切换,占用 CPU;
  • 某些构造甚至可以导致拒绝服务攻击(ReDoS):通过一个恶意的长字符串,拖垮服务器。
4.3 典型危险模式

易引发回溯的正则示例:

  • (a+)+:嵌套贪婪量词。
  • (.+)*:模糊匹配,路径分配多样。
  • ((a|aa)+):多分支嵌套。

5. 问题分析与优化

5.1 原始正则问题分析

原始正则:

^http://www\.([a-z0-9\-]+)\.com/(.+)*(.+)*.html$

问题点:

  1. 嵌套量词 (.+)*:模糊匹配,引发大量回溯。
  2. 双重 (.+)*:组合数随输入长度指数增长。
  3. 冗余设计:两个捕获组功能重叠。
5.2 回溯触发场景:
  • 不匹配输入:如 http://www.example.com/aaa…aaa(缺少 .html),引擎会:
    1. 让 (.+)* 捕获所有 aaa…aaa。
    2. 发现末尾不匹配 .html,回溯,尝试不同子序列分配。
    3. 对于长输入(例如 1000 个 a),回溯次数可能高达 2^n 级别。
  • 部分匹配输入:如 http://www.example.com/aaa%25,% 不匹配 .html 的 .,但引擎仍会回溯,尝试调整 (.+)* 的分配。
  • 长输入:如 http://www.example.com/ + 10000 个 a,回溯路径极多,可能导致程序卡死。
5.2 正则优化方案

优化后:

^http://www\.([a-z0-9\-]+)\.com/([a-zA-Z0-9_/]+)\.html$

改进措施:

  1. 替换模糊匹配:用 [a-zA-Z0-9_/]+ 限制字符集。
  2. 简化结构:移除多余捕获组。
  3. 强制非空:用 + 替代 *。
5.3 优化效果对比
  • 性能:优化后匹配时间 <1ms,原始正则耗时数秒。
  • 功能:仍支持如 http://www.example.com/path.html。
  • 安全性:避免长输入引发的性能问题。

6. 进一步优化建议

  1. 避免嵌套的可重复匹配,比如不要用 (a+)+

  2. 使用懒惰匹配+?*?),限制贪婪;

  3. 更严格的路径字符集

    • 如果路径只允许小写字母和数字:([a-z0-9/]+)。
    • 如果支持更多字符(如连字符或点号):([a-zA-Z0-9_/.-]+)。
  4. 路径长度限制

    • 添加量词限制,如 ([a-zA-Z0-9_/]{1,100}),限定路径长度 1-100 字符。
    • 防止超长路径影响性能。
  5. 支持其他协议或域名

    • 扩展前缀为 ^(http|https)😕/([a-z0-9-]+).com/,支持 https 或其他域名。
    • 示例:^(http|https)😕/([a-z0-9-]+).com/([a-zA-Z0-9_/]+).html$
  6. 防御性编程

    • 输入预处理机制

      public boolean isValidUrl(String url) {
      // 长度校验前置if(url.length() > 1024) return false;// 关键后缀快速判断if(!url.endsWith(".html")) return false;// 执行正则匹配return url.matches(optimizedRegex);
      }
      
    • 正则编译缓存

      private static final Pattern URL_PATTERN = Pattern.compile("^http://www\\.([a-z0-9-]+)\\.com/([a-zA-Z0-9_/-]+)\\.html$");public boolean isValidUrl(String url) {return URL_PATTERN.matcher(url).matches();
      }
      
  7. 测试和分析:对关键正则表达式进行性能测试,确保不会触发灾难性回溯。

  8. 监控告警配置

    • 方法耗时监控:对isValidUrl方法设置500ms超时阈值
    • 线程堆栈监控:发现大量线程卡在java.util.regex时触发告警
      使用工具(如regex101.com)分析正则表达式的回溯行为。

附录:系列目录

  1. JVM生产环境问题定位与解决实战(一):掌握jps、jmap、jstat、jstack、jcmd等基础工具
  2. JVM生产环境问题定位与解决实战(二):JConsole、VisualVM到MAT的高级应用
  3. JVM生产环境问题定位与解决实战(三):揭秘Java飞行记录器(JFR)的强大功能
  4. JVM生产环境问题定位与解决实战(四):使用JMC进行JFR性能分析指南
  5. JVM生产环境问题定位与解决实战(五):Arthas——不可错过的故障诊断利器
  6. JVM生产环境问题定位与解决实战(六):总结篇——问题定位思路与工具选择策略
  7. JVM 生产环境问题定位与解决实战(七):实战篇——OSSClient泄漏引发的FullGC风暴
  8. ➡️ 当前:JVM 生产环境问题定位与解决实战(八):实战篇——正则表达式回溯导致的 CPU 100%

🔥 下篇预告:《JVM 生产环境问题定位与解决实战(九):实战篇——JVM 内存区域分配不合理导致的频繁 Full GC》
🚀 关注作者,获取实时更新通知!有问题欢迎在评论区交流讨论~

相关文章:

  • 【备份】杂谈
  • mybatis的xml ${item}总是更新失败
  • 【网络原理】TCP提升效率机制(三):延时应答和捎带应答
  • el-transfer穿梭框数据量过大的解决方案
  • 浏览器插件,提示:此扩展程序未遵循 Chrome 扩展程序的最佳实践,因此已无法再使用
  • 非标机械设备的动画制作
  • 如何通过Google Chrome增强网页内容的安全性
  • prompt提示词编写技巧
  • 【Pandas】pandas DataFrame rmod
  • Eureka 深度解析:从原理到部署的全场景实践
  • Spring生命周期
  • SNMP协议之详解(Detailed Explanation of SNMP Protocol)
  • 人工智能-深度学习之多层感知器
  • C++ 嵌套类 (详解 一站式讲解)
  • Flink Checkpoint 与实时任务高可用保障机制实战
  • SpeedyAutoLoot
  • Linux中的shell脚本练习
  • MCP之二_服务器与客户端实现
  • Python实例题:Pvthon实现键值数据库
  • 【计网】认识跨域,及其在go中通过注册CORS中间件解决跨域方案,go-zero、gin
  • 4月人文社科联合书单|天文学家的椅子
  • 新华社评论员:汇聚起工人阶级和广大劳动群众的磅礴力量
  • 安阳一村支书微信群骂村民被警方行拘,辩称对方先“污蔑造谣”
  • 楼下电瓶车起火老夫妻逃生时被烧伤,消防解析躲火避烟注意事项
  • 中公教育:去年全面扭亏,经营性现金流增长169.6%
  • 西北大学党委副书记吕建荣调任西安财经大学党委书记