大数据调优总结

一、 MapReduce

1、数据倾斜调优

数据中不可避免地会出现离群值(outlier),并导致数据倾斜。这些离群值会显著地拖慢MapReduce的执行。
数据倾斜会导致map和reduce的任务执行时间大为延长,也会让需要缓存数据集的操作消耗更多的内存资源

常见的数据倾斜有以下几类:

  • 1)数据频率倾斜——某一个区域的数据量要远远大于其他区域。比如某一个key对应的键值对远远大于其他键的键值对。
    • 2)数据大小倾斜——部分记录的大小远远大于平均值。

2、如何诊断哪些键存在数据倾斜?

  • 发现倾斜数据之后,有必要诊断造成数据倾斜的那些键。有一个简便方法就是在代码里实现追踪每个键的最大值
  • 为了减少追踪量,可以设置数据量阀值,只追踪那些数据量大于阀值的键,并输出到日志中。
  • 运行作业后就可以从日志中判断发生倾斜的键以及倾斜程度;跟踪倾斜数据是了解数据的重要一步,也是设计MapReduce作业的重要基础

1)预聚合

提前在map进行combine,减少传输的数据量

2)自定义分区

根据数据分布情况,自定义散列函数,将key均匀分配到不同Reducer

3)局部聚合加全局聚合。

二次mr,第一次将key随机散列到不同reducer进行处理达到负载均衡目的。第二次再根据去掉key的随机前缀,按原key进行reduce处理,性能稍差。

4)增加Reducer,提升并行度

JobConf.setNumReduceTasks(int)

5)数据大小倾斜,

调参line.maxlength,限制RecordReader读取最大长度。

2、MR优化

Map阶段优化

(1)增大环形缓冲区大小。由100m扩大到200m
(2)增大环形缓冲区溢写的比例。由80%扩大到90%
(3)减少对溢写文件的merge次数。(10个文件,一次20个merge)
(4)不影响实际业务的前提下,采用Combiner提前合并,减少 I/O。

Reduce阶段优化

(1)合理设置Map和Reduce数:两个都不能设置太少,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致 Map、Reduce任务间竞争资源,造成处理超时等错误。
(2)设置Map、Reduce共存:调整slowstart.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,减少Reduce的等待时间。
(3)规避使用Reduce,因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。
(4)增加每个Reduce去Map中拿数据的并行数
(5)集群性能可以的前提下,增大Reduce端存储数据内存的大小。

IO传输

采用数据压缩的方式,减少网络IO的时间。安装Snappy和LZOP压缩编码器。

压缩:

(1)map输入端主要考虑数据量大小和切片,支持切片的有Bzip2、LZO。注意:LZO要想支持切片必须创建索引。
(2)map输出端主要考虑速度,速度快的snappy、LZO。
(3)reduce输出端主要看具体需求,例如作为下一个mr输入需要考虑切片,永久保存考虑压缩率比较大的gzip。

3、如何减小reduce端数据倾斜

数据频率倾斜 ,常用方式有:分区、预聚合

1、自定义分区:基于输出键的背景知识,进行自定义分区。

例如,如果map输出键的单词来源于一本书。其中大部分必然是省略词(stopword)。那么就可以将自定义分区将这部分省略词发送给固定的一部分reduce实例。而将其他的都发送给剩余的reduce实例。

2、Combine预聚合:使用Combine,可大量减小数据频率倾斜和数据大小倾斜。combine的目的就是聚合并精简数据。

3、抽样和范围分区

  • Hadoop默认的分区器是HashPartitioner,基于map输出键的哈希值分区。这仅在数据分布比较均匀时比较好。在有数据倾斜时就很有问题。
  • 使用分区器,需要首先了解数据的特性。TotalOrderPartitioner中,可以通过对原始数据进行抽样得到的结果集来预设分区边界值
  • TotalOrderPartitioner中的范围分区器可以通过预设的分区边界值进行分区。因此它也可以很好地用在矫正数据中的部分键的数据倾斜问题。

4、数据大小倾斜

在map端或reduce端的数据大小倾斜,都会对缓存造成较大的影响,乃至OOM异常。

方法就是:根源上处理;以及设置RecordReader读取的line.maxlength最大长度,默认无限制

  • 设置mapreduce.input.linerecordreader.line.maxlength,来限制RecordReader读取的最大长度。
  • RecordReader在TextInputFormat和KeyValueTextInputFormat类中使用。默认长度没有上限。

二、hive调优

1、配置上开启:Fetch抓取、 本地模式、 严格模式、数据压缩、并行执行

1)开启Fetch抓取,不必使用MapReduce计算

对某些情况的查询可以不必使用MapReduce计算,在全局查找、字段查找、limit查找等都不走mapreduce。
把hive-default.xml.template文件中hive.fetch.task.conversion设置成more,然后执行查询语句,查询方式都不会执行mr程序。 默认是more,(老版本minimal);设置成none,然后执行查询语句,都会执行mapreduce程序

2)开启本地模式:如果数据量小,只启动一个Maptask

默认情况下是启用hadoop的job模式,把任务提交到集群中运行,这样会导致计算非常缓慢;

开启本地模式,并执行查询语句

set hive.exec.mode.local.auto=true; //开启本地mr

3)开启严格模式,禁止3种类型的查询

开启严格模式,可以禁止3种类型的查询。

  • 分区表,where 含有分区字段 过滤条件 来限制范围,否则不允许执行
  • order by,必须使用limit语句
  • 限制笛卡尔积的查询

防止用户执行,那些可能意想不到的不好的影响的查询。
配置:set hive.mapred.mode=strict; 默认是非严格模式nonstrict

4)开启数据的压缩,

Hive表中间数据压缩Hive表最终输出结果压缩,

5)设置并行执行

