大数据治理域——计算管理

摘要

本文主要探讨了大数据治理域中的计算管理问题,特别是系统优化和任务优化两个方面。文章首先指出MaxCompute集群任务众多,资源消耗巨大,因此需要优化计算资源以提高性能和任务产出时间。文章介绍了HBO(基于历史的优化器)和CBO(基于代价的优化器)两种优化方式,详细阐述了它们的原理和优势。HBO通过任务历史执行情况和集群状态信息为任务分配合理资源,而CBO则通过收集统计信息计算执行代价,选择最优执行方式。文章还介绍了MaxCompute原资源分配策略以及HBO的提出背景,最后探讨了任务优化中的Map、Join和Reduce倾斜问题及其解决方案。

1. 系统优化

目前内部MaxCompute集群上有200多万个任务,每天存储资源、计算资源消耗都很大。如何降低计算资源的消耗,提高任务执行的性能,提升任务产出的时间,是计算平台和ETL开发工程师孜孜追求的目标。本章分别从系统优化和任务优化层面介绍计算优化。

Hadoop等分布式计算系统评估资源的方式,一般是根据输入数据量进行静态评估,Map任务用于处理输入,对于普通的Map任务,评估一般符合预期;而对于Reduce任务,其输入来自于Map的输出,但一般只能根据Map任务的输入进行评估,经常和实际需要的资源数相差很大,所以在任务稳定的情况下,可以考虑基于任务的历史执行情况进行资源评估,即采用HBO(History-Based Optimizer,基于历史的优化器)。提到CBO(Cost-Based Optimizer,.基于代价的优化器),首先会想到Oracle的CBO。Oracle会根据收集到的表、分区、索引等统计信息来计算每种执行方式的代价(Cost),进而选择其中最优的执行方式。

一般来说,对于更多的、更准确的统计信息,CBO则可能生成代价更小的执行计划。但对表和列上统计信息的收集也是有代价的,尤其是在大数据环境下,表的体量巨大,消耗大量资源收集的统计信息利用率很低。MaxCompute采用各种抽样统计算法,通过较少的资源获得大量的统计信息,基于先进的优化模型,具备了完善的CBO能力,与传统的大数据计算系统相比,性能提升明显。

1.1. HBO(History-Based Optimizer)

HBO是根据任务历史执行情况为任务分配更合理的资源,包括内存、CPU以及Instance个数。HBO是对集群资源分配的一种优化,概括起来就是:任务执行历史+集群状态信息+优化规则→更优的执行配置。

1.1.1. MaxCompute原资源分配策略

首先看一下MaxCompute最初是如何分配MR执行过程的Instance个数的。默认的Instance分配算法如表13.1所示。

在这个分配算法的基础上,根据历史数据统计,各个Instance处理的数据量分布如下:

注解:fivenum是R语言中统计数据分布的函数,统计值有5个,分别是最小值、下四分位数、中位数、上四分位数、最大值。

Smap_input_bytes、Sreduce_input_bytes、Sjoin_input_bytes分别表示Map、Reduce、Join三种Task的输入数据量(Bytes)。从上面内容可以看出,大部分Instance处理的数据量远远没有达到预期,即一个Instance处理256MB的数据。同时有些Instance处理的数据量很大,很容易导致任务长尾。

假如把处理数据量小的任务称作小任务,处理数据量大的任务称作大任务,总结:在默认的Instance算法下,小任务存在资源浪费,而大任务却资源不足。综上所述,需要有更合理的方法来进行资源分配,HBO应运而生。

1.1.2. HBO的提出

通过数据分析,发现在系统中存在大量的周期性调度的脚本(物理计划稳定),且这些脚本的输入一般比较稳定,如果能对这部分脚本进行优化,那么对整个集群的计算资源的使用率将会得到显著提升。由此,我们想到了HBO,根据任务的执行历史为其分配更合理的计算资源。HBO一般通过自适应调整系统参数来达到控制计算资源的目的。HBO分配资源的步骤如下:

  1. 前提:最近7天内任务代码没有发生变更且任务运行4次。
  2. Instance分配逻辑:基础资源估算值+加权资源估算值。

基础资源数量的逻辑

  • 对于Map Task,系统需要初始化不同的输入数据量,根据期望的每个Map能处理的数据量,再结合用户提交任务的输入数据量,就可以估算出用户提交的任务所需要的Map数量。为了保证集群上任务的整体吞吐量,保证集群的资源不会被一些超大任务占有,我们采用分层的方式,提供平均每个Map能处理的数据量。
  • 对于Reduce Task,比较Hive使用Map输入数据量,MaxCompute使用最近7天Reduce对应Map的平均输出数据量作为Reduce的输入数据量,用于计算Instance的数量。对于Reduce个数的估算与Map估算基本相同,不再赘述。

