在堆中,我们使用Apache Spark来为我们的堆连接产品供电。我们的系统的这一部分是任务的,迅速移动大量数据和我们的客户'仓库。使用Spark使我们能够轻松扩展我们的数据处理以支持我们最大的客户,并且我们还会出于over基于列的文件格式的开箱支持。
这篇博文界面涵盖了我们必须解决的第一个性能障碍之一。我们的数据处理工作似乎挂在我们的一些客户。我们的根本原因分析确定了兽人文件读取器的Spark实现中的慢速。
意外的算法GOTCHA导致树处理算法执行指数运行。这意味着即使具有50个节点的微小树也可能导致算法无限期地实现。
幸运的是,我们设法在我们身边的问题上工作,而不根据火花来解决修复。在修复我们的直接生产问题后,我们也有助于解决火花。
我们的客户关心用户,活动(操作用户在网站或产品上采取)和会话。他们还关心过滤这些实体。例如,他们可能对“联合王国发生的所有事件的计数”感兴趣。
事实证明,表示这组谓词的非常自然的方式是用二叉树,如此:
叶节点表示单个过滤条件,如“Country = UK”。非叶节点表示在其子树上执行的操作。 AND节点表示“只匹配符合我左子子树中的条件的数据以及在我的右子树中”。一个或节点会类似,但匹配左或右子树,而不需要同时匹配两个子树。
我提到了早些时候的“有50个节点的树木”。有没有人真正想要将这么多的过滤器应用于他们的数据?导致此原因的标准情况是客户想要过滤事件:
在不引入基于阵列的过滤器的情况下,以直接的方式表达这一点,需要一个带有50个叶节点的树。
现在我们已经涵盖了树木如何进入图片,让我们讨论为什么处理它们需要这么多时间。
虽然代表过滤器作为树木是一个标准的做法,但没有单一,广泛接受的实现。因此,像Spark这样的大型项目可能需要支持和集成多个基于树的表示。在我们的情况下,一些调试指向朝向从Spark的内部树木滤波器转换为orc特定的算法 searchargument。
由于两种格式不等效100%,因此转换必须检查输入树是否可以在实际尝试之前转换。这导致了这样的代码:
起初,这看起来很合理。但是,Cantransform和变换的方法含有几乎相同的逻辑。因此,在代码重用的精神中,实现了这一点:
仍然看起来很合理。但是,注意到次要,但非常重要的区别:我们正调用两次转换方法。
为什么这会非常重要?做某事两次而不是只有一次听起来是次优,但不应该真的那么糟糕。正确的...?
更加谨慎地表明我们不仅仅是“两次做某事”。相反,它更接近“两次做某事;然后再次,在这两个调用中的每一个中;然后再次,......“等等。也就是说,我们“两次做某事”,提升到H的力量(其中H是树的高度)。
让我们稍微正式看看。让我们召集转换高度H,F(H)树所需的操作数。然后:
或者,再次翻译成单词,转换函数的复杂性在被转换的树的高度中是指数的。
对于高度45的树,我们得到F(45)= 2 ^ 44 * F(1)= 17,592,186,044,416 * F(1)。
为简单起见,假设F(1)= 1.假设现代CPU每秒可以运行〜3亿次操作,这一无辜的树木转换将需要〜5800秒,或大约一个半小时。树中的每个附加层使得操作需要2倍。
此外,请注意,1.5小时只需将过滤器表达式从一个树格式转换为另一个树格式所需的时间。一次。在1.5小时内没有分析客户数据。那部分尚未开始。
值得注意的是,在花费这个时间。运行火花作业时,所有实际数据处理都由Spark Executors完成。因此,大多数标准监测和优化方法都在很大程度上专注于执行者性能。但在我们的情况下,在执行者绩效指标中的任何地方都没有任何东西出现。看起来官员一直只是闲置。
虽然火花执行器负责数据处理,但是Spark驱动程序是负责查询规划的组件。上面的指数算法是查询规划的一部分。因此,它的缓慢意味着火花司机从未成功地计划查询。因此,求助者没有工作才能拾取 - 因此,他们是怠速。
在引入问题时,我表示该算法是“树的大小的指数”。然而,我们更加仔细的分析表明它不是树的大小,而是,它在树的高度是它是指数的。我们可以使用该细节来构建一个解决方法。
在我们这样做之前,让我们看看我们如何用这么深的树木结束。如果你没有积极地试图避免它,那实际上很容易。在我们的情况下,我们有代码如下所示:
此代码非常容易读写。然而,给定长期的和奇特列表,它导致了严重倾斜的树木。
给定45"国家= X",这种直接实现的结果导致一个深度45的树,它立即触发指数行为。这会导致作业卡在谓词转换步骤上。
幸运的是,在我们的具体用例中,非常深的树木总是具有相同的操作。例如,我们可以拥有一个具有上述45个或节点的树,或者使用45个和节点。但是,我们从未创建过45层深的树木,含有和或ors的混合。这种一致性使我们可以自由操纵树,而不担心意外地改变语义。
这是因为,由于关联性,我们可以组合和子条款,但是我们想要的。我们可以创建由和条款子集组成的子树,然后将它们合并在一起,从而保留结果。
这使我们可以创建具有不同形状的等效谓词树。从一系列元素构建平衡树是一个相对标准的树算法。使用它而不是重右歪斜的树,导致巨大的性能增益,具有多小时的性能(或从未完成更大的树)以取出秒。
将此更改部署到我们的生产作业使与此代码相关的所有性能问题消失。我们不需要等待Spark释放或叉码Base才能改变Spark内部,这对于我们速度进行修复而言。
但是,我们知道正确的事情是从火花中删除来自火花的指数算法,因此我们接下来解决了这一点。
我们贡献了在[Spark-27105] [SQL]中的指数复杂性进行了修复,以通过Ivanvergiliev·拉请求的兽人谓词转换中的兽人谓词转换#24068·apache /火花。它包含在Spark 3.0释放(Spark Release 3.0.0 | Apache Spark)中。
避免指数行为的高级别思想或多或少地清除 - Don' T呼叫与&#34的相同方法;检查变形性和#34;和#34;执行转型"通过不同的代码路径使两种不同的行动避免了"两次做某事,递归和#34;我们上面覆盖的问题。
获取可读和可维护的代码结构,每个人都很满意,但花了一点时间。在许多不同的迭代之后,并将思想与多个人合并,我们最终用了可读和表演的代码。
如果您喜欢用算法优化解决生产性能问题,并改善您&#39的底层工具;重新使用,我们正在招聘! 了解有关我们工程团队的令人敬畏的人的更多信息。