hadoop进阶

Java 多线程安全机制

 

1、操作系统有两个容易混淆的概念,进程和线程。

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。

 

 

 

 

在开始讨论java多线程安全机制之前,首先从内存模型来了解一下什么是多线程的安全性。

我们都知道java的内存模型中有主内存和线程的工作内存之分,主内存上存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去,JVM提供了8中原子操作来完成这一过程:lock, unlock, read, load, use, assign, store, write。深入理解java虚拟机-jvm最高特性与实践这本书中有一个图很好的表示了线程,主内存和工作内存之间的关系:

 

如果只有一个线程当然不会有什么问题,但是如果有多个线程同时在操作主内存中的变量,因为8种操作的非连续性和线程抢占cpu执行的机制就会带来冲突的问题,也就是多线程的安全问题。线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。

 

Java里面一般用以下几种机制保证线程安全:

1.互斥同步锁(悲观锁)

 

1)Synchorized

2)ReentrantLock

互斥同步锁也叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。

要理解互斥同步锁,首选要明白什么是互斥什么是同步。简单的说互斥就是非你即我,同步就是顺序访问。互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一个时刻只能有一个或者一组线程访问同一个资源。

Java里面的互斥同步锁就是Synchorized和ReentrantLock,前者是由语言级别实现的互斥同步锁,理解和写法简单但是机制笨拙,在JDK6之后性能优化大幅提升,即使在竞争激烈的情况下也能保持一个和ReentrantLock相差不多的性能,所以JDK6之后的程序选择不应该再因为性能问题而放弃synchorized。ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和synchorized相比更加的灵活,体现在三个方面:等待可中断,公平锁以及绑定多个条件。但是如果程序猿对ReentrantLock理解不够深刻,或者忘记释放lock,那么不仅不会提升性能反而会带来额外的问题。另外synchorized是JVM实现的,可以通过监控工具来监控锁的状态,遇到异常JVM会自动释放掉锁。而ReentrantLock必须由程序主动的释放锁。

互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比较消耗性能。JVM开发团队在JDK5-JDK6升级过程中采用了很多锁优化机制来优化同步无竞争情况下锁的性能。比如:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。

 

 

2.非阻塞同步锁

1) 原子类(CAS)

非阻塞同步锁也叫乐观锁,相比悲观锁来说,它会先进行资源在工作内存中的更新,然后根据与主存中旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没有更新,可以把新值写回内存,否则就一直重试直到成功。它的实现方式依赖于处理器的机器指令:CAS(Compare And Swap)

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作

JUC中提供了几个Automic类以及每个类上的原子操作就是乐观锁机制。

不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。 

非阻塞锁是不可重入的,否则会造成死锁。

3.无同步方案

1)可重入代码

在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源

2)ThreadLocal/Volaitile

线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理。

3)  线程本地存储

如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计

 

三:解决机制

1. 加锁。

(1) 锁能使其保护的代码以串行的形式来访问,当给一个复合操作加锁后,能使其成为原子操作。一种错误的思想是只要对写数据的方法加锁,其实这是错的,对数据进行操作的所有方法都需加锁,不管是读还是写。

(2) 加锁时需要考虑性能问题,不能总是一味地给整个方法加锁synchronized就了事了,应该将方法中不影响共享状态且执行时间比较长的代码分离出去。

(3) 加锁的含义不仅仅局限于互斥,还包括可见性。为了确保所有线程都能看见最新值,读操作和写操作必须使用同样的锁对象。

2. 不共享状态

(1) 无状态对象: 无状态对象一定是线程安全的,因为不会影响到其他线程。

(2) 线程关闭: 仅在单线程环境下使用。

3. 不可变对象

可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,也即不能对外提供可以修改final对象的接口。

 

mapreduce的原理

下面这幅图就是mapreduce的工作原理

1)首先文档的数据记录(如文本中的行,或数据表格中的行)是以“键值对”的形式传入map 函数,然后map函数对这些键值对进行处理(如统计词频),然后输出到中间结果。

2)在键值对进入reduce进行处理之前,必须等到所有的map函数都做完,所以既为了达到这种同步又提高运行效率,在mapreduce中间的过程引入了barrier(同步障)

