大数据之路——计算管理

降低计算资源的消耗,提高任务执行的性能,提升任务产出的时间

13.1 系统优化

通过分析计算系统的数据运行情况,判断计算系统对内存、CPU、Instance 个数这些资源的运用是否合理(指计算时间更短,使用的资源更少)

Hadoop 等分布式计算系统的根据 输入的数据量进行静态评估;对于普通的 Map 任务,评估一般符合预期;而对于 Reduce 任务,其输入来自于 Map 的输出,但进行评估是也是根据 Map 任务的输出进行,这样评估的结果经常和实际需要的资源数相差很大;在任务稳定的情况下,基于任务的历史执行情况进行评估,即采用 HBO(History - Based Optimizer,基于历史的优化器);

13.1.1 HBO History-Based Optimizer

根据任务的历史执行情况,为任务分配更合理的资源,包括内存、CPU、Instance (指操作系统中一系列的进程以及为这些进程所分配的内存块)个数

HBO 是对集群资源分配的一种优化,概括起来就是:任务执行历史 + 集群状态信息 + 优化原则 → 更优的执行配置

产生背景

  • Task的个数按照 Input_Table_Size/Map_Split_Size进行简单的分配

  • 大部分的 Instance 处理的数据量远远没有达到预期,即一个 Instance 处理 256 MB 的数据;

  • 有些 Instance 处理的数据量很大,很容易导致任务长尾多个 Instance 同时计算数据,但由于各个 Instance 上的数据量分布不均匀等情况,使得有的 Instance 先计算完,而有的要等执行很久,计算完成时间相差很大);

  • 总结:默认的 Instance 算法下,小任务存在资源浪费,而大任务却资源不足;

HBO的提出:

  • 发现系统中存在大量的周期性调度的脚本,且数据一般比较稳定
  • 可以根据任务的执行历史为其分配更加合理的计算资源,自适应调节系统参数

HBO原理:

  • 前提:最近 7 天内,任务代码没有发生变更,且任务运行 4 次;

  • Instance 分配逻辑:基础资源估算值 +加权资源估算值

    • 基础资源分配逻辑:Map:考虑每个Map能处理的数据量和用户提交任务的输入数据,并采用分层的方式,提供平均每个Map能处理的数据量。Reduce:使用最近7天Reduce对应Map的平均输出数据作为reduce的输入数据
    • 加权资源估算:通过比较Map在最近一段时间内的平均处理速度和系统设定预期的比较,如果平均的处理速度小于期望的,按照同等比例对基础资源进行加权
  • CPU和内均的分配逻辑和Instance类似

HBO的优点

  1. 提高CPU利用率

  2. 提高内存利用率

  3. 提高Instance并发数

  4. 降低执行时长

HBO的改进

HBO是基于执行历史来设置计划的,但是在特定场合(双11流量暴增)下要增加根据数据量动态调整Instance数的功能,主要依据Map的数据量增长调整。

13.1.2 CBO Cost-Based Optimizer

基于代价的优化器,Oracle中的CBO根据收集到的表、分区、索引等统计信息,计算每种执行方式的代价(Cost),进而选择其中最优的(也就是代价最小的)执行方式;但是对表和列上统计信息的收集也是有代价的。消耗大量资源收集到的统计信息,其利用率却很低。MaxCompute采用各种抽样统计算法,通过较少的资源获得大量的统计信息,基于先进的优化模型,具备了完善的 CBO 能力;

优化器原理

