HADOOP集群各组件性能调优[SPARK,HBASE,HIVE,HDFS...]

HADOOP集群各组件性能调优

配置原则

如何发挥集群最佳性能

原则1:CPU核数分配原则

数据节点:建议预留2~4个核给OS和其他进程(数据库,HBase等)外,其他的核分
配给YARN。
控制节点:由于运行的进程较多,建议预留6~8个核。

原则2:内存分配

除了分配给OS、其他服务的内存外,剩余的资源应尽量分配给YARN。

原则3:虚拟CPU个数分配

节点上YARN可使用的虚拟CPU个数建议配置为物理核数的1~2倍之间。如果上层计算应用对CPU的计算能力要求不高,可以配置为2倍的物理CPU

原则4:提高磁盘IO吞吐率

尽可能挂载较多的盘,以提高磁盘IO吞吐

影响性能的因素

因素1:文件服务器磁盘I/O

一般磁盘顺序读写的速度为百兆级别,如第二代SATA盘顺序读的理论速度为300Mps,
只从一个盘里读,若想达到1Gps每秒的导入速度是不可能的。并且若从一个磁盘读,
单纯依靠增加map数来提高导入速率也不一定可以。因为随着map数变多,对于一个磁
盘里的文件读,相当由顺序读变成了随机读,map数越多,磁盘读取文件的随机性越
强,读取性能反而越差。如随机读最差可变成800Kps。 因此需要想办法增大文件服务
器的磁盘IO读效率,可以使用专业的文件服务器,如NAS系统,或者使用更简单的方
法,把多个磁盘进行Raid0或者Raid5。

因素2:文件服务器网络带宽

单个文件服务器的网络带宽越大越好,建议在10000Mb/s以上。

因素3:集群节点硬件配置

集群节点硬件配置越高,如CPU核数和内存都很多,可以增大同时运行的map或reduce
个数,如果单个节点硬件配置难以提升,可以增加集群节点数。

因素4:SFTP参数配置

不使用压缩、加密算法优先选择aes128-cbc,完整性校验算法优先选择
umac-64@openssh.com

因素5:集群参数配置

因素6:Linux文件预读值

设置磁盘文件预读值大小为16384,使用linux命令:
echo 16384 > /sys/block/sda/queue/read_ahead_kb
说明:sda表示当前磁盘的磁盘名。

因素7:Jsch版本选择

选择最新版本jsch jar包,即jsch-0.1.51.jar,性能可提升20%。

HBase

提升 BulkLoad 效率

操作场景

批量加载功能采用了MapReduce作业直接生成符合HBase内部数据格式的文件,然后把
生成的StoreFiles文件加载到正在运行的群集。使用批量加载会比直接使用HBase的API
节约更多的CPU和网络资源。
ImportTSV是一个HBase的索引表数据加载工具。

前提条件

在执行批量加载时需要指定文件的输出路径:Dimporttsv.bulk.output。

操作步骤

参数入口:执行批量加载任务时,在BulkLoad命令行中加入如下参数。

增强 Bulk Load 效率的配置项

-Dimporttsv.mapper.class
新的自定义mapper通过把键值对的构造从mapper移动到reducer以帮助提高性能。mapper只需要把每一行的原始文本发送给reducer,reducer解析每一行的每一条记录并创建键值对。 默认值:org.apache.hadoop.hbase.mapreduce.TsvImporterTextMapper

提升索引 BulkLoad 效率

操作场景

索引批量加载功能采用了MapReduce作业直接生成符合HBase内部数据格式的文件,然
后把生成的数据文件加载到正在运行的群集。使用批量加载会比直接使用HBase的API
节约更多的CPU和网络资源。

预置条件

l 在执行批量加载时需要指定文件的输出路径:Dimporttsv.bulk.output。
l 用户表需要为创建了索引的表。

增强 Bulk Load 效率的配置项

-Dimporttsv.mapper.class
新的自定义mapper通过把键值队的构造从mapper移动到reducer以帮助提高性能。mapper只需要把每一行的原始文本发送给reducer,reducer解析每一行的每一条记录并创建键值对。 默认值:org.apache.hadoop.hbase.index.mapreduce.IndexTsvImporterTextMapper

提升数据实时入库的效率

操作场景

需要把数据实时保存到HBase中。

前提条件

调用HBase的put或delete接口,把数据保存到HBase中。

影响数据实时入库性能的参数

hbase.regionserver.wal.durable.sync
控制HLog文件在写入到HDFS时的同步程度。如果为true,HDFS在把数据写入
到硬盘后才返回;如果为false,HDFS在把数据写入OS的缓存后就返回。把该值设置为false比true在写入性能上会更优。默认值:true
hbase.regionserver.hfile.durable.sync
控制HFile文件在写入到HDFS时的同步程度。如果为true,HDFS在把数据写入
到硬盘后才返回;如果为false,HDFS在把数据写入OS的缓存后就返回。
把该值设置为false比true在写入性能上会更优。默认值:true

