1.20180603遇到一个问题。报异常如下

18/06/03 11:07:18 INFO mapreduce.Job:  map 100% reduce 20%
18/06/03 11:07:51 INFO mapreduce.Job: Task Id : attempt_1521708288017_5121173_m_000054_0, Status : FAILED
Error: null
Container killed by the ApplicationMaster.
Container killed on request. Exit code is 143
Container exited with a non-zero exit code 143

18/06/03 11:07:54 INFO mapreduce.Job:  map 100% reduce 21%
18/06/03 11:08:02 INFO mapreduce.Job: Task Id : attempt_1521708288017_5121173_m_000054_1, Status : FAILED
Error: null
Container killed by the ApplicationMaster.
Container killed on request. Exit code is 143
Container exited with a non-zero exit code 143

18/06/03 11:08:02 INFO mapreduce.Job: Task Id : attempt_1521708288017_5121173_m_000054_2, Status : FAILED
Error: null
Container killed by the ApplicationMaster.
Container killed on request. Exit code is 143
Container exited with a non-zero exit code 143

18/06/03 11:08:12 INFO mapreduce.Job:  map 100% reduce 22%
18/06/03 11:08:18 INFO mapreduce.Job:  map 100% reduce 23%
18/06/03 11:08:27 INFO mapreduce.Job:  map 100% reduce 24%
18/06/03 11:08:50 INFO mapreduce.Job:  map 100% reduce 100%
18/06/03 11:08:53 INFO mapreduce.Job: Job job_1521708288017_5121173 failed with state KILLED due to: Kill job job_1521708288017_5121173 received from portal/dev@HADOOP.HZ.NETEASE.COM (auth:TOKEN) at 10.160.247.22
Job received Kill while in RUNNING state.

18/06/03 11:08:53 INFO mapreduce.Job: Counters: 48
        File System Counters
                FILE: Number of bytes read=0
                FILE: Number of bytes written=28783321393
                FILE: Number of read operations=0
                FILE: Number of large read operations=0
                FILE: Number of write operations=0
                HDFS: Number of bytes read=137047448392
                HDFS: Number of bytes written=0
                HDFS: Number of read operations=10358
                HDFS: Number of large read operations=0
                HDFS: Number of write operations=0
        Job Counters 
                Failed map tasks=3
                Killed map tasks=1
                Killed reduce tasks=10
                Launched map tasks=2632
                Launched reduce tasks=10
                Other local map tasks=3
                Data-local map tasks=2206
                Rack-local map tasks=423
                Total time spent by all maps in occupied slots (ms)=350841900
                Total time spent by all reduces in occupied slots (ms)=12092616
                Total time spent by all map tasks (ms)=87710475
                Total time spent by all reduce tasks (ms)=3023154
                Total vcore-seconds taken by all map tasks=87710475
                Total vcore-seconds taken by all reduce tasks=3023154
                Total megabyte-seconds taken by all map tasks=359262105600
                Total megabyte-seconds taken by all reduce tasks=12382838784
        Map-Reduce Framework
                Map input records=778612729
                Map output records=916562904
                Map output bytes=97212743254
                Map output materialized bytes=27612690815
                Input split bytes=861781
                Combine input records=0
                Spilled Records=916562904
                Failed Shuffles=0
                Merged Map outputs=0
                GC time elapsed (ms)=644333
                CPU time spent (ms)=45191640
                Physical memory (bytes) snapshot=3752265723904
                Virtual memory (bytes) snapshot=6942799056896
                Total committed heap usage (bytes)=5532261089280
        mcount
                begin=1642440
                deviceChannel=761535589
                end=1281400
                event=149873746
                illegalDevice=17525
                illegalPush=39
                pushChannel=84290
        File Input Format Counters 
                Bytes Read=0
18/06/03 11:08:53 INFO odm.MobileLogMR: cost:6 min 9 seconds

爆内存溢出。光从这里面看不出来是map还是reduce出了错误。别看map显示100%。得看后台,后台运行一看 map还有一个没有运行完。
那开始查找原因吧?
一开始看不到hadoop后台界面 只能猜
猜的时候原以为map或者reduce的内存设置小了
索性在MR程序中加上了

conf.set("mapreduce.reduce.memory.mb","5120");
            conf.set("mapreduce.reduce.java.opts","-Xmx3027M -Xms3027M -XX:PermSize=64M");
            conf.set("mapreduce.map.memory.mb","5120");
            conf.set("mapreduce.map.java.opts","-Xmx3072M");

或者
在hadoop客户端的配置文件中进行修改这4样儿

