除了Join之外还有其它计算长尾现象产生,本文将为您介绍典型的长尾问题的场景及其解决方案。

长尾问题是分布式计算中最常见的问题之一。造成长尾问题的主要原因是数据分布不均,导致各个节点的工作量不同,整个任务需要等最慢的节点完成才能结束。

为了避免一个Worker单独运行大量的工作,需要把工作分给多个Worker去执行。

Group By 长尾

问题原因

Group By Key出现长尾,是因为某个Key内的计算量特别大。

解决办法

您可以通过以下两种方法解决:
  • 对SQL进行改写,添加随机数,把长Key进行拆分。举例如下。
    SELECT Key,COUNT(*) AS Cnt FROM TableName GROUP BY Key;

    不考虑Combiner,M节点会Shuffle到R上,然后R再做Count操作。对应的执行计划是 M > R。但是如果对长尾的Key再做一次工作再分配,就变成如下语句。

    -- 假设长尾的Key已经找到是KEY001。
    SELECT a.Key
      , SUM(a.Cnt) AS Cnt
    FROM (
      SELECT Key
        , COUNT(*) AS Cnt
    FROM TableName
    GROUP BY Key, 
      CASE 
        WHEN Key = 'KEY001' THEN Hash(Random()) % 50
        ELSE 0
       END
    ) a
    GROUP BY a.Key;

    由上可见,这次的执行计划变成了M>R>R。虽然执行的步骤变长了,但是长尾的Key经过2个步骤的处理,整体的时间消耗可能反而有所减少。

    说明 若数据的长尾并不严重,用这种方法人为地增加一次R的过程,最终的时间消耗可能反而更大。
  • 通过设置系统参数优化长尾问题。
    set odps.sql.groupby.skewindata=true。

    此设置为通用性的优化策略,无法针对具体的业务进行分析,得出的结果不一定是最优的。您可以根据实际的数据情况,用更加高效的方法来改写SQL。

Distinct 长尾

对于Distinct,把长Key进行拆分的策略已经不生效了。对这种场景,您可以考虑通过其它方式解决。

解决办法

--原始SQL,不考虑Uid为空。
SELECT COUNT(uid) AS Pv
    , COUNT(DISTINCT uid) AS Uv
FROM UserLog;
可以改写成如下语句。
SELECT SUM(PV) AS Pv
    , COUNT(*) AS UV
FROM (
    SELECT COUNT(*) AS Pv
      , uid
    FROM UserLog
    GROUP BY uid
) a;

该方法是把Distinct改成了普通的Count,这样的计算压力不会落到同一个Reducer上。而且这样改写后,既能支持前面提到的Group By优化,系统又能做Combiner,性能会有较大的提升。

动态分区长尾

问题原因

  • 动态分区功能为了整理小文件,会在最后启用一个Reduce,对数据进行整理,所以如果使用动态分区写入数据时有倾斜,就会发生长尾。
  • 一般情况下,滥用动态分区的功能也是产生这类长尾的一个常见原因。

解决办法

若已经确定需要把数据写入某个具体分区,则可以在Insert的时候指定需要写入的分区,而不是使用动态分区。

通过 Combiner 解决长尾

对于MapRedcuce作业,使用Combine是一种常见的长尾优化策略。通过Combiner,减少Mapper Shuffle往Reducer的数据,可以大大减少网络传输的开销。对于MaxCompute SQL,这种优化会由系统自动完成。

说明 Combiner只是Map端的优化,需要保证执行Combiner的结果是一样的。以WordCount为例,传2个 (KEY,1) 和传1个(KEY,2) 的结果是一样的。但是在做平均值时,便不能直接在Combiner里把 (KEY,1) (KEY,2) 合并成(KEY,1.5)

通过系统优化解决长尾

针对长尾这种场景,除了前面提到的Local Combiner,MaxCompute系统本身还做了一些优化。例如,在运行任务的时候,日志里突然打出如下的内容(+N backups 部分)。

M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047[100%] 
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047[100%] 
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047(+1 backups)[100%] 
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047(+1 backups)[100%]

可以看到1047个Reducer,有1046个已经完成了,但是最后一个一直没完成。系统识别出这种情况后,自动启动了一个新的Reducer,运行一样的数据,然后取运行结束较早的数据归并到最后的结果集里。

通过业务优化解决长尾

虽然前面的优化策略有很多,但仍然不能解决所有问题。有时碰到的长尾问题,还需要从业务角度上去考虑是否有更好的解决方法。

  • 实际数据可能包含非常多的噪音。例如,需要根据访问者的ID进行计算,看每个用户的访问记录的行为。需要先去掉爬虫的数据(现在的爬虫已越来越难识别),否则爬虫数据很容易在计算时长尾。类似的情况还有根据xxid进行关联的时候,需要考虑这个关联字段是否存在为空的情况。
  • 一些业务特殊情况。例如:ISV的操作记录,在数据量、行为方式上会和普通个人有很大的区别。那么可以考虑针对大客户,使用特殊的分析方式进行单独处理。
  • 数据分布不均匀的情况下,不要使用常量字段做Distribute by字段来实现全排序。