把一个sql语句中没有相互依赖的阶段,并行去运行,提高集群资源利用率配置:
set hive.exec.parallel=true;
set hive.exec.parallel.thread.number=16;

2、开发上:表的join优化、列裁剪、 限制笛卡尔积、避免数据倾斜

1)表的join优化

  • 1)老版本hive,大小表 join 时,小表放在join的左边;
  • 2)大表 join 大表 时,空 key 过滤,空 key 赋一个随机的值;
  • 3)map join ,在Map端先进行部分聚合,最后在Reduce端得出最终结果;
  • 4)count distinct,使用先group by 再count的方式替换;
  • 5)多个表关联时,最好分拆成小段,避免大sql(无法控制中间Job);

2)使用分区剪裁、列剪裁 ,

  • 尽可能早地过滤掉尽可能多的数据量,避免大量数据流入外层SQL。
  • 尽量使用分区过滤,少用select *

3)限制笛卡尔积的查询

4)避免数据倾斜:

合理设置Map数 ;合理设置Reduce数;
小文件合并; 复杂文件增加Map数 ;

三、spark调优

1、资源调优:

(1)分配更多的资源:executor-memory、executor-cores、driver-memory

(2)提高并行度:task的数量cpu core数量的2~3倍,使用rdd.repartition 来重新分区

2、开发调优

(1)RDD持久化

可以把多次使用到的rdd,也就是公共rdd进行持久化,避免后续需要,再次重新计算,提升效率。

可以调用rdd的cache或者persist方法。

  • 1)cache方法默认是把数据持久化到内存中 ,例如:rdd.cache ,其本质还是调用了persist方法
  • 2)persist方法中有丰富的缓存级别,这些缓存级别都定义在StorageLevel这个object中,可以结合实际的应用场景合理的设置缓存级别。例如:
    rdd.persist(StorageLevel.MEMORY_ONLY),这是cache方法的实现。

(2)使用广播变量

若要处理的共享数据量非常大,并且一个stage中出现大量的task
,会通过网络将数据传输到各个task中去,给task使用,会涉及大量的网络传输开销与内存开销,可能会导致频繁的垃圾回收器的回收GC。

一些维度数据进行广播,该executor上的各个task再从所在节点的BlockManager获取变量,而不是从Driver获取变量,从而提升了效率。

task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝试获取变量副本;如果本地没有,那么就从Driver远程拉取广播变量副本,并保存在本地的BlockManager中;此后这个executor上的task,都会直接使用本地的BlockManager中的副本。

注意:
(1)不能将一个RDD使用广播变量广播出去,因为RDD是不存储数据的。可以将RDD的结果广播出去。
(2)广播变量只能在Driver端定义,不能在Executor端定义。
(3)在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
(4)如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
(5)如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。

配置:
  (1)通过sparkContext的broadcast方法把数据转换成广播变量,类型为Broadcast,
  val broadcastArray: Broadcast[Array[Int]] = sc.broadcast(Array(1,2,3,4,5,6))
  (2) 然后executor上的BlockManager就可以拉取该广播变量的副本获取具体的数据。
  获取广播变量中的值可以通过调用其value方法
	 val array: Array[Int] = broadcastArray.value

(3)避免shuffle类算子, joingroupByKey distinctrepartition

shuffle涉及到数据要进行大量网络传输,下游阶段task任务需要通过网络拉取上阶段task输出数据,将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。

解决方法:

1) Broadcast+map代替join操作

传统的join操作会导致shuffle操作。因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。

使用Broadcast将一个数据量较小的RDD作为广播变量。Broadcast+map的join操作,不会导致shuffle操作。

2) reduceByKey或aggregateByKey代替groupByKey ,进行预聚合
  • reduceByKey/aggregateByKey 可以进行预聚合操作,减少数据的传输量,提升性能
  • groupByKey 不会进行预聚合操作,进行数据的全量拉取,性能比较低

一定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。

map-side预聚合,指在每个节点本地,对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。

建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。 而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。

(4)使用高性能算子:mapPartitionsforeachPartitionsrepartitionAndSortWithinPartitionsfiltercoalesce

1)mapPartitions替代普通map

mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。

但是有的时候,使用mapPartitions会出现**OOM(内存溢出)**的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!

2)foreachPartitions替代foreach

也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。

在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。
比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;

但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。

3)filter之后进行coalesce操作

通常对一个RDD执行filter算子,过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。

  • 因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。

  • 因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。

4)repartitionAndSortWithinPartitions替代repartition与sort类操作

repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。

因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

(5)使用Kryo优化序列化性能

数据跨进程的网络传输、数据的持久化,这个时候就需要对数据进行序列化。

Spark默认采用Java序列化器。默认java序列化的优缺点如下:

  • 优点: 处理方便,不需我们手动做其他操作,只是在使用一个对象和变量的时候,需要实现Serializble接口。

  • 缺点: 默认的序列化机制的效率不高,序列化的速度比较慢;序列化后数据,占用的内存空间相对还是比较大。

Kryo序列化,比Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。

Kryo序列化机制,一旦启用以后,会生效的几个地方

(1)算子函数中使用到的外部变量,算子中的外部变量可能来着与driver需要涉及到网络传输,就需要用到序列化。最终可以优化网络传输的性能,优化集群中内存的占用和消耗
(2)持久化RDD时进行序列化StorageLevel.MEMORY_ONLY_SER 将rdd持久化时,对应的存储级别里,需要用到序列化。 最终可以优化内存的占用和消耗;持久化RDD占用的内存越少,task执行的时候,创建的对象,就不至于频繁的占满内存,频繁发生GC。
(3)产生shuffle的地方,也就是宽依赖 下游的stage中的task,拉取上游stage中的task产生的结果数据,跨网络传输,需要用到序列化。最终可以优化网络传输的性能;