在负责同步的同时完成对map的中间结果的统计,包括 a. 对同一个map节点的相同key的value值进行合并b. 之后将来自不同map的具有相同的key的键值对送到同一个reduce进行处理

3)在reduce阶段,每个reduce节点得到的是从所有map节点传过来的具有相同的key的键值对。reduce节点对这些键值进行合并。

 

1)Combiner 节点负责完成上面提到的将同一个map中相同的key进行合并,避免重复传输,从而减少传输中的通信开销。

2)Partitioner节点负责将map产生的中间结果进行划分,确保相同的key到达同一个reduce节点.

 

 

 

MapReduce shuffle阶段详解

 

Mapreduce中,Shuffle过程是Mapreduce的核心,它分布在Mapreduce的map阶段和reduce阶段,共可分为6个详细的阶段:

1).Collect阶段:将MapTask的结果输出到默认大小为100M的MapOutputBuffer内部环形内存缓冲区,保存
的是key/value,Partition分区

2).Spill阶段:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘
之前需要对数据进行一次排序的操作,先是对partition分区号进行排序,再对key排序,如果配置了
combiner,还会将有相同分区号和key的数据进行排序,如果有压缩设置,则还会对数据进行压缩操作。

3).Combiner阶段:等MapTask任务的数据处理完成之后,会对所有map产生的数据结果进行一次合并操作,
以确保一个MapTask最终只产生一个中间数据文件。

4).Copy阶段:当整个MapReduce作业的MapTask所完成的任务数据占到MapTask总数的5%时,JobTracker就会
调用ReduceTask启动,此时ReduceTask就会默认的启动5个线程到已经完成MapTask的节点上复制一份属于自
己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写
到磁盘之上。

5).Merge阶段:在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存中和本地中的数据文件进行
合并操作。

6).Sort阶段:在对数据进行合并的同时,会进行排序操作,由于MapTask阶段已经对数据进行了局部的排序,
ReduceTask只需做一次归并排序就可以保证Copy的数据的整体有效性。

 

Spark Shuffle进化史

先以图为例简单描述一下Spark中shuffle的整一个流程:

spark shuffle process

  • 首先每一个Mapper会根据Reducer的数量创建出相应的bucket,bucket的数量是M×RM×R,其中MM是Map的个数,RR是Reduce的个数。
  • 其次Mapper产生的结果会根据设置的partition算法填充到每个bucket中去。这里的partition算法是可以自定义的,当然默认的算法是根据key哈希到不同的bucket中去。
  • 当Reducer启动时,它会根据自己task的id和所依赖的Mapper的id从远端或是本地的block manager中取得相应的bucket作为Reducer的输入进行处理。

这里的bucket是一个抽象概念,在实现中每个bucket可以对应一个文件,可以对应文件的一部分或是其他等。

Apache Spark 的 Shuffle 过程与 Apache Hadoop 的 Shuffle 过程有着诸多类似,一些概念可直接套用,例如,Shuffle 过程中,提供数据的一端,被称作 Map 端,Map 端每个生成数据的任务称为 Mapper,对应的,接收数据的一端,被称作 Reduce 端,Reduce 端每个拉取数据的任务称为 Reducer,Shuffle 过程本质上都是将 Map 端获得的数据使用分区器进行划分,并将数据发送给对应的 Reducer 的过程。

 

 

 

 

 

2.OOM咋办

 首先,要搞清OOM的分类:
OMM主要三类: permgen OOM , heap OOM, stack overflow 
permgen OOM: 这个主要是由于加载的类太多,或者反射的类太多, 还有 调用 String.intend(jdk7之前)也会造成这个问题。所以出现了这个问题,就检查这三个方面;
heap OOM: 基本是按照 1楼的方式就可以解决了,主要是因为一些无用对象没有及时释放造成的,检查代码加上 heap dump 去分析吧
stack overflow: 这个主要是由于调用层数,或者递归深度太大造成的,看异常信息,基本上就能定位得出来了

 

什么情况下会出现OOM?

(1)内存泄漏(连接未关闭,单例类中不正确引用了对象)

