老工对Hive的调优理解

老工在职场多年,从事过海量(PB 级)数据的关系型数据库数据处理工作,后由于数据平台升级的要求,将数据迁移到Hadoop集群,做了多年的数据研发和数据产品的研发工作,从业务理解、数据模型构建、数据采集、数据清洗,到数据产品前端/服务端的研发都做过,基本涵盖了数据的生命周期。对于Hive 调优,老工自有一番理解。下面将从一个过度优化的案例说起。

从一个过度优化案例说起

某天,老工在对小白的代码进行代码评审,发现了一个去重计数的代码案例,下面具体介绍。

【案例2.10】 去重计数案例。

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重

这是简单统计年龄的枚举值个数,为什么不用distinct?

【案例2.11】 简化的去重计数。

hive 去掉星号等特殊字符 hive怎么去重_数据_02

小白认为:案例2.10的代码在数据量特别大的情况下能够有效避免Reduce的数据倾斜,案例2.10可能会比案例2.11效率高。

我们先不管数据量特别大这个问题,就当前的业务和环境下,案例2.11一定会比案例2.10的效率高,原因有以下几点:

(1)进行去重的列是s_age列,它的业务含义表示年龄。既然是年龄,说明它的可枚举值非常有限,如果转化成MapReduce来解释的话,在Map阶段,每个Map会对s_age去重。由于s_age枚举值有限,因而每个Map得到的s_age也有限,最终得到reduce的数据量也就是map数量*s_age枚举值的个数。

假如执行案例2.10的代码Map的数量有100个,s_age的最大枚举值有100个,每个Map过滤后的数据都含有s_age的所有枚举值,且s_age是int型占4个字节,那么传输到Reduce的数据量就是10000条记录,总数据量是40KB,这么小的数据量,不需要避免数据倾斜。

(2)案例2.11中,distinct的命令会在内存中构建一个hashtable,查找去重的时间复杂度是O(1);案例2.10中,group by在不同版本间变动比较大,有的版本会用构建hashtable的形式去重,有的版本会通过排序的方式,排序最优时间复杂度无法到O(1)。另外,案例2.10会转化为两个任务,会消耗更多的磁盘网络I/O资源。

(3)最新的Hive 3.0中新增了count(distinct)优化,通过配置hive.optimize.countdistinct,即使真的出现数据倾斜也可以自动优化,自动改变SQL执行的逻辑。

(4)案例2.11比案例2.10代码简洁,表达的意思简单明了,如果没有特殊的问题,代码简洁就是优。为了佐证这个想法,可以一起执行下这两段代码,比较一下代码的执行结果。老工执行完后,分别贴出了上面的两个案例,即案例2.10和案例2.11的执行结果。

案例2.10的执行结果如下。

hive 去掉星号等特殊字符 hive怎么去重_数据_03

案例2.11的执行结果如下:

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重_04

案例2.10和2.11执行结果对比:

● 案例2.10总共耗时47秒;

● 案例2.11总共耗时28秒。

看到案例2.10和案例2.11的执行结果,通过执行计划可以查看两者执行过程中的逻辑差别。如果读者之前对执行计划不熟悉,也没关系,只要能看懂下面执行计划中的几个关键字,理清SQL的执行逻辑就好。随后老工贴出了两个案例的执行计划,并逐一做了解释。

案例2.10的执行计划如下:

hive 去掉星号等特殊字符 hive怎么去重_hive 去掉星号等特殊字符_05

注意:原有的执行计划太长,为了突出重点,方便阅读,将执行计划中的部分信息省略了。

上面有两个Stage,即Stage-1和Stage-2(Stage-0一般表示计算完后的操作,对程序集群中的运行没有影响),分别表示两个任务,说明这个SQL会转化成两个MapReduce。

我们先只关注上面执行结果中的黑体字,整个案例2.10的执行计划结构可以抽象成如图所示的形式。

hive 去掉星号等特殊字符 hive怎么去重_数据_06

图2.6

案例2.10执行计划简化图在Stage-1框中,整个作业又被抽象成Map和Reduce两个操作,分别用S-1 MAP和S-1 REDUCE表示。我们循着S-1MAP/REDUCE来解读案例2.10的执行计划。x2.10的执行计划如下:

(1)扫描操作。

(2)在步骤1的基础上执行列筛选(列投影)的操作。

