慢任务

那天我们的安全数据分析员小Z告诉我,他的一个计算量并不大的Spark SQL作业跑了3个小时还没有结束(最终是6小时后运行结束),很不正常。

我看了一下,基本现象如下:

当前的stage的任务规模是59,58个任务早已结束(分钟级别),只剩一个在运行;

看了下这个作业本身,当前执行的stage是一个条件比较复杂的filter的逻辑。

(对,已经被我涂抹得面目全非了)

分析

这种问题定位还是比较简单的,尤其是只有一个task在运行。

Spark UI上找到这个运行的任务。看一下输入数据,无论size还是条数都并没有数据倾斜的情况,不比其他任务的输入重。

进一步直接去看对应Spark Executor的栈信息。

通过观察、分析这个栈信息,我们发现这个任务计算就重在正则表达式匹配,也就是我们SQL中的RLIKE的部分。

优化一: 减少慢计算

RLIKE本身的实现,就是spark利用代码生成技术生成计算代码,计算代码其实是采用java.util.regex类对字符串进行正则匹配。

我们知道正则表达式可能会非常耗时,这里一个比较简单达到的优化效果就是先把其他计算条件算完再进行RLIKE计算。

如下,我们隐藏实现细节,得到利用cache表的计算方式。

cache table temp select * from ....//  (把其他计算的结果cache为临时表temp)

....  select * from temp where xxx rlike (.....)   (针对临时表temp做rlike计算)

本例中,我们靠这种方式,把参与rlike计算的数据从数千万行减少到了几千行,只用了数秒钟就得到了结果。

优化二: 优化正则表达式

我们前面的优化是减少关键计算量,可说是一种“降维”的优化方式,并没有触及rlike本身的性能优化。

我们该怎么提高正则匹配的性能呢?

关于正则的性能,最关键的一点就是记住: 匹配失败,尤其是复杂规则的匹配失败,总是最耗性能的。

正则表达式是一个比较复杂的主题,另外由于我本身不做安全数据分析,而小Z的正则表达式涉及一些公司的安全相关数据我也不便公开,下面只简单列举一些我在本例中用到的技巧。

减少”|”的选项的长度

比如遇到 “password passwd”, 我们应该尽量用 “passw(ord d)”来替代。这个道理也很简单,减少正则匹配回溯的长度。

取消非必须的分组提取

对于rlike,我们并不需要提取分组内容,所以很多编组可以写成(?:),对性能有一定提升。

使用侵占量词(Possessive Quantifiers) 减少回溯

正则有三种量词。贪婪(Greedy),勉强(Reluctant)和侵占(Possessive)。

简单说,贪婪就是在匹配过程中总是吞下尽量多的匹配模式,勉强总是吞下尽量少的匹配模式,而这两种都是存在回溯的(只是次序或曰方向不同)。而侵占则是独占式的,使用得当可以明显减少回溯过程。具体可以google下相关的术语。

取一段(为简单我改掉了一些内容)看看。

&?[^=&]*(user|name|account)

这个写法就是贪婪的写法,在我们这个场景里,字符扫描过程中会有大量的回溯过程(读者您可以构造一个比较长的随机字符串试试看),性能是会非常差的。

我们改写成侵占,即”*+”:

&?[^=&una]*+(user|name|account)

事实证明这一步的优化效果是非常好的。

优化效果

为方便效果对比,我不采用优化一,只替换正则表达式,优化之后的执行时间是17分钟。

对比之前的6小时,可以说是提速非常多了。

总结

数据倾斜是常见的慢任务原因之一,本文是另一种情况的”数据倾斜”,即并非数据量的倾斜,而是不同的数据内容之间,计算效率相差较大的倾斜。

对于计算性能较差的算子,提前过滤好其他条件,减少计算规模是一种比较容易实现的优化,效果也非常明显。

数据分析是一个综合学科,特定情况下的优化需要借助一些专门知识。本次遇到的问题就需要分析人员加强正则表达式的知识。



最后更新: 2017-03-06 16:01