如何处理大数据性能问题?

 

下一代开源大数据框架如spark和Flink的目标是达到大规模并行处理数据库如Netezza那样.........







下一代开源大数据框架如spark和Flink的目标是达到大规模并行处理数据库如Netezza那样的性能和良好的用户体验

由于所有主流的大数据框架(Hadoop,spark,Flink)使用JVM,所有他们都有着两个JVM限制:

1、Java对象消耗的内存远大于对象中所有属性的大小

2、Major GC 的停顿会使得性能下降,尤其是当你尝试缓存更多的数据(Java对象)以便后期对数据进行纯内存处理

所有Flink,Spark,Hadoop为高性能引入的工程技术主要目的就是为了尽可能的减少这两方面的影响.本文将为你展示这些技术。

在所有性能改进的之外,Alexey Grishchenko所写的如下blog给了我们一个清晰的提醒,虽然基于Java的大数据框架在性能方面已经有很大提升,但是要追上大规模并行处理系统的性能还有很长一段路要走

性能的80/20原则

不是所有操作所需要的开销都是一样的,极少数的操作产生了很高的性能开销,许多大数据框架常用操作组合是:

1、压缩vs不压缩-这直接影响到数据被读出和写入的速率,不同的压缩方式/算法消耗的cpu资源差异很大

2、排序-大数据处理基本都会有"reduce"操作,而排序在map-reduce处理方式中则是"reduce"阶段的关键。由于"join"操作会依赖于"reduce",所以对key的排序也会关系到"join"的操作上

3、清洗-"reduce"阶段第二个最重要的操作就是清洗了,清洗操作也是关联操作的关键

4、关联-只有map阶段的job通常有很好的性能.但当有reduce阶段的时候性能会变成一个需要考虑的重要因素。一个reduce操作包含了关联,在只有map阶段的任务中可以使用广播关联,但广播关联通常在关联的一端数据量足够小的时候才用,这个很小的数据集被广播到所有节点,由一个hashmap的数据结构来维护同时由关联键来索引。另外一端大一点的数据集通过使用关联键在这个内存中的hashmap中查找相应的记录。当只有map阶段时,即使两端数据集都比较大也可以按照关联键来排序,Pig框架就是充分利用了这一点,被称为"merge-joins"技术,它会把两边的数据集都在lock-step中遍历,然后在map阶段中关联起来(这里实际上有一个预先索引的过程)。当然有关联键进行自然排序对数据集是很少见的,在关联大数据集的时候reduce操作还是必不可少的。

搞定上面的这些问题是使得大数据处理效率变得更快的关键。

性能约束和JVM特征

  就像前面提到的,Java在性能方面引入的两个约束因素:

1、Java对象的存储开销不容小觑

2、使用大量生存周期很短的对象会使得吞吐量性能下降,因为大量的时间会花在Major GC上

只在JVM堆栈上操作处理大数据很容易导致内存溢出错误,这会使得JVM进程被杀死退出(不像其他错误可以被处理).这是大数据应用中最常见的一个性能问题.

比如,在一个有许多记录但是只有少数键值对能够发生关联的不均衡数据集中,当所有的键值都保存在内存中时reduce操作会失败

从索引技术可以避免在reduce阶段时把所有的键值放到内存中,图像应用就是属于这类应用中的一种.

这就是大数据框架在使用内存时小心谨慎的重要原因了,比如磁盘溢出技术.这虽然会影响性能,但是会使得框架足够健壮来处理不同的负载.

比如,Pig框架会把数据保存在磁盘上,DataBag接口的实现及其广泛的应用在Pig中,当tuples的数量达到一个预先定义的值时会把数据刷到磁盘上(默认是20000)

JVM 存储开销

  在Java堆栈中一个典型的对象存储由如下组成:

1.对象头-只有几个字节用作维护信息.Hotspot 版本的VM使用8个字节来表示一个普通对象的头,另外再使用4个字节来表示数组对象的长度

2.原始类型-原始类型也需要字节来存储.比如boolean类型需要1个字节,char需要两个字节,int需要4个。

3.引用类型存储-引用类型占用4个字节

4.空占位符-每个对象存放时总字节数是8的倍数,所有这中间总是有空的占位符

如图1所示,一个Java类对象的实例包含了一个原始类型boolean实例所占用的8个字节用作头,另外一个字节表示boolean实例本身,剩余7个字节就会被空占位符填满。