HDFS

提升读写性能

操作场景

在HDFS中,通过调整属性的值,使得HDFS集群更适应自身的业务情况,从而提升HDFS的读写性能。

HDFS 读写性能优化配置

dfs.blocksize
表示新建文件的默认块大小。单位:字节。指定大小或者指定具体字节大小(例如134217728,表示128MB)。最优值:268435456(256MB)说明:参数值必须为512的倍数,否则向HDFS写入文件时会出现错误。缺省值:134217728(128MB)范围:512-1073741824

提升写性能

操作场景

在HDFS中,通过调整属性的值,使得HDFS集群更适应自身的业务情况,从而提升HDFS的写性能。

dfs.datanode.drop.cache.behind.reads
设置为true表示丢弃缓存的数据(需要在DataNode中配置)。默认值:true

MapReduce

多 CPU 内核下的调优配置

操作场景

当CPU内核数很多时,如CPU内核为磁盘数的3倍时的调优配置。

说明

HDFS客户端配置文件路径:客户端安装目录/hadoop/etc/hadoop/hdfs-site.xml。

Yarn客户端配置文件路径:客户端安装目录/hadoop/etc/hadoop/yarn-site.xml。

MapReduce客户端配置文件路径:客户端安装目录/hadoop/etc/hadoop/mapred-site.xml。

节点容器槽位数

如下配置组合决定了每节点任务(map、reduce)的并发数。

  • yarn.nodemanager.resource.memory-mb 
    
    : 默认值: 8192
  • mapreduce.map.memory.mb
    
    : 默认值: 4096
  • mapreduce.reduce.memory.mb
    
    : 默认值: 4096

影响:如果所有的任务(map/reduce)需要读写数据至磁盘,多个进程将会同时访问一个磁盘。这将会导致磁盘的IO性能非常的低下。为了改善磁盘的性能,请确保客户端并发访问磁盘的数不大于3。

  • 容器数应该为[ 2.5 * Hadoop中磁盘配置数 ]。
  • 容器槽位的数量应少于可使用CPU数的70%,除去节点中运行的操作系统以及其它进程如Datanode,节点管理器需要更多的CPU来处理高负载量的Map任务。
Map输出与压缩

Map任务所产生的输出可以在写入磁盘之前被压缩,这样可以节约磁盘空间并得到更快的写盘速度,同时可以减少至Reducer的数据传输量。需要在客户端进行配置

  • mapreduce.map.output.compress 
     指定了Map任务输出结果可以在网络传输前被压缩。这是一个per-job的配置。
    
  • mapreduce.map.output.compress.codec
    指定用于压缩的编解码器. 默认值:org.apache.hadoop.io.compress.SnappyCodec
    

在这种情况下,磁盘的IO是主要瓶颈。所以可以选择一种压缩率非常高的压缩算法。
编解码器可配置为LZO和Snappy中的任意一种,Benchmark测试结果显示LZO和Snappy是非常平衡以及高效的编码器。

Spills
  • mapreduce.map.sort.spill.percent
    默认值:0.8
    

在这种情况下,磁盘I/O将成为主要的瓶颈。所以io.sort.mb的配置如溢出将会被最小化。io.sort.mb应该配置成使溢出不再产生。例如HDFS的块大小设置成256MB,而每条记录只有100字节,设置io.sort.mb为316MB,io.sort.spill.percent为0.99(99%填补阈值)来完成消除Map方的溢出。

数据包大小

当HDFS客户端写数据至数据节点时,数据会被累积,直到形成一个包。然后这个数据包会通过网络传输。dfs.client-write-packet-size配置项可以指定该数据包的大小。这个可以通过每个job进行指定。

  • dfs.client-write-packet-size
    默认值:262144
    

数据节点从HDFS客户端接收数据包,然后将数据包里的数据单线程写入磁盘。当磁盘处于并发状态并且数据包的大小可以增加,磁盘的寻道时间以及IO性能将会增加。

CPU 使用率过高时的调优配置

操作场景

在集群整体CPU使用率过高的时候,可以通过调整dfs.client.read.shortcircuit”参数配置,使其更合理利用集群资源,来达到降低CPU使用率。

  • dfs.client.read.shortcircuit
      最短路径读取。当client和DataNode在一起时,数据以码流的方式从DataNode节点传到客户端。这种方式比较低效,并且导致大量的上下文切换。相反的,客户端可以选择最短路径读取,tr直接从磁盘读取数据。如果将本参数设置为“true”,则可以在“dfs.block.local-path-access.user”参数中指定对应的角色。
    

确定 Job 基线

操作场景
确定Job基线是调优的基础,一切调优项效果的检查,都是通过和基线数据做对比来获得。
Job基线的确定有如下三个原则:
  • 集群的资源要吃满
  • reduce阶段尽量放在一轮
  • 每个task的执行时间要合理操作步骤

原则一:集群的资源要吃满。