hadooop_home路径下的/home/hadoop/hadoop/etc/hadoop/mapred-site.xml
文件中的 也是那4样儿配置
<!--memory configuration-->
<property>
<name>yarn.app.mapreduce.am.resource.mb</name>
<value>1536</value>
</property>
<property>
<name>mapreduce.map.memory.mb</name>
<value>1024</value>
</property>
<property>
<name>mapreduce.reduce.memory.mb</name>
<value>1536</value>
</property>
<property>
<name>mapreduce.map.java.opts</name>
<value>-Xmx820M</value>
</property>
<property>
<name>mapreduce.reduce.java.opts</name>
<value>-Xmx1228M</value>
</property>
<property>
<name>yarn.app.mapreduce.am.command-opts</name>
<value>-Xmx1228M</value>
</property>

结果还是报错。应该不是单纯的那个问题了

看问题发现报栈溢出,这是怎么回事呢...

2018-06-03 10:23:34,627 INFO [main] org.apache.hadoop.mapred.MapTask: Spilling map output
2018-06-03 10:23:34,628 INFO [main] org.apache.hadoop.mapred.MapTask: bufstart = 0; bufend = 204872717; bufvoid = 1048576000
2018-06-03 10:23:34,628 INFO [main] org.apache.hadoop.mapred.MapTask: kvstart = 262143996(1048575984); kvend = 258512520(1034050080); length = 3631477/65536000
2018-06-03 10:23:39,890 INFO [main] org.apache.hadoop.io.compress.CodecPool: Got brand-new compressor [.lzo_deflate]
2018-06-03 10:23:41,787 INFO [main] org.apache.hadoop.mapred.MapTask: Finished spill 0
2018-06-03 10:23:41,796 ERROR [main] org.apache.hadoop.mapred.YarnChild: Error running child : java.lang.StackOverflowError
    at java.lang.Character.codePointAt(Character.java:4671)
    at java.util.regex.Pattern$CharProperty.match(Pattern.java:3693)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4158)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4161)

可以看到正则匹配出了问题。我以为是正则出了问题呢?原来是正则匹配导致的堆栈溢出。

Java 正则表达式 StackOverflowError 问题及其优化
正则可以看做一门 DSL,但它却应用极其广泛,可以轻松解决很多场景下的字符串匹配、筛选问题。同时呢有句老话:

“ 如果你有一个问题,用正则表达式解决,那么你现在就有两个问题了。”

 Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems.

今天我们就来聊聊 Java 正则表达式 StackOverflowError 的问题及其一些优化点。
1、问题

最近,有同事发现一段正则在本地怎么跑都没问题,但是放到 Hadoop 集群上总会时不时的抛 StackOverflowError 。

代码我先简化下:

package java8test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {

public static void main(String[] args) {

    final String TEST_REGEX = "([=+]|[\\s]|[\\p{P}]|[A-Za-z0-9]|[\u4E00-\u9FA5])+";
    StringBuilder line = new StringBuilder();
    System.out.println("++++++++++++++++++++++++++++++");
    for (int i = 0; i < 10; i++) {
        line.append(
                "http://hh.ooxx.com/ershoufang/?PGTID=14366988648680=+.7342327926307917&ClickID=1&key=%2525u7261%2525u4E39%2525u5BCC%2525u8D35%2525u82B1%2525u56ED&sourcetype=1_5");
        line.append(
                "http://wiki.corp.com/index.php?title=Track%E6%A0%87%E5%87%86%E6%97%A5%E5%BF%97Hive%E8%A1%A8-%E5%8D%B3%E6%B8%85%E6%B4%97%E5%90%8E%E7%9A%84%E6%97%A5%E5%BF%97");
        line.append(
                "http://www.baidu.com/s?ie=UTF-8&wd=58%cd%ac%b3%c7%b6%fe%ca%d6%b3%b5%b2%e2%ca%d4%ca%fd%be%dd&tn=11000003_hao_dg");
        line.append("http://cs.ooxx.com/yewu/?key=城&cmcskey=的设计费开始低&final=1&jump=1&specialtype=gls");
        line.append(
                "http%3A%2F%2Fcq.ooxx.com%2Fjob%2F%3Fkey%3D%25E7%25BD%2591%25E4%25B8%258A%25E5%2585%25BC%25E8%2581%258C%26cmcskey%3D%25E7%25BD%2591%25E4%25B8%258A%25E5%2585%25BC%25E8%2581%258C%26final%3D1%26jump%3D2%26specialtype%3Dgls%26canclequery%3Disbiz%253D0%26sourcetype%3D4");
    }
    line.append(" \001 11111111111111111111111111");
    Pattern p_a = null;
    try {
        p_a = Pattern.compile(TEST_REGEX);
        Matcher m_a = p_a.matcher(line);
        while (m_a.find()) {
            String a = m_a.group();
            System.out.println(a);
        }
    } catch (Exception e) {
        // TODO: handle exception
    }

    System.out.println("line size: " + line.length());
}

}

