hive优化

1,236 阅读16分钟

浅谈优化

第一次写优化相关的文章,先简单谈谈关于优化看法。首先一点是很多优化设计不管是缓存、索引还是排序等等,其核心的思想就是减少IO。然后在分布式场景下效率还遵循木桶效应,任务是并行执行的,最后执行完的任务决定了整个任务的耗时。所以均衡各个任务节点的任务是在分布式场景下的重要优化思路。

物理影响

当相同角色(datanode和datanode,datanode和namenode物理配置不一样并不会影响)的物理配置不一样时,对分布式中的任务也会产生消极的影响。不过一般在分布式集群搭建时相同的角色都会采用相同的配置。主要是新增集群节点时可能会出现物理硬件信息不一致的情况影响性能或者说对性能没有提升,比如之前的节点用的是机械硬盘,新加的节点用固态对任务效率能大部分情况下并不会有明显的提升。新节点改用主频更低的CPU对任务还会有消极影响。

数据影响

分布式中一般都是采用数据并行的方式进行分布计算,即将数据拆分成多分,每个任务领取一份数据做相同的处理,处理后的结果汇聚后就是整个任务的结果。在hadoop生态中数据的影响主要有三种情况:

  1. 数据原始情况就分布不均衡。如图假如每个节点同时最多支持的任务就是两个,由于数据不均衡,node1就必须去node3拉取数据才能启动第二个任务(或者node3执行完第一个任务再开始第三个任务的执行),导致node1和node3执行就会比node2慢拖垮整个任务的效率。这种情况会出现在mapreduce、tez和spark的map端。可定时和在新加节点后通过hdfs的balancer去重新平衡各个节点的数据。

  2. 数据进行传输后数据不均导致的数据倾斜或者数据热点问题,也是我们讲优化时主要要解决的问题。不管是mapreduce、tez、spark还是hbase和kafka,在数据分发的时候默认都是采用哈希取模的方式决定数据的去向。根据哈希规则去设计key,合理设置分区数或者重新设计分发数据的规则(分区器)是解决这类问题的关键。

  3. 有种特殊情况是即使数据均匀传输到各个节点,但是每个节点的任务耗时也会相差很大。比如计算逻辑为某些数据过滤,某些数据计算,恰好过滤的数据都分配到一个节点,计算的数据都分配到另一个节点,导致只包含过滤数据的节点一下就结束了任务浪费资源,而包含计算数据的节点耗时很久才结束任务。这种情况需要根据数据和任务逻辑具体分析。

配置优化

回到hive优化上面,首先从配置上列举些hive优化的点。

map

  1. 合理控制map task数量,map task数量是由minSplitSize,maxSplitSize和blockSize计算中间值得出的,默认为block数量。需要的时候可以调整minSplitSize和maxSplitSize的值,但是根据数据本地行原则,设置后的splitSize尽量为blockSize的整数倍以减少数据的网络传输:

    -- 调整mapTask数量,只有大于默认的任务数量才有效
    set mapred.map.tasks=10;
    
    -- 设置为大于blockSize的数,即可减少task数量
    set maperd.min.split.size=9999999999
  2. map join,map join是将join中较小的表分发到各个map task的内存中,直接在内存进行join,这样就不用进行reduce步骤,从而提高效率:

    -- 开启Map Join,默认开启
    set hive.auto.convert.join=true;
    
    -- 大表小表的阀值设置(默认25M一下认为是小表):
    set hive.mapjoin.smalltable.filesize=25000000;
    
    -- Map Join所处理的最大的行数。超过此行数,Map Join进程会异常退出
    set hive.mapjoin.maxsize=1000000; 
    
    -- 可以通过该参数调整map join的内存阀值
    /* 关于这个参数官网有个解释:Whether Hive enables the optimization about converting common join into mapjoin based on the input file size. If this parameter is on, and the sum of size for n-1 of the tables/partitions for an n-way join is smaller than the size specified by hive.auto.convert.join.noconditionaltask.size, the join is directly converted to a mapjoin (there is no conditional task).
    */
    set hive.auto.convert.join.noconditionaltask.size=25000000;
    
    -- 如果默认配置没有开启mapjoin, 可以通过/*+ MAPJOIN(smalltable)*/ 显示地指定进行mapjoin的表;
    -- 但是要关闭忽视mapjoin标识,默认开启
    set hive.ignore.mapjoin.hint=true;
    
  3. map端聚合,并不是所有的聚合操作都需要在Reduce端完成,很多聚合操作都可以先在Map端进行部分聚合,最后在Reduce端得出最终结果:

    -- 开启Map端聚合参数设置,默认开启
    set hive.map.aggr=true;
    -- 在Map端进行聚合操作的条目数目,不确定这个参数还有没有用,查下官网配置文件!
    set hive.groupby.mapaggr.checkinterval=100000;
    -- map端进行聚合的最小比例,默认0.5。预先对100000条记录进行聚合,若聚合之后的数据量/100000大于该配置值,则不聚合;
    set hive.map.aggr.hash.min.reduction=0.5;
    
  4. 有数据倾斜时进行负载均衡,默认情况下,Map阶段同一Key数据分发给一个reduce,当一个key数据过大时就倾斜了:

    -- 有数据倾斜(gropu by产生的)的时候进行负载均衡(默认是false)
    set hive.groupby.skewindata=true;
    