Job运行时,会让所有的节点都有任务处理,且处于繁忙状态,这样才能保证资源充分利用,任务的并发度达到最大。可以通过调整处理的数据量大小,以及调整map和reduce个数来实现。Reduce个数的控制使用mapreduce.job.reduces.Map数取决于使用了哪种InputFormat,以及文件是否可分割。默认的nTextFileInputFormat将根据block的个数来分配map数(一个block一个map)。通过如下配置参数进行调整。

  •   mapreduce.input.fileinputformat.split.maxsize
     可以设置数据分片的数据最大值。由用户定义的分片大小的设置及每个文件block大小的设置,可以计算得分片的大小。计算公式如下:splitSize = Math.max(minSize, Math.min(maxSize, blockSize)) 如果maxSize设置大于blockSize,那么每个block就是一个分片,否则就会将一个block文件分隔为多个分
    

片,如果block中剩下的一小段数据量小于splitSize,还是认为它是独立的分片。

  •   mapreduce.input.fileinputformat.split.minsize
      可以设置数据分片的数据最小值。 默认:0
    

原则二:reduce阶段尽量放在一轮。
存在一个场景,大部分的reduce在第一轮跑完后,剩下唯一一个reduce继续跑。这种情况下,这个reduce的执行时间将极大影响这个job的运行时间。因此需要将reduce个数减少。
另一种场景,所有的map跑完后,只有个别节点有reduce在跑。这时候集群资源没有得到充分利用,需要增加reduce的个数以便每个节点都有任务处理。
原则三:每个task的执行时间要合理。
如果一个job,每个map或reduce的执行时间只有几秒钟,就意味着这个job的大部分时间都消耗在task的调度和进程启停上了,因此需要增加每个task处理的数据大小。一般一个task处理时间在1分钟左右比较合适。控制单个task处理任务的大小可通过如下配置来调整。

  • mapreduce.input.fileinputformat.split.maxsize
    可以设置数据分片的数据最大值。由用户定义的分片大小的设置及每个文件block大小的设置,可以计算得分片的大小。计算公式如下:splitSize = Math.max(minSize, Math.min(maxSize, blockSize)) 如果maxSize设置大于blockSize,那么每个block就是一个分片,否则就会将一个block文件分隔为多个分片,如果block中剩下的一小段数据量小于splitSize,还是认为它是独立的分片。
    
  • mapreduce.input.fileinputformat.split.minsize
    可以设置数据分片的数据最小值。 默认:0
    

Shuffle 调优

操作场景

Shuffle阶段是MapReduce性能的关键部分,包括了从Map task将中间数据写到磁盘一直到Reduce task拷贝数据并最终放到reduce函数的全部过程。这一块adoop提供了大量的调优参数。

操作步骤

  1. Map阶段的调优
    判断Map使用的内存大小判断Map分配的内存是否足够,一个简单的办法是查看运行完成的job的Counters中,对应的task是否发生过多次GC,以及GC时间占总task运行时间之比。通常,GC时间不应超过task运行时间的10%,即GC time elapsed(ms)/CPU time spent (ms)<10%。
    当然,Map需要的内存还需要随着Map buffer的调大而对应调整。主要通过如
    下参数进行调整。
  • mapreduce.map.memory.mb
    设置进程占用的总内存。 默认:4096
    
使用Combinner
在Map阶段,有一个可选过程,将同一个key值的中介结果合并,叫做
combinner。一般将reduce类设置为combinner即可。通过combine,一般情况下
可以显著减少Map的输出中间结果,减少写磁盘的内容。
  1. Copy阶段的调优
数据是否压缩
对Map的中间结果进行压缩,当数据量大时,会显著减少网络传输的数据量,但是也因为多了压缩和解压,带来了更多的CPU消耗。因此需要做好权衡。当任务属于网络瓶颈类型时,压缩Map中间结果效果明显。之前调优的bulkload,压缩中间结果后性能提升60%左右。压缩算法建议选择FusionInsight附带的snappy算法,拥有较好的压缩速率和压缩比。
配置方法:将“mapreduce.map.output.compress”参数值设置为“true”,将
“mapreduce.map.output.compress.codec”参数值设置为
“org.apache.hadoop.io.compress.SnappyCodec”。
  1. Merge阶段的调优
    通过调整如下参数减少reduce写磁盘的次数。
  • mapreduce.reduce.merge.inmem.threshold
    允许多少个文件同时存在reduce内存里。当达到这个阈值时,reduce就会触发mergeAndSpill,将数据写到硬盘上。默认:1000
    
  • mapreduce.reduce.shuffle.merge.percent
    当reduce中存放map中间结果的buffer使用达到多少百分比时,会触发merge操作。默认:0.66
    
  • mapreduce.reduce.shuffle.input.buffer.percent
    允许Map中间结果占用reduce堆大小的百分比。默认:0.70
    
  • mapreduce.reduce.input.buffer.percent
    当开始执行reduce函数时,允许map文件占reduce堆大小的百分比。当map文件比较小时,可以将这个值设置成1.0,这样可以避免reduce将拷贝过来的map中间结果写磁盘。默认:0
    