加权资源数量的逻辑

  • 对于Map Task,系统需要初始化期望的每个Map能处理的数据量。通过该Map在最近一段时间内的平均处理速度与系统设定的期望值做比较,如果平均处理速度小于期望值,则按照同等比例对基础资源数量进行加权,估算出该Map的加权资源数量。
  • 对于Reduce Task,方法同上。最终的Instance个数为:基础资源估算值+加权资源估算值。CPU/内存分配逻辑:类似于Instance分配逻辑,也是采用基础资源估算值+加权资源估算值的方法。

HBO效果

  1. 提高CPU利用率:通过适当降低每个Instance的CPU资源数,集群利用率从40%提升到80%。其中早上4:00一7:00节省的CPU资源可以供6万个Instance并发使用。
  2. 提高内存利用率在保障并行度,同时又能提高执行效率的基础上,合理分配内存,早上4:00一7:00节省的内存资源可以供4万个Instance并发使用。
  3. 提高Instance并发数合理设置Task的Instance个数,Instance峰值并发数提升了57%。
  4. 降低执行时长在某机器上测试效果很明显。该集群有3700台机器,任务数约16万个,总执行时长减少4472小时(没有开启HB0时总执行时长是8356小时,开启HB0后总执行时长为3884小时)。

HBO改进与优化

HBO是基于执行历史来设置计划的,对于日常来说,数据量波动不大,工作良好。但是某些任务在特定场合下依旧有数据量暴涨的情况发生,尤其是在大促"双11"和"双12"期间,这个日常生成的HBO计划就不适用了。针对这个问题,HBO也增加了根据数据量动态调整Instance数的功能,主要依据Map的数据量增长情况进行调整。

1.2. CBO(Cost-Based Optimizer)

MaxCompute2.0引入了基于代价的优化器(CBO),根据收集的统计信息来计算每种执行方式的代价,进而选择最优的执行方式。该优化器对性能提升做出了卓越改进。通过性能评测,MaxCompute2.0离线计算比同类产品Hive2.0 on Tez快90%以上。

1.2.1. 优化器原理

优化器(Optimizer)引入了Volcano模型,该模型是基于代价的优化器(CBO),并且引入了重新排序Join(Join Reorder)和自动MapJoin(Auto MapJoin)优化规则等,同时基于Volcano模型的优化器会尽最大的搜索宽度来获取最优计划。优化器有多个模块相互组合协调工作,包括Meta Manager(元数据)、Statistics(统计信息)、Rule Set(优化规则集)、Volcano Planner Core(核心计划器)等。

  1. Meta Manager

Meta模块主要提供元数据信息,包括表的元数据、统计信息元数据等。当优化器在选择计划时,需要根据元数据的一些信息进行优化。

比如表分区裁剪(TableScan Partition Prunning)优化时,需要通过Meta信息获取表数据有哪些分区,然后根据过滤条件来裁剪分区。同时还有一些基本的元数据,如表是否是分区表、表有哪些列等。

对于Meta的管理,MaxCompute提供了Meta Manager与优化器进行交互。Meta Manager与底层的Meta部分对接,提供了优化器所需要的信息。

  1. Statistics

Statistics主要是帮助优化器选择计划时,提供准确的统计信息,如表的Count值、列的Distinct值、TopN值等。优化器只有拥有准确的统计信息,才能计算出真正最优的计划。比如Join是选择Hash Join还是Merge Join,优化器会根据Join的输入数据量(即Count值)来进行选择。优化器提供了UDF来收集统计信息,包括Distinct值、TopN值等,而Count值等统计信息是由底层Meta直接提供的。

  1. Rule Set

优化规则是根据不同情况选择不同的优化点,然后由优化器根据代价模型(Cost Model)来选择启用哪些优化规则。比如工程合并规则(Project Merge Rule),将临近的两个Project合并成一个Project;过滤条件下推规则(Filter Push Down),将过滤条件尽量下推,使得数据先进行过滤,然后再进行其他计算,以减少其他操作的数据量。这些所有的优化都放置在优化规则集中。