(3)在步骤2的基础上按s_age列分组聚合(group by),最后只输出key值,value的值抛弃,不输出。

按S-1 Reduce框的缩进解读案例2.10的执行计划如下:

(1)按KEY._col0(s_age)聚合。

(2)计算步骤

(1)中每个s_age包含的学生个数,即count(1),最终输出key(s_age),抛弃无用的计算结果,即每个s_age包含的学生个数这个结果抛弃。 

注意:这里只是算出每个年龄段的个数,而计算结果是要计算出不同年龄枚举值的个数。

经过上面的分析知道,Stage-1其实表达的就是子查询selects_age from student_tb_orc group by s_age的实际逻辑输出的结果只是去重后的s_age

为了计算去重后s_age的个数,Hive启动了第二个MapReduce作业,在执行计划里面用Stage-2表示。

Stage-2被抽象成Map和Reduce两个操作。在图2.6中分别用S-2 MAP和S-2 REDUCE框表示,我们循着S-2 MAP/REDUCE来解读案例2.11的执行计划。

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重_07

图2.7 

案例2.10的程序流程图按S-2 Map框的缩进解读案例2.11的执行计划如下:

(1)读取Stage-1输出的结果。

(2)直接输出一列_col0,由于没有指定要去读的列,因而这里只是输出了每个s_age所在文件行的偏移量

按S-2 Reduce框的缩进解读案例2.11的执行计划计算vlaue._col0(map输出的_col0)的个数,并输出。整个Stage-2的逻辑就是select count(1) from (…)a这个SQL的逻辑。为了方便理解,可以对照图2.7的程序流程图来理解逻辑。接着来看案例2.11对应的执行计划:

hive 去掉星号等特殊字符 hive怎么去重_数据_08

案例2.11的执行计划相对于案例2.10来说简单得多。同时,也可以看到只有一个Stage-1,即只有一个MapReduce作业。将上述执行计划抽象成图2.8的结构来进行解读。

hive 去掉星号等特殊字符 hive怎么去重_Hive_09

图2.8

按S-1 Map框的缩进解读案例2.11的执行计划如下:

(1)获取表的数据。

(2)列的投影,筛选出s_age列。

(3)以s_age作为分组列,并计算s_age去重后的个数,最终输出的只有s_age列,计算s_age去重后个数的值会被抛弃。

注意:这里计算s_age 去重后的个数,仅仅只是操作一个Map 内处理的数据,即只是对部分数据去重。一个任务中有多个Map,如果存在相同的值则是没有做去重,要做到全局去重,就只能在Reduce中做

按S-1 Reduce 框的缩进解读案例2.11的执行计划。可以看到,Reduce 阶段只是对key._col0(s_age)进行全局去重,并输出该值。为了方便理解,可以对照图2.9来理解。

hive 去掉星号等特殊字符 hive怎么去重_Hive_10

图2.9

对比上面两个执行计划的逻辑我们可以知道,案例2.10是将去重(distinct)和计数放到两个MapReduce作业中分别处理;

而案例2.11是将去重和计数放到一个MapReduce作业中完成。

下面将两个案例流程放在一起对比,如图2.10所示。

hive 去掉星号等特殊字符 hive怎么去重_hive 去掉星号等特殊字符_11

图2.10 

案例2.10和案例2.11的逻辑对比图从图2.10中可以知道,案例2.10的数据处理逻辑集中在Stage-1-Map、Stage-1-Reduce和Stage-2-Reduce这3个部分

案例2.11的数据处理逻辑集中在Stage-1-Map、Stage-1-Reduce这两个部分

从实际业务来讲,不同s_age的枚举个数相比于源表student_tb_orc的总数是非常有限的,且两个用到的算法相似,因此在这里可以认为案例2.10整体的数据处理逻辑的总体耗时和案例2.11的数据处理复杂度近似。这一点在YARN的日志中也会看到。这两个案例的时间差主要集中在数据传输和中间任务的创建下,就是图2.10中的虚线框部分,因此通过distinct关键字比子查询的方式效率更高

采用案例2.10的写法,什么时候会比案例2.11高呢?

在有数据倾斜的情况下,案例2.10的方式会比案例2.11更优

什么是数据倾斜?