大任务的 AM 调优

操作场景

任务场景:运行的一个大任务,map总数达到了10万的规模,但是一直没有跑成功。经过查询,发现是AM反映缓慢,最终超时失败。
此任务的问题是,task数量变多时,AM管理的对象也线性增长,因此就需要更多的内存来管理。AM默认分配的内存堆大小是1GB。

操作步骤

通过调大如下的参数来进行AM调优。

  • yarn.app.mapreduce.am.resource.mb
    该参数值必须大于下面参数的堆大小。默认1.5GB
    
  • yarn.app.mapreduce.am.command-opts
    传递到 MapReduce ApplicationMaster的 Java 命令行参数。
    默认:-Xmx1024m -XX:CMSFullGCsBeforeCompaction=1 -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -verbose:gc -Xloggc:/tmp/@taskid@.gc -Djava.security.krb5.conf=#{conf_dir:KerberosClient}/kdc.conf -Djava.security.auth.login.config=#{conf_dir}/jaas.conf -Dzookeeper.server.principal=zookeeper/hadoop
    

推测执行

操作场景

当集群规模很大时(如几百上千台节点的集群),个别机器出现软硬件故障的概率就变大了,并且会因此延长整个任务的执行时间(跑完的任务都在等出问题的机器跑结束)。推测执行通过将一个task分给多台机器跑,取先运行完的那个,会很好的解决这个问题。对于小集群,可以将这个功能关闭。

  • mapreduce.map.speculative 
    是否开启map的推测执行。true表示开启。默认false
    
  • mapreduce.reduce.speculative
    是否开启reduce的推测执行。true表示开启。默认false
    

通过“Slow Start”调优

操作场景

MapReduce的AM在申请资源的时候,会一次性申请所有的Map资源,延后申请reduce的资源,这样就能达到先执行完大部分Map再执行Reduce的目的。在有些场景下,需要确保Map执行完,再执行Reduce,或者提前执行Reduce。

  • mapreduce.job.reduce.slowstart.completedmaps
    当多少占比的Map执行完后开始执行reduce。默认100%的Map跑完后开始起reduce。默认1
    

通过 Merge/Sort 流程提升 MR 性能

操作场景

您可以通过调整Merge/Sort流程来提升MR任务的执行性能。

操作步骤

参数入口:在客户端的“${HADOOP_HOME}/etc/hadoop”目录下,编辑hdfs-site.xml文件,增加参数配置。

  • mapreduce.job.reduce.shuffle.consumer.plugin.class
    org.apache.hadoop.mapreduce.task.reduce.ShuffleWithoutBarriers
    

Spark

数据序列化

操作场景

Spark支持两种方式的序列化 :
  • Java原生序列化JavaSerializer
  • Kryo序列化KryoSerializer

序列化对于Spark应用的性能来说,具有很大的影响。在特定的数据格式的情况下,KryoSerializer的性能可以达到JavaSerializer的10倍以上,而对于一些Int之类的基本类型数据,性能的提升就几乎可以忽略。
KryoSerializer依赖Twitter的Chill库来实现,相对于JavaSerializer,主要的问题在于不是所有的Java Serializable对象都能支持,兼容性不好,所以需要手动注册类。
序列化功能用在两个地方 : 序列化任务和序列化数据。Spark任务序列化只支持
JavaSerializer,数据序列化支持JavaSerializer和KryoSerializer。

Spark程序运行时,在shuffle和RDD Cache等过程中,会有大量的数据需要序列化,默认使用JavaSerializer,通过配置让KryoSerializer作为数据序列化器来提升序列化性能。
在开发应用程序时,添加如下代码来使用KryoSerializer作为数据序列化器。

  • 实现类注册器并手动注册类。
public class DemoRegistrator implements KryoRegistrator
{
@Override
public void registerClasses(Kryo kryo)
{
//以下为示例类,请注册自定义的类
kryo.register(AggrateKey.class);
kryo.register(AggrateValue.class);
}
}
  • 配置KryoSerializer作为数据序列化器和类注册器。
val conf = new SparkConf();
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.registrator", "com.etl.common.DemoRegistrator");

配置内存

操作场景

Spark是内存计算框架,计算过程中内存不够对Spark的执行效率影响很大。可以通过监控GC(Gabage Collection),评估内存中RDD的大小来判断内存是否变成性能瓶颈,并根据情况优化。
监控节点进程的GC情况(在conf目录下的spark-env.sh添加export SPARK_JAVA_OPTS="-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"会在节点日志里打印出GC信息),如果频繁出现Full GC,需要优化GC。把RDD做Cache操作,通过日志查看RDD在内存中的大小,如果数据太大,需要改变RDD的存储级别来优化。