MaxCompute优化器提供了大量的优化规则,用户也可以通过set等命令来控制所使用的规则。规则被分为Substitute Rule(被认为是优化了肯定好的规则)、Explore Rule(优化后需要考虑多种优化结果)、Build Rule(可以认为优化后的结果不能再次使用规则进行优化)。

  1. Volcano Planner Core

Volcano Planner是整个优化器的灵魂,它会将所有信息(Meta信息、统计信息、规则)统一起来处理,然后根据代价模型的计算,获得一个最优计划。

  1. 代价模型。代价模型会根据不同操作符(如Join、Project等)计算不同的代价,然后计算出整个计划中最小代价的计划。MaxCompute代价模型目前提供的Cost由三个维度组成,即行数、I/O开销、CPU开销,通过这三个维度来衡量每一个操作符的代价。
  2. 工作原理。假设Planner的输入是一棵由Compiler解析好的计划树,简称RelNode树,每个节点简称RelNode。
  1. Volcanno Planner创建

Planner的创建主要是将Planner在优化过程中要用到的信息传递给执行计划器,比如规则集,用户指定要使用的规划;Meta Provider,每个RelNode的Meta计算,如RowCount值计算、Distinct值计算等;代价模型,计算每个RelNode的代价等。这些都是为以后Planner提供的必要信息。

  1. Planner优化

规则匹配(Rule Match):是指RelNode满足规则的优化条件而建规则匹配(Rule Match):是指RelNode满足规则的优化条件而建立的一种匹配关系。Planner首先将整个RelNode树的每一个RelNode注册到Planner内部,同时在注册过程中,会在规则集中找到与每个RelNode匹配的规则,然后加入到规则应用(Rule Apply)的队列中。

所以整个注册过程处理结束后,所有与RelNode可以匹配的规则全部加入到队列中,以后应用时只要从队列中取出来就可以了。规则应用(Rule Apply):是指从规则队列(Rule Queue)中弹出(Pop)一个已经匹配成功的规则进行优化。当获取到一个已经匹配的规则进行处理时,如果规则优化成功,则肯定会产生至少一个新的RelNode,因为进行了优化,所以与之前未优化时的RelNode有差异。这时需要再次进行注册以及规则匹配操作,把与新产生的RelNode匹配的规则加入到规则队列中,然后接着下次规则应用。

Planner会一直应用所有的规则,包括后来叠加的规则,直到不会有新的规则匹配到。至此,整个优化结束,这时就可以找到一个最优计划。代价计算(Cost Compute):每当规则应用之后,如果规则优化成功,则会产生新的RelNode。在新的RelNode注册过程中,有一个步骤是计算RelNode的代价。

代价计算由代价模型对每个RelNode的代价进行估算。如果不存在代价,或者Child的代价还没有估算(默认是最大值),则忽略。

如果存在代价,则会将本身的代价和Child(即输人的所有RelNode)的代价进行累加,若小于Best,则认为优化后的RelNode是当前最优的。并且会对其Parent进行递归估算代价,即传播代价计算(PropagateCalculateCost)。比如计划:Project->TableScan,当TableScan计算代价为1时,则会继续估算Project的代价,假设为1,则整个Project的代价就是1+1=2。也就是说,当RelNode本身的代价估算出来后,会递归地对Parent进行代价估算,这样就可以对整条链路的计划进行估算。在这个估算过程中借助了MetaManager和Statistics提供的信息(见图13.2)。

1.2.2. 优化器新特性

优化器具有一些新特性,主要是重新排序Join(JoinReorder)和自动MapJoin(AutoMapJoin)

  1. 重新排序Join

Join可以认为是关系数据库中最重要的操作符之一。Join的性能也直接关系到整个SQL的性能。重新排序Join可以认为是将Join的所有不同输人进行一个全排列,然后找到代价最小的一个排列。之前仅仅保持了用户书写SQL语句的Join顺序,这样的Join顺序不一定是最优的,所以通过重新排序Join规则可以实现更好的选择,提供更优的性能。

  1. 自动MapJoin

Join实现算法有多种,目前主要有MergeJoin和MapJoin。对于小数据量,MapJoin比MergeJoin性能更优。之前是通过Hint方式来指定是否使用MapJoin,这样对用户不是很友好,且使用不方便。自动MapJoin充分利用优化器的代价模型进行估算,获得更优的MapJoin方式,而不是通过Hint方式来进行处理。

  1. 优化器使用