是指当所需处理的数据量级较大时,某个类型的节点所需要处理的数据量级,大于同类型的节点一个数量级(10倍)以上这里的某个类型的节点可以指代执行Map或者Reduce的节点。当数据大到一定的量级时,案例2.10有两个作业,可以把处理逻辑分散到两个阶段中,即第一个阶段先处理一部分数据,缩小数据量,第二个阶段在已经缩小的数据集上继续处理。而案例2.11,经过Map 阶段处理的数据还非常多时,所有的数据却都需要交给一个Reduce 节点去处理,就好比千军万马过独木桥一样,不仅无法利用到分布式集群的优势,还要浪费大量时间在等待,而这个等待的时间远比案例2.10多个MapReduce所延长的流程导致额外花费的时间还多

如前面所说,在Hive 3.0中即使遇到数据倾斜,案例2.11将hive.optimize.countdistinct设置为true,则整个写法也能达到案例2.10的效果。调优讲究适时调优,过早进行调优有可能做的是无用功甚至产生负效应,在调优上投入的工作成本和回报不成正比。调优需要遵循一定的原则

2.2.2 编码和调优的原则

对于调优,在有多年开发经验的老工看来要从需求和架构(代码、模块、系统)这俩大方面入手

例如,代码中看起来逻辑别扭,结构怪异的地方必然有其优化的空间。需要经常进行特殊调整的需求,可能是对某些方面没有理解到位的地方;可能是没有理解需求的意图;还有可能是需求有待商榷。在老工看来,能够在线上部署的工程化项目适用2/8原则,即80%的需求都可以用简单做法去实现。让程序员脱离“懒”性的工作都是可以得到调整和优化的。

在上一节中,老工看到案例2.10的代码,结合对实际业务的理解,认为小白对代码有点过度优化,积极性太高。在老工看来,能用简单的代码就不要用复杂的代码去编写,至于调优,要讲求适时优化,在发现并定位到性能瓶颈点才开始启动调优。当然也不是说开始写代码时随便写,除了代码要尽量简单,也要遵循一些原则。例如,在面向对象编程中,我们强调日常开发要遵循s(rp)o(cp)l(sp)i(sp)d(ip)五原则。同样,在编写HiveSQL代码时,也需要遵循一些简单的原则。这里分享一下笔者总结的代码优化原则:

● 理透需求原则,这是优化的根本;

● 把握数据全链路原则,这是优化的脉络;

● 坚持代码的简洁原则,这让优化更加简单;

● 没有瓶颈时谈论优化,是自寻烦恼。

1.理透需求小白能写出案例2.10的代码,说明在如何更好地使用Hive上下了功夫,但小白摒弃了案例2.11用distinct的写法,改用案例2.10使用子查询的方法,除了自身技术知识存在欠缺,还在于对需求的把握不够到位,没有遵循不同实际业务需求场景应选用不同的技术原则。其实不只是小白没有理透需求,甚至连HiveSQL 的执行计划也没有理透需求,最原始的需求是求年龄枚举值个数,只要每个年龄各取一个即可,统计这些个数根本用不到排序,但是在执行计划中明显看到了排序。下面是一个改进版本的MapReduce伪代码。

【案例2.12】 改进版的MapReduce。

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重_12

上面的程序摒弃了无用的排序逻辑,去重的计算复杂度达到O(1)级别,已经达到去重的最优。建议可以亲自实现上面的代码,会发现速度至少提升了一个量级。

2.理透数据全链路

当业务人员提出一个需求时,我们需要知道相关的信息,大体可以分为以下3个方面。

(1)业务数据准备

理清支持这个业务的基础数据有哪些,输出的数据要求是什么,这些业务的数据以什么样的方式存储,其占用空间及元数据等信息如何等。

要获取上述信息,除了日常要做好需求等相关文件的梳理工作以外,在技术层面,Hive还提供了一些工具可以获取到一些业务信息。

例如,通过Hive 提供的交互命令来获取所要处理的数据信息;

通过收集统计信息,查看Hive的metadata库对表的数据量和占用空间等信息。在这里有两种方式可以做到快速了解Hive的元数据信息。

方式1:通过desc formatted。通过desc formatted tablename来查看表信息,可以获取到注释、字段的含义(comment)、创建者用户、数据存储地址、数据占用空间和数据量等信息,具体可看下面的案例。

【案例2.13】 使用desc formatted来查看表的描述信息

hive 去掉星号等特殊字符 hive怎么去重_hive 去掉星号等特殊字符_13

在上面的案例中,可以看到以下几部分的信息:

● 表的字段信息;