开启Kryo序列化机制:

// 创建SparkConf对象。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 设置序列化器为KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")// 注册要序列化的自定义类型。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

四、Spark数据倾斜调优

1、数据倾斜原因

数据倾斜只会发生在shuffle过程中。 可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。

出现数据倾斜时,可能就是你的代码中使用了这些算子中的某一个所导致的。因为某个或者某些key对应的数据,远远的高于其他的key。

数据本身问题
(1)key本身分布不均衡(包括大量的key为空)
(2)key的设置不合理
spark使用不当的问题
(1)shuffle时的并发度不够
(2)计算方式有误

2、数据倾斜的后果

(1)spark中的stage的执行时间受限于最后那个执行完成的task,因此运行缓慢的任务会拖垮整个程序的运行速度(分布式程序运行的速度是由最慢的那个task决定的)。
(2)过多的数据在同一个task中运行,将会把executor内存撑爆,导致OOM内存溢出。

3、数据倾斜的解决方法

1、过滤,少数导致倾斜的key

如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。

2、提高shuffle操作的并行度(效果差)

增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。

举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。

只是缓解了数据倾斜而已,没有彻底根除问题,根据实践经验来看,其效果有限。

3、两阶段聚合,局部聚合+全局聚合

  • 第一次是局部聚合,先给每个key都打上一个随机数,对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。

  • 然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

rdd.flatMap( line => line.split(" "))
      .map(word =>{
        val prefix = (new util.Random).nextInt(3)
        (prefix+"_"+word,1) })
       .reduceByKey(_+_)
       .map( wc =>{
         val newWord=wc._1.split("_")(1)
         val count=wc._2
         (newWord,count)
       }).reduceByKey(_+_)
      .foreach( wc =>{
        println("单词:"+wc._1 + " 次数:"+wc._2)
      })

4、大RDDjoin小RDD,小RDD转为广播+map,避免shuffle ,

普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。

但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。

  • 不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。

  • 将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。

5、大RDDjoin大RDD,采样倾斜key并分拆join操作

对于join导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key分拆成独立RDD,并附加随机前缀打散成n份去进行join,此时这几个key对应的数据就不会集中在少数几个task上,而是分散到多个task进行join了。

6、大量key倾斜,使用随机前缀与扩容RDD

如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用这一种方案来解决问题了。

方案实现思路:   
 1、 首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。  
  2、然后,将该RDD的每条数据,都打上一个n以内的随机前缀
 3、同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。
  4、最后将两个处理后的RDD进行join即可。

方案实现原理:
 将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。
 
该方案与“解决方案5”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;

而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。

  • 方案优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
  • 方案缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。

方案实践经验:曾经开发一个数据需求的时候,发现一个join导致了数据倾斜。优化之前,作业的执行时间大约是60分钟左右;使用该方案优化之后,执行时间缩短到10分钟左右,性能提升了6倍。

7、整合hive时,使用Hive ETL预处理数据

五、sparkSQL调优

1、设置在内存中缓存数据

性能调优主要是将数据放入内存中操作,spark缓存注册表的方法

spark2.+
spark.catalog.cacheTable("tableName")缓存表 spark.catalog.uncacheTable("tableName")解除缓存

spark1.+
sqlContext.cacheTable("tableName")缓存 sqlContext.uncacheTable("tableName") 解除缓存

spark.sql.inMemoryColumnarStorage.batchSize:默认10000,缓存批处理大小。缓存数据时, 较大的批处理大小可以提高内存利用率和压缩率,但同时也会带来 OOM(Out Of Memory)的风险。

2、设置并行度参数

spark.sql.shuffle.partitions:默认200, 用于配置 join 或aggregate混洗(shuffle)数据时使用的分区数。

spark.default.parallelism: 对于分布式shuffle操作像reduceByKey和join,父RDD中分区的最大数目。

  • 对于无父RDD的并行化等操作,它取决于群集管理器:
    • 本地模式:本地计算机上的核心数
    • 其他:所有执行节点上的核心总数或2,

3、 设置广播参数

表join时,将小表广播可以提高性能,spark2.+中可以调整以下参数、

spark.sql.broadcastTimeout:默认300,广播等待超时时间,单位秒。

spark.sql.autoBroadcastJoinThreshold: 默认10M 用于配置一个表在执行 join 操作时能够广播给所有 worker 节点的最大字节大小。
通过将这个值设置为 -1 可以禁用广播。注意,当前数据统计仅支持已经运行了 ANALYZE TABLE COMPUTE STATISTICS noscan 命令的 Hive Metastore 表。

4、设置压缩

spark.sql.inMemoryColumnarStorage.compressed:默认值true。Spark SQL 将会基于统计信息自动地为每一列选择一种压缩编码方式。

5、文件读取与分区参数

spark.sql.files.maxPartitionBytes:默认值134217728 (128 MB) 打包传入一个分区的最大字节,在读取文件的时候。

  • 读取文件的时候一个分区接受多少数据;
  • 整要结合你想要的并发度及内存的大小来进行。

spark.sql.files.openCostInBytes:默认值4194304 (4 MB),合并小文件的阈值,小于这个阈值的文件将会合并

  • 文件打开的开销,通俗理解就是小文件合并的阈值。
  • 用相同时间内可以扫描的数据的大小,用来衡量打开一个文件的开销。当将多个文件写入同一个分区的时候该参数有用。该值设置大一点有好处,有小文件的分区会比大文件分区处理速度更快(优先调度)

6、文件格式,使用parquet或者orc

parquet已经可以达到很大的性能了

六、SparkStreaming调优

1、调整 并行度

BlockRDD的分区数

  • a. 通过Receiver接受数据的特点决定
  • b. 也可以自己通过repartition设置