MaxCompute优化器具有一些新特性,也提供了许多优化规则,将内部已经实现的优化规则进行分类,并且提供st等命令方便用户使用。一些基础优化规则都会默认打开,用户也可以自己任意搭配使用。优化器提供的Flag有:

  • 规则白名单一odps.optimizer.cbo.rule.filter.white
  • 规则黑名单一odps.optimizer..cbo.rule.filter..black

使用方法很简单,如果用户需要使用哪些优化规则,只要将规则的缩写名称加入白名单即可;反之,需要关闭哪些优化规则,只要将名称加入黑名单即可。比如set odps.optimizer.cbo.rule.filter.black=xxx,yyy;,就表示将xxx,yyy对应的优化规则关闭。对于重新排序Join和自动MapJoin,对应的标记分别是porj和hj。即如果想使用上述优化,则可以进行如下设置:

复制代码
set odps.optimizer.cbo.rule.filter.white=pojr,hj; 

注:优化规则之间请使用","分隔。优化前:5129s,优化后:3814s,性能提升:25.7%。

  1. 注意事项

由于用户书写SQL语句时可能存在一些不确定因素,所以应尽量避免这些因素带来的性能影响,甚至结果非预期。

Optimizer会提供谓词下推(Predicate Push Down)优化,主要目的是尽量早地进行谓词过滤,以减少后续操作的数据量,提高性能。但需要注意的是:

UDF :对于UDF是否下推,优化器做了限制,不会任意下推这种带有用户意图的函数,主要是因为不同用户书写的函数含义不一样,不可以一概而论。如果用户需要下推UDF,则要自己改动SQL,这样做主要的好处是用户自己控制UDF执行的逻辑,最了解自己的UDF使用在SQL的哪个部分,而不是优化器任意下推等。例如UDF可能和数据顺序有关,下推和不下推会导致出现不同的结果。理论上,过滤条件可以下推到Exchange之后,但不是所有UDF都是这样的;否则会导致结果违背了用户书写SQL语句的本意。不确定函数:对于不确定函数,优化器也不会任意下推,比如sample函数,如果用户将其写在where子句中,同时语句存在Join,则优化器是不会下推到TableScan的,因为这样可能改变了原意。

复制代码
SELECT
FROM t1
JOIN t2
ON t1.c1=t2.d1
WHERE sample(4,1)true;

则sample函数在Join之后执行,而不会直接在TableScan后执行。如果用户需要对TableScan进行抽样,则需要自已修改SQL来达到。目的;否则优化器进行下推可能会错误地理解用户的意图。对上述SQL语句修改如下:

复制代码
SELECT
FROM (
    SELECT
    FROMt1
    WHEREsample(4,1)=true
)t1
JOIN t2
ONt 1.c1=t2.d1;
  1. 隐式类型转换

书写SQL语句时,应尽量避免JoinKey存在隐式类型转换。例如String=Bigint,在这种情况下会转换为ToDouble(String)=ToDouble(Bigint),这是不是用户原本的意图,数据库本身不得而知。这样可能引发的后果有两种:一种是转换失败,报错;另一种是虽然执行成功了,但结果与用户期望的不一致。

2. 任务优化

SQL/MR作业一般会生成MapReduce任务,在MaxCompute中则会生成MaxComputeInstance,通过唯一ID进行标识。

  1. FuxiJob:对于MaxComputeInstance,则会生成一个或多个由FuxiTask组成的有向无环图,即FuxiJob。MaxComputeInstance和FuxiJob类似于Hive中Job的概念。
  2. FuxiTask:主要包含三种类型,分别是Map、Reduce和Join,类似于Hive中Task的概念。
  3. FuxiInstance:真正的计算单元,和Hive中的概念类似,一般和槽位(slot)对应。

2.1. Map倾斜

2.1.1. 背景

Map端是MR任务的起始阶段,Map端的主要功能是从磁盘中将数据读入内存,Map端的两个主要过程如图所示。

每个输入分片会让一个Map Instance来处理,在默认情况下,以Pangu文件系统的一个文件块的大小(默认为256MB)为一个分片。Map Instance输出的结果会暂时放在一个环形内存缓冲区中,当该缓冲区快要溢出时会在本地文件系统中创建一个溢出文件,即Write Dump。在Map读数据阶段,可以通过"set odps.mapper.split.size=256"来调节Map Instance的个数,提高数据读入的效率,同时也可以通过"set odps.mapper.merge,limit.size=64"来控制Map Instance读取文件的个数。如果输人数据的文件大小差异比较大,那么每个Map Instance读取的数据量和读取时间差异也会很大。