● 表所属的数据信息;

● 表的持有者;

● 表的创建时间、最近修改时间;

● 表所在HDFS的路径信息;

● 表的参数信息(Table Parameters),包括表的文件个数、数据量大小等;

● 表的存储信息(Storage Information),包括序列化/反序列化方式,输入/输出的文件格式,是否压缩、是否分桶等信息。

方式2:查询Hive元数据。

例如,查询Hive的元数据信息,快速获取一些数据库的相关信息。

【案例2.14】 查询Hive元数据。

hive 去掉星号等特殊字符 hive怎么去重_Hive_14

注意:如果查询不到对应的统计信息,可以通过如下命令手动收集表或者分区来统计信息:anaylze table表名partition(分区列)compute statistics。

除了上面的信息,Hive元数据还包括库的相关信息、表所属的列相关信息、数据存储的相关信息等,这些信息在后面的中会提到。

(2)运行环境梳理

当开发出一个业务程序后,需要清楚从提交到运行之前所需要的流程和环境,对所需的基础环境和资源要有一个简单的预估,确保程序能够正确且有足够的资源运行。

要把握这个准备工作,最关键的还是要了解整个大数据的资源管理和任务管理。常见的资源管理组件有YARN和Mesos,常见的任务管理调度工具有oozie、azkaban和airflow 等。

这些组件会以各种形式来反馈当前集群的情况,可以对作业提交之前的运行环境有精确的了解。

例如,可以访问资源管理集群资源整体状况的数据接口。下面提供了两种方式去访问这些接口。

方式1,使用Python代码来获取度量信息,如下:

hive 去掉星号等特殊字符 hive怎么去重_数据_15

这种方式可以嵌入到我们的应用程序。怎么使用呢?

在提交一个到集群时,可以先使用上述代码,来获取当前集群的信息以判断当前集群的状态是否可以提交任务,或者判断集群资源是否紧张,如果紧张是否需要做特别处理。

方式2,使用shell命令获取度量信息,如下:

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重_16

返回的结果如下:

hive 去掉星号等特殊字符 hive怎么去重_hive 列表去重_17

通过这些接口可以看到即将要提交的作业所在集群的资源情况是否充足,此外这些接口还会反馈其他的内容,对于更好去使用集群、监控集群也提供了很大的帮助。对于这些数据接口内容及所表示的含义和应用场景,将在后面的章节做更多介绍。

(3)程序运行过程的数据流

我们需要知道Hive在运行HiveSQL时,数据在计算引擎各个阶段的变换形式和流转步骤。例如,HiveSQL 在处理数据时,数据可能只需要经过Map 阶段,也可能需要经过Map-Reduce-Map-Reduce这4个阶段,还有可能只需要经过Map- Reduce-Reduce这3个阶段。要想理清这点,需要简单掌握以下几点:

● 理解HiveSQL的基本执行原理。把握这个执行逻辑,就需要去通读它的执行计划,理解执行脉络,以及如何转化映射成MapReduce。

● 了解HiveSQL 所用执行引擎的基本原理,以及在这个计算引擎内部各个节点的数据流。这里就需要去全面地读一读官网文档。例如,在查看MapReduce的文档信息时会发现,MapReduce 执行引擎不仅包括Map 环节、Reduce环节,还有FileInputFormat、FileOutputFormat、Combiner和Partition等环节,这些环节的调优其实也影响到了整个计算引擎的性能,同时也影响上层Hive的性能

● 了解Hive 在运行时除了计算引擎之外所依赖的其他组件及其运行的基本原理,还需要了解Hive和这些组件之间的关联关系。

3.坚持代码的简洁不管是写SQL,还是用其他的语言写工程化项目,都需要尽量保持代码的简洁。一份简洁的代码,是逻辑思维清晰的体现,也是对业务较好理解的一种体现。一份简洁的代码,在后期的维护及工作交接时都能给予自己和别人极大的帮助。

相反,一份读起来逻辑别扭、冗长复杂的代码,有更高的几率潜藏Bug,潜藏性能不友好的逻辑,甚至它是对真实需求的一种扭曲实现。这种案例,在实际开发中比比皆是。

4.没有瓶颈而讨论优化

