诊断错误的正则表达式导致的 StackOverflowError
在生产环境中, 见过很多错误的正则表达式导致的各种错误, 比如: 导致CPU 100%(完整占用一个CPU), 导致StackOverflowError, 导致性能下降很严重. 这里以一个错误的正则表达式导致StackOverflowError 为例子, 看如何诊断并找到根本原因.
案例
我们看到某个应用的错误最近非常多的 StackOverflowError, 并且CPU 使用率明显上升, 下面是我们在错误日志看到的:
st=java.lang.StackOverflowError
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4340)
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4347)
at java.base/java.util.regex.Pattern$GroupHead.match(Pattern.java:4807)
at java.base/java.util.regex.Pattern$Loop.match(Pattern.java:4944)
at java.base/java.util.regex.Pattern$GroupTail.match(Pattern.java:4866)
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4347)
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4347)
at java.base/java.util.regex.Pattern$GroupHead.match(Pattern.java:4807)
at java.base/java.util.regex.Pattern$Loop.match(Pattern.java:4944)
at java.base/java.util.regex.Pattern$GroupTail.match(Pattern.java:4866)
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4347)
at java.base/java.util.regex.Pattern$BmpCharPropertyGreedy.match(Pattern.java:4347)
...
这是截取的部份在日志中的栈, 但是即便日志中的栈有 1024 行, 但是这1024行都是上面最后面5行的重复. 也就是说尽管抛出 StackOverflowError 错误的时候, 打印出最后的1024行栈, 但是这1024 行都是正则表达式处理的栈, 没有我们期望看到的到底是我们的哪段代码调用了这个正则.
问题分析
要想找到这个错误的正则表达式, 我们必须找到我们自己的代码, 如果我是代码开发者, 并且这个应用仅有一处使用正则表达式的地方, 那么很容易就能找到这个正则表达式. 如果做为SRE, 对这个应用代码不熟悉, 可能就要想其它方法了.
如果找到了这个正则表达式, 除非这个正则表达式的错误看起来很明显, 否则, 我们都需要一个被处理的字符串样本, 如何拿到一个样本?
StackOverflowError 栈的深度
- 多深的栈会发生 StackOverflowError?
发生 StackOverflowError 时, 会打印最内层多少层栈?
我们可以通过命令来看一下(当前JDK是 MAC 上 JDK17):java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | grep Stack intx CompilerThreadStackSize = 1024 {pd product} {default} intx MaxJavaStackTraceDepth = 1024 {product} {default} bool OmitStackTraceInFastThrow = true {product} {default} bool StackTraceInThrowable = true {product} {default} intx ThreadStackSize = 1024 {pd product} {default} intx VMThreadStackSize = 1024 {pd product} {default} openjdk version "17.0.4.1" 2022-08-12 LTS
上面各个选项的说明:
CompilerThreadStackSize
JVM 编译器的栈大小, 单位是 Kbytes, 所以是栈的大小, 不是深度.MaxJavaStackTraceDepth
当有异常的时候, 打印的Java 栈的深度的行数, 0 表示所有行, 默认1024行.OmitStackTraceInFastThrow
如果一个异常抛出太快太多, 那就省略它的栈, 直接打印异常名字. 一般开始还打印栈, 后面就直接打印异常名字了(因为太热, 编译成二进制). 笔者在生产环境见过多次这样的问题, 都是到最开始出错的地方去看原始栈, 或重启它.StackTraceInThrowable
在 Throwable 抛出时, 带着栈.ThreadStackSize
线程栈的大小, 单位是 Kbytes, 所以不是深度.VMThreadStackSize
非 Java 栈的大小, 单位是 Kbytes, 所以不是深度.
所以, 我们可以回答上面的2个问题: 能运行栈的深度是由 ThreadStackSize 决定的, 它的单位是字节, 在上面的 JVM 中, 它最大允许 1024 KB, 而出异常后能打印的栈, 是由 MaxJavaStackTraceDepth
决定的, 它默认只打1024行.
如何找到错误使用正则表达式的代码以及错误的正则表达式?
通过设置 -XX:MaxJavaStackTraceDepth=0
就能打印出所有栈, 那么就能逐级向上找到错误正则.
如何找到一个导致 StackOverflowError 的样本?
在 上面我们自己的代码处 加上 try {} catch (StackOverflowError e) {}, 当捕获着异常的时候, 打印导致出错的样本.