在写人磁盘之前,线程首先根据Reduce Instance的个数划分分区,数据将会根据Key值Hash到不同的分区上,一个ReduceInstance对应一个分区的数据。Map端也会做部分聚合操作,以减少输人Reduce端的数据量。由于数据是根据Hash分配的,因此也会导致有些Reduce Instance会分配到大量数据,而有些Reduce Instance却分配到很少数据,甚至没有分配到数据。

在Map端读数据时,由于读人数据的文件大小分布不均匀,因此会导致有些MapInstance读取并且处理的数据特别多,而有些MapInstance处理的数据特别少,造成Map端长尾。以下两种情况可能会导致Map端长尾:上游表文件的大小特别不均匀,并且小文件特别多,导致当前表Map端读取的数据分布不均匀,引起长尾。Map端做聚合时,由于某些MapInstance读取文件的某个值特别多而引起长尾,主要是指CountDistinct操作。

2.1.2. 方案

第一种情况导致的Map端长尾,可通过对上游合并小文件,同时调节本节点的小文件的参数来进行优化,即通过设置"set odps.sqlmapper..merge.limit..size=64"和"set odps.sql.mapper..split.size=256"两个参数来调节,其中第一个参数用于调节Map任务的Map Instance的个数;第二个参数用于调节单个Map Instance读取的小文件个数,防止由于小文件过多导致Map Instance读取的数据量很不均匀;两个参数配合调整。下面主要讨论第二种情况的处理方式。

如下代码的作用是获取手机APP日志明细中的前一个页面的页面组信息,其中pre_page是前一个页面的页面标识,page_ut表是存储的手机APP的页面组,pre_page只能通过匹配正则或者所属的页面组信息,进行笛卡儿积Join。

复制代码
SELECT ......
FROM
(
  SELECT ds,unique_id,pre_page
  FROM tmp app_ut_1
  WHERE ds='$(bizdate}'
  AND pre_page is not null
) a
LEFT OUTER JOIN
(
  SELECT t.* ,length(t.page type rule)rule length
  FROM page_ut t
  WHERE ds='s(bizdate}'
  AND is_enable ='Y'
) b
0N 1=1
WHERE a.pre_page rlike b.page_type_rule;

L1Stg4是MapJoin小表的分发阶段;M3Stgl是读取明细日志表的Map阶段,与MapJoin小表的Join操作也发生在这个阶段;R53Stg2是进行分组排序的阶段。

通过日志发现,M3Stg1阶段单个Instance的处理时间达到了40分钟,而且长尾的Instance个数比较固定,应是不同的Map读入的文件块分布不均匀导致的,文件块大的Map数据量比较大,在与小表进行笛卡儿积操作时,非常耗时,造成Map端长尾。针对这种情况,可以使用"distribute by rand()"来打乱数据分布,使数据尽可能分布均匀。

复制代码
SELECT ......
FROM
(
  SELECT ds,unique_id,pre_page
  FROM tmp app_ut_1
  WHERE ds='$(bizdate}'
  AND pre_page is not null
  DISTRIBUTE BY rand ()
) a
LEFT OUTER JOIN
(
  SELECT t.* ,length(t.page type rule)rule length
  FROM page_ut t
  WHERE ds='s(bizdate}'
  AND is_enable ='Y'
) b
0N 1=1
WHERE a.pre_page rlike b.page_type_rule;

通过"distribute by rand()"会将Map端分发后的数据重新按照随机值再进行一次分发。原先不加随机分发函数时,Map阶段需要与使用MapJoin的小表进行笛卡儿积操作,Map端完成了大小表的分发和笛卡儿积操作。使用随机分布函数后,Map端只负责数据的分发,不再有复杂的聚合或者笛卡儿积操作,因此不会导致Map端长尾。

2.1.3. 思考

Map端长尾的根本原因是由于读入的文件块的数据分布不均匀,再加上UDF函数性能、Join、聚合操作等,导致读入数据量大的MapInstance耗时较长。在开发过程中如果遇到Map端长尾的情况,首先考虑如何让Map Instance读取的数据量足够均匀,然后判断是哪些操作导致Map Instance比较慢,最后考虑这些操作是否必须在Map端完成,在其他阶段是否会做得更好。

2.2. Join 倾斜

2.2.1. 背景

Join操作需要参与Map和Reduce的整个阶段。首先通过一段Join的SQL来看整个Map和Reduce阶段的执行过程以及数据的变化,进而对Join的执行原理有所了解。

复制代码
SELECT student id,student name,course id
FROM student
LEFT JOIN student course
ON student.student_id=student_course.student_id;

