数据进入到Mapper作业中后,有可能会发生数据的倾斜,Mapper数据倾斜启发式算法(mapper data skew heuristic)能够判定这种情况是否存在。启发式算法会将所有的Mapper分成两部分,其中一部分的所有作业(task)的平均数据量会大于另一部分的平均数据量。
例如:第一部分有900个Mapper作业,每个Mapper作业平均数据量为7MB,而另一份包含1200个Mapper作业,且每个Mapper作业的平均数据量是500MB。
启发式算法的第一部分就是将所有的Mapper作业分成两部分。首先算法会递归的计算作业输入数据量的平均值,然后根据作业的平均输入数据量将所有的作业分成两部分。其中误差被定义为:两部分的平均内存消耗的差除以这两部分最小的平均内存消耗的差得到的值(The deviation is then found as the ratio of difference between the average memory of the two groups to the minimun of average memory of the two groups)。
伪代码:
首先定义如下变量,
误差(deviation):分成两部分后输入数据量的误差
作业数量(num_of_tasks):map作业的数量
文件大小(file_size):较大的那部分的平均输入数据量的大小
作业数量的严重度(num_tasks_severity):一个List包含了作业数量的严重度阈值,例如num_tasks_severity = {10, 20, 50, 100}
误差严重度(deviation severity):一个List包含了两部分Mapper作业输入数据差值的严重度阈值,例如deviation_severity: {2, 4, 8, 16}
文件严重度(files_severity):一个List包含了文件大小占HDFS块大小比例的严重度阈值,例如files_severity = { ⅛, ¼, ½, 1}
然后定义如下的方法,
方法 avg(x):返回List x的平均值
方法 len(x):返回List x的长度大小
方法 min(x,y):返回x和y中较小的一个
方法 getSeverity(x,y):比较x和y中的严重度阈值,返回严重度的值
接下来,根据两个部分的平均内存消耗,进行递归计算。
假设分成的两部分分别为group_1和group_2
为了不失一般性,假设
avg(group_1) > ave(group_2) and len(group_1) < len(group_2)
以及
deviation = avg(group_1) - avg(group_2) / min(avg(group_1) - avg(group_2))
file_size = avg(group_1)
num_of_tasks = len(group_0)
启发式算法的严重度可以通过下面的方法来计算:
severity = min(
getSeverity(deviation, deviation_severity)
, getSeverity(file_size,files_severity)
, getSeverity(num_of_tasks,num_tasks_severity)
)
阈值参数deviation_severity、num_tasks_severity和files_severity能够简单的进行配置。如果想进一步了解如何配置这些参数,可以点击这里进行查看。
Mapper GC会分析任务的GC效率。它会计算出GC时间占所有CPU时间的比例。
启发式算法对Mapper GC严重度的计算按照如下过程进行。首先,计算出所有作业的平均的CPU使用时间、平均运行时间以及平均垃圾回收消耗的时间。我们要计算Mapper GC严重度的最小值,这个值可以通过平均运行时间和平均垃圾回收时间占平均CPU总消耗时间的比例来计算。
首先,定义几个变量:
平均GC时间(avg_gc_time): 垃圾回收消耗的平均时间
平均CPU运行时间(avg_cpu_time): 所有作业的平均CPU运行时间
平均运行时间(avg_runtime): 所有作业的平均总运行时间
GC时间占CPU时间的比例(gc_cpu_ratio): avg_gc_time/ avg_cpu_time(平均GC时间/平均CPU运行时间)
GC比例严重度(gc_ratio_severity): 一个List包含了GC时间占CPU时间的比例的阈值
运行时间严重度(runtime_severity): 一个List包含了平均运行时间的阈值
接下来定义如下的方法:
方法 min(x,y): 返回x和y之间较小的一个
方法 getSeverity(x,y): 比较x和严重度的List的各个阈值,返回严重度的值
启发式算法的严重度可以计算如下:
severity = min(getSeverity(avg_runtime, runtime_severity), getSeverity(gc_cpu_ratio, gc_ratio_severity)
阈值参数gc_ratio_severity和runtime_severity也是可以简单配置的。如果想进一步了解如何配置这些参数,可以参考这里。
这部分分析Mapper内存消耗的检查。这部分会检查作业消耗的内存比例以及容器的总内存。作业消耗的内存是指所有作业的平均最大物理内存使用快照的内存消耗。容器的内存是指当前的任务声明的参数"mapreduce.map/reduce.memory.mb”定义的内存需求,也就是这个任务最大可以使用的内存空间大小。
首先定义如下的变量
平均物理内存消耗(avg_physical_memory): 所有作业的平均物理内存消耗
容器内存(container_memory): 容器的内存大小
容器内存严重度(container_memory_severity): 一个List包含了作业的容器内存的一系列阈值
内存比例严重度(memory_ratio_severity): 一个List包含了平均物理内存消耗/容器内存的比值的一系列阈值
然后定义如下的方法
方法 min(x,y): 返回x和y之间较小的
方法 getSeverity(x,y): 比较x和y中的严重度阈值,返回相应的严重度
启发式算法的严重度值可以按照如下计算:
severity = min(getSeverity(avg_physical_memory/container_memory, memory_ratio_severity)
, getSeverity(container_memory,container_memory_severity)
)
阈值参数container_memory_severity和memory_ratio_severity也是可以简单配置的。如果想进一步了解如何配置这些参数,可以参考这里。
这部分分析Mapper代码的运行效率。通过这些分析可以知道mapper是否受限于CPU,或者处理的数据量过大。这个分析能够分析mapper运行速度快慢和处理的数据量大小之间的关系。
这个启发式算法的严重度值,是mapper作业的运行速度的严重度和mapper作业的运行时间严重度中较小的一个。
首先定义如下的变量:
运行速度中位数(median_speed): 所有mapper作业运行速度的中位数. 运行速度是指mapper的单位时间处理的输入数据量
大小总位数(median_size): 所有mapper的大小的中位数
运行时间中位数(median_runtime): 所有mapper运行时间的中位数
硬盘速度严重度(disk_speed_severity): 一个List包含运行速度中位数值得一系列阈值
运行时间严重度(runtime_severity): 运行时间中位数的一系列阈值
然后定义如下的方法
方法 min(x,y): 返回x和y之间较小的
方法 getSeverity(x,y): 比较x和y中的阈值,返回对应的严重度
启发式算法的严重度计算方法:
severity = min(getSeverity(median_speed, disk_speed_severity), getSeverity(median_runtime, median_runtime_severity)
阈值参数disk_speed_severity和runtime_severity可以很简单的配置。如果想进一步的了解这些参数配置,可以点击这里查看。
这个启发式算法从磁盘IO的角度去评测mapper的性能。mapper溢出比例(溢出的记录数/总输出的记录数)是衡量mapper性能的一个重要指标:如果这个值接近2,表示几乎每个记录都溢出了,并临时写到磁盘两次(其中一次发生在内存排序缓存溢出时,另一次发生在归并排序所有溢出的切块时)。当这些发生时表明mapper输入输出的数据量过大了。
首先定义如下参数
总溢出记录数(total_spills): 所有map作业溢出的记录数量之和
总输出记录数(total_output_records): 所有map作业的输出记录之和
作业数目(num_tasks): 所有作业的数量之和
溢出比例(ratio_spills): 总溢出数目/总输出记录数(total_spills/ total_output_records)
spill_severity: List of the threshold values for ratio_spills
num_tasks_severity: List of threshold values for total number of tasks.
然后定义如下方法:
方法 min(x,y): 返回x和y较小的值
方法 getSeverity(x,y): 比较x和y中阈值,返回对应的严重度值
启发式算法的严重度计算方法如下
severity = min(getSeverity(ratio_spills, spill_severity), getSeverity(num_tasks, num_tasks_severity)
阈值spill_severity和num_tasks_severity可以简单的进行配置。如果想进一步了解配置参数的详细信息,可以点击这里查看。
这部分分析mappers的数量是否合适。通过分析结果,我们可以更好的优化任务中mapper的数量这个参数的设置。有以下两种情况发生时,这个参数的优化就显得很迫切了:
mapper的数量过多
mapper的平均运行时间很短
mapper的数量很少
mapper的平均运行时间很长
文件的大小过大(个别文件是GB级别)
首先定义如下的变量:
平均大小(avg_size):所有mapper的平均数据大小
平均时间(avg_time):所有作业的平均运行时间
作业数量(num_tasks):所有作业的数量
运行时间过短严重度(short_runtime_severity):一个List包含作业运行时间过短的一系列阈值
运行时间过长严重度(long_runtime_severity):一个List包含作业运行时间过长的一系列阈值
作业数量严重度(num_tasks_severity):一个List包含作业数量的一系列阈值
然后定义下面的方法:
方法 min(x,y):返回x和y之间较小的值
方法 getSeverity(x,y):比较x和y中一系列的阈值,返回合适的严重度的值
启发式算法的严重度值可以按照如下方法计算:
short_task_severity = min(getSeverity(avg_time,short_runtime_severity), getSeverity(num_tasks, num_tasks_severity))
severity = max(getSeverity(avg_size, long_runtime_severity), short_task_severity)
阈值short_runtime_severity 、long_runtime_severity 以及num_tasks_severity 可以很简单的配置。如果想进一步了解参数配置的详细信息,可以点击这里查看。
这部分分析进入到每个Reduce中的数据是否有倾斜。这部分分析能够发现Reducer中是否存在这种情况,将Reduce分为两部分,其中一部分的输入数据量明显大于另一部分的输入数据量。
计算步骤如下:首先,递归的计算所有Reducer作业输入数据的平均值,将所有Reducer作业分为两部分。误差表示为两个部分Reducer的平均内存消耗之差除以两个部分最小内存消耗之差得到的比例。
首先定义以下变量:
误差(deviation): 两部分reducer输入数据之间的差距
作业数量(num_of_tasks): reduce作业的数量
文件大小(file_size): 较大的那部分的平均输入数据大小
作业数量严重度(num_tasks_severity): 一个List包含了一系列作业数量严重度阈值,例如:num_tasks_severity = {10,20,50,100}
误差严重度(deviation_severity): 一个List包含了一系列的误差阈值,例如:deviation_severity = {2,4,8,16}
文件严重度(files_severity): 一乐List包含了所占HDFS块大小比例的一系列阈值,例如:files_severity = { ⅛, ¼, ½, 1}
接下来定义如下方法:
方法 avg(x): 返回List x中数值的平均值
方法 len(x): 返回List x的长度
方法 min(x,y): 返回x和y中较小的一个
方法 getSeverity(x,y): 比较x和y中的一系列阈值,返回合适的严重度
根据两个部分消耗的内存,递归的计算两个部分的严重度。
假设分成的两部分分别是:group_1和group_2
为了不失一般性,我们假设:
avg(group_1) > avg(group_2) and len(group_1)< len(group_2)
然后,
deviation = avg(group_1) - avg(group_2) / min(avg(group_1)) - avg(group_2))
file_size = avg(group_1)
num_of_tasks = len(group_0)
启发式算法计算严重度的方法是:
severity = min(getSeverity(deviation, deviation_severity)
, getSeverity(file_size,files_severity)
, getSeverity(num_of_tasks,num_tasks_severity)
)
阈值deviation_severity、num_tasks_severity和files_severity,可以很简单的进行配置。如果想进一步了解这些参数的配置,可以点击这里查看。
这部分分析任务的GC效率,能够计算并告诉我们GC时间占所用CPU时间的比例。
首先,会计算出所有任务的平均CPU消耗时间、平均运行时间以及平均垃圾回收所消耗的时间。然后,算法会根据平均运行时间以及垃圾回收时间占平均CPU时间的比值来计算出最低的严重度。
首先定义如下的变量:
平均GC时间(avg_gc_time): 垃圾回收消耗的平均时间
平均占用CPU时间(avg_cpu_time): 所有作业平均占用CPU的时间
平均运行时间(avg_runtime): 所有作业的平均运行时间
GC占CPU运行时间的比例(gc_cpu_ratio): 平均GC时间/平均占用CPU时间(avg_gc_time/ avg_cpu_time)
GC比例严重度(gc_ratio_severity): 一个List包含一系列GC占CPU运行时间的比例阈值
运行时间严重度(runtime_severity): 一个List包含一系列运行时间阈值
然后定义以下方法:
方法 min(x,y): 返回x和y中较小的一个
方法 getSeverity(x,y): 比较x和y中一系列的阈值,返回合适的严重度的值
启发式算法的严重度计算方法是:
severity = min(getSeverity(avg_runtime, runtime_severity), getSeverity(gc_cpu_ratio, gc_ratio_severity)
阈值gc_ratio_severity、runtime_severity可以很简单的配置,如果想进一步了解参数配置的详细过程,可以点击这里查看。
这部分分析作业的内存使用情况。算法会比较作业消耗的内存以及容器要求的内存分配。消耗的内存是指每个作业消耗的最大内存的平均值。容器需求的内存是指任务配置的“mapreduce.map/reduce.memory.mb”,也就是任务能够使用最大物理内存。
首先定义以下变量:
平均物理内存(avg_physical_memory): 所有作业消耗的平均物理内存
容器内存(container_memory): 作业运行能使用的最大内存
容器内存严重度(container_memory_severity): 一个List包含一系列容器内存阈值
内存比例严重度(memory_ratio_severity): 一个List包含一系列平均物理内存/容器内存比例的阈值
然后定义以下的方法:
方法 min(x,y): 返回x和y中较小的一个
方法 getSeverity(x,y): 比较x和y中一系列阈值,返回合适的严重度值
严重度可以按如下方式计算:
severity = min(getSeverity(avg_physical_memory/container_memory, memory_ratio_severity)
, getSeverity(container_memory,container_memory_severity)
)
阈值参数container_memory_severity和memory_ratio_severity可以很简单的配置,如果想进一步了解配置参数的信息,可以点击这里查看。
这部分分析Reducer的执行效率,可以帮助我们更好的配置任务中reducer的数量。当出现以下两种情况时,说明Reducer的数量需要进行调优:
Reducer数量过多
Reducer的运行时间很短
Reducer数量过少
Reducer运行时间很长
首先定义如下变量:
平均大小(avg_size): 所有mapper输入数据的平均大小
平均时间(avg_time): 所有作业的平均运行时间
作业数量(num_tasks): 所有作业的数量
运行时间过短严重度(short_runtime_severity): 一个List包含运行时间过短的一系列阈值
运行时间过长严重度(long_runtime_severity): 一个List包含运行时间过长的一系列阈值
作业数量严重度(num_tasks_severity): 作业的数量的严重度
接下来定义以下方法:
方法 min(x,y): 返回x和y中较小的值
方法 getSeverity(x,y): 比较x和y中一系列的阈值,返回合适的严重度的值
启发式算法的严重度计算方法如下:
short_task_severity = min(getSeverity(avg_time,short_runtime_severity), getSeverity(num_tasks, num_tasks_severity))
severity = max(getSeverity(avg_size, long_runtime_severity), short_task_severity)
阈值参数short_runtime_severity、long_runtime_severity以及num_tasks_severity可以很简单的配置,如果想进一步了解参数配置的详细过程,可以点击这里查看。
这部分分析reducer消耗的总时间以及reducer在进行洗牌和排序时消耗的时间,通过这些分析,可以理解reducer的执行效率。
首先定义以下变量
平均运行时间(avg_exec_time): 所有Reducer作业的平均运行时间
平均洗牌时间(avg_shuffle_time): 洗牌消耗的平均时间
平均排序时间(avg_sort_time): 排序消耗的平均时间
运行时间所占比例的严重度(runtime_ratio_severity): 一个List包含了一系列洗牌加排序时间占总运行时间比例的阈值
运行时间严重度(runtime_severity): 一个List包含了一系列洗牌加排序时间的阈值
启发式算法的严重度可以按照如下方法计算:
severity = max(shuffle_severity, sort_severity)
其中shuffle_severity和sort_severity可以按照如下方式计算:
shuffle_severity = min(getSeverity(avg_shuffle_time, runtime_severity), getSeverity(avg_shuffle_time*2/avg_exec_time, runtime_ratio_severity))
sort_severity = min(getSeverity(avg_sort_time, runtime_severity), getSeverity(avg_sort_time*2/avg_exec_time, runtime_ratio_severity))
阈值参数avg_exec_time、avg_shuffle_time和avg_sort_time可以很简单的进行配置。更多关于参数配置的相信信息可以点击这里查看。
目前,Spark事件日志的处理器不能处理很大的日志文件,Dr.Elephant需要花费很长时间去处理一个很大的Spark事件日志,这有可能会危害整个Dr.Elephant服务的运行。所以,我们设置Spark事件日志最大为100MB。如果日志大小超过这个限制,会使用另一个进程去处理这个日志文件。
如果数据被节流限制了,那么启发式算法认为严重度是最高的CRITICAL,否则,就没有严重度。
和MapReduce任务的执行机制不同,Spark应用在启动后会一次性分配它所需要的所有资源,直到整个任务结束才会释放这些资源。根据这个机制,对Spark的处理器的负载均衡就显得非常重要,可以避免对集群某些节点的过度使用。
首先要得到最大内存消耗、最小内存消耗以及平均内存消耗,然后才能计算Spark处理器的负载均衡的严重度。
首先定义以下参数:
最大内存使用(peak_memory): 一个List包含所有作业的最大内存消耗
持续时间(durations): 一个List包含所有作业的持续时间
输入数据大小(inputBytes): 一个List包含所有作业的输入数据的大小
输出数据大小(outputBytes): 一个List包含所有作业的输出数据的大小
使用宽松标准的误差严重度(looser_metric_deviation_severity): 一个List包含误差严重度的一系列阈值,是一个比较宽松的标准
使用严格标准的误差严重度(metric_deviation_severity): 一个List包含误差严重度的一系列阈值,是一个比较严格的标准
然后定义以下方法:
方法 getDeviation(x): 返回 max(|maximum-avg|, |minimum-avg|)/avg, 其中
x = 一个List包含一些值
maximum = x中最大值
minimum = x中最小值
avg = x中所有值的平均值
方法 getSeverity(x,y): 比较x和y中一系列阈值,返回合适严重度的值
方法 max(x,y): 返回x和y中比较大的一个值
方法 Min(l): 返回List l中最小的值
启发式算法的严重度的值可以计算如下:
severity = Min( getSeverity(getDeviation(peak_memory), looser_metric_deviation_severity),
getSeverity(getDeviation(durations), metric_deviation_severity),
getSeverity(getDeviation(inputBytes), metric_deviation_severity),
getSeverity(getDeviation(outputBytes), looser_metric_deviation_severity).
)
阈值参数looser_metric_deviation_severity和metric_deviation_severity 可以简单的进行配置。如果想进一步了解参数配置的详细过程,可以点击这里查看。
这个启发式算法对Spark任务的运行时间进行调优分析。每个Spark应用程序可以拆分成多个任务,每个任务又可以拆分成多个阶段。
首先定义以下变量:
平均任务失败率(avg_job_failure_rate): 所有任务失败的平均比例
平均任务失败率的严重度(avg_job_failure_rate_severity): 一个List包含一系列平均任务失败率的阈值
接下来为每个任务定义以下变量:
单个任务失败率(single_job_failure_rate): 每个任务失败的概率
单个任务失败率的严重度(single_job_failure_rate_severity): 一个List包含一系列单个任务失败率的阈值
启发式的严重度计算放下:为每个任务找到单个任务失败率的严重度,并计算得到平均任务失败率的严重度,返回他们中较大的那个值。
severity = max(getSeverity(single_job_failure_rate, single_job_failure_rate_severity),
getSeverity(avg_job_failure_rate, avg_job_failure_rate_severity)
)
其中,需要计算所有任务的单个任务失败率(single_job_failure_rate)。
阈值参数single_job_failure_rate_severity和avg_job_failure_rate_severity可以很简单的进行配置。更多详细信息,可以点击这里查看。
目前,Spark应用程序缺少动态资源分配的功能。MapReduce任务在运行时,能够为每个map/reduce进程分配所需要的资源,并且在执行过程中逐步释放占用的资源。而Spark在应用程序执行时,会一次性的申请所需要的所有资源,直到任务结束才释放这些资源。过多的内存使用会对集群节点的稳定性产生影响。所以,我们需要限制Spark应用程序能使用的最大内存比例。
首先定义以下变量:
执行器使用的总内存(total_executor_memory): 所有的执行器使用的总内存
执行器存储用的总内存(total_storage_memory): 所有的执行器存储使用的总内存
总的驱动内存(total_driver_memory): 所有驱动使用的内存
内存使用峰值(peak_memory): 所有内存使用的峰值
内存使用严重度(mem_utilization_severity): 一个List包含一系列内存使用的阈值
总内存使用严重度(total_memory_severity_in_tb): 一个List包含一系列总内存使用的阈值
然后定义以下方法:
方法 max(x,y): 返回x和y中较大的值
方法 getSeverity(x,y): 比价x和y中一系列阈值,返回合适的严重度
启发式算法的严重度计算如下:
severity = max(getSeverity(total_executor_memory,total_memory_severity_in_tb),
getSeverity(peak_memory/total_storage_memory, mem_utilization_severity)
)
阈值参数total_memory_severity_in_tb和mem_utilization_severity可以很简单的配置。进一步了解,可以点击这里查看。
就像Spark任务一样,Spark应用程序可以分为多个任务,每个任务又可以分为多个阶段。
首先为每个Spark任务定义以下变量:
阶段失败率(stage_failure_rate): 任务的每个阶段失败的概率
阶段失败率严重度(stagge_failure_rate_severity): 一个List包含一系列阶段失败率的阈值
接下来为任务的每个阶段定义以下变量:
任务失败率(task_failure_rate): 任务在每个阶段失败的概率
运行时间(runtime): 每个阶段的运行时间
单阶段任务失败率严重度(single_stage_tasks_failure_rate_severity): 一个List包含一系列任务在单阶段失败率的阈值
阶段运行时间严重度(stage_runtime_severity_in_min): 一个List包含一系列阶段运行时间的阈值
接下来定义以下方法:
方法 max(x,y): 返回x和y中较大的值
方法 getSeverity(x,y): 比较x和y中一系列的值,返回合适的严重度的值
启发式算法的严重度可以计算如下:
severity_stage = max(getSeverity(task_failure_rate, single_stage_tasks_faioure_rate_severity),
getSeverity(runtime, stage_runtime_severity_in_min)
)
severity_job = getSeverity(stage_failure_rate,stage_failure_rate_severity)
severity = max(severity_stage, severity_job)
其中task_failure_rate需要为每个任务计算。
阈值参数single_stage_tasks_failure_rate_severity、stage_runtime_severity_in_min和stage_failure_rate_severity可以很简单的配置。进一步了解,请点击这里。
作者简介:屈世超,专注于大数据。曾任职小米科技公司服务端后台开发工程师,现担任EverString数据平台组高级开发工程师。