操作步骤

  • 优化GC,调整老年代和新生代的大小和比例。在conf目录下的spark-env.sh设置-
    XX:NewRatio可以调整老年代和新生代的比例,如export SPARK_JAVA_OPTS="-XX:NewRatio=2",则新生代占整个堆空间的1/3,老年代占2/3
  • 开发Spark应用程序时,优化RDD的数据结构。
    – 使用原始类型数组替代集合类(fastutil)。
    – 避免嵌套结构。
    – Key尽量不要使用String。
  • 开发Spark应用程序时,序列化RDD
    RDD做cache时默认是不序列化数据的,可以通过设置存储级别来序列化RDD减小内存。例如:
testRDD.persist(StorageLevel.MEMORY_ONLY_SER)

设置并行度

操作场景

并行度控制任务的数量,影响shuffle操作后数据被切分成的块数。调整并行度让任务的数量和每个任务处理的数据与机器的处理能力达到最优。
查看CPU使用情况和内存占用情况,当任务和数据不是平均分布在各节点,而是集中在个别节点时,可以增大并行度使任务和数据更均匀的分布在各个节点。增加任务的并行度,充分利用集群机器的计算能力,一般并行度设置为集群CPU总和的2-3倍。

操作步骤

  • 使用spark.default.parallelism设置并行度,请根据实际的内存、CPU、数据以及应用程序逻辑的情况调整并行度参数。
val conf = new SparkConf();
conf.set("spark.default.parallelism", 24);
  • 在会产生shuffle的操作函数内设置并行度参数,请根据实际的内存、CPU、数据以及应用程序逻辑的情况调整并行度参数。
testRDD.groupByKey(24) ;

使用广播变量

操作场景

Broadcast(广播)可以把数据集合分发到每一个节点上,Spark任务在执行过程中要使
用这个数据集合时,就会在本地查找Broadcast过来的数据集合。如果不使用
Broadcast,每次任务需要数据集合时,都会把数据序列化到任务里面,不但耗时,还

使任务变得很大。

  1. 每个任务分片在执行中都需要同一份数据集合时,就可以把公共数据集Broadcast到每个节点,让每个节点在本地都保存一份。
  2. 大表和小表做join操作时可以把小表Broadcast到各个节点,从而就可以把join操作转变成普通的操作,减少了shuffle操作。

操作步骤

在开发应用程序时,添加如下代码,将“testArr”数据广播到各个节点。

val testArr: Array[Long] = new Array[Long](200);
val testBroadcast: Broadcast[Array[Long]] = sc.broadcast(testArr);
val resultRdd: RDD[Long] = Operater.handleData(testBroadcast);

Spark SQL join 优化

操作场景

Spark SQL 中,当对两个表进行join操作时,利用Broadcast,将小表BroadCast到各个节点上,从而转变成普通的操作,减少了shuffle操作,提高任务执行性能。
在Spark SQL中进行Join操作时,可以按照以下步骤进行优化。为了方便说明,设表A
和表B,且A、B表都有个名为name列。对A、B表进行join操作。

  1. 估计表的大小。
    根据每次加载数据的大小,来估计表大小。
    也可以在Hive的数据库路径下查看表的大小。在Spark的配置文件hive-site.xml中,查看Hive的数据库路径的配置,默认为/user/hive/warehouse。
<property>
 <name>hive.metastore.warehouse.dir</name>
 <value>${test.warehouse.dir}</value>
 <description></description>
</property>

表的大小。如查看表A的大小
hadoop fs -du -s -h ${test.warehouse.dir}/a
2. 配置自动广播的阈值。
Spark中,判断表是否广播的阈值为10485760(即10M)。如果两个表的大小至少有一个小于10M时,可以跳过该步骤。

自动广播阈值的配置参数

  • spark.sql.autoBroadcastJoinThreshold
    默认10485760。当进行join操作时,配置广播的最大值;当表的字节数小于该值时便进行广播。当配置为-1时,将不进行广播。参见https://spark.apache.org/docs/latest/sql-programming-guide.html。
    
配置自动广播阈值的方法:
在Spark的配置文件“spark-defaults.conf”中,设置“spark.sql.autoBroadcastJoinThreshold”的值。其中,根据场景而定,但要求该值至少比其中一个表大。
spark.sql.autoBroadcastJoinThreshold = < size>
利用Hive CLI命令,设置阈值。在运行Join操作时,提前运行下面语句SET spark.sql.autoBroadcastJoinThreshold=< size>
其中,根据场景而定,但要求该值至少比其中一个表大。
  1. 进行join操作。
    这时join的两个table,至少有个表是小于阈值的。
    如果A表和B表都小于阈值,且A表的字节数小于B表时,则运行 B join A,如
  • SELECT A.name FROM B JOIN A ON A.name = B.name;
    

否则 运行A join B。

  • SELECT A.name FROM A JOIN B ON A.name = B.name;
    

使用 Netty 提升传输效率

操作场景