通过上面的执行过程可以看出,MaxCompute SQL在Join执行阶段会将Join Key相同的数据分发到同一个执行Instance上处理。如果某个Key上的数据量比较大,则会导致该Instance执行时间较长。其表现为:在执行日志中该Join Task的大部分Instance都已执行完成,但少数几个Instance一直处于执行中(这种现象称之为长尾)。

因为数据倾斜导致长尾的现象比较普遍,严重影响任务的执行时间,尤其是在"双11"等大型活动期间,长尾程度比平时更严重。比如某些大型店铺的PV远远超过一般店铺的PV,当用浏览日志数据和卖家维表关联时,会按照卖家D进行分发,导致某些大卖家所在Instance处理的数据量远远超过其他Instance,而整个任务会因为这个长尾的Instance迟迟无法结束。

本文的目的就是对MaxCompute SQL执行中Join阶段的数据倾斜情况进行分析总结,根据不同的倾斜原因给出对应的解决方案。这里主要讲述三种常见的倾斜场景。

Join的某路输入比较小,可以采用MapJoin,避免分发引起长尾。Join的每路输入都较大,且长尾是空值导致的,可以将空值处理成随机值,避免聚集。Join的每路输入都较大,且长尾是热点值导致的,可以对热点值和非热点值分别进行处理,再合并数据。下面会针对这三种场景给出具体的解决方案。首先我们了解一下如何确认Join是否发生数据倾斜。

从图l3.9中可以看到每一个Map、Join、Reduce的Fuxi Task任务,点击其中一个Join任务,可以看到有115个Instance长尾;再点击StdOut,.可以查看Instance读入的数据量,如图13.l0所示。

图13.10中显示Join的一路输入读取的数据量是1389941257行。如果Long-Tails中Instance读入的数据量远超过其他Instance读取的数据量,则表示某个Instance处理的数据量超大导致长尾。

2.2.2. 方案

针对上面提到的三种倾斜场景,给出以下三种对应的解决方案。

  1. MapJoin方案

Join倾斜时,如果某路输入比较小,则可以采用MapJoin避免倾斜。MapJoin的原理是将Join操作提前到Map端执行,将小表读人内存,顺序扫描大表完成Join。这样可以避免因为分发key不均匀导致数据倾斜。但是MapJoin的使用有限制,必须是Join中的从表比较小才可用。所谓从表,即左外连接中的右表,或者右外连接中的左表。MapJoin的使用方法非常简单,在代码中select后加上"/*+mapjoin(a)*/"即可,其中a代表小表(或者子查询)的别名。现在MaxCompute已经可以自动选择是否使用MapJoin,可以不使用显式Hint。例如:

复制代码
SELECT /*+MAPJOIN (b)*/
  a.c2
  ,b.c3
(
    SELECT
    c1
    ,C2
    FROM tl
)a
LEFT OUTER JOIN
(
    SELECT
    cl
    ,C3
    FROM t2
)b
on a.c1= b.cl;

另外,使用MapJoin时对小表的大小有限制,默认小表读入内存后的大小不能超过5l2MB,但是用户可以通过设置"set odps.sql.mapjoin.memory.max=2048"加大内存,最大为2048MB。

  1. Join因为空值导致长尾

另外,数据表中经常出现空值的数据,如果关联ky为空值且数据量比较大,Joi时就会因为空值的聚集导致长尾,针对这种情况可以将空值处理成随机值。因为空值无法关联上,只是分发到一处,因此处理成随机值既不会影响关联结果,也能很好地避免聚焦导致长尾。例如:

复制代码
SELECT ......
FROM table_a
LEFT OUTER JOIN table_b
ON coalesce(table_a.key,rand()*9999)=table_b.key  --key为空值时用随机值替代
  1. Join因为热点值导致长尾

如果是因为热点值导致的长尾,并且Joi的输入比较大无法使用MapJoin,则可以先将热点key取出,对于主表数据用热点key切分成热点数据和非热点数据两部分分别处理,最后合并。这里以淘宝的PV日志表关联商品维表取商品属性为例进行介绍。

  • 取热点key:将PV大于50000的商品ID取出到临时表中。

    INSERT OVERWRITE TABLE topk_item
    SELECT item_id
    FROM
    (
    SELECT item_id,count (1) as cnt
    FROM pv --pv表
    WHERE
    ds ='s(bizdate)'
    AND url type 'ipv'
    AND item_id is not null
    GROUP BY item id
    )a
    WHERE cnt>=50000

  • 取出非热点数据。