reduce

  1. 调整reduce task数量:

    -- 每个reduce处理数据量,减小此值可提高并行度,并可能会改善性能。 但过度减小也可能生成过多的reducer,从而对性能产生潜在的负面影响。
    set hive.exec.reducers.bytes.per.reducer=256000000;
    
    -- 每个任务最大的reduce数,默认为1009
    set hive.exec.reducers.max=1009;
    
    -- hive根据上面两个参数和总数据量计算reduce个数
    -- reduceTaskNum = min(reducers.max, total_bytes / bytes.per.reduce)
    
    -- 也可以手动设置reduceTask数量 (0.95 * datanode数量)
    set mapreduce.job.reduces=5;
    

启用压缩

可用的压缩类型包括:

格式工具算法文件扩展名是否可拆分?
GzipGzipDEFLATE.gz
Bzip2Bzip2Bzip2.bz2
LZOLzopLZO.lzo是(如果已编制索引)
Snappy空值SnappySnappy
  1. map输出压缩:

    set mapreduce.map.output.compress=true;
    set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec;
    
  2. 中间数据压缩,一般规则是,尽量使用可拆分的压缩方法,否则会创建极少的映射器。 如果输入数据为文本,则 bzip2 是最佳选项。 对于 ORC 格式,Snappy 是最快的压缩选项:

    set hive.exec.compress.intermediate=true;
    set hive.intermediate.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
    set hive.intermediate.compression.type=BLOCK;
    
  3. 结果数据压缩:

    set hive.exec.compress.output=true;
    set mapreduce.output.fileoutputformat.compress=true;
    set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec;
    set mapreduce.output.fileoutputformat.compress.type=BLOCK;
    

其他配置

  1. 并行执行,并行执行是一个sql中解析出很多个stage,解析器会判断各个stage之间的关系,不存在依赖的stage可以并行执行:

    -- 打开任务并行执行 默认为false
    set hive.exec.parallel=true; 
    -- 同一个sql允许最大并行度,默认为8。
    set hive.exec.parallel.thread.number=16;
    
  2. 严格模式:

    -- # hive2.x默认为stric,2.x之前默认为nonstrict
    set hive.mapred.mode=stric;
    
    /* 严格模式下禁止三种类型的查询:
    1. 对于分区表,要求必须限定分区字段,换句话说就是不允许扫描所有的分区,这是因为通常所有分区的数据量都比较大,这样可以避免消耗大量资源。
    2. 对于使用order by的查询,要求必须使用limit语句,因为order by为了执行排序过程会将所有的结果数据分发到同一个reducer中进行处理,这样可以避免reducer执行过长的时间。
    3. 限制笛卡尔积查询,要求两张表join时必须有on语句。
    */
    
  3. 小文件合并,如果不进行小文件合并,任务启动的时候每个小文件都会认定为一个split分配一个map task,且增加namenode消耗:

    -- 在 map only 的任务结束时合并小文件
    set hive.merge.mapfiles = true;
    -- 小文件大小
    set hive.merge.smallfiles.avgsize = 16000000;
    -- true 时在 MapReduce 的任务结束时合并小文件
    set hive.merge.mapredfiles = false;
    -- 合并文件的大小
    set hive.merge.size.per.task = 256*1000*1000;
    
  4. 启用矢量化计算,Hive 默认逐行处理数据。 矢量化指示 Hive 以块(一个块包含 1,024 行)的方式处理数据,而不是以一次一行的方式处理数据。 矢量化只适用于 ORC 文件格式:

    -- Hive 0.13.0 或更高版本的默认值为 true
    set hive.vectorized.execution.enabled = true;
    
  5. 启用CBO,默认情况下,Hive 遵循一组规则来找到一个最佳的查询执行计划。 基于成本的优化 (CBO) 会评估多个查询执行计划, 并为每个计划分配成本,然后确定执行查询的成本最低的计划:

    -- 启动CBO
    set hive.cbo.enable=true; 
    -- 开启使用其元存储中存储的统计信息来应答类似于 count(*) 的简单查询。
    set hive.compute.query.using.stats = true;
    -- 启用 CBO 时,会创建列统计信息。 Hive 使用元存储中存储的列统计信息来优化查询。 如果列数较多,则提取每个列的列统计信息需要花费很长时间。 如果设置为 false,则会禁用从元存储中提取列统计信息。
    set hive.stats.fetch.column.stats = true;
    -- 行数、数据大小和文件大小等基本分区统计信息存储在元存储中。 如果设置为 true,则从元存储中提取分区统计信息。 如果设置为 false,则从文件系统中提取文件大小。 行数从行架构中提取。
    set hive.stats.fetch.partition.stats = true;
    
  6. JVM重用,Hive 语句最终会转换为一系列的 MapReduce 任务,每一个MapReduce 任务是由一系列的Map Task 和 Reduce Task 组成的,默认情况下,MapReduce 中一个 Map Task 或者 Reduce Task 就会启动一个 JVM 进程,一个 Task 执行完毕后,JVM进程就会退出。这样如果任务花费时间很短,又要多次启动 JVM 的情况下,JVM的启动时间会变成一个比较大的消耗,这时,可以通过重用 JVM 来解决。:

    -- 设置重用JVM数量
    set mapred.job.reuse.jvm.num.tasks=10;
    
  7. 动态分区:

    -- 开启动态分区,默认开启
    set hive.exec.dynamic.partition=true;
    
    -- 调整创建的动态分区总数
    set hive.exec.max.dynamic.partitions=5000;
    -- 每个节点的动态分区总数
    set hive.exec.max.dynamic.partitions.pernode=2000;
    
  8. 推理执行,推理执行会启动一定数量的重复任务,用于检测运行缓慢的任务跟踪器并将它们列入拒绝列表。 同时通过优化各个任务结果来改进作业的整体执行:

    -- 启用推理执行,默认为false
    set hive.mapred.reduce.tasks.speculative.execution=true;
    