图1:一个只有一个boolean实例变量的java类在内存中的占用情况


这个例子证实了一个普通java对象很大的存储开销,并且随着如hashmap 和list这样复杂对象变得非常大

JVM 存储开销影响序列化和反序列化

  在大数据范围内发生如下情形时你肯定需要序列化和反序列化:

1、Mapper输出结果到本地磁盘

2、Mappers从本地磁盘读出数据来做map端的排序

3、Reducers从mapper 节点上获取排序和清洗好的结果

在上面的每种情形中,实际的数据量都比原始字节表示的要多很多

Java 对象的存储开销影响到内存如何有效的使用分级缓存。即使所有你想要的数据在内存中,L1/L2/L3级别的缓存也不可能大到能装下这些所有的数据,这也会使得缓存在过渡的交换之中从而降低性能。

 JVM 存储开销影响GC

JavaGC最关键的是要更多的对象在年轻代时就被收集. Java堆栈被划分为两个区域,年轻代和年老代。一般来说年轻代占用整个堆栈空间的20%,而年老代占用了剩余的80%。当一个对象第一次被创建时,他们被分配在年轻代,当年轻代满了的时候,minorGC就会执行。在这个过程中不再被引用的对象就被清除掉,剩余的还在被引用的就移到年老代。当年老代被填满时,major GC最终就会发生。Major收集算法会遍历在图中的每个对象,从老年代中回收内存。注意这只是非常简单的描述了GC的过程,更加详细的了解可以上官方网站查找。

JVM的这些额外开销降低了OOM的可能性,但使得你真正可用的内存空间减少。

大数据框架一直朝着全内存存储和计算的方向进行。 Map-Reduce过去一直主要是I/O资源受限型的做法。其早期实现的迭代算法使得在每一次迭代都需要从磁盘文件系统中读取结果进行下次迭代,并且还要把结果写回磁盘上。这种做法使得整个MR的任务运行非常慢。

Spark提出以数据能被以分布式方式缓存在内存中的RDD概念,这种方式没有了过渡的I/O开销,在启动和停止任务的开销方面也更小。然而,这种折中把风险转移到了GC上,原因是,当Spark使用磁盘溢出技术来控制对内存的过渡使用时,用户应用程序能利用的内存也越来越少了

在Java中序列化方案的演变

Java的序列化方案一直在进化,默认的java.io.Serializable性能消耗非常高,因为每个实例都存储了类的metadata以及引用到的类型的meta-data信息。这种方案只适合当你想要序列化字节里面包含所有信息且这些都会反序列化为一个Java实例的情况。

Java.io.Externalizable接口已经能够处理解决部分问题了,当使用这个接口时,程序员可以控制类中的属性如何序列化和反序列化,因此类属性的元信息不再需要存放到序列化字节流中。但是当java.io.Externaizable接口需要调用包含没有参数的构造函数类反序列化时,类的这些元信息还是需要存放到序列化字节流中的。

Java.io.Serializable和java.io.Externaizable都有同样的限制,对每一个反序列化的实例都会创建一个单独的java对象,这就意味着当有非常多的实例反序列化时就会生成大量的对象,这增加了GC的压力。

Kryo类库改进了这些问题。Spark支持默认的Java序列化方式,但它推荐你使用Kryo方案以提升性能。Kryo通过如下的方式改建了以上的两个问题:

首先,Kryo提供了一种选择来存储整型索引以代表类被序列化,因为一个完整的类名称包含了包名称,这在序列化一个类的时候严重的降低了性能。当然缺点是,你必须舍弃可移植性,因为当你反序列化一个实例时,你必须使用同一个整型索引来指向这个类。在实际应用中,这确实是个问题。

第二,Kryo支持在反序列化过程中池化实例,意思就是,即使你有数以万计的实例在某个时刻被反序列化也只有非常少的实例占用内存
......
源自《51测试天地》原创测试文章系列(四十四)




推荐阅读

点击阅读☞测试公主之为人鱼公主找到真爱

点击阅读☞鱼儿妈的4年测试生活

点击阅读☞利用代码覆盖率进行精准测试和回归

点击阅读☞再论纯软件测试方法

点击阅读☞大数据测试中的功能和性能测试


喜欢我们的会点赞,爱我们的会分享!


    关注 51Testing软件测试网


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册