ODPS JOB 长尾问题调优

引言

上篇JOB logview 查看问题

提到长尾问题,本文深入探讨下 长尾调优的方法

概述

因为数据分布不均,导致各个节点的工作量不同,整个任务就需要等最慢的节点完成才能完成。这种问题就是长尾问题,是分布式计算里最常见的问题之一,也是典型的疑难杂症。

处理这类问题的思路就是把工作分给多个Worker去执行,而不是一个Worker单独抗下最重的那份工作。本文分享平时工作中遇到的一些典型的长尾问题的场景及其解决方案。

分类

Join长尾

Join时出现某个Key里的数据特别多的情况会出现Join长尾,是因为

解法:

  • 排除两张表都是小表的情况,若两张表里有一张大一张小,可以考虑使用Mapjoin,对小表进行缓存。具体语法和说明见下段文字详细解释。如果是MapReduce作业,可以使用资源表的功能,对小表进行缓存。

  • 但是如果两张表都比较大,就需要先尽量去重。

  • 若还是不能解决,就需要从业务上考虑,为什么会有这样的两个大数据量的Key要做笛卡尔积,直接考虑从业务上进行优化。

MAPJOIN HINT

当一个大表和一个或多个小表做join时,可以使用mapjoin,性能比普通的join要快很多。mapjoin的基本原理是:在小数据量情况下,SQL会将用户指定的小表全部加载到执行join操作的程序的内存中,从而加快join的执行速度。需要注意,使用mapjoin时:

  • left outer join的左表必须是大表;
  • right outer join的右表必须是大表;
  • inner join左表或右表均可以作为大表;
  • full outer join不能使用mapjoin;
  • mapjoin支持小表为子查询;
  • 使用mapjoin时需要引用小表或是子查询时,需要引用别名;
  • 在mapjoin中,可以使用不等值连接或者使用or连接多个条件;
  • 目前MaxCompute 在mapjoin中最多支持指定6张小表,否则报语法错误;
  • 如果使用mapjoin,则所有小表占用的内存总和不得超过512MB。请注意由于MaxCompute 是压缩存储,因此小表在被加载到内存后,数据大小会急剧膨胀。此处的512MB限制是加载到内存后的空间大小;
  • 多个表join时,最左边的两个表不能同时是mapjoin的表。

下面是一个简单的示例:

  1.  select /* + mapjoin(a) */
  2. a.shop_name,
  3. b.customer_id,
  4. b.total_price
  5.  from shop a join sale_detail b
  6. on a.shop_name = b.shop_name;

MaxCompute SQL不支持支持在普通join的on条件中使用不等值表达式、or 逻辑等复杂的join条件,但是在mapjoin中可以进行如上操作,例如:

  1.  select /*+ mapjoin(a) */
  2. a.total_price,
  3. b.total_price
  4.  from shop a join sale_detail b
  5. on a.total_price < b.total_price or a.total_price + b.total_price < 500;

Group By长尾

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

解法 一:可对SQL进行改写,添加随机数,把长Key进行拆分。如SQL:

  1. Select Key,Count(*) As Cnt From TableName Group By Key;

不考虑Combiner,M节点会Shuffle到R上,然后R再做Count操作。对应的执行计划是M->R

但是如果对长尾的Key再做一次工作再分配,就变成:

  1. -- 假设长尾的Key已经找到是KEY001
  2. SELECT a.Key
  3.  , SUM(a.Cnt) AS Cnt
  4. FROM (
  5. SELECT Key
  6.  , COUNT(*) AS Cnt
  7. FROM TableName
  8. GROUP BY Key, 
  9. CASE
  10. WHEN Key = 'KEY001' THEN Hash(Random()) % 50
  11. ELSE 0
  12.  END
  13. ) a
  14. GROUP BY a.Key;

可以看到,这次的执行计划变成了M->R->R。虽然执行的步骤变长了,但是长尾的Key经过了2个步骤的处理,整体的时间消耗可能反而有所减少。注意,若数据的长尾并不严重,用这种方法人为地增加一次R的过程,最终的时间消耗可能反而更大。

解法二:使用通用的优化策略——系统参数,设置

  1. set odps.sql.groupby.skewindata=true

但是通用性的优化策略无法针对具体的业务进行分析,得出的结果不总是最优的。开发人员可以根据实际的数据情况,用更加高效的方法来改写SQL。

Distinct长尾

可以看到,对于Distinct,上述Group By长尾“把长Key进行拆分”的策略已经不生效了。对这种场景,我们可以考虑其他方式解决。

解法:

  1. --原始SQL,不考虑Uid为空
  2. SELECT COUNT(uid) AS Pv
  3.  , COUNT(DISTINCT uid) AS Uv
  4. FROM UserLog;

可以改写成

  1. SELECT SUM(PV) AS Pv
  2.  , COUNT(*) AS UV
  3. FROM (
  4. SELECT COUNT(*) AS Pv
  5.  , uid
  6. FROM UserLog
  7. GROUP BY uid
  8. ) a;

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

动态分区长尾

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

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

解决长尾问题的方法

通过Combiner解决长尾

对于MapRedcuce作业,使用Combiner是一种常见的长尾优化策略。在WordCount的例子里,就已经有提到这种做法。通过Combiner,减少Maper 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部分):

  1. 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%] 
  2. 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%] 
  3. 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(+1backups)[100%] 
  4. 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(+1backups)[100%]

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

通过业务优化解决长尾

虽然前面的优化策略有很多,但是实际上还是有限。有时候碰到长尾问题,还需要从业务角度上想想是否有更好的解决方法,比如:

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

你可能感兴趣的:(ODPS JOB 长尾问题调优)