Spark应用中往往存在大量的网络数据传输,因此需使用Java NIO来实现。Spark1.2以前默认是基于原生的Java NIO实现的数据传输服务,因此其鲁棒性并非很完善,而Netty框架简化了原生Java NIO的操作,并提升了整体的鲁棒性。因其提升了数据传输的稳定性,所以在配合Sort-based Shuffle,External Shuffle等使用时,其整体性能有一定的提升。

  • spark.shuffle.blockTransferService
    数据传输的实现方式,当前有两种实现方式:基于原生NIO和基于Netty框架。默认netty
    
  • spark.shuffle.io.numConnectionsPerPeer
    基于Netty下的数据传输服务,两个节点之间的连接数,这些连接在数据传输期间是一直保持的且可重用。如果集群中磁盘多而节点少,可以考虑增加该值来提升并发效率。1
    
说明
1. Spark 1.2+默认使用Netty作为数据传输服务;
2. 当传输数据量较大时增加连接数(增大spark.shuffle.io.numConnectionsPerPeer)可以提高并行
传输的数据量,从而提升传输性能,但该值并非越大越好,数据传输速度依然受限于网络
I/O。

使用 SortShuffle 提升 shuffle 性能

操作场景

Shuffle是Spark中最为重要的一块,也是性能调优必须要考虑的。在Spark1.2以前默认使用的是Hash-based Shuffle,其存在着以下几个问题:

  1. 产生大量的shuffle文件;
  2. 因shuffle结果需要往大量的shuffle文件写,会导致持续不断的I/O操作,从而磁盘一直处于繁忙状态而影响性能。Sort-based Shuffle很好的解决了Hash-based存在的问题,其产生的shuffle文件数远远于-based Shuffle,因shuffle文件数的减少因此I/O相对来说只是很短时间内会很大,不会造成持续不断的I/O操作。
说明
1. Spark 1.2+默认使用Sort-based Shuffle;
2. 当内存足够大时,Hash-based和Sort-based这两种shuffle性能差距;
3. 当使用MLLib时在某些算法下Sort-based的性能会低于Hash-based。
  • spark.shuffle.manager
    Shuffle的实现方式,当前有两种实现方式:基于sort和基于hash。默认sort
    

使用 External Shuffle Service 提升性能

操作场景

Spark系统在运行含shuffle过程的应用时,Executor进程除了运行task,还要负责写shuffle数据以及给其他Executor提供shuffle数据。当Executor进程任务过重,导致触发GC(Garbage Collection)而不能为其他Executor提供shuffle数据时,会影响任务运行。External shuffle Service是长期存在于NodeManager进程中的一个辅助服务。通过该服务来抓取shuffle数据,减少了Executor的压力,在Executor GC的时候也不会影响其他Executor的任务运行。

  • spark.shuffle.service.enabled  修改为 true
    

Yarn 模式下动态资源调度

操作场景

对于Spark应用来说,资源是影响Spark应用执行效率的一个重要因素。当一个长期运行的服务(比如Thrift Server),若分配给它多个Executor,可是却没有任何任务分配给它。而此时有其他的应用却资源紧张,这就造成了很大的资源浪费和资源不合理的调度。
动态资源调度就是为了解决这种场景,根据当前应用任务的负载情况,实时的增减Executor个数,从而实现动态分配资源,使整个Spark系统更加健康。

操作步骤

将“spark.dynamicAllocation.enabled”参数的值设置为“true”,表示开启动态资源调度功能。

下面是一些可选配置

  • spark.dynamicAllocation.minExecutors
    最小Executor个数。 默认0
    
  • spark.dynamicAllocation.initialExecutors
    初始Executor个数。 默认spark.dynamicAllocati
    
  • on.minExecutorsspark.dynamicAllocation.maxExecutors
    最大executor个数。默认 Integer.MAX_VALUE
    
  • spark.dynamicAllocation.schedulerBacklogTimeout
    调度第一次超时时间。 默认1(s)
    
  • spark.dynamicAllocation.sustainedSchedulerBacklogTimeout
    调度第二次及之后超时时间。 默认spark.dynamicAllocati
    
  • on.schedulerBacklogTimeoutspark.dynamicAllocation.executorIdleTimeout
    普通Executor空闲超时时间。 默认60(s)
    
  • spark.dynamicAllocation.cachedExecutorIdleTimeout
    含有cached blocks的Executor空闲超时时间。默认spark.dynamicAllocation.executorIdleTimeout的2倍
    

性能调优示例:广播变量

操作场景

示例描述:使用Broadcast来实现大小表的join操作,去掉shuffle优化执行效率。
当前环境情况:

  • 集群 :2台
  • CPU : 28核
  • 内存 : 315G,程序实际申请10G数据量
  • 大表:25000000条,252M
  • 小表: 5000条 ,34k
  1. 原代码示例如下:
def main(args: Array[String])
{
val conf = new SparkConf();
val sc = new SparkContext(conf);
val bigTableRDD = sc.textFile("/BigTable.csv")
.map(x =>{val token = x.split(","); (token(0), token(1))});
val smallTableRDD = sc.textFile("/SmallTable.csv")
.map(x =>{val token = x.split(","); (token(0), token(1))});
val resuleRDD = bigTableRDD.rightOuterJoin(smallTableRDD);
resuleRDD.count();
}
  1. 该代码的性能问题如下:
    使用join算子,会出现shuffle操作,数量较大时,对性能影响很大,因为数据需要
    在节点之间传输,而且中间节点还要写入磁盘。
  2. 对代码进行优化。
    使用广播把小表广播出去,作为公共查找数据,然后实现join操作,不会造成节点
    间的数据交换也不会写数据在本地磁盘。
  3. 优化后代码如下所示:
def main(args: Array[String])
{
val conf = new SparkConf();
val sc = new SparkContext(conf);
val bigTableRDD = sc.textFile("/BigTable.csv")
.map(x =>{val token = x.split(","); (token(0), token(1))});
val smallTableRDD = sc.textFile("/SmallTable.csv")
.map(x =>{val token = x.split(","); (token(0), token(1))});
val smallTable = smallTableRDD.collect();
val smallTableMap = new HashMap[String, String]();
for(data <- smallTable)
{
smallTableMap.put(data._1, data._2);
}
val smallTableBroadcast: Broadcast[HashMap[String, String]] = sc.broadcast(smallTableMap);
val resultRDD = bigTableRDD.map(x => handle(x, smallTableBroadcast))
.filter(x => x != null)
resultRDD.count();
}
def handle(data: (String, String), smallTableBroadcast: Broadcast[HashMap[String, String]]): 
(String, String, String) =
{
val smallTableMap = smallTableBroadcast.value;
val joinData = smallTableMap.get(data._1);
if(joinData == None)
{
return null;
}
else
{
return (data._1, data._2, joinData.get)
}
}
  1. 优化后的效果如下所示:
  • 未优化前时间 :| 117s| 126s|118s
  • 优化后时间 :| 14s | 14s | 13s
    由上面的运行时间可以看出优化后的效率是未优化效率的近8倍。合理的使用
    broadcast减少shuffle操作,会使程序性能得到很大的提升。

经验总结

使用 mapPartitions,按每个分区计算结果

如果每条记录的开销太大。例:
rdd.map{x=>conn=getDBConn;conn.write(x.toString);conn.close}
则可以使用MapPartitions,按每个分区计算结果
rdd.mapPartitions(records => conn.getDBConn;for(item <- records)
write(item.toString); conn.close)

使用 coalesce 调整分片的数量

coalesce可以调整分片的数量。

  • 当之前的操作有很多filter时,使用coalesce减少空运行的任务数量。
  • 当输入切片个数太大,导致程序无法正常运行时使用。
  • 当任务数过大时候Shuffle压力太大导致程序挂住不动,或者出现linux资源受限的问题。

localDir 配置

Spark的Shuffle过程需要写本地磁盘,Shuffle是Spark性能的瓶颈,I/O是Shuffle的瓶颈。配置多个磁盘则可以并行的把数据写入磁盘。如果节点中挂载多个磁盘,在每个磁盘配置一个Spark的localDir。可以有效分散Shuffle文件的存放,提高磁盘I/O的效率。如果只有一个磁盘,配置了多个目录,性能提升不大。

Collect 小数据

大数据量时不适用collect操作。
Collect操作会将Executor的数据发送到Driver端,使用collect前请确保Driver端内存足够,以免发生OutOfMemory。当不确定数据量大小时,可使用saveAsTextFile等操作把数据写入HDFS,当能大致确定数据大小且driver内存可以存的下的时候,可使用Collect,方便本地调测。

使用 reduceByKey

reduceByKey会在Map端做本地聚合,reduceByKey的Shuffle过程更加平缓。
groupByKey等Shuffle操作不会在Map端做聚合,能使用reduceByKey的地方尽量使用该方式,避免出现groupByKey().map(x=>(x._1,x._2.size))。

广播 map 代替数组

当每个记录需要查表,如果是Driver端传过去的,用广播方式传递数据,数据结构采用set/map替代Iterator。因为Set/Map的查询速率接近O(1),而Iterator是O(n)。

数据倾斜

当数据发生倾斜(某一部分数据量特别大), 虽然没有GC(Gabage Collection,垃圾回收),但是task执行时间严重不一致。

  • 需要重新设计key,以更小粒度的key使得task大小合理化。
  • 修改并行度。

优化数据结构

  • 把数据按列存放,读取数据时就可以只扫描需要的列。
  • 通过设置spark.shuffle.consolidateFiles为true,来合并shuffle中间文件,减少shuffle文件的数量,减少文件IO操作以提升性能。最终文件数为reduce tasks数目。

Hive

建立表分区

操作场景

Hive在做Select查询时,一般会扫描整个表内容,会消耗较多时间去扫描不关注的数据。此时,可根据业务需求及其查询维度,建立合理的表分区,从而提高查询效率。