表设计优化

  1. 选择合适的数据存储格式:

    1. TextFile
      • 为默认格式,如果建表时不指定默认为此格式;
      • 每一行都是一条记录,每行都以换行符\n结尾。数据不做压缩时,磁盘会开销比较大,数据解析开销也比较大;
      • 可结合 GzipBzip2 等压缩方式一起使用(系统会自动检查,查询时会自动解压),但对于某些压缩算法 hive 不会对数据进行切分,从而无法对数据进行并行操作。
    2. SequenceFile
      • 一种Hadoop API 提供的二进制文件,使用方便、可分割、个压缩的特点;
      • 支持三种压缩选择:NONE、RECORD、BLOCK。RECORD压缩率低,一般建议使用BLOCK压缩。
    3. RCFile
      • 数据按行分块,每块按照列存储;
      • 首先,将数据按行分块,保证同一个record在一个块上,避免读一个记录需要读取多个block;
      • 其次,块数据列式存储,有利于数据压缩和快速的列存取。
    4. ORC
      • 数据按行分块,每块按照列存储;
      • Hive 提供的新格式,属于 RCFile 的升级版,性能有大幅度提升,而且数据可以压缩存储,压缩快,快速列存取。
    5. Parquet
      • 列式存储;
      • 对于大型查询的类型是高效的,对于扫描特定表格中的特定列查询,Parquet特别有用;
      • 一般使用 Snappy、Gzip 压缩。默认为Snappy。
    6. 一般用得比较多的是ORC,官网还有很多其他格式可以选择,具体可查看:FileFormats
  2. 选择合适的数据压缩格式:

    格式工具算法文件扩展名是否可拆分?
    GzipGzipDEFLATE.gz
    Bzip2Bzip2Bzip2.bz2
    LZOLzopLZO.lzo是(如果已编制索引)
    Snappy空值SnappySnappy
  3. 合理设计表分区,分区表在分区维度对数据进行分类存储,一个分区对应一个目录。注意动态分区时指定分区字段的一个值为一个分区,而不是一个字段为一个分区,比如age=20。筛选时根据分区筛选可以过滤大量无关数据进而提高查询效率,常用的如根据时间设置分区:

  4. 合理设计分桶,分桶和分区的原理一样,都是通过减少处理数据加速查询效率,但是分区是根据字段值将数据存储到不同目录,分桶是根据字段哈希取模将数据分散存储到不同文件中。一般用于测试,另一个是如果hive要支持事务,表必须是ORC的桶表。