ShuffleRDD的分区数

  • a. 默认的分区数为spark.default.parallelism(core的大小)
  • b. 通过我们自己设置决定
 words.map(x => (x, 1))
 .reduceByKey((a: Int, b: Int)
  => a + b, new HashPartitioner(10))

2、调优内存

需要内存大小和transformation的类型有关,如果使用的是updateStateByKey,Window这样的算子,那么内存就要设置得偏大。

如果把接收到的数据,设置的存储级别是MEMORY_DISK这种级别,也就是说如果内存不够可以把数据存储到磁盘上,其实性能还是不好的,
性能最好的就是所有的数据都在内存里面,所以如果在资源允许的情况下,把内存调大一点,让所有的数据都存在内存里面。

3、使用Kryo序列化

使用Kryo序列化机制,比Java序列化机制性能好

SparkStreaming,两种需要序列化的数据:

  • a.输入的数据:默认是 存储在executor上的内存中
  • b.缓存的数据:默认是存储的内存中
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
val sc = new SparkContext(conf)

4、设置反压

Feedback Loop : 动态使得Streaming app从unstable状态回到stable状态
从Spark1.5版本开始:spark.streaming.backpressure.enabled = true

5、设置资源动态分配

动态分配资源:批处理动态的决定这个application中需要多少个Executors,当一个Executor空闲的时候,将这个Executor杀掉;当task太多的时候,动态的启动Executors。

Streaming分配Executor的原则是比对 process time / batchInterval 的比率。如果延迟了,那么就自动增加资源

从Spark2.0有这个功能:spark.streaming.dynamicAllocation.enabled = true

6、避免数据倾斜

因为SparkStreaming的底层就是RDD,之前我们讲SparkCore的所有的数据倾斜的调优策略(见SparkCore调优)都适合于SparkStreaming,大家一定要灵活掌握,这个在实际开发的工作当中用得频率较高,各位同学面试的时候也可以从这个角度跟面试官聊。

五、flink调优

job作业的问题定位

问题定位口诀:
   一压二查三指标,
   延迟吞吐是核心。
   时刻关注资源量 ,
   排查首先看GC 。

  • 1)一压,是指背压,遇到问题先看背压的情况。
    看反压 :通常最后一个被压高的 subTask 的下游就是 job 的瓶颈之一。
  • 2)二查,就是指 checkpoint ,对齐数据的时间是否很长,state 是否很大,这些都是和系统吞吐密切相关的。
    看 Checkpoint 时长 :Checkpoint 时长能在一定程度影响 job 的整体吞吐。
  • 3)三指标,就是指 Flink UI 一些展示, 延迟和吞吐,系统资源,还有就是 GC logs。
  • 看核心指标 :指标是对一个任务性能精准判断的依据,延迟指标吞吐则是其中最为关键的指标。
  • 资源的使用率:提高资源的利用率是最终的目的。

1、避免出现反压

1)反压理解

Flink 拓扑中每个节点(Task)间的数据都,以阻塞队列的方式传输,下游来 不及消费导致队列被占满后,上游的生产也会被阻塞,最终导致数据源的摄入被阻塞。

短时间的负载高峰导致系统接收数据的速率远高于它处理数据的速率。许多日常问题都会导致反压,例如,垃圾回收停顿可能会导致流入的数据快速堆积,或遇到大促、秒杀活动导致流量陡增。

2)反压危害

反压如果不能得到正确的处理,可能会影响到 checkpoint 时长和 state 大小,甚至可能会导致资源耗尽甚至系统崩溃。

  • 1)影响 checkpoint 时长:barrier 不会越过普通数据,数据处理被阻塞也会导致checkpoint barrier 流经整个数据管道的时长变长,导致 checkpoint 总体时间变长。

  • 2)影响 state 大小:barrier 对齐时,接受到较快的输入管道的 barrier 后,它后面数据会被缓存起来但不处理,直到较慢的输入管道的 barrier 也到达,这些被缓存的数据会被放到 state 里面,导致 checkpoint 变大。

这两个影响对于生产环境的作业来说是十分危险的,
因为 checkpoint 是保证数据一致性的关键,checkpoint 时间变长有可能导致checkpoint 超时失败,而 state 大小同样可能拖慢 checkpoint 甚至导致 OOM (使用 Heap-based StateBackend)或者物理内存使用超出容器资源(使用 RocksDBStateBackend)的稳定性问题

因此,我们在生产中要尽量避免出现反压的情况。

3)反压定位

1) 利用 Flink Web UI 定位
Flink Web UI 的反压监控提供了 SubTask 级别的反压监控,1.13 版本以前是通过周期性对 Task 线程的栈信息采样,得到线程被阻塞在请求 Buffer(意味着被下游队列阻塞) 的频率来判断该节点是否处于反压状态 。
2)利用 Metrics 定位
监控反压时会用到的 Metrics 主要与 Channel接受端的 Buffer 使用率有关

4)反压的原因及处理

反压可能是暂时的,可能是由于负载高峰、CheckPoint 或作业重启引起的数据积压而导致反压。
如果反压是暂时的,应该忽略它

另外,断断续续的反压会影响我们分析和解决问题。
定位到反压节点后,分析造成原因的办法主要是观察 Task Thread。按照下面的顺序,
一步一步去排查

  • 1)数据倾斜排查
    在实践中,很多情况下的反压是由于数据倾斜造成的,这点我们可以通过 Web UI 各个 SubTask 的 Records Sent 和 Record Received 来确认,另外 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标;
  • 2)使用火焰图分析
    如果不是数据倾斜,最常见的问题可能是用户代码的执行效率问题(频繁被阻塞或者性 能问题),需要找到瓶颈算子中的哪部分计算逻辑消耗巨大。
    Flink 1.13 直接在 WebUI 提供 JVM 的 CPU 火焰图,这将大大简化性能瓶颈的分析,默认是不开启的,需要修改参数
  • 3)分析 GC 情况
    TaskManager 的内存以及 GC 问题也可能会导致反压,包括 TaskManager JVM 各区内存不合理导致的频繁 Full GC 甚至失联。通常建议使用默认的 G1 垃圾回收器。
    可以通过打印 GC 日志(-XX:+PrintGCDetails),使用 GC 分析器(GCViewer 工具) 来验证是否处于这种情况

