窥探数据研发中的任务优化策略

本文就目前较为冷门的一些数据任务优化手段做了简单的分析和总结,内容相对比较零散,不会聚焦常用的优化手段(比如小文件合并,数据倾斜等的优化等),只是对一些相对不太常见,但是在研发中又比较重要且有效的方式进行简单的一些沉淀,希望大家批评指正!

1. 前言

在离线数据研发中,随着业务复杂程度不断增加,数据任务逻辑也随之变得复杂,与此同时,数据量越大,对任务性能的要求就越高。因此,调优的话题被研发技术人员广泛关注,特别是被戏称“SQL BOY”的数据研发来说,调优也成了在各大公司招聘常考的话题。然而,很多时候我们依然会感慨:调优其实有时候依然比较难。计算机领域,为了提高运算的性能,几代人不断地榨取CPU和指令计算的每一个细节,都是为了得到更高的性能。但是当面对具体的场景我们发现依然需要耗费大量的时间和精力来优化一些复杂逻辑,有时候发现做了所有的事情依然得不到理想的效果。但就算如此,作为一个专业的技术开发人员,我们依然可以在日常的点滴中,总结构建出调优的一套方法体系,在一定程度上能指导我们开发和遇到问题时的方案选择。本文从日常常用的一些点出发,总结部分优化可选的手段,与大家一起探讨关于数据优化的一些事情。对于一些耳熟能详的部分优化,比如mapjoin、map和reduce参数设置,skewjoin和groupby优化等等,就不做阐述了,本文主要探讨一些技巧、策略和方法。

2.任务优化概述

在实际研发工作中,调优的场景非常多,比如单任务出现卡点瓶颈,一条产出线产出SLA需要提速,一个业务线的整体基线产出延迟等等。对于优化的选择我们也可以总结起来看待,从点、线、面三个方面概括(如图1所示),解决的思路大致分为这几个方向。

而在技术层面,需要注意,敏感性优化和非敏感性优化的差异,理论上,我们期待在一定周期内,优化带来的效果不会随着数据在一定范围的波动产生影响的调优,也就是非敏感调优是比较好的结果,相反,调优结果存在不确定,或者会引起因为调优本身导致的新的瓶颈问题,是研发需要或者尽可能规避的。同时在研发过程中,尽可能保持基本的研发素养,保持敬畏之心,为了求快急切的快速上线,短期来看确实非常快,但是长期来看,我们整体花在维护的成本和时间,远比想象中的要多得多。

图1 优化方向

对于优化采用的手段,大家最多使用到的也是技术手段,除了技术手段之外,对于有些业务场景,比如笔者之前遇到的同异步风险事件数据量极其庞大,单天分区几十TB,甚至PB,这时候无论技术怎么优化,都不能解决耗时以及资源过度占用的问题,这时候就需要深入业务端,从业务侧寻找破题的方法。

因此,我们可以简单地将解决手段分为技术手段业务手段(如图2所示),其中:

  • 技术手段:多为研发层面技术领域通用的一种解法;

  • 业务手段:更多聚焦不同的业务特性,根据具体的业务特性对数据全链路进行采集、整合、计算方面的调整和设计。

 

 

 

图2 优化手段

在实际操作中,对于严选业务而言,大多数调优问题多存在于单点任务或者单链路层面。因此,我们主要针对的是一些复杂逻辑的任务进行优化,相信在其它的业务中应该都是差不多的情况。对于全链路或者全业务和技术的架构升级,很少有机会能遇到,本次我们不重点讨论相关的情况。

其实在优化手段中应该还包括一个非常重要的方向——基础算力能力,该方向依赖于整体科技行业的发展和基建领域的突破得以提升行业整体水平(比如量子计算),也不在我们本次讨论的范畴。

接下来,我们将选取一些比较有效的方案来探讨不同策略的使用,如果有错误的地方,还请大家批评指正。

3.优化案例实践

在实际的生产中,针对任务的卡点问题,我们总结了非常多的优化方式,在任务运行卡点中,比较耗时的操作一个是IO操作,一个是网路通信操作,再有一个就是因为单节点问题导致的——就是常说的某一个节点处理了过多的数据引起的数据倾斜。

针对这些问题我们在IO和网络传输层面做了非常多的技术优化:比如采用了更高效的序列化手段和压缩手段,更高效的RPC通信机制,在很多框架底层通信层面引入了诸如Netty等优秀的通信架构来解决分布式处理的问题。

而在数据应用层,对于数据研发来说,我们不需要过多关注太底层的实现,在单任务层面我们依然有非常多的技术手段来解决任务卡点导致的问题,通过对链路、对方案、对模型等的不断优化,依然能有效的突破瓶颈,那么我们主要针对以下几个点来总结一下在生产中比较有效的一些方式,与大家一起学习和分享。

3.1 数据重分发(Distribute & Rand)首先我们第一个探讨的优化点就是数据重分发(随机),对于数据重分发,大体作用上分为以下几点:
  • 优化小文文件;
  • 数据倾斜的处理;
  • 随机与排序。