(2)代码中存在死循环或循环产生过多重复的对象实体

(3)Space(空间)大小设置不正确

(4)内存中加载的数据量过于庞大,如一次从数据库取出过多数据

(5)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收

 

Spark面对OOM问题的解决方法及优化总结

 
    Spark中的OOM问题不外乎以下两种情况
  • map执行中内存溢出
  • shuffle后内存溢出
    map执行中内存溢出代表了所有map类型的操作,包括:flatMap,filter,mapPatitions等。shuffle后内存溢出的shuffle操作包括join,reduceByKey,repartition等操作。后面先总结一下我对Spark内存模型的理解,再总结各种OOM的情况相对应的解决办法和性能优化方面的总结。如果理解有错,希望在评论中指出。
 
Spark 内存模型:
    Spark在一个Executor中的内存分为三块,一块是execution内存,一块是storage内存,一块是other内存。
  • execution内存是执行内存,文档中说join,aggregate都在这部分内存中执行,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘,能够减少IO。其实map过程也是在这个内存中执行的。
  • storage内存是存储broadcast,cache,persist数据的地方。
  • other内存是程序执行时预留给自己的内存。
    execution和storage是Spark Executor中内存的大户,other占用内存相对少很多,这里就不说了。在spark-1.6.0以前的版本,execution和storage的内存分配是固定的,使用的参数配置分别是spark.shuffle.memoryFraction(execution内存占Executor总内存大小,default 0.2)和spark.storage.memoryFraction(storage内存占Executor内存大小,default 0.6),因为是1.6.0以前这两块内存是互相隔离的,这就导致了Executor的内存利用率不高,而且需要根据Application的具体情况,使用者自己来调节这两个参数才能优化Spark的内存使用。在spark-1.6.0以上的版本,execution内存和storage内存可以相互借用,提高了内存的Spark中内存的使用率,同时也减少了OOM的情况。
    在Spark-1.6.0后加入了堆外内存,进一步优化了Spark的内存使用,堆外内存使用JVM堆以外的内存,不会被gc回收,可以减少频繁的full gc,所以在Spark程序中,会长时间逗留再Spark程序中的大内存对象可以使用堆外内存存储。使用堆外内存有两种方式,一种是在rdd调用persist的时候传入参数StorageLevel.OFF_HEAP,这种使用方式需要配合Tachyon一起使用。另外一种是使用Spark自带的spark.memory.offHeap.enabled 配置为true进行使用,但是这种方式在1.6.0的版本还不支持使用,只是多了这个参数,在以后的版本中会开放。
    OOM的问题通常出现在execution这块内存中,因为storage这块内存在存放数据满了之后,会直接丢弃内存中旧的数据,对性能有影响但是不会有OOM的问题。
 
内存溢出解决方法:
1. map过程产生大量对象导致内存溢出:
    这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。
    面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有shuffle的过程。
 
2.数据不平衡导致内存溢出:
    数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。这里就不再累赘了。
 
3.coalesce调用导致内存溢出:
    这是我最近才遇到的一个问题,因为hdfs中不适合存小问题,所以Spark计算后如果产生的文件太小,我们会调用coalesce合并文件再存入hdfs中。但是这会导致一个问题,例如在coalesce之前有100个文件,这也意味着能够有100个Task,现在调用coalesce(10),最后只产生10个文件,因为coalesce并不是shuffle操作,这意味着coalesce并不是按照我原本想的那样先执行100个Task,再将Task的执行结果合并成10个,而是从头到位只有10个Task在执行,原本100个文件是分开执行的,现在每个Task同时一次读取10个文件,使用的内存是原来的10倍,这导致了OOM。解决这个问题的方法是令程序按照我们想的先执行100个Task再将结果合并成10个文件,这个问题同样可以通过repartition解决,调用repartition(10),因为这就有一个shuffle的过程,shuffle前后是两个Stage,一个100个分区,一个是10个分区,就能按照我们的想法执行。
 
4.shuffle后内存溢出:
    shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。
 
5. standalone模式下资源分配不均匀导致内存溢出:
    在standalone的模式下如果配置了--total-executor-cores 和 --executor-memory 这两个参数,但是没有配置--executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置--executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。
 