操作步骤

  1. 使用PuTTY工具,以root用户登录HiveServer所在节点。
  2. 执行以下命令,进入客户端安装目录,例如“/opt/client”。
    cd /opt/client
  3. 配置客户端环境变量。
  4. 在客户端中执行如下命令,执行登录操作。
    kinit 用户名
  5. 执行以下命令登录客户端工具。
    beeline
  6. 指定静态分区或者动态分区。
  • 静态分区:
    静态分区是手动输入分区名称,在创建表时使用关键字PARTITIONED BY指
    定分区列名及数据类型。应用开发时,使用ALTER TABLE ADD PARTITION
    语句增加分区,以及使用LOAD DATA INTO PARTITON语句将数据加载到分
    区时,只能静态分区。
  • 动态分区:通过查询命令,将结果插入到某个表的分区时,可以使用动态分
    区。动态分区通过在客户端工具执行如下命令来开启:
set hive.exec.dynamic.partition=true

动态分区默认模式是strict,也就是必须至少指定一列为静态分区,在静态分
区下建立动态子分区,可以通过如下设置来开启完全的动态分区:

set hive.exec.dynamic.partition.mode=nonstrict
注意
动态分区可能导致一个DML语句创建大量的分区,对应的创建大量新文件夹,对系统
性能可能带来影响。

Join 优化

操作场景

使用Join语句时,如果数据量大,可能造成命令执行速度和查询速度慢,此时可进行Join优化。Join优化可分为Map join和Sort Merge Bucket Map Join两种方式。

Map join

Hive的Map Join适用于能够在内存中存放下的小表(指表大小小于25M),通过
“hive.mapjoin.smalltable.filesize”定义小表的大小,默认为25M。
Map Join的方法有两种:

  • 使用/*+ MAPJOIN(join_table) */来使用map join。
  • 执行语句前设置如下参数,当前版本中该值默认为true。
set hive.auto.convert.join=true

使用Map Join时没有Reduce任务,而是在Map任务前起了一个MapReduce Local Task,这个Task通过TableScan读取小表内容到本机,在本机以HashTable的形式保存并写入硬盘上传到DFS,并在distributed cache中保存,在Map Task中从本地磁盘或者distributed cache中读取小表内容直接与大表join得到结果并输出。
使用Map Join时需要注意小表不能过大,如果小表将内存基本用尽,会使整个系统性能下降甚至出现内存溢出的异常。

Sort Merge Bucket Map Join

使用Sort Merge Bucket Map Join必须满足以下2个条件:

  1. join的两张表都很大,内存中无法存放。
  2. 两张表都按照join key进行分桶(clustered by (column))和排序(sorted by(column)),且
    两张表的分桶数正好是倍数关系。
    通过如下设置,启用Sort Merge Bucket Map Join:
set hive.optimize.bucketmapjoin=true
set hive.optimize.bucketmapjoin.sortedmerge=true

这种Map Join也没有Reduce任务,是在Map任务前启动MapReduce Local Task,将小表内容按桶读取到本地,在本机保存多个桶的HashTable备份并写入HDFS,并保存在Distributed Cache中,在Map Task中从本地磁盘或者Distributed Cache中按桶一个一个读取小表内容,然后与大表做匹配直接得到结果并输出。

注意事项

Join数据倾斜问题

执行任务的时候,任务进度长时间维持在99%,这种现象叫数据倾斜。数据倾斜是经常存在的,因为有少量的Reduce任务分配到的数据量和其他Reduce差异过大,导致大部分Reduce都已完成任务,但少量Reduce任务还没完成的情况。解决数据倾斜的问题,可通过设置set hive.optimize.skewjoin=true并调整hive.skewjoin.key的大小。hive.skewjoin.key是指Reduce端接收到多少个key即认为数据是倾斜的,并自动分发到多个Reduce。

Group By 优化

操作场景

优化Group by语句,可提升命令执行速度和查询速度。Group by的时候, Map端会先进行分组, 分组完后分发到Reduce端, Reduce端再进行分组。可采用Map端聚合的方式来进行Group by优化,开启Map端初步聚合,减少Map的输出数据量。

操作步骤

在Hive客户端进行如下设置:

set hive.map.aggr=true

注意事项

Group By数据倾斜

Group By也同样存在数据倾斜的问题,设置hive.groupby.skewindata为true,生成的查询计划会有两个MapReduce Job,第一个Job的Map输出结果会随机的分布到Reduce中,每个Reduce做聚合操作,并输出结果,这样的处理会使相同的Group By Key可能被分发到不同的Reduce中,从而达到负载均衡,第二个Job再根据预处理的结果按照Group By Key分发到Reduce中完成最终的聚合操作。

Count Distinct聚合问题

当使用聚合函数 count distinct完成去重计数时,处理值为空的情况会使Reduce产生很严重的数据倾斜,可以将空值单独处理,如果是计算count distinct,可以通过where字句将该值排除掉,并在最后的count distinct结果中加1。如果还有其他计算,可以先将值为空的记录单独处理,再和其他计算结果合并。

posted @ 2020-07-15 10:26  sssuperMario  阅读(948)  评论(0)    收藏  举报