在这里我们重点讨论充分发生在小文件优化与数据倾斜层面的作用和注意的点,而这两点多与Rand()随机数有莫大的联系。  数据分发的核心:既对数据做一次shuffle操作,可以理解为按照某种规则进行打散重新发送出去(做了一次MapReduce过程)。使用的一般方式如下:
SELECT column1,.... FROM TABLEX  DISTIRBUTE  BY column1[,...]SELECT column1,.... FROM TABLEX  DISTIRBUTE  BY rand([,seed])[,....]

对于Rand()需要清楚几点:

  • 首先,我们来简单说下关于Rand()随机数的生成,随机数生成是有大学问的,随机生成的数的规律与数学概率有莫大关系,在算法中经常会被问到的一个问题,给定随机生成的N个数,构造等概率事件的发生器。在hive中,Rand()函数随机生成的是0~1的double类型的数字。

  • 其次,Rand(int seed) 函数可以根据种子参数,构造出一个稳定的随机值,也就是说,加上种子参数,得到的函数结果是稳定的。

  • 再次,在hive中,随机函数多与pmod()、mod()、floor()、ceil()函数放在一起使用。一般的,可以构造任意范围内随机整数来实现不同业务场景的需求。需要清楚rand()可以构造任意区间的数据范围(例如:可以使用归一化手段构造一个在[a,b]之间的任意数,a、b可以是整数,也可以是小数):

    • 取0~N之间的整数:floor(rand())*N / ceil(rand())*N 或者+1可以取 1—N之间的整数。

    • 取0.8~0.85之间的小数:1-(rand()*0.05+(1-0.85))

3.1.1 数据重分发的作用

对于数据重分发,我们主要是用来对处理数据结果进行小文件合并以及对数据处理中的倾斜问题进行优化。在大多数的处理中,我们习惯于使用Distribute by Rand() *N 的方式,其实这个方式可能存在问题,在处理类似问题时候,我们可以选择基于seed种子的Rand函数,来维持随机数的稳定性。这里需要知晓,distribute by 实际上做了一次shuffle的分发,默认是按照给定key进行的hash操作(可以理解为一次repartion重新分区),这里面是可以进行定制分区逻辑的,可以通过重写hive当中partition的接口,实现不同策略的重分发。

  • 处理小文件合并

    • 使用方式一:指定固定分发列,做一次shuffle的merge操作,DEMO如下:

SELECT column1, column2,column.... FROM TABLEX WHERE ds = '${bizdate}'DISTRIBUTE BY '${bizdate}',columns1....
    • 使用方式二:指定给定的文件数,这里要用到rand()函数了,一般有两种写法:
      • 第一种写法(上文讨论过,这种写法在一定情况下会出现数据问题):
SELECT column1, column2,column.... FROM TABLEX WHERE ds = '${bizdate}'DISTRIBUTE BY FLOOR(RAND()*N)/CEIL(RAND()*N)
    • 第二种写法(加随机种子,产生稳定的随机序列):
SELECT column1,column2,column.... FROM (    SELECT column1, column2,column...., FLOOR(RAND(seed)*N) AS rep_partion FROM      TABLEX      WHERE ds = '${bizdate}')DISTRIBUTE BY rep_partion
  • 处理JOIN中的倾斜:与上述逻辑同理,主要是借助一次分发,使得需要shuffle的数据能在一个节点进行数据处理。

3.2 数据膨胀(Explode)在join过程中,我们之前提到了一种基于BLOOMFILTER算法的优化方法。在某些情况下,当join的表中出现一个表的量级很大,另外一个表无法mapjoin切热键key在概率分布上呈现随机性,这个时候就可以在一定程度上,对较小表中的join key进行一定程度的膨胀,由于join的发生是在reduce阶段,因此可以构造出稳定的多条主键,在不同的reduce中对数据进行jion操作,进而一定程度上解决join倾斜带来的问题。基本原理如下图所示:

图3 数据膨胀逻辑

一个小的例子如下:

SELECT  A.*FROM (  SELECT * FROM a) ALEFT JOIN(  SELECT  CONCAT(C.rand_num,'_',D.key) AS key  FROM  (    SELECT  rand_num    FROM dual LATERAL VIEW explode(ARRAY(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) rand_num_list AS rand_num  )C)BON CONCAT(CAST(9*RADN() AS BIGINT), '_', A.key) = B.keyWHERE A.ds = '${bizdate}';
目前,膨胀函数已经有开发出来有现成的UDTF函数来支持,可以支撑任意膨胀量级的数据进行膨胀。只需要构造膨胀区间对应的随机函数即可,还是需要用到Rand()函数来实现。
数据膨胀方式带来的问题:在解决了数据倾斜重新打散的问题之后,在计算层面会增加一定的数据计算量。此外,如果能基于分桶进行二次索引分片,也可以在引擎侧考虑基于该方向的自适应倾斜优化。3.3 数据分桶(Bucket)在数据量比较大的情况下,单表数据做分区会存在下游使用效率上的限制,而数据在某些列上(或者构造业务列)存在高度聚集,或者存在可以优化提升的巨大空间,在此时,我们就可以对列进行散列分桶,在分区的基础上进行桶表的设计,桶上可以对应索引向量,将极大的提升数据使用上的效率。在数据随机抽样、JOIN场景中,也会极大的提升整个数据的计算性能和效率。在hive中,该功能默认是关闭的,需要set hive.enforce.bucketing=true打开支持,需要注意一般而言,桶的个数将与一次作业中对应的reduce数量一致。       其实,基于分桶的逻辑,在引擎侧可以做更多的优化(比如引擎侧可以优化分桶存储的策略)。在join中,根据索引进行join层面的动态优化,在超大数据join过程中,基于桶进行单位数据的本地优化等等都是可以做非常多的优化操作的,由于在目前的业务场景中,较少用到数据分桶,因此这里不做更深入的拓展,详细的可以自行百度,查看关于桶表的使用,更进一步,合理分桶,加上排序后的索引,能高效优化单表查询使用的效率。3.4 并发与并行控制在计算机入门的时候,我们就经常听到并发与并行,线程与进程等概念。而在数据研发中,我们发现,其实对于整个作业来说,同样遵循类似的调优规则。一般的,一个作业最大的map数是9999,reduce数最大是1000。虽然可以提高单个任务吞吐量,但是会消耗更长的时间和资源调度上的等待。另一方面,当完成一个同类作业,往往需要多个任务进行,如果任务下面可以多个作业并行处理,单个作业也能够并发执行,那么就能够更大程度地榨取整个集群的资源,从而达到突破计算瓶颈和上线的目的。目前在开源HADOOP体系中,我们没有脚本模式来支持灵活的任务自动分配和调度,但是可以采用SHELL/PYTHON脚本+SQL的方式来实现这一目的,其实借助猛犸调度在一定范围内也能达到同样的效果。3.5 多路输出与物化(Read Once Output More)这个部分我们主要谈谈HIVE(spark)的CTE写法(WITH...AS...)以及From语法的应用。这两个语法,在日常开发稍微复杂的任务时候,可以大大清晰整个复杂SQL的逻辑,同时,在多路读写中,通过物化的方式还能在一定程度上加速作业的运行。
  • CTE(with.... as ...)使用
    • 基本使用非常简单,cte的语法主要是为了提高代码的可读性,虽然在整个性能的优化上未必达到很好的效果,但是在一定程度上,能大大提高任务的逻辑清晰度。很多时候,我们在多个逻辑过程中,通过临时表的方式进行任务的串行,使用with...as...能达到类似的效果。同时with...as...可以深层嵌套,因此是比较好的一种选择方式。无论是线上任务还是视图,都可以使用CTE的写法——目前比较遗憾的是HIVE的CTE目前不支持递归。
WITH      cte_name AS    (        cte_query    )    [,cte_name2  AS      (     cte_query2     )    ,……]
    • 物化设置

      由于with...as...等同于一个SQL片段,下文中会多次引用该片段的别名,相当于视图的味道。所以,这里面使用是一个虚拟的概念,实际上只是逻辑生效,实际运行是则是翻译成实际的MR逻辑去执行,如果下游引用该SQL片段较多,这时候MR执行会多次扫描原始数据,执行多次相同的MR操作逻辑,此时,就可以在第一次执行中来物化CTE写法中定义的SQL片段,从而达到优化的目的。在hive之前的版本中,该功能是默认关闭的,可以通过下面参数来开启,在新的hive版本中,该功能是默认开启,但是默认引用次数是3次。

hive.optimize.cte.materialize.threshold
  • FROM使用(一读多写)FROM也是本人在实际研发中遇到多路输出时采用比较多的一种手段之一。当有多个不同的分区,或者多个不同的目标输出,或者有多个不同的子逻辑的过程中,可以将主逻辑全部开发完成,然后再进行多路输出。多路输出操作的使用限制如下:
    • 单条 multi insert语句中最多可以写255路输出。超过255路,会上报语法错误。
    • 单条 multi insert语句中,对于分区表,同一个目标分区不允许出现多次。
    • 单条 multi insert语句中,对于非分区表,该表不能出现多次。
FROM <from_statement>INSERT OVERWRITE | INTO TABLE <table_name1> [PARTITION (<pt_spec1>)]<select_statement1>INSERT OVERWRITE | INTO TABLE <table_name2> [PARTITION (<pt_spec2>)]<select_statement2>...;
4.思考&总结
在数据研发领域,数据的技术手段无论多么丰富,平台发展何等完善,都不能说能解决业务的所有问题。一定是先有业务,才会有对应的问题。在面对大数据量,高时效性,高复杂计算的场景,我们需要结合业务的特性,模型的改造,链路的设计,甚至打破常规等方式来产出不同的方案。在另一个方面,数据研发的工作也远远不是单点问题的解决和兜底,相反需要各方的配合与共同的智慧。
posted @ 2023-03-27 16:25  MRO物料采购服务  阅读(70)  评论(0编辑  收藏  举报