6.在RDD中,共用对象能够减少OOM的情况:
    这个比较特殊,这里说记录一下,遇到过一种情况,类似这样rdd.flatMap(x=>for(i <- 1 to 1000) yield ("key","value"))导致OOM,但是在同样的情况下,使用rdd.flatMap(x=>for(i <- 1 to 1000) yield "key"+"value")就不会有OOM的问题,这是因为每次("key","value")都产生一个Tuple对象,而"key"+"value",不管多少个,都只有一个对象,指向常量池。具体测试如下:
    这个例子说明("key","value")和("key","value")在内存中是存在不同位置的,也就是存了两份,但是"key"+"value"虽然出现了两次,但是只存了一份,在同一个地址,这用到了JVM常量池的知识.于是乎,如果RDD中有大量的重复数据,或者Array中需要存大量重复数据的时候我们都可以将重复数据转化为String,能够有效的减少内存使用.
 
优化:
    这一部分主要记录一下到spark-1.6.1版本,笔者觉得有优化性能作用的一些参数配置和一些代码优化技巧,在参数优化部分,如果笔者觉得默认值是最优的了,这里就不再记录。
代码优化技巧:
1.使用mapPartitions代替大部分map操作,或者连续使用的map操作:
    这里需要稍微讲一下RDD和DataFrame的区别。RDD强调的是不可变对象,每个RDD都是不可变的,当调用RDD的map类型操作的时候,都是产生一个新的对象,这就导致了一个问题,如果对一个RDD调用大量的map类型操作的话,每个map操作会产生一个到多个RDD对象,这虽然不一定会导致内存溢出,但是会产生大量的中间数据,增加了gc操作。另外RDD在调用action操作的时候,会出发Stage的划分,但是在每个Stage内部可优化的部分是不会进行优化的,例如rdd.map(_+1).map(_+1),这个操作在数值型RDD中是等价于rdd.map(_+2)的,但是RDD内部不会对这个过程进行优化。DataFrame则不同,DataFrame由于有类型信息所以是可变的,并且在可以使用sql的程序中,都有除了解释器外,都会有一个sql优化器,DataFrame也不例外,有一个优化器Catalyst,具体介绍看后面参考的文章。
    上面说到的这些RDD的弊端,有一部分就可以使用mapPartitions进行优化,mapPartitions可以同时替代rdd.map,rdd.filter,rdd.flatMap的作用,所以在长操作中,可以在mapPartitons中将RDD大量的操作写在一起,避免产生大量的中间rdd对象,另外是mapPartitions在一个partition中可以复用可变类型,这也能够避免频繁的创建新对象。使用mapPartitions的弊端就是牺牲了代码的易读性。
 
2.broadcast join和普通join:
    在大数据分布式系统中,大量数据的移动对性能的影响也是巨大的。基于这个思想,在两个RDD进行join操作的时候,如果其中一个RDD相对小很多,可以将小的RDD进行collect操作然后设置为broadcast变量,这样做之后,另一个RDD就可以使用map操作进行join,这样能够有效的减少相对大很多的那个RDD的数据移动。
 
3.先filter在join:
    这个就是谓词下推,这个很显然,filter之后再join,shuffle的数据量会减少,这里提一点是spark-sql的优化器已经对这部分有优化了,不需要用户显示的操作,个人实现rdd的计算的时候需要注意这个。
 
4.partitonBy优化:
    这一部分在另一篇文章《spark partitioner使用技巧 》有详细介绍,这里不说了。
 
5. combineByKey的使用:
    这个操作在Map-Reduce中也有,这里举个例子:rdd.groupByKey().mapValue(_.sum)比rdd.reduceByKey的效率低,原因如下两幅图所示(网上盗来的,侵删)
 
    上下两幅图的区别就是上面那幅有combineByKey的过程减少了shuffle的数据量,下面的没有。combineByKey是key-value型rdd自带的API,可以直接使用。
 
6. 在内存不足的使用,使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache():
    rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重算,只是消耗点IO时间。
 