2、Checkpoint 调优

一般需求,我们的 Checkpoint 时间间隔可以设置为分钟级别(1 ~5 分钟)。对于状态很大的任务每次 Checkpoint 访问 HDFS 比较耗时,可以设置为 5~10 分钟一次Checkpoint,

并且调大两次 Checkpoint 之间的暂停间隔,例如设置两次 Checkpoint 之间至少暂停 4 或 8 分钟。

同时,也需要考虑时效性的要求,需要在时效性和性能之间做一个平衡,如果时效性要求高,结合 end- to-end 时长,设置秒级或毫秒级。

如果 Checkpoint 语义配置为 EXACTLY_ONCE,那么在 Checkpoint 过程中还会存在 barrier 对齐的过程, 可以通过 Flink Web UI 的 Checkpoint 选项卡来查看 Checkpoint 过程中各阶段的耗时情况,从而确定到底是哪个阶段导致 Checkpoint 时间过长然后针对性的解决问题。

3、状态调优

RocksDB 是基于 LSM Tree 实现的(类似 HBase),写数据都是先缓存到内存中, 所以 RocksDB
的写请求效率比较高。

  • RocksDB 使用内存结合磁盘的方式来存储数据,每次获取数据时,先从内存中 blockcache中查找,如果内存中没有再去磁盘中查询。使用 RocksDB 时,状态大小仅受可用磁盘空间量的限制,性能瓶颈主要在于 RocksDB 对磁盘的读请求,每次读写操作都必须对数据进行反序列化或者序列化。当处理性能不够时,仅需 要横向扩展并行度即可提高整个 Job 的吞吐量。

1)开启增量检查点

RocksDB 是目前唯一可用于支持,有状态流处理应用程序 增量检查点的状态后端,可以 修改参数开启增量检查点: state.backend.incremental: true #默认 false,改为 true。
或代码中指定 new EmbeddedRocksDBStateBackend(true)

2)开启本地恢复

当 Flink 任务失败时,可以基于本地的状态信息进行恢复任务,可能不需要从 hdfs 拉 取数据。本地恢复目前仅涵盖键控类型的状态后端(RocksDB),MemoryStateBackend
不支持本地恢复并忽略此选项。 state.backend.local-recovery: true

3)设置多目录

如果有多块磁盘,也可以考虑指定本地多目录
state.backend.rocksdb.localdir: /data1/flink/rocksdb,/data2/flink/rocksdb,/data3/flink/rocksdb存储

注意:不要配置单块磁盘的多个目录,务必将目录配置到多块不同的磁盘上,让多块磁盘来
分担压力。

4)增大 block 缓存

整个 RocksDB 共享一个 block cache,读数据时内存的 cache 大小,该参数越大读 数据时缓存命中率越高,默认大小为 8 MB,建议设置到 64 ~ 256 MB。
state.backend.rocksdb.block.cache-size: 64m #默认 8m

5)增大 write buffer 内存、数量和 level 阈值大小

RocksDB 中,每个 State 使用一个 Column Family,每个 Column Family 使用独 占的 write buffer,默认 64MB,建议调大。

每个 Column Family 对应的 writebuffer 最大数量,这实际上是内存中“只读内存 表“的最大数量,默认值是 2。对于机械磁盘来说,如果内存足够大,可以调大到 5 左右
state.backend.rocksdb.writebuffer.count: 5调整这个参数通常要适当增加 L1 层的大小阈值 max-size-level-base,默认 256m。

4 、解决数据倾斜

通过 Web UI 各个 SubTask 的 Records Sent 和 Record Received 来确认,另外 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标;

1)使用 LocalKeyBy 的思想

在 keyBy 上游算子数据发送之前,首先在上游算子的本地对数据进行预聚合后,再发送 到下游,使下游接收到的数据量大大减少,从而使得 keyBy 之后的聚合操作不再是任务的 瓶颈。类似 MapReduce 中 Combiner 的思想,但是这要求聚合操作必须是多条数据或者一批数据才能聚合,单条数据没有办法通过聚合来减少数据量。

从 Flink LocalKeyBy 实 现原理来讲,必然会存在一个积攒批次的过程,在上游算子中必须攒够一定的数据量,对这些数据聚合后再发送到下游。

实现方式:
➢ DataStreamAPI 需要自己写代码实现
➢ SQL 可以指定参数,开启 miniBatch 和 LocalGlobal 功能(推荐,后续介绍)

2)强制rebalance 或 rescale 将数据均匀分配

对于不存在 keyBy 的 Flink 任务也会出现该情况。 这种情况,需要让 Flink 任务强制进行 shuffle。使用 shuffle、rebalance 或 rescale 算子即可将数据均匀分配,从而解决数据倾斜的问题。

3)窗口两阶段聚合

因为使用了窗口,变成了有界数据(攒批)的处理,窗口默认是触发时才会输出一条结 果发往下游,

➢ 第一阶段聚合:key 拼接随机数前缀或后缀,进行 keyby、开窗、聚合
注意:聚合完不再是 WindowedStream,要获取 WindowEnd 作为窗口标记作为第二
阶段分组依据,避免不同窗口的结果聚合到一起)

➢ 第二阶段聚合:按照原来的 key 及 windowEnd 作 keyby、聚合

5、调整内存

在这里插入图片描述

6、调整并行度