执行之后的结果是:

++++++++++++++++++++++++++++++
Exception in thread "main" java.lang.StackOverflowError

at java.util.regex.Pattern$Loop.match(Unknown Source)
at java.util.regex.Pattern$GroupTail.match(Unknown Source)
at java.util.regex.Pattern$BranchConn.match(Unknown Source)
at java.util.regex.Pattern$CharProperty.match(Unknown Source)

......

起初这个问题是从集群上抛出来的,大家可以看到这个异常有两个特点:

(1)不可用 Exception 捕获,因为 Error 直接继承自 Throwable 而非 Exception,所以即使你要捕获也应当捕获 Error。

(2)另外一点是大家可以看到抛出的错误并没有指明行号,当这段代码混在一个数百行的工具类,有数十条类似的正则的时候,无疑给定位问题带来了难度,这就需要我们能有一定的单元测试能力。

注:

(1)如果你的环境没有抛出上述错误,尝试调大 for 循环的次数或者指定 jvm 参数:-Xss1k

(2)如果你还不明白 StackOverflowError 是什么含义,可以参考上一篇文章:JVM 运行时数据区简介
2、问题分析

正则表达式引擎分成两类,一类称为DFA(确定性有穷自动机),另一类称为NFA(非确定性有穷自动机)。两类引擎要顺利工作,都必须有一个正则式和一个文本串。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。

 DFA与NFA机制上的不同带来5个影响: 

 1. DFA 对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,所以反而应用广泛,当今主要的正则表达式引擎,如Perl、Ruby、Python的re模块、Java和.NET的regex库,都是NFA的。 

 2. 只有NFA才支持lazy和backreference等特性; 

 3. NFA急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA则是“最长的左子正则式优先匹配成功”。 

 4. NFA缺省采用greedy量词; 

 5. NFA可能会陷入递归调用的陷阱而表现得性能极差。 

在使用正则表达式的时候,底层是通过递归方式调用执行的,每一层的递归都会在栈线程的大小中占一定内存,如果递归的层次很多,就会报出stackOverFlowError异常。所以在使用正则的时候其实是有利有弊的。

Java程序中,每个线程都有自己的Stack Space。这个Stack Space不是来自Heap的分配。所以Stack Space的大小不会受到-Xmx和-Xms的影响,这2个JVM参数仅仅是影响Heap的大小。Stack Space用来做方法的递归调用时压入Stack Frame。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。Stack Space的大小随着OS,JVM以及环境变量的大小而发生变化。一般说来默认的大小是512K。在64位的系统中,这个Stack Space值会更大。一般说来,Stack Space为128K是够用的。这时你说需要做的就是观察。如果你的程序没有爆出StackOverflow的错误,可以使用-Xss来调整Stack Space的大小为128K。(eg:-Xss128K)

文章开头的问题可以简单理解为方法的嵌套调用层次太深,上层的方法栈一直得不到释放,导致栈空间不足。

下面我们要做的就是了解一些正则性能的优化点,规避这种深层次的递归调用。
3、Java 正则的一些优化点
3.1 Pattern.compile() 预编译表达式

如果在程序中多次使用同一个正则表达式,一定要用Pattern.compile()编译,代替直接使用Pattern.matches()。如果一次次对同一个正则表达式使用Pattern.matches(),例如在循环中,没有编译的正则表达式消耗比较大。因为matches()方法每次都会预编译使用的表达式。另外,记住你可以通过调用reset()方法对不同的输入字符串重复使用Matcher对象。
3.2 留意选择(Beware of alternation)

类似“(X|Y|Z)”的正则表达式有降低速度的坏名声,所以要多留心。首先,考虑选择的顺序,那么要将比较常用的选择项放在前面,因此它们可以较快被匹配。另外,尝试提取共用模式;例如将“(abcd|abef)”替换为“ab(cd|ef)”。后者匹配速度较快,因为NFA会尝试匹配ab,如果没有找到就不再尝试任何选择项。(在当前情况下,只有两个选择项。如果有很多选择项,速度将会有显著的提升。)选择的确会降低程序的速度。在我的测试中,表达式“.(abcd|efgh|ijkl).”要比调用String.indexOf()三次——每次针对表达式中的一个选项——慢三倍。
3.3 减少分组与嵌套