优化器引入了Volcano模型,是基于代价的优化器,并引入了重新排序Join和自动Mapjoin优化规则,会尽最大的搜索宽度来获取最优计划。有多个模块:Meta Manager(元数据)、Statistics(统计信息)、Rule Set(优化规则集)、Volcano Planner Core(核心计划器)
请添加图片描述

  • Meta Manger:主要提供元数据信息,根据元数据的信息进行优化,比如表分区裁剪,表是否是分区表、有哪些列。Count值的统计信息直接在这提供

  • Statistics:提供准确的统计信息,如列的Distinct值、TopN值。比如会根据join的输入数据量来选择是Hash Join还是Merge Join

  • Rule Set:根据不同情况选择不同的优化点,然后根据Cost Model选择启动哪些优化。比如工程合并规则(Project Merge Rule,临近的Project合并成一个)和过滤条件下推(Filter Push Down,过滤条件下沉,先过滤再计算)

  • 规则分为:Substitute被认为是优化了肯定好的规则;Explore Rule:优化后需要考虑多种优化结果;Build Rule:可以认为优化后的结果不能再次使用规则再进行优化;

  • Volcano Planner Core:是优化器的灵魂,将所有信息统一起来处理,根据代价模型(根据不同操作符Join、Project等计算不同代价,然后计算出最小代价的计划,目前由行数、I/O开销、CPU开销衡量每个操作符代价

请添加图片描述

  • 工作原理:将需要输入给 Planner 的数据,用 Compiler 解析为一个 “计划树”,简称 “RelNode 树”,树的每个节点简称 RelNode;

  • Volcanno Planner创建,将Planner在优化过程中要用到的信息传递给执行计划器。比如规则集、Meta Provider、RowCount值计算和每个RelNode的代价,提供必要信息。

  • Planner优化

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

  • 规则应用优化每个规则队列中的规则;从规则队列(Rule Queue)中弹出(Pop) 一个已经匹配成功的规则进行优化;如果优化成功后,会产生至少一个新的 RelNode;使用新的 RelNode 再次进行注册以及规则匹配操作,再把匹配的规则加入到规则应用的规则队列中,然后接着下次规则应用; Planner 会一直应用所有的规则,,直到不会有新的规则匹配到,则优化结束,得到一个最优计划;
    产出:新的 “RelNode 树”,也就是新的 RelNode 数据节点集合;

  • 代价计算, 每当规则应用之后,如果规则优化成功,则会产生新的 RelNode,在新的 RelNode 注册过程中,有一个步骤是计算 RelNode 的代价;如果不存在代价,或者 Child 的代价还没有估算(默认是最大值),则忽略;如果存在代价,则会将本身的代价和 Child (即输入的所有 RelNode)的代价进行累加;若累加结果小于 Best(期望值),则认为优化后的 RelNode 是当前最优的;并且会对其 Parent 进行递归估算代价,即传播代价计算(Propagate Calculate Cost

优化器新特性

一、自动排序Join,将Join的所有不同输入进行一个全排列,然后找到代价最小的一个排序。可以打乱用户书写SQL语句的Join顺序

二、自动MapJoin,目前常用的是Merge Join 和 MapJoin,小数据量Mapjoin性能更好,之前是通过Hint方式来指定是否用MapJoin,现在能充分利用代价模型进行估算。

优化器使用

  • 规则白名单 —— odps.optimizer.cbo.rule.filter.white
  • 规则黑名单 —— odps.optimizer.cbo.rule.filter.black
set odps.optimizer.cbo.rule.filter.white = pojr, hj ;
表示:使用重排序 Join 规则和自动 MapJoin 规则;
重排序规则 Join = pojr 、自动 MapJoin = hj ;

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

一、UDF。由于同用户书写的函数含义不一样,不可以一概而论,所以优化器不会任意下推带有用户意图的函数

二、不确定函数。 优化器不会任意下推不确定函数;(如,sample 函数)

SELECT *
FROM t1
JOIN t2
ON t1.c1 = t2.d1
WHERE sample( 4, 1) = true ; -- sample 函数在 Join 之后执行,而不会直接在 TableScan 后执行;

优化
SELECT *
FROM
(
    SELECT *
    FROM t1
    WHERE sample(4, 1) = true
) t1
JOIN t2
ON t1.c1 = t2.d1 ;

三、隐式类型转换,避免Join Key存在隐式类型转换。如String = Bigint,会转换为 ToDouble(String) = toDouble(Bigint)。可能导致转换失败报错或者结果与期望不一致

13.2 任务优化

请添加图片描述

  • SQL / MR 作业一般会生成 MapReduce 任务,在 MaxCompute 中则会生成 MaxCompute Instance,通过唯一 ID 进行标识
  • Fuxi Job:对于 MaxCompute Instance,会生成一个或多个由 Fuxi Task 组成的有向无环图
  • Fuxi Task(任务类型):主要包含三种类型,分别是 Map、Reduce、Join,类似于 Hive 中 Task 的概念;
  • Fuxi Instance:真正的计算单元,和 Hive 中的概念类似,一般和槽位(slot)对应;

13.2.1 Map倾斜及优化

Map 倾斜:数据在 Map Instance 上的分布不均匀,即有的 Map Instance 上分布的数据量很大,有的 Map Instance 分布的数据量很少;

后果(Map 端长尾现象):有的 Map Instance 的资源浪费,有的 Map Instance 的资源不够,计算所用时间很长,导致最终整个 Map 端的计算时间变长;

请添加图片描述

背景:

  1. Map 端是 MR 任务的起始阶段,Map 端的主要功能是从磁盘中将数据读入内存

  2. 输入分片。每个输入分片会让一个 Map Instance 来处理;默认情况下,一个 Pangu 文件系统的一个文件快的大小(默认为 256 MB)为一个分片(Fuxi Task);

  3. Map 读数据阶段。 调节 Map Instance 的个数:如,set odps.mapper.split.size = 256;

  4. Map Instance 输出结果会暂时放在一个环形内存缓冲区;( 当该缓冲区快要溢出时会在本地文件系统中创建一个溢出文件,即 Write Dump)

  5. 写入磁盘。 写入磁盘之前,线程首先根据 Reduce Instance 的个数划分分区,数据将会根据 Key 值 Hash 到不同的分区上,一个 Reduce Instance 对应一个分区的数据;Map 端也会做部分聚合操作,以减少输入 Reduce 端的数据量;

问题

在 Map 端读数据时,由于读入数据的文件大小分布不均匀,因此会导致有些 Map Instance 读取并且处理的数据特别多,而有些 Map Instance 处理的数据特别少,造成 Map 端长尾;一般有两种情况可能会导致 Map 端长尾:

  1. 上游表文件(数据仓库中的维表和事实表)的大小特别不均匀,并且小文件特别多,导致当前表 Map 端读取的数据分布不均匀,引起长尾;

  2. Map 端做聚合时,由于某些 Map Instance 读取文件的某个值特别多而引起长尾,主要指 Count Distinct 操作;

方案

  • 情况一:上游表文件的大小特别不均匀,并且小文件特别多,导致当前表 Map 端读取的数据分布不均匀,引起长尾;优化方案:合并上游的小文件,同时调节本节点的小文件的参数

    set odps.sql.mapper.merge.limit.size=64
    作用:设定控制文件被合并的最大阈值,单位M,默认64M。
    set odps.sql.mapper.split.size=256
    作用:设定一个map的最大数据输入量,单位M,默认256M。
    
  • 情况二:Map 端做聚合时,由于某些 Map Instance 读取文件的某个值特别多而引起长尾,主要指 Count Distinct 操作。解决方法:使用 “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 = '${bizdate}'
    AND is_enable = 'Y'
) b
ON 1 = 1
WHERE a.pre_page rlike b.page_type_rule;

思考

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

13.2.2 Join倾斜及优化

背景

Join 的功能: 在 Join 执行阶段会将 Join Key 相同的数据分发到同一个执行 Instance 上处理;如果某个 Key 上的数据量比较大,则会导致该 Instance 执行时间较长(长尾情况);

长尾表现:在执行日志中该 Join Task 的大部分 Instance 都已执行完成,但少数几个 Instance 一致处于执行中;

Join的实现原理

 select u.name, o.orderid from order o join user u on o.uid = u.uid;

sql语句中on后面的字段就是key,在map阶段的输出(value)中为不同表的数据打上tag标记在reduce阶段根据tag判断数据来源。MapReduce的过程如下(这里只是说明最基本的Join的实现,还有其他的实现方式)

请添加图片描述

倾斜场景

  1. Join 的某输入比较小,可以采用 MapJoin,避免分发引起长尾;

  2. Join 的每路输入都较大,且长尾是空值导致的。可以将空值处理成随机值,避免聚集;

  3. Join 的每路输入都较大,且长尾是热点值导致的。可以对热点值和非热点值分别进行处理,再合并数据;

方案

  • 方案一:采用 MapJoin;将 Join 操作提前到 Map 端执行,将小表读入内存,顺序扫描大表完成 Join;优点:可以避免因为分发 Key 不均匀导致数据倾斜;弊端:MapJoin 的使用有限制,必须是 Join 中的从表比较小才可用;

  • 方案二: 把空值处理成随机值。

    SELECT   ...
    FROM table_a
    LEFT OUTER JOIN table_b
    ON  coalesce(table_a.key, rand()*9999) = table_b.key    --当 key 值为空值时用随机值代替
    
  • 方案三:把主表数据用热点key切分成热点数据和非热点数据这两部,再合并

-- 取出热点key
INSERT OVERWRITE TABLE topk_item
SELECT item_id
FROM
(
    SELECT  item_id,count(1) as cnt
    FROM pv        --pv表
    WHERE ds = '${bizdate}'
    AND url_type = 'ipv'
    AND item_id is not null
    GROUP BY item_id
) a
WHERE cnt >= 50000
-- 非热点数据
SELECT ...
FROM
(
  SELECT *
  FROM  item        --商品表
  WHERE ds = '${bizdate}'
) a
RIGHT OUTER JOIN
(
  SELECT /*+MAPJOIN*/
  b2.*
  FROM
  (
    SELECT item_id
    FROM topk_item        --热点表
    WHERE ds = '${bizdate}'
  ) b1
  RIGHT OUTER JOIN
  (
    SELECT *
    FROM pv            --PV表
    WHERE ds = '${bizdate}'
    AND url_type = 'ipv'
  ) b2
  ON b1.item_id = coalesce(b2.item_id, concat("tbcdm", rand())
  WHERE b1.item_id is null
) 1
ON a.item_id = coalesce(1.item_id, concat("tbcdm", rand())
-- 取出热点数据
SELECT    /*+MAPJOIN*/   ...
FROM
(
    SELECT    /*+MAPJOIN*/
    b2.*
    FROM
    (
      SELECT item_id
      FROM topk_item
      WHERE ds = '${bizdate}'
    ) b1
    JOIN
    (
      SELECT *
      FROM pv        --pv 表
      WHERE ds = '${bizdate}'
      AND url_type = 'ipv'
      AND  item_id is not null
    ) b2
    ON (b1.item_id = b2.item_id)
) 1
LEFT OUTER JOIN
(
    SELECT  /*+MAPJOIN*/
    a2.*
    FROM
    (
      SELECT item_id
      FROM topk_item
      WHERE ds = '${bizdate}'
    ) a1
    JOIN
    (
      SELECT *
      FROM item        --商品表
      WHERE ds = '${bizdate}'
    ) a2
    ON (a1.item_id = a2.item_id)
) a
ON  a.item_id = 1.item_id
  • 方案四:专门的参数解决长尾问题
set odps.sql.skewjoin = true / false
set odps.sql.skewinfo = skewed_src: (skewed_key)    --设置倾斜的 key 值(skewed_ey)

13.2.3 Reduce 倾斜及优化

背景

Reduce 端对 Map 端梳理后的有序 key-value 键值对进行聚合,即 进行 Count、Sum、Avg 等聚合操作,得到最终聚合的结果

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

倾斜场景

  1. 对同一个表按照维度对不同的列进行 Count Distinct 操作,造成 Map 端数据膨胀,从而使下游的 Join 和 Reduce 出现链路上的长尾;

  2. Map 端直接做聚合时,出现 key 值分布不均匀,造成 Reduce 端长尾;

  3. 动态分区数过多时,可能造成小文件过多,从而引起 Reduce 端长尾;

  4. 多个 Distinct 同时出现在一端 SQL 代码中时,数据会被分发多次,不仅会造成数据膨胀 N 倍,还会把长尾现象放大 N 倍;

方案

  • 场景二:Map 端直接做聚合时,出现 key 值分布不均匀,造成 Reduce 端长尾.解决方案对热点 key 单独处理,然后通过 “Union All” 合并;(具体操作与 “Join 因为热点值导致长尾” 的处理方式一样)

  • 场景三:动态分区数过多时,可能造成小文件过多,从而引起的 Reduce 端长尾。解决方案:把符合不同条件的数据放到不同的分区,避免通过多次 “Insert Overwrite” 写入表中,特别是分区数比较多时,能够很多的简化代码; 对动态分区的处理是引入额外一级的Reduce Task,把相同的目标分区交由同一个Reduce Instance来写入。

  • 场景四:多个 Distinct 同时出现在一端 SQL 代码中时,数据会被分发多次,不仅会造成数据膨胀 N 倍,还会把长尾现象放大 N 倍;比如因为需要根据日期、终端等多种条件组合对买家和商品进行去重,因此需要有 12 个 Count Distinct 计算;解决方法:先分别进行查询,执行 Group By 原表粒度 + buyer_id,计算出 PC 端、无线端、所有终端以及 7 天、30 天等统计口径下的 buyer_id(这里可以理解为买家支付的次数);
    在子查询外,Group By 原表粒度:当上一步的 Count 值大于 0 时,说明这一买家在这个统一口径下油锅支付,计入支付买家数,否则不计入;

SELECT    t2.seller_id,t2.price_seg_id
 ,SUM(case when pay_ord_byr_cnt_1w_001 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1w_001    --最近 7 天支付买家数
 ,SUM(case when pay_ord_byr_cnt_1w_002 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1w_002    --最近 7 天 PC 端支付买家数
 ,SUM(case when pay_ord_byr_cnt_1w_003 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1w_003    --最近 7 天无线端支付买家数
 ,SUM(case when pay_ord_byr_cnt_1m_002 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1m_002    --最近 30 天支付买家数
 ,SUM(case when pay_ord_byr_cnt_1m_003 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1m_003    --最近 30 天 PC 端支付买家数
 ,SUM(case when pay_ord_byr_cnt_1m_004 > 0 then 1 else 0 end) AS pay_ord_byr_cnt_1m_004    --最近 30 天无线端支付买家数
FROM
(
  SELECT    a1.seller_id,a2.price_seg_id,buyer_id
  ,COUNT(buyer_id) AS pay_ord_byr_cnt_1m_002    --最近 30 天支付买家数
  ,COUNT(CASE WHEN is_wireless = 'N' THEN buyer_id ELSE NULL END) AS pay_ord_byr_cnt_1m_003    --最近 30 天 PC 端支付买家数
  ,COUNT(CASE WHEN is_wireless = 'Y' THEN buyer_id ELSE NULL END) AS pay_ord_byr_cnt_1m_004    --最近 30 天无线端支付买家数
  ,COUNT(case when a1.ds >= To_CHAR(DATEADD(TO_DATE('${bizdate}', 'yyyymmdd'), -6, 'dd'), 'yyyymmdd') then buyer_id else null end) AS pay_ord_byr_cnt_1w_001    --最近 7 天支付买家数
  ,COUNT(case when a1.ds >= To_CHAR(DATEADD(TO_DATE('${bizdate}', 'yyyymmdd'), -6, 'dd'), 'yyyymmdd') and is_wireless = 'N' THEN buyer_id ELSE NULL END) AS pay_ord_byr_cnt_1w_002    --最近 7 天 PC 端支付买家数
  ,COUNT(case when a1.ds >= To_CHAR(DATEADD(TO_DATE('${bizdate}', 'yyyymmdd'), -6, 'dd'), 'yyyymmdd') and is_wireless = 'Y' THEN buyer_id ELSE NULL END) AS pay_ord_byr_cnt_1w_003    --最近 7 天无线端支付买家数
  FROM
  (
    select *
    from    table_pay        --支付表
   ) a1
   JOIN 
   ( 
     SELECT item_id,price_seg_id
     FROM    tag_itm        --商品 tag 表
     WHERE  ds = '${bizdate}'
   ) a2
   ON ( a1.item_id = a2.item_id )
   GROUP BY a1.seller_id          --原表粒度
            ,a2.price_seg_id        --原表粒度
            ,buyer_id
) t2
GROUP BY t2.seller_id               --原表粒度
,t2.price_seg_id        --原表粒度

上述方案中如果出现多个需要去重的指标,那么在把不同指标 Join 在一起之前,一定要确保指标的粒度是原始表的数据粒度;如,支付买家数和支付商品数,在子查询中指标粒度分别是:原始表的数据粒度 + buyer_id 和原始表的数据粒度 + item_id,这时两个指标不是同一数据粒度,所以不能 Join,需要再套一层代码,分别把指标 Group By 到 “原始表的数据粒度”,然后再进行 Join 操作

两个要注意的问题

  1. 目前 Reduce 端数据倾斜很多是由 Count Distinct 问题引起的,因此,在 ETL 开发工作中应该予以重视 Count Distinct 问题,避免数据膨胀;

  2. 对于一些表的 Join 阶段的 NULL 值问题,应该对表的数据分布要有清楚的认识,在开发时解决这个问题;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值