从接触代码开始,笔者对编码这份工作就有了极大的好感,对自己写的代码要求也比较高,尤其是对性能十分敏感,有时可以说是“偏执”。只要凭借以往经验验证的某些技术细节是性能不好的,那么在新的代码遇到类似情况就会极力避免,即使代价大一些。但技术在更新,业务要求在变化,线上环境也在变。技术在经过版本的更新后,某些有性能问题的用法可能变得没有问题;某些业务,之前为了避免引发性能问题而过早花大力气调优的地方,甚至可以直接舍弃;某些环境可能由于业务的调整发生巨变,则导致某些原本不耗费时间的处理环节其性能却急速下降,如线上环境数据激增。有优化意识是好的,但优化不区分对象而谈优化,容易一叶障目。

下面将优化所要关注的问题分为两类:

● 影响项目整体落地的问题、重大性能问题;

● 不影响项目整体落地,但是影响部分功能。

第一种情况,在实际项目中,有经验的老工一般能够预知且提早介入,一般在项目设计阶段就已经规避

第二种情况,在具体实现上,由于所处的环节较为靠后,且和实际的业务有较强的关联,会根据实际情况而反复地调整。这种情况就需要在具体环境下,依据具体的业务要求进行调优,将优化放到有瓶颈点的地方去考虑和讨论,否则只是做更多的投入和产出不成正比的工作。

2.2.3 Hive程序相关规范

一份拥有良好代码风格的程序,有助于开发者发现性能问题,缩短调优的时间,降低维护成本,同时也能促进程序员的自我提高。规范分为3类:开发规范、设计规范和命名规范。

1.开发规范

● 单条SQL长度不宜超过一屏。

● SQL子查询嵌套不宜超过3层。

● 少用或者不用Hint,特别是在Hive 2.0后,增强HiveSQL对于成本调优(CBO)的支持,在业务环境变化时可能会导致Hive无法选用最优的执行计划。

● 避免SQL 代码的复制、粘贴。如果有多处逻辑一致的代码,可以将执行结果存储到临时表中。

尽可能使用SQL 自带的高级命令做操作。例如,在多维统计分析中使用cube、grouping set和rollup等命令去替代多个SQL子句的union all

● 使用set命令,进行配置属性的更改,要有注释。

● 代码里面不允许包含对表/分区/列的DDL语句,除了新增和删除分区。

Hive SQL 更加适合处理多条数据组合的数据集,不适合处理单条数据,且单条数据之间存在顺序依赖等逻辑关系。例如,有A、B、C 3行数据,当A符合某种条件才能处理B行时,只有A、B符合某种条件,才能处理C行。

保持一个查询语句所处理的表类型单一。例如,一个SQL语句中的表都是ORC类型的表,或者都是Parquet表。

关注NULL值的数据处理

SQL表连接的条件列和查询的过滤列最好要有分区列和分桶列。

存在多层嵌套,内层嵌套表的过滤条件不要写到外层,例如:

hive 去掉星号等特殊字符 hive怎么去重_数据_18

应当写为:

hive 去掉星号等特殊字符 hive怎么去重_Hive_19

2.设计规范

● 表结构要有注释。

● 列等属性字段需要有注释。

● 尽量不要使用索引。在传统关系型数据库中,通过索引可以快速获取少部分数据,这个阀值一般是10%以内。但在Hive的运用场景中,经常需要批量处理大量数据,且Hive索引在表和分区有数据更新时不会自动维护,需要手动触发,使用不便。如果查询的字段不在索引中,则会导致整个作业效率更加低下。索引在Hive 3.0后被废弃,使用物化视图或者数据存储采用ORC格式可以替代索引的功能。

● 创建内部表(托管表)不允许指定数据存储路径,一般由集群的管理人员统一规划一个目录并固化在配置中,使用人员只需要使用默认的路径即可。

创建非接口表,只允许使用Orc 或者Parquet,同一个库内只运行使用一种数据存储格式。

接口表指代与其他系统进行交互的数据表,例如从其他系统导入Hive 时暂时存储的表,或者数据计算完成后提供给其他系统使用的输出表。

Hive适合处理宽边(列数多的表),适当的冗余有助于Hive的处理性能

表的文件块大小要与HDFS的数据块大小大致相等

分区表分桶表的使用

3.命名规范库/表/字段命名要自成一套体系

● 表以tb_开头。

● 临时表以tmp_开头。

● 视图以v_开头。

● 自定义函数以udf_卡头。

● 原始数据所在的库以db_org_开头,明细数据所在库以db_detail_开头,数据仓库以db_dw_开头。