全局并行度计算

  • 开发完成后,先进行压测。任务并行度给 10 以下,测试单个并行度的处理上限。然后 总 QPS /单并行度的处理能力 = 并行度
  • 开发完Flink 作业,压测的方式很简单,先在 kafka 中积压数据,之后开启 Flink 任务,出现反压,就是处理瓶颈。相当于水库先积水,一下子泄洪。
  • 不能只从 QPS 去得出并行度,因为有些字段少、逻辑简单的任务,单并行度一秒处理几万条数据。而有些数据字段多,处理逻辑复杂,单并行度一秒只能处理 1000 条数据。
    最好根据高峰期的 QPS 压测,并行度*1.2倍,富余一些资源。

1)并行度分为4个等级

增加任务的并行度,充分利用集群机器的计算能力,一般并行度设置为集群CPU核数总和的2-3倍。
Flink程序运行在执行环境中。执行环境为所有执行的算子、数据源、data sink定义了一个默认的并行度。
(1)算子层次 : 跟在某个算子后面用setParallelism()指定

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = [...]
wordCounts = text.flatMap(new LineSplitter()).keyBy(0)
                  .timeWindow(Time.seconds(5))
                  .sum(1).setParallelism(5);
wordCounts.print();
env.execute("Word Count Example");

(2)执行环境层次

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(3);
    DataStream<String> text = [...]
    DataStream<Tuple2<String, Integer>> wordCounts = [...]
    wordCounts.print();
    env.execute("Word Count Example");

(3)客户端层次, ‘-p’ 指令 ./bin/flink run -p 5 ../xxxx/xxxxx.jar
(4)系统层次 在客户端conf目录下的“flink-conf.yaml”文件中的“parallelism.default” 配置选项来指定所有执行环境的默认并行度。

2)Source 端并行度的配置

数据源端是 Kafka,Source 的并行度设置为 Kafka 对应 Topic 的分区数。 如果已经等于 Kafka 的分区数,消费速度仍跟不上数据生产速度,考虑下 Kafka 要扩大分区,同时调大并行度等于分区数。
Flink 的一个并行度可以处理一至多个分区的数据,如果并行度多于 Kafka 的分区数, 那么就会造成有的并行度空闲,浪费资源。

3)Transform 端并行度的配置

➢ Keyby 之前的算子
一般不会做太重的操作,都是比如 map、filter、flatmap 等处理较快的算子,并行度可以和 source 保持一致。

➢ Keyby 之后的算子
如果并发较大,建议设置并行度为 2 的整数次幂,例如:128、256、512;
小并发任务的并行度不一定需要设置成 2 的整数次幂;
大并发任务如果没有 KeyBy,并行度也无需设置为 2 的整数次幂;

4)Sink 端并行度的配置

Sink 端是数据流向下游的地方,可以根据 Sink 端的数据量及下游的服务抗压能力进行评估。

  • 如果 Sink 端是 Kafka,可以设为 Kafka 对应 Topic 的分区数。
  • Sink 端的数据量小,比较常见的就是监控告警的场景,并行度可以设置的小一些。

Source 端的数据量是最小的,拿到 Source 端流过来的数据后做了细粒度的拆分,数
据量不断的增加,到 Sink 端的数据量就非常大。那么在 Sink 到下游的存储中间件的时候就需要提高并行度。

另外 Sink 端要与下游的服务进行交互,并行度还得根据下游的服务抗压能力来设置, 如果在 Flink Sink 这端的数据量过大的话,且 Sink 处并行度也设置的很大,但下游的服务完全撑不住这么大的并发写入,可能会造成下游服务直接被写挂,所以最终还是要在 Sink 尚硅谷大数据技术之 Flink 优化处的并行度做一定的权衡;

三、Hbase读优化

1、HBase客户端优化

1)大scan场景下,将scan缓存从100增大到500或者1000,用以减少RPC次数

一次scan会返回大量数据,因此客户端发起一次scan请求,实际并不会一次就将所有数据加载到本地,而是分成多次RPC请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生OOM

在这样的设计体系下,用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在scan缓存中,默认100条数据大小。

通常情况下,默认的scan缓存设置就可以正常工作的。但是在一些大scan(一次scan可能需要查询几万甚至几十万行数据)来说,每次请求100条数据意味着一次scan需要几百甚至几千次RPC请求,这种交互的代价无疑是很大的。因此可以考虑将scan缓存设置增大,比如设为500或者1000就可能更加合适。
在一次scan扫描10w+条数据量的条件下,将scan缓存从100增加到1000,可以有效降低scan请求的总体延迟,延迟基本降低了25%左右。

2)使用批量get进行读取,get(List gets)函数,减少RPC请求

HBase分别提供了单条get以及批量get的API接口,使用批量get接口可以减少客户端到RegionServer之间的RPC连接数,提高读取性能。另外需要注意的是,批量get请求要么成功返回所有请求数据,要么抛出异常。

3)指定列族或者列名,进行精确查找

HBase是典型的列族数据库,意味着同一列族的数据存储在一起,不同列族的数据分开存储在不同的目录下。如果一个表有多个列族,只是根据Rowkey而不指定列族进行检索的话,不同列族的数据需要独立进行检索,性能必然会比指定列族的查询差很多,很多情况下甚至会有2倍~3倍的性能损失。

4)离线批量读取请求,设置禁用缓存,scan.setBlockCache(false)

通常离线批量读取数据,会进行一次性全表扫描,一方面数据量很大,另一方面请求只会执行一次。这种场景下如果使用scan默认设置,就会将数据从HDFS加载出来之后放到缓存。可想而知,大量数据进入缓存必将其他实时业务热点数据挤出,其他业务不得不从HDFS加载,进而会造成明显的读延迟毛刺;

2、HBase服务器端优化