将主表(pv表)和热点key表(topk item表)外关联后,通过条件"bl.item id is null'"取关联不到的数据即非热点商品的日志数据,此时需要使用MapJoin。再用非热点数据关联商品维表,因为已经排除了热点数据,所以不会存在长尾。

  1. 取出热点数据。

将主表(pv表)和热点key表(topk_item表)内关联,此时需要使用MapJoin,取到热点商品的日志数据。同时,需要将商品维表(item表)和热点key表(topk item表)内关联,取到热点商品的维表数据。

然后将第一部分数据外关联第二部分数据,因为第二部分数据只有热点商品的维表,数据量比较小,可以使用MapJoin避免长尾。

  • 将上面取到的非热点数据和热点数据通过"union all"合并后即得到完整的日志数据,并且关联了商品信息。

针对倾斜问题,MaxCompute系统也提供了专门的参数用来解决长尾问题,如下所示。

针对倾斜问题,MaxCompute系统也提供了专门的参数用来解决长尾问题,如下所示。

  • 开启功能:

    set odps.sql.skewjoin=true/false

  • 设置倾斜的key及对应的值:

    set odps.sql.skewinfo=skewed src:(skewed key)
    [("skewed value")]

其中skewed key代表倾斜的列,skewed value代表倾斜列上的倾斜值。设置参数的好处很明显,简单方便;坏处是如果倾斜值发生变化需要修改代码,而且一般无法提前知道变化。另外,如果倾斜值比较多也不方便在参数中设置。需要根据实际情况选择拆分代码或者设置参数。

2.2.3. 思考

当大表和大表Joi因为热点值发生倾斜时,虽然可以通过修改代码来解决,但是修改起来很麻烦,代码改动也很大,且影响阅读。而MaxCompute现有的参数设置使用不够灵活,倾斜值多的时候不可能将所有值都列在参数中,且倾斜值可能经常变动。因此,我们还一直在探索和思考,期望有更好的、更智能的解决方案,如生成统计信息,MaxCompute内部根据统计信息来自动生成解决倾斜的代码,避免投入过多的人力。

2.3. Reduce倾斜

2.3.1. 背景

Reduce端负责的是对Map端梳理后的有序key-value键值对进行聚合,即进行Count、Sum、Avg等聚合操作,得到最终聚合的结果。Distinct是MaxCompute SQL中支持的语法,用于对字段去重。比如计算在某个时间段内支付买家数、访问UV等,都是需要用Distinct进行去重的。MaxCompute中Distinct的执行原理是将需要去重的字段以及Group By字段联合作为key将数据分发到Reduce端。

因为Distinct操作,数据无法在Map端的Shuffle阶段根据Group By先做一次聚合操作,以减少传输的数据量,而是将所有的数据都传输到Reduce端,当key的数据分发不均匀时,就会导致Reduce端长尾。

Reduce端产生长尾的主要原因就是key的数据分布不均匀。比如有些Reduce任务Instance处理的数据记录多,有些处理的数据记录少,造成Reduce端长尾。如下几种情况会造成Reduce端长尾:

  • 对同一个表按照维度对不同的列进行Count Distinct操作,造成Map端数据膨胀,从而使得下游的Join和Reduce出现链路上的长尾。
  • Map端直接做聚合时出现key值分布不均匀,造成Reduce端长尾。
  • 动态分区数过多时可能造成小文件过多,从而引起Reduce端长尾。
  • 多个Distinct同时出现在一段SQL代码中时,数据会被分发多次,不仅会造成数据膨胀N倍,还会把长尾现象放大N倍。

2.3.2. 方案

对于上面提到的第二种情况,可以对热点ky进行单独处理,然后通过"Union All'"合并。对于上面提到的第三种情况,可以把符合不同条件的数据放到不同的分区,避免通过多次"Insert Overwrite'"写入表中,特别是分区数比较多时,能够很好地简化代码。但是动态分区也有可能会带来小文件过多的困扰。以最简SQL为例:

那么在最坏的情况下,可能产生K×N个小文件,而过多的小文件会对文件系统造成巨大的管理压力,因此MaxCompute对动态分区的处理是引入额外一级的Reduce Task,把相同的目标分区交由同一个(或少量几个)Reduce Instance来写入,避免小文件过多,并且这个Reduce肯定是最后一个Reduce Task操作。MaxCompute是默认开启这个功能的,也就是将下面参数设置为true。