7.在spark使用hbase的时候,spark和hbase搭建在同一个集群:
     在spark结合hbase的使用中,spark和hbase最好搭建在同一个集群上上,或者spark的集群节点能够覆盖hbase的所有节点。hbase中的数据存储在HFile中,通常单个HFile都会比较大,另外Spark在读取Hbase的数据的时候,不是按照一个HFile对应一个RDD的分区,而是一个region对应一个RDD分区。所以在Spark读取Hbase的数据时,通常单个RDD都会比较大,如果不是搭建在同一个集群,数据移动会耗费很多的时间。
 
参数优化部分:
8. spark.driver.memory (default 1g):
    这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。
 
9. spark.rdd.compress (default false) :
    这个参数在内存吃紧的时候,又需要persist数据有良好的性能,就可以设置这个参数为true,这样在使用persist(StorageLevel.MEMORY_ONLY_SER)的时候,就能够压缩内存中的rdd数据。减少内存消耗,就是在使用的时候会占用CPU的解压时间。
 
10. spark.serializer (default org.apache.spark.serializer.JavaSerializer )
    建议设置为 org.apache.spark.serializer.KryoSerializer,因为KryoSerializer比JavaSerializer快,但是有可能会有些Object会序列化失败,这个时候就需要显示的对序列化失败的类进行KryoSerializer的注册,这个时候要配置spark.kryo.registrator参数或者使用参照如下代码:
valconf=newSparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1],classOf[MyClass2]))
valsc =newSparkContext(conf)
 
11. spark.memory.storageFraction (default 0.5)
    这个参数设置内存表示 Executor内存中 storage/(storage+execution),虽然spark-1.6.0+的版本内存storage和execution的内存已经是可以互相借用的了,但是借用和赎回也是需要消耗性能的,所以如果明知道程序中storage是多是少就可以调节一下这个参数。
 
12.spark.locality.wait (default 3s):
    spark中有4中本地化执行level,PROCESS_LOCAL->NODE_LOCAL->RACK_LOCAL->ANY,一个task执行完,等待spark.locality.wait时间如果,第一次等待PROCESS的Task到达,如果没有,等待任务的等级下调到NODE再等待spark.locality.wait时间,依次类推,直到ANY。分布式系统是否能够很好的执行本地文件对性能的影响也是很大的。如果RDD的每个分区数据比较多,每个分区处理时间过长,就应该把 spark.locality.wait 适当调大一点,让Task能够有更多的时间等待本地数据。特别是在使用persist或者cache后,这两个操作过后,在本地机器调用内存中保存的数据效率会很高,但是如果需要跨机器传输内存中的数据,效率就会很低。
 
13. spark.speculation (default false):
    一个大的集群中,每个节点的性能会有差异,spark.speculation这个参数表示空闲的资源节点会不会尝试执行还在运行,并且运行时间过长的Task,避免单个节点运行速度过慢导致整个任务卡在一个节点上。这个参数最好设置为true。与之相配合可以一起设置的参数有spark.speculation.×开头的参数。参考中有文章详细说明这个参数。
 
以后有遇到新的内容再补充。
 
参考:
 
转载请保持完整性并注明来源链接: http://blog.csdn.net/yhb315279058/article/details/51035631

 

 

spark streaming流量控制

 

随着计算机机硬件的快速发展,机器的内存大小也从原来的以兆为单位到现在的上百G,这也推动了分布式计算从原来的基于硬盘存储发展到现在的基于内存存储,spark作为实时计算的佼佼者也逐渐的走上了大规模商业应用的道路,spark streaming常常用在实时流计算的各个领域,在这一章节我们主要讲解一下streaming处理过程中的流量控制,在我们平时的streaming程序开发过程中应该注意哪些方面以提高程序的吞吐量