1)读请求数据均衡,RowKey散列化处理,同时建表必须进行预分区处理

极端情况下,假如所有的读请求都落在一台RegionServer的某几个Region上,这一方面不能发挥整个集群的并发处理能力,另一方面势必造成此台RegionServer资源严重消耗(比如IO耗尽、handler耗尽等),落在该台RegionServer上的其他业务会因此受到很大的波及。
可见,读请求不均衡不仅会造成本身业务性能很差,还会严重影响其他业务。当然,写请求不均衡也会造成类似的问题,可见负载不均衡是HBase的大忌。

2)BlockCache的设置,读多写少业务可以将BlockCache占比调大。

BlockCache作为读缓存,对于读性能来说至关重要。默认情况下BlockCache和Memstore的配置相对比较均衡(各占40%),可以根据集群业务进行修正,比如读多写少业务可以将BlockCache占比调大。

另一方面,BlockCache的策略选择也很重要,不同策略对读性能来说影响并不是很大,但是对GC的影响却相当显著,尤其BucketCache的offheap模式下GC表现很优越。另外,HBase 2.0对offheap的改造(HBASE-11425)将会使HBase的读性能得到2~4倍的提升,同时GC表现会更好!

观察确认:观察所有RegionServer的缓存未命中率、配置文件相关配置项一级GC日志,确认BlockCache是否可以优化

优化建议:JVM内存配置量 < 20G,BlockCache策略选择LRUBlockCache;否则选择BucketCache策略的offheap模式;期待HBase 2.0的到来!

3)HFile文件是否太多,设置compactionThreshold不能太大,默认是3个

HBase读取数据,通常首先会到Memstore和BlockCache中检索(读取最近写入数据&热点数据),如果查找不到就会到文件中检索。

HBase的类LSM结构会导致每个store包含多数HFile文件,文件越多,检索所需的IO次数必然越多,读取延迟也就越高。

文件数量通常取决于Compaction的执行策略,一般和两个配置参数有关: hbase.hstore.compactionThreshold 、hbase.hstore.compaction.max.size前者表示一个store中的文件数超过多少就应该进行合并,后者表示参数合并的文件大小最大是多少,超过此大小的文件不能参与合并。

这两个参数不能设置太’松’(前者不能设置太大,后者不能设置太小),导致Compaction合并文件的实际效果不明显,进而很多文件得不到合并。这样就会导致HFile文件数变多。

观察确认:观察RegionServer级别以及Region级别的storefile数,确认HFile文件是否过多
优化建议:hbase.hstore.compactionThreshold设置不能太大,默认是3个;设置需要根据Region大小确定,通常可以简单的认为 hbase.hstore.compaction.max.size = RegionSize / hbase.hstore.compactionThreshold

4)Compaction是否消耗系统资源过多?

Compaction是将小文件合并为大文件,提高后续业务随机读性能,但是也会带来IO放大以及带宽消耗问题(数据远程读取以及三副本写入都会消耗系统带宽)。

正常配置情况下Minor Compaction并不会带来很大的系统资源消耗,除非因为配置不合理导致Minor Compaction太过频繁, 或者Region设置太大情况下发生Major Compaction。
观察确认:观察系统IO资源以及带宽资源使用情况,再观察Compaction队列长度,确认是否由于Compaction导致系统资源消耗过多

优化建议:
1.Minor Compaction设置:hbase.hstore.compactionThreshold设置不能太小,又不能设置太大,因此建议设置为5~6;hbase.hstore.compaction.max.size = RegionSize / hbase.hstore.compactionThreshold
2.Major Compaction设置:大Region读延迟敏感业务( 100G以上)通常不建议开启自动Major Compaction,手动低峰期触发。小Region或者延迟不敏感业务可以开启Major Compaction,但建议限制流量;
3.期待更多的优秀Compaction策略,类似于stripe-compaction尽早提供稳定服务

3、HBase列族设计优化,设置Bloomfilter,为row

HBase列族设计,对读性能影响也至关重要,其特点是只影响单个业务,并不会对整个集群产生太大影响。列族设计主要从以下方面检查:

Bloomfilter主要用来,过滤不存在待检索RowKey或者Row-Col的HFile文件,避免无用的IO操作。它会告诉你在这个HFile文件中是否可能存在待检索的KV,如果不存在,就可以不用消耗IO打开文件进行seek。很显然,通过设置Bloomfilter可以提升随机读写的性能

Bloomfilter取值有两个,row以及rowcol,需要根据业务来确定具体使用哪种。

  • 如果业务大多数随机查询仅仅使用row作为查询条件,Bloomfilter一定要设置为row,
  • 否则如果大多数随机查询使用row+cf作为查询条件,Bloomfilter需要设置为rowcol。
  • 如果不确定业务查询类型,设置为row。

优化建议:任何业务都应该设置Bloomfilter,通常设置为row就可以,除非确认业务随机查询类型为row+cf,可以设置为rowcol

4、HDFS相关优化,避免Region无故迁移,提高数据本地率有效优化随机读性能

HDFS作为HBase最终数据存储系统,通常会使用三副本策略存储HBase数据文件以及日志文件。从HDFS的角度望上层看,HBase即是它的客户端,HBase通过调用它的客户端进行数据读写操作,因此HDFS的相关优化也会影响HBase的读写性能。

这里主要关注如下三个方面:

1) Short-Circuit Local Read功能是否开启?
当前HDFS读取数据都需要经过DataNode,客户端会向DataNode发送读取数据的请求,DataNode接受到请求之后从硬盘中将文件读出来,再通过TPC发送给客户端。Short Circuit策略允许客户端绕过DataNode直接读取本地数据。(具体原理参考此处)
优化建议:开启Short Circuit Local Read功能,具体配置戳这里