复制代码
set odps.sql.reshuffle.dynamicpt=true;

设置这个参数引入额外一级的Reduce Task的初衷是为了解决小文件过多的问题,那么如果目标分区数比较少,根本就不会造成小文件过多,这时候默认开启这个功能不仅浪费了计算资源,而且还降低了性能。因此,在此种情况下关闭这个功能:

复制代码
set odps.sql.reshuffle.dynamicpt=false

上面几种情况相对比较简单,这里重点介绍第四种情况。

如图13.11所示这段代码是在7天、30天等时间范围内,分PC端、无线端、所有终端,计算支付买家数和支付商品数,其中支付买家数和支付商品数指标需要去重。因为需要根据日期、终端等多种条件组合对买家和商品进行去重计算,因此有l2个Count Distinct计算。在计算过程中会根据12个组合ky分发数据来统计支付买家数和支付商品数。这样做使得节点运行效率变低。

针对上面的问题,可以先分别进行查询,执行Group By原表粒度+buyer id,计算出PC端、无线端、所有终端以及在7天、30天等统计口径下的buyer id(这里可以理解为买家支付的次数),然后在子查询外Group By原表粒度,当上一步的Count值大于0时,说明这一买家在这个统计口径下有过支付,计入支付买家数,否则不计入。计算支付商品数采用同样的处理方式。最后对支付商品数和支付买家数进行Join操作。

经测试,修改后的运行时间为13min,优化后的效果还是非常明显的。整体运行的LogView日志如图l3.l3所示,可以看到和Count Distinct计算方式相比数据没有膨胀,约为原方式的1/10。

2.3.3. 思考

对Multi Distinct的思考如下:

上述方案中如果出现多个需要去重的指标,那么在把不同指标Join在一起之前,一定要确保指标的粒度是原始表的数据粒度。

  • 比如支付买家数和支付商品数,在子查询中指标粒度分别是:原始表的数据粒度+buyer id和原始表的数据粒度+item id,这时两个指标不是同一数据粒度,所以不能Joi,需要再套一层代码,分别把指标Group By到"原始表的数据粒度",然后再进行Join操作。
  • 修改前的Multi Distinct代码的可读性比较强,代码简洁,便于维护;修改后的代码较为复杂。当出现的Distinct个数不多、表的数据量也不是很大、表的数据分布较均匀时,不使用MutiDistinct的计算效果也是可以接受的。所以,在性能和代码简洁、可维护之间需要根据具体情况进行权衡。另外,这种代码改动还是比较大的,需要投入一定的时间成本,因此可以考虑做成自动化,通过检测代码、优化代码自动生成将会更加方便。
  • 当代码比较臃肿时,也可以将上述子查询落到中间表里,这样数据模型更合理、复用性更强、层次更清晰。当需要去除类似的个Distinct时,也可以查一下是否有更细粒度的表可用,避免重复计算。

目前Reduce端数据倾斜很多是由Count Distinct问题引起的,因此在ETL开发工作中应该予以重视Count Distinct问题,避免数据膨胀。对于一些表的Join阶段的Null值问题,应该对表的数据分布要有清楚的认识,在开发时解决这个问题。

博文参考

《阿里巴巴大数据实战》

相关推荐
1892280486136 分钟前
NY339NY341美光固态闪存NW841NW843
大数据·数据库
DavidSoCool44 分钟前
Elasticsearch 中实现推荐搜索(方案设想)
大数据·elasticsearch·搜索引擎
lilye661 小时前
精益数据分析(108/126):媒体网站用户参与时间优化与分享行为解析
大数据·数据分析·媒体
TDengine (老段)1 小时前
使用 Prometheus 访问 TDengine ---
大数据·数据库·prometheus·时序数据库·iot·tdengine·涛思数据
大千AI助手3 小时前
饼图:数据可视化的“切蛋糕”艺术
大数据·人工智能·信息可视化·数据挖掘·数据分析·pie·饼图
高小秋4 小时前
Hadoop 技术生态体系
大数据·hadoop·分布式
TDengine (老段)4 小时前
TDengine 与开源可视化编程工具 Node-RED 集成
大数据·物联网·开源·node.js·时序数据库·tdengine·涛思数据
isNotNullX4 小时前
据字典是什么?和数据库、数据仓库有什么关系?
大数据·数据库·数据仓库·oracle·数据治理
isNotNullX5 小时前
一文辨析:数据仓库、数据湖、湖仓一体
大数据·数据仓库·数据治理·etl·元数据