流控的目标 
系统进行流控的主要目的是维护系统的稳定性,避免大流量数据的处理造成系统的扰动,最终导致系统宕机;流式处理平台系统在进行流控设计时,需要综合考虑稳定性、吞吐量、端到端的延迟,流控经常是在这三者之间做选择; 
spark streaming流控 
spark streaming程序通常是以指定时间间隔(batch interval)周期性的处理这个时间片内的批量数据,在这种场景下,稳定性指定的是这批数据必须在当前这个周期内处理完毕,系统最大吞吐量为处理时间等于时间间隔时的数据流量;由于spark streaming的周期在系统启动的时候就已经确定了,其流控退化为调整数据的流入速率以最大的提高系统的吞吐量;线上的streaming程序的处理时间和数据的批量大小并没有固定的规律可循,同时一个其他的突发因素(如发生GC)也会影响到数据的处理速率;spark streaming的流控需要做的是基于过去一段时间内的已经处理完成的批量数据来推算出下一个周期内应该处理的数据量,另外,一个好的流控算法需要更敏捷、更准确、更通用。 
流控示意图
PID流控算法 
上述篇幅已经讨论了流控的目标以及好的流控算法的标准,工程实践中广泛应用的流控算法有很多,spark采用了PID控制算法(spark.streaming.backpressure.rateEstimator参数指定),细节请参考WIKI, 
这里写图片描述
spark streaming中误差为lastestRate - currentRate,目标值也为lastestRate,积分部分为系统当前的积压消息速率,计算方式为schedulingDelay * processingRate / batchIntervalMillisschdulingDelay为这批数据从开始执行时间减去提交时间,newRate = (latestRate - proportional * error - integral * historicalError - derivative * dError).max(minRate),dError为误差的微分,详细代码见PIDRateEstimator

def compute(
  time: Long, // in milliseconds
  numElements: Long,
  processingDelay: Long, // in milliseconds
  schedulingDelay: Long // in milliseconds
): Option[Double] = {
logTrace(s"\ntime = $time, # records = $numElements, " +
  s"processing time = $processingDelay, scheduling delay = $schedulingDelay")
this.synchronized {
  if (time > latestTime && numElements > 0 && processingDelay > 0) {

    // in seconds, should be close to batchDuration
    val delaySinceUpdate = (time - latestTime).toDouble / 1000

    // in elements/second
    val processingRate = numElements.toDouble / processingDelay * 1000

    // In our system `error` is the difference between the desired rate and the measured rate
    // based on the latest batch information. We consider the desired rate to be latest rate,
    // which is what this estimator calculated for the previous batch.
    // in elements/second
    val error = latestRate - processingRate

    // The error integral, based on schedulingDelay as an indicator for accumulated errors.
    // A scheduling delay s corresponds to s * processingRate overflowing elements. Those
    // are elements that couldn't be processed in previous batches, leading to this delay.
    // In the following, we assume the processingRate didn't change too much.
    // From the number of overflowing elements we can calculate the rate at which they would be
    // processed by dividing it by the batch interval. This rate is our "historical" error,
    // or integral part, since if we subtracted this rate from the previous "calculated rate",
    // there wouldn't have been any overflowing elements, and the scheduling delay would have
    // been zero.
    // (in elements/second)
    val historicalError = schedulingDelay.toDouble * processingRate / batchIntervalMillis

    // in elements/(second ^ 2)
    val dError = (error - latestError) / delaySinceUpdate

    val newRate = (latestRate - proportional * error -
                                integral * historicalError -
                                derivative * dError).max(minRate)
    logTrace(s"""
        | latestRate = $latestRate, error = $error
        | latestError = $latestError, historicalError = $historicalError
        | delaySinceUpdate = $delaySinceUpdate, dError = $dError
        """.stripMargin)

    latestTime = time
    if (firstRun) {
      latestRate = processingRate
      latestError = 0
      firstRun = false
      logTrace("First run, rate estimation skipped")
      None
    } else {
      latestRate = newRate
      latestError = error
      logTrace(s"New rate = $newRate")
      Some(newRate)
    }
  } else {
    logTrace("Rate estimation skipped")
    None
  }
}

 

 

 

提示:该处的所有时间均是以批量数据作为单位的,如一个streaming应用程序消费了三个数据源,其最终会生成一个Jobset,其总共包含三个Job,submitTime为JobSet的创建时间,processStartTime为第一个Job的处理时间,processEndTime为最后一个Job的处理完成的时间。

posted on 2017-12-17 15:33  曹明  阅读(529)  评论(0编辑  收藏  举报