前言
这篇文章继续探讨聚合策略,主要介绍Spark SQL提供的两个基于hash的聚合操作符,即HashAggregateExec和ObjectHashAggregateExec。
在上一篇文章:Spark SQL深入分析之图解SortAggregateExec执行流程中已经解释过,基于hash的聚合性能比基于排序的聚合高,因为基于hash的聚合不需要在聚合之前进行额外的排序步骤。对于HashAggregateExec操作符来说,使用堆外内存作为聚合的缓存,可以通过减少GC时间来进一步提高性能。
HashAggregateExec
当聚合表达式的所有aggBufferAttributes(从聚合逻辑计划中提取)为可变数据类型时,首选HashAggregateExec作为为聚合操作符。HashAggregateExec操作符使用一个名为UnsafeFixedWidthAggregationMap的堆外hash map去存储分组键和相应的聚合缓冲区。当hash map变得太大,并且不能从内存管理器中分配更多的内存时,hash map将被溢出到磁盘中,并且将创建一个新的hash map来处理剩余的行。当所有的输入行被处理后,所有的溢出到磁盘的结果将被合并,并进行基于排序的聚合,以计算出最终结果。
当HashAggregateExec操作符被执行时,它为每个分区创建一个TungstenAggregationIterator实例。TungstenAggregationIterator实例封装了进行基于hash的聚合、缓冲区溢出到磁盘以及回写到基于排序的聚合的核心功能。
TungstenAggregationIterator维护一个UnsafeFixedWidthAggregationMap的实例,它是存储所有分组键和它们相应的中间聚合缓冲区的hash map。在内部,UnsafeFixedWidthAggregationMap创建了一个BytesToBytesMap的实例,它是用于保存hash map键值对的数据结构,键和值被存储在内存中,如下图所示:
UnsafeFixedWithAggregationMap以UnsafeRow格式对分组键和聚合缓冲区进行编码,BytesToBytesMap中的键为分组键,值为聚合缓冲区(即:group key –> aggregation buffer)。如果分组键已在hash map中,则可以通过分组键调用UnsafeFixedWithAggregationMap的getaggregatationbuffer方法来返回聚合缓冲区,如果hash map中不存在分组键,则getAggregationBuffer方法首先将分组键和空的聚合缓冲区添加到hash map中,然后返回聚合缓冲区。
当构造分区的TungstenAggregationIterator实例时,该分区中输入行的迭代器被传递到TungstengAggregation迭代器实例。TungstenAggregationIterator实例调用其processInputs方法以开始处理输入行。与此同时,回退(fallback)行计数阈值Int.MaxValue(默认为2147483647)也被传递到processInputsmethod中,该方法将用于测试是否要回退到基于排序的聚合。
与基于排序的聚合的输入行不同,基于hash的聚合的输入行是没有排序的。processInputs方法从第一行到最后一行逐一读取并处理输入行。在处理每一条输入行时,首先将分组键编码为UnsafeRow格式,然后用它来查找相应的聚合缓冲区。如果分组键还不在hash map中,分组键和一个空的聚合缓冲区将被添加到hash map中。processRow方法被调用以使用相应的聚合函数更新缓冲区的值。
当当前要处理的输入行的分组键已经在hash map中,与这个分组键对应的现有聚合缓冲区将被聚合函数更新。
在处理每个输入行时,将处理的行数与上面提到的回退阈值进行比较,即Int.MaxValue(2,147,483,647)。如果处理的行数达到阈值或者没有内存可以分配给hash map,就会调用hash map(UnsafeFixedWidthAggregationMap)的destructAndCreateExternalSorter方法,该方法通过适当的分组键对hash map进行排序,将hash map溢出到磁盘,并返回一个UnsafeKVExternalSorter,它在磁盘中保存溢出的hash map的信息。然后将创建一个新的空hash map来处理剩余的行。如果再次发生溢出,这次溢出的新的UnsafeKVExternalSorter将被合并到现有的UnsafeKVExternalSorter。
当处理最后一条输入行时,如果发生了任何溢出,当前的hash map将溢出到磁盘,这个溢出的UnsafeKVExternalSorter将被合并到现有的UnsafeKVExternalSorter,合并后的UnsafeKVExternalSorter的排序迭代器将被用来作为基于排序的聚合的输入。基于排序的聚合的原理已经在上一篇文章中解释,请参考:Spark SQL深入分析之图解SortAggregateExec执行流程。
ObjectHashAggregateExec
虽然HashAggregateExec在Tungsten执行引擎的支持下,对聚合操作表现良好,但它只能支持具有固定大小的可变原始数据类型。对于用户定义的聚合函数(UDAFs)和一些集合相关的函数(如collect_list和collect_set),它们不被HashAggregateExec所支持。在Spark 2.2.0之前,他们不得不退回到性能较差的SortAggregateExec。从Spark 2.2.0开始,ObjectHashAggregateExec的发布填补了这一空白,它可以对HashAggregateExec不支持的数据类型进行基于hash的聚合。
与HashAggregateExec不同的是,ObjectHashAggregateExec将聚合缓冲区存储在堆外内存的UnsafeRow中,它将聚合缓冲区存储在SpecificInternalRow中,后者在内部持有Java堆内存中聚合缓冲区字段的Java数组集合。ObjectHashAggregateExec使用ObjectAggregationMap实例作为hash map,而不是HashAggregateExec使用的UnsafeFixedWidthAggregationMap。ObjectAggregationMap支持将任意的Java对象存储为聚合缓冲区的值。
ObjectHashAggregateExec的执行流程与前面提到的HashAggregateExec的执行流程非常相似。输入的行将被读取,并使用基于hash的聚合方式从头到尾逐一进行处理。当hash mpa变得太大时,按组键对hash map进行排序,并将其溢出到磁盘。当所有的输入行被处理后,如果发生了任何溢出,则退回到基于排序的聚合。唯一的区别是ObjectHashAggregateExec的回退阈值是以不同的方式定义的,它测试hash map中的键的数量,而不是已处理的输入行数。ObjectHashAggregateExec的阈值可以通过spark.sql.objectHashAggregate.sortBased.fallbackThreshold属性进行配置,默认设置为128。
总结
本文主要对HashAggregateExec和ObjectHashAggregateExec两种基于hash的聚合策略的执行流程以及它们之间的区别进行了分析,至此,聚合相关的策略就告一段落了,下一篇将介绍连接(Join)相关的策略,敬请关注。
- THE END -