2) Hedged Read功能是否开启?
HBase数据在HDFS中一般都会存储三份,而且优先会通过Short-Circuit Local Read功能尝试本地读。但是在某些特殊情况下,有可能会出现因为磁盘问题或者网络问题引起的短时间本地读取失败,为了应对这类问题,社区开发者提出了补偿重试机制 – Hedged Read。该机制基本工作原理为:客户端发起一个本地读,一旦一段时间之后还没有返回,客户端将会向其他DataNode发送相同数据的请求。哪一个请求先返回,另一个就会被丢弃。
优化建议:开启Hedged Read功能,具体配置参考这里

3) 数据本地率是否太低?
HDFS数据通常存储三份,假如当前RegionA处于Node1上,数据a写入的时候三副本为(Node1,Node2,Node3),数据b写入三副本是(Node1,Node4,Node5),数据c写入三副本(Node1,Node3,Node5),可以看出来所有数据写入本地Node1肯定会写一份,数据都在本地可以读到,因此数据本地率是100%。现在假设RegionA被迁移到了Node2上,只有数据a在该节点上,其他数据(b和c)读取只能远程跨节点读,本地率就为33%(假设a,b和c的数据大小相同)。

数据本地率太低很显然会产生大量的跨网络IO请求,必然会导致读请求延迟较高,因此提高数据本地率可以有效优化随机读性能。数据本地率低的原因一般是因为Region迁移(自动balance开启、RegionServer宕机迁移、手动迁移等),因此一方面可以通过避免Region无故迁移来保持数据本地率,另一方面如果数据本地率很低,也可以通过执行major_compact提升数据本地率到100%。

优化建议:避免Region无故迁移,比如关闭自动balance、RS宕机及时拉起并迁回飘走的Region等;在业务低峰期执行major_compact提升数据本地率

四、HBase写优化

HBase写数据流程倒是显得很简单:数据先顺序写入HLog,再写入对应的缓存Memstore,当Memstore中数据大小达到一定阈值(128M)之后,系统会异步将Memstore中数据flush到HDFS形成小文件。

HBase数据写入通常会遇到两类问题,一类是写性能较差,另一类是数据根本写不进去。这两类问题的切入点也不尽相同,如

1、写入请求数据均衡,检查RowKey设计以及预分区策略

写入请求是否均衡,如果不均衡,一方面会导致系统并发度较低,另一方面也有可能造成部分节点负载很高,进而影响其他业务。分布式系统中特别害怕一个节点负载很高的情况,一个节点负载很高可能会拖慢整个集群,这是因为很多业务会使用Mutli批量提交读写请求,一旦其中一部分请求落到该节点无法得到及时响应,就会导致整个批量请求超时。因此不怕节点宕掉,就怕节点奄奄一息!
优化建议:检查RowKey设计以及预分区策略,保证写入请求均衡。

2、可关闭WAL写入,或者WAL异步写入,接受数据丢失追求吞吐量的业务(推荐

写入流程,可以理解为一次顺序写WAL + 一次写缓存,通常写缓存延迟很低,因此提升写性能就只能从WAL入手。WAL机制一方面是为了确保数据即使写入缓存丢失也可以恢复,另一方面是为了集群之间异步复制。默认WAL机制开启且使用同步机制写入WAL。

首先考虑业务是否需要写WAL,通常情况下大多数业务都会开启WAL机制(默认),但是对于部分业务可能并不特别关心异常情况下部分数据的丢失 ,而更关心数据写入吞吐量,比如某些推荐业务,这类业务即使丢失一部分用户行为数据可能对推荐结果并不构成很大影响,但是对于写入吞吐量要求很高,不能造成数据队列阻塞。

这种场景下可以考虑关闭WAL写入,写入吞吐量可以提升2x~3x。退而求其次,有些业务不能接受不写WAL,但可以接受WAL异步写入,也是可以考虑优化的,通常也会带来1x~2x的性能提升。

优化推荐:根据业务关注点在WAL机制与写入吞吐量之间做出选择
其他注意点:对于使用Increment操作的业务,WAL可以设置关闭,也可以设置异步写入,方法同Put类似。相信大多数Increment操作业务对WAL可能都不是那么敏感

3、使用批量put进行写入请求,减少RPC连接数

HBase分别提供了单条put以及批量put的API接口,使用批量put接口可以减少客户端到RegionServer之间的RPC连接数,提高写入性能。另外需要注意的是,批量put请求要么全部成功返回,要么抛出异常。
优化建议:使用批量put进行写入请求

4、接受数据丢失的情况下,开启异步批量提交

业务如果可以接受异常情况下少量数据丢失的话,还可以使用异步批量提交的方式提交请求。

提交分为两阶段执行:

  • 用户提交写请求之后,数据会写入客户端缓存,并返回用户写入成功;
  • 当客户端缓存达到阈值(默认2M)之后批量提交给RegionServer。
  • 需要注意的是,在某些情况下客户端异常的情况下缓存数据有可能丢失。
    优化建议:在业务可以接受的情况下开启异步批量提交
    使用方式:setAutoFlush(false)

5、Region数少于RegionServer数,考虑切分Region

当前集群中表的Region个数如果小于RegionServer个数,即Num(Region of Table) < Num(RegionServer),可以考虑切分Region并尽可能分布到不同RegionServer来提高系统请求并发度,如果Num(Region of Table) > Num(RegionServer),再增加Region个数效果并不明显。

优化建议:在Num(Region of Table) < Num(RegionServer)的场景下切分部分请求负载高的Region并迁移到其他RegionServer;

6、写入KeyValue数据是否太大

KeyValue大小对写入性能的影响巨大,一旦遇到写入性能比较差的情况,需要考虑是否由于写入KeyValue数据太大导致。KeyValue大小对写入性能影响曲线图如下:

  • 4
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值