语法优化

  1. 列裁剪和分区裁剪,都是减少IO来加速查询;

  2. join优化:

    1. join前尽可能地过滤更多地数据,减少参与到join的数据量;
    2. 小表join大表,原因是 join 操作的 reduce 阶段,位于 join 左边的表内容会被加载进内存,将条目少的表放在左边,可以有效减少发生内存溢出的几率。join 中执行顺序是从左到右生成 Job,应该保证连续查询中的表的大小从左到右是依次增加的。
    3. 使用相同的连接键。在 hive 中,当对 3 个或更多张表进行 join 时,如果 on 条件使用相同字段,那么它们会合并为一个 MapReduce Job,利用这种特性,可以将相同的 join on 的放入一个 job 来节省执行时间。
    4. 启用map join;
  3. order by、sort by、distribute by、cluster by:

    1. order by,对查询结果做去全排序,只允许一个reduce task(数据量大时慎用);
    2. sort by,对当前reduce task进行排序。其在数据进入reducer前完成排序,因此,如果用sort by进行排序,并且设置mapred.reduce.tasks>1,则sort by只会保证每个reducer的输出有序,并不保证全局有序;
    3. distribute by,控制在map端如何拆分数据给reduce端的。hive会根据distribute by后面列,对应reduce的个数进行分发,默认是采用hash算法。sort by为每个reduce产生一个排序文件。在有些情况下,你需要控制某个特定行应该到哪个reducer,这通常是为了进行后续的聚集操作。distribute by刚好可以做这件事。因此,distribute by经常和sort by配合使用;
    4. cluster by,相当于 sort by + distribute by,但是不能指定排序方式desc or asc,默认asc;
    5. 如果是去排序后的前N条数据,可以使用distribute bysort by在各个reduce上进行排序后前N条,然后再对各个reduce的结果集合合并后在一个reduce中全局排序,再取前N条。
  4. count distinct优化:

    -- 优化前(只有一个reduce,先去重再count负担比较大):
    select count(distinct id) from tablename;
    
    -- 优化后(启动两个job,一个job负责子查询(可以有多个reduce),另一个job负责count(1)):
    select count(1) from (select distinct id from tablename) tmp;
    

其他优化

  1. 尽量避免一个SQL中包含过于复杂的逻辑,特别是一些数据计算逻辑,固定或者相似的逻辑可以添加中间表来简化sql的逻辑;

  2. 比较复杂的计算逻辑,或者sql比较难实现的逻辑可以通过UDF实现;

  3. 数据倾斜:

    1. 不同key导致,不同key导致的意思是,数据key是相对分散的,但是数据分发默认是根据key值哈希取模,不同的key哈希取模后大部分数据都落在了某个分区,导致该分区数据倾斜。可通过合理调整分区数,或者自定义分区器根据数据情况均匀将数据分发到不同分区以避免数据倾斜。

    2. 相同key导致,相同key导致的话意味着无论调整分区数还是自定义分区器,都无法解决数据倾斜问题:

      1. 数据采集时丢失(如用户名,用户ID等)且对计算不影响或影响不大,比如统计UV,计算时过滤这部分数据;

      2. 数据采集时丢失(访问地址)且对计算影响,比如统计PV,计算前更新对应字段为可选值中的随机值;

      3. 数据特点,比如广州的数据量就是比中山多,商圈A的数据量就是比商圈B多,时间段A的记录数比时间断B的记录数多等等情况。可根据数据情况对key加一些特殊信息,使其均匀分散计算出中间结果后再计算最终结果:

总结

总的来说hive的优化主要从两个方面着手,一个是减少IO,另一个是均衡各个节点任务(更常见且具体的问题为数据倾斜问题)。减少IO的话尽可能地跳过不需要的数据即可比如列裁剪、分区裁剪等。

至于数据倾斜,会导致数据倾斜的操作主要有:

关键词情形后果
join其中一个表较小,但是key集中分发到某一个或几个Reduce上的数据远高于平均值
join大表与大表,但是分桶的判断字段0值或空值过多这些空值都由一个reduce处理,非常慢
group bygroup by 维度过小,某值的数量过多处理某值的reduce非常耗时
count distinct某特殊值过多处理此特殊值的reduce耗时

其实根本原因是计算时shuffle的过程进行了数据的分发,默认情况下是根据key值哈希取模决定数据的去向,基于这个原理再根据数据特点去做数据倾斜的优化即可,上面的配置也好,sql优化也好只不过是具体的操作手段而已。

当然在实际生产过程中,还需要观察任务执行过程中,CPU、内存、磁盘和网络等信息的使用情况去判断任务的性能瓶颈。

参考文档:

  1. 从0开始学大数据-Hive性能优化篇
  2. 在 Azure HDInsight 中通过 Apache Ambari 优化 Apache Hive
  3. Apache Hive Performance Tuning