如果你实际并不需要获取一个分组内的文本,那么就使用非捕获分组。例如使用“(?:X)”代替“(X)”。

总结下来就是:减少分支选择、减少捕获嵌套、减少贪婪匹配
4、解决方案
4.1 临时工方案

try...catch.../增加-Xss,治标不治本,不推荐。
4.2 优化正则才是王道
4.2.1 语法层面优化

根据 3.2 提到的,我们这样优化下:

final String TEST_REGEX = "([=+\s\p{P}A-Za-z0-9u4E00-u9FA5])+";

经测试,JVM 参数不变的情况下,for 循环 100w 次直到 OOM 了都不会再发生文章开头的栈溢出的问题了。
4.2.2 业务逻辑层面优化

由于我不清楚作者的业务场景,不好做业务优化,总的原则是当你的正则太复杂的时候,可以考虑逻辑拆分,或者部分不走正则,如果把正则当做万能工具可能会得不偿失。

总结:在字符串查找与匹配领域,正则可以说几乎是“万能”的,但是许多场景下,它的代价不容小觑,如何写出高效率、可维护的正则或者怎么能避开正则都是值得咱们思考的问题。
5、NFA引擎正则性能优化Tips

 1. 优先选择最左端的匹配结果
 
 2.标准量词优先匹配
 

比如'.0-9' 来匹配字符串"abcd12efghijklmnopqrstuvw",这时候的匹配方式是‘.’先匹配了整行,但是不能满足之后的两个数字的匹配,所以‘.*’就退还一个字符‘w’,还是无法匹配,继续退还一个‘v’,循环退还字符到‘2’发现匹配了一个,但是还是无法匹配两个数字,所以继续退还‘1’

 3.谨慎使用捕获性括号(),选择使用非捕获性括号(?:expression)
 

捕获性括号需要消耗一部分内存

 4.使用字符组代替分支(替换)条件
 

例如用[a-d] 代替 a|b|c|d避免不必要的回溯

 5.不要滥用字符组(单个字符时不要用字符组)
 

. 代替 [.]

 6.使用锚点^ $ \b 加速定位
 
 7.从两次中提取必须元素
 

a{2,4} 写成 aa{0,2}

 8.提取多选结构开头的相同字符
 

the|this 改成th(?:e|is)

 9.选择字符串中最常出现的字符串放到分支最前面
 
 10.能懒则懒,不要贪婪
 

在 * + {m,n}后面加上问好?就会变成非贪婪模式

总结:引用CFC4N大牛的一句话 滥用. 点号 * 星号 +加号 ()括号 是不环保,不负责任的做法 !

 11.简单字符串处理应避免使用正则表达式
 

Refer:

[1] 关于Java正则引起的StackOverFlowError问题以及解决方案

http://blog.csdn.net/qq522935502/article/details/8161273

[2] Java正则与栈溢出

http://daimojingdeyu.iteye.com/blog/385304

[3] 优化Java中的正则表达式

http://blog.csdn.net/mydeman/article/details/1800636

[4] 从一个正则表达式造成的StackOverflowError说起

http://ren.iteye.com/blog/1828562

[5] 正则表达式(三):Unicode诸问题(下)

http://www.infoq.com/cn/news/2011/03/regular-expressions-unicode-2

http://www.infoq.com/cn/author/%E4%BD%99%E6%99%9F

[6] StackOverflowError when matching large input using RegEx

http://stackoverflow.com/questions/15082010/stackoverflowerror-when-matching-large-input-using-regex

[7] try/catch on stack overflows in java?

http://stackoverflow.com/questions/2535723/try-catch-on-stack-overflows-in-java

[8] Java正则达式引起死循环问题解决办法

http://blog.csdn.net/shixing_11/article/details/5997567

[9] JAVA 正则表达式的溢出问题 及不完全解决方案

http://www.blogjava.net/roymoro/archive/2011/04/28/349163.html

[10] NFA引擎正则优化TIPS、Perl正则技巧及正则性能评测方法

http://danqingdani.blog.163.com/blog/static/18609419520144523853586/

[11] Java正则引发的思考

http://blogread.cn/it/article/5982?f=wb

[12] 进阶正则表达式

http://www.barretlee.com/blog/2014/01/18/cb-how-regular-expressions-work/

[13] 一个由正则表达式引发的血案