hadoop优化
优化主要有四个方面:linux系统环境优化、hadoop配置优化、应用程序优化和hadoop源代码优化。
一、hadoop源码角度系统优化
这种优化主要是解决hadoop系统的现有缺陷和性能表现不佳的地方,包括工作流程和系统算法等方面的优化。
1.单个task任务调度延迟的优化。
Hadoop采用的是动态调度算法,即:当某个tasktracker上出现空slot时,它会通过HEARBEAT(默认时间间隔为3s,当集群变大时,会适当调大)告诉jobtracker,之后jobtracker采用某种调度策略从待选task中选择一个,再通过HEARBEAT告诉tasktracker。从整个过程看,HDFS在获取下一个task之前,一直处于等待状态,这造成了资源利用率不高。此外,由于tasktracker获取新task后,其数据读取过程是完全串行化的,即:tasktracker获取task后,依次连接namenode,连接datanode并读取数据,处理数据。在此过程中,当tasktracker连接namenode和datanode时,HDFS仍在处于等待状态。
为了解决调度延迟问题,可以考虑的解决方案有:重叠I/O和CPU阶段(pipelining),task预取(task prefetching),数据预取(data prefetching)等
2. 可移植性假设:java封装的hdfs写线程模型,并发写情况下造成磁盘碎片,降低访问速度。
为了增加Hadoop的可移植性,它采用java语言编写,这实际上也潜在的造成了HDFS低效。Java尽管可以让Hadoop的可移植性增强,但是它屏蔽了底层文件系统,这使它没法利用一些底层的API对数据存储和读写进行优化。首先,在共享集群环境下,大量并发读写会增加随机寻道,这大大降低读写效率;另外,并发写会增加磁盘碎片,这将增加读取代价(HDFS适合文件顺序读取)。
为了解决该问题,可以考虑的解决方案有:修改tasktracker上的线程模型,现在Hadoop上的采用的模型是one thread per client,即每个client连接由一个线程处理(包括接受请求,处理请求,返回结果);修改之后,可将线程分成两组,一组用于处理client通信(Client Thread),一组用于存取数据(Disk Threads,可采用one thread per disk)。
3.数据预取和提前shuffle
数据预取包括map任务处理的数据块一边计算一边预读部分数据。以及处理当前map时,预取下一个数据块,以准备接下来要执行的map的数据块。
提前shuffle:在map任务处理数据之前,对数据进行分析,预测要处理的数据块经过map之后会被送到哪个reduce处理,将这些数据交由靠近该reduce task的节点上的map task处理。
4.计算模型:二次排序的必要性
在Hadoop中,map task产生的中间结果经过sort-merge策略处理后交给reduce task。而这种处理策略(指sort-merge)不能够定制,这对于有些应用而言(有些应用程序可能不需要排序处理),性能不佳。此外,即使是需要排序归并处理的,sort-merge也并不是最好的策略。本文实现了Fingerprinting Based Grouping(基于hash)策略,该方法明显提高了Hadoop性能。
5.IO模型:本地数据和远程数据均通过TCP方式读取
Reader可以采用两种方式从底层的存储系统中读取数据:direct I/O和streaming I/O。direct I/O是指reader直接从本地文件中读取数据;streaming I/O指使用某种进程间通信方式(如TCP或者JDBC)从另外一个进程中获取数据。从性能角度考虑,direct I/O性能更高,各种数据库系统都是采用direct I/O模式。但从存储独立性考虑,streaming I/O使Hadoop能够从任何进程获取数据,如datanode或database,此外,如果reader不得不从远程节点上读取数据,streaming I/O是仅有的选择。
本文对hadoop的文件读写方式进行了改进,当文件位于本地时,采用direct I/O方式;当文件位于其它节点上时,采用streaming I/O方式。(改进之前,hadoop全是采用streaming I/O方式)。改进后,效率约提高10%。
6.数据解析:生成Key/value时java对象如String的immutable特性导致重新创建对象。
在hadoop中,原始数据要被转换成key/value的形式以便进一步处理,这就是数据解析。现在有两种数据解析方法:immutable decoding and mutable decoding。Hadoop是采用java语言编写的,java中很多对象是immutable,如String。当用户试图修改一个String内容时,原始对象会被丢弃而新对象会被创建以存储新内容。在Hadoop中,采用了immutable对象存储字符串,这样每解析一个record就会创建一个新的对象,这就导致了性能低下。
本文比较了immutable实现和mutable实现, mutable性能远高于immutable(join是10倍,select是2倍)。
7.任务调度:动态调度单个任务
Hadoop采用的是动态调度策略,即每次调度一个task运行,这样会带来部分开销。而database采用的静态调度的策略,即在编译的时候就确定了调度方案。当用户提交一个sql时,优化器会生成一个分布式查询计划交给每一个节点进行处理。
本文使用一个benchmark评估运行时调度的代价,最终发现运行时调度策略从两个角度影响性能:需要调度的task数;调度算法。对于第一个因素,可以调整block的大小减少task数,对于第二个因素,需要做更多研究,设计新的算法。
本文调整block大小(从64增大到5G),发现block越大,效率越高,提升性能约20%~30%。
8.单点故障:
Hadoop采用的是C/S架构,因而存在明显的namenode/jobtracker单点故障问题。相比于jobtracker,namenode的单点故障问题更为急迫,因为namenode的故障恢复时间很长,其时间主要花在fsimage加载和blockReport上,下面是一组测试数据:
当前主要的解决思路有:
(1) Zookeeper。利用分布式系统的可靠协调系统zookeeper维护主从namenode之间的一致性。
(2) 热备。添加热备从namenode,主从namenode之间通过分布式协议维护数据一致性。
(3) 分布式namespace。多个namenode共同管理底层的datanode。
9.小文件问题
小文件是指文件size小于HDFS上block大小的文件。这样的文件会给hadoop的扩展性和性能带来严重问题。首先,在HDFS中,任何block,文件或者目录在内存中均以对象的形式存储,每个对象约占150byte,如果有1000 0000个小文件,每个文件占用一个block,则namenode需要2G空间(存两份)。如果存储1亿个文件,则namenode需要20G空间。这样namenode内存容量严重制约了集群的扩展。其次,访问大量小文件速度远远小于访问几个大文件。HDFS最初是为流式访问大文件开发的,如果访问大量小文件,需要不断的从一个datanode跳到另一个datanode,严重影响性能。最后,处理大量小文件速度远远小于处理同等大小的大文件的速度。每一个小文件要占用一个slot,而task启动将耗费大量时间甚至大部分时间都耗费在启动task和释放task上。
对于Hadoop小文件问题,当前主要有两种解决方案,(1)设计一种工具(比如mapreduce作业)交给用户,让用户自己每隔一段时间将小文件打包成大文件,当前Hadoop本身提供了几个这样的工具,包括Hadoop Archive(Hadoop提供了shell命令),Sequence file(需自己写程序实现)和CombineFileInputFormat(需自己写程序实现)。(2)从系统层面解决HDFS小文件,论文[10][11]介绍了它们思路,大体上说思路基本一致:在原有HDFS基础上添加一个小文件处理模块,当用户上传一个文件时,判断该文件是否属于小文件,如果是,则交给小文件处理模块处理,否则,交给通用文件处理模块处理。小文件处理模块的设计思想是,先将很多小文件合并成一个大文件,然后为这些小文件建立索引,以便进行快速存取和访问。
二、应用程序优化
1、避免不必要的reduce任务
如果要处理的数据是排序且已经分区的,或者对于一份数据, 需要多次处理, 可以先排序分区;然后自定义InputSplit, 将单个分区作为单个mapred的输入;在map中处理数据, Reducer设置为空。
这样, 既重用了已有的 “排序”, 也避免了多余的reduce任务。
2、外部文件引入
有些应用程序要使用外部文件,如字典,配置文件等,这些文件需要在所有task之间共享,可以放到分布式缓存DistributedCache中(或直接采用-files选项,机制相同)。
更多的这方面的优化方法,还需要在实践中不断积累。
3、为job添加一个Combiner
为job添加一个combiner可以大大减少shuffle阶段从map task拷贝给远程reduce task的数据量。一般而言,combiner与reducer相同。
4、 根据处理数据特征使用最适合和简洁的Writable类型
Text对象使用起来很方便,但它在由数值转换到文本或是由UTF8字符串转换到文本时都是低效的,且会消耗大量的CPU时间。当处理那些非文本的数据时,可以使用二进制的Writable类型,如IntWritable, FloatWritable等。二进制writable好处:避免文件转换的消耗;使map task中间结果占用更少的空间。
5、 重用Writable类型
很多MapReduce用户常犯的一个错误是,在一个map/reduce方法中为每个输出都创建Writable对象。例如,你的Wordcout mapper方法可能这样写:
1 2 3 4 5 6 7 8 9 10 11 | public void map(...) {
…
for (String word : words) {
output.collect(new Text(word), new IntWritable(1));
}
} |
这样会导致程序分配出成千上万个短周期的对象。Java垃圾收集器就要为此做很多的工作。更有效的写法是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MyMapper … {
Text wordText = new Text();
IntWritable one = new IntWritable(1);
public void map(...) {
for (String word: words) {
wordText.set(word);
output.collect(wordText, one);
}
}
} |
6、 使用StringBuffer而不是String
当需要对字符串进行操作时,使用StringBuffer而不是String,String是read-only的,如果对它进行修改,会产生临时对象,而StringBuffer是可修改的,不会产生临时对象。
三、linux系统参数
1、 noatime 和 nodiratime属性
文件挂载时设置这两个属性可以明显提高性能。。默认情况下,Linux ext2/ext3 文件系统在文件被访问、创建、修改时会记录下文件的时间戳,比如:文件创建时间、最近一次修改时间和最近一次访问时间。如果系统运行时要访问大量文件,关闭这些操作,可提升文件系统的性能。Linux 提供了 noatime 这个参数来禁止记录最近一次访问时间戳。
2、readahead buffer
调整linux文件系统中预读缓冲区地大小,可以明显提高顺序读文件的性能。默认buffer大小为256 sectors,可以增大为1024或者2408 sectors(注意,并不是越大越好)。可使用blockdev命令进行调整。
3、 避免RAID和LVM操作
避免在TaskTracker和DataNode的机器上执行RAID和LVM操作,这通常会降低性能。
4、文件描述符打开数量限制:file-max
max-file表示系统级别的能够打开的文件句柄的数量,而ulimit -n控制进程级别能够打开的文件句柄的数量。 对于hadoop来说,datanode读写会打开大量文件,mapreduce任务也会创建很多临时文件。有必要设置较高的file-max值:
echo 100000 >/proc/sys/fs/file-max 或者
echo""fs.file-max=65535" >> /etc/sysctl.conf
或者(推荐)/etc/security/limits.conf
写入:
* soft nofile 32768
* hard nofile 65536
ulimit命令修改的数值只对当前登录用户的目前使用环境有效,系统重启或者用户退出后就会失效。查看当前的文件打开数量:/proc/sys/fs/file-nr
5.文件系统选择
EXT4格式要比EXT3更好。EXT4中的新特性,比如多块延迟分配,相比于EXT3要提高很大一部分的性能。在EXT3中,当一个文件被创建或数据被添加到一个已经存在的文件时,会直接调用文件block allocator,每个block一次;而EXT4则不同,它会做一个缓冲,以便以后能够最优化地将数据连续地放入硬盘。连续的文件能够很容易地被机械硬盘读写以能够提高存储IO的整体性能。
6、套接字最大链接设置
net.core.somaxconn Linux内核设置能够支持NameNode和JobTracker的大量爆发性的HTTP请求。net.core.somaxconn定义了系统中每一个端口最大的监听队列的长度,是套接字listen()的默认参数,挂起请求的最大数量.默认是128.对繁忙的服务器,增加该值有助于网络性能,当前已经被调整到32768。这个参数同样可以通过编辑/etc/sysctl.conf文件来改变,其中有一行:
net.core.somaxconn=32768或者:echo 2048> /proc/sys/net/core/somaxconn 但是这样系统重启后保存不了。
7.网卡参数:
设置txqueuelen到4096及以上能够更好地适应在Hadoop集群中的突发流量, txqueuelen代表用来传输数据的缓冲区的储存长度,通过下面的命令可以对该参数进行设置为4096:
sudo ifconfig eth# txqueuelen 4096
8.虚拟内存或交换分区的使用
swappiness的值的大小对如何使用swap分区是有着很大的联系的。swappiness=0的时候表示最大限度使用物理内存,然后才是 swap空间,swappiness=100的时候表示积极的使用swap分区,并且把内存上的数据及时的搬运到swap空间里面。linux的基本默认设置为60,也就是说,你的内存在使用到100-60=40%的时候,就开始出现有交换分区的使用。任何进程只要涉及到换页向磁盘写文件都会降低hadoop的性能,Linux内核进程vm.swappiness会检查无用的内存分页并将它们交换到磁盘上。默认的值是60,可以设置为0——100。对于Hadoop来说,设置成0是一个好主意,这并没有将这个特性关闭,Linux仍然进行换页操作,但是由于这个进程在仍然还有一大部分空闲内存时仍然会进行换页,将它设置成0可以尽可能地减少内存和磁盘的延迟。
这个参数仍然可以通过编辑/etc/sysctl.conf来进行修改。
也可以如下:echo 0> /proc/sys/vm/swappiness
9.THP(transparent huge page)
Linux的特性Transparent HugePages在大部分的应用中都提高了整体性能,包括Hadoop的工作负载。但是,其中的一项被称为Compaction的子特性会导致Hadoop工作负载的问题,在设置了Compaction的Hadoop benchmark测试中,结果会存在25%的浮动,而关闭Compaction后浮动消失。
当进行内存碎片整理时,Compaction会提高CPU资源利用率,这能够帮助优化Transparent HugePages,但是偷取了CPU资源,却影响了hadoop中正在运行的task性能。
通过以下命令可以查看是否启用compaction:
cat /sys/kernel/mm/redhat_transparent_hugepages/defrag
以及以下命令来禁用Compaction:
echo never > /sys/kernel/mm/redhat_transparent_hugepages/defrag
这个特定暂时没有进行修改,因为CentOS中没找到在哪儿设置。
四、HADOOP配置参数调整
1. dfs.namenode.handler.count或mapred.job.tracker.handler.count
namenode或者jobtracker中用于处理RPC的线程数,默认是10,较大集群,可调大些,比如64。
2. dfs.datanode.handler.count
datanode上用于处理RPC的线程数。默认为3,较大集群,可适当调大些,比如8。需要注意的是,每添加一个线程,需要的内存增加。
3. tasktracker.http.threads
HTTP server上的线程数。运行在每个TaskTracker上,用于处理maptask输出。大集群,可以将其设为40~50。
4. dfs.replication
文件副本数,通常设为3,不推荐修改。
5. dfs.block.size
HDFS中数据block大小,默认为64M,对于较大集群,可设为128MB或者256MB。(也可以通过参数mapred.min.split.size配置)
6. mapred.local.dir和dfs.data.dir
这两个参数mapred.local.dir和dfs.data.dir 配置的值应当是分布在各个磁盘上目录,这样可以充分利用节点的IO读写能力。运行 Linux sysstat包下的iostat -dx 5命令可以让每个磁盘都显示它的利用率。
7. {map/reduce}.tasks.maximum
同时运行在TaskTracker上的最大map/reduce task数,一般设为(core_per_node)/2~2*(cores_per_node)。
8. io.sort.factor
当一个map task执行完之后,本地磁盘上(mapred.local.dir)有若干个spill文件,map task最后做的一件事就是执行merge sort,把这些spill文件合成一个文件(partition)。执行merge sort的时候,每次同时打开多少个spill文件由该参数决定。打开的文件越多,不一定merge sort就越快,所以要根据数据情况适当的调整。
9. mapred.child.java.opts
设置JVM堆的最大可用内存,需从应用程序角度进行配置。
10. io.sort.mb
Map task的输出结果和元数据在内存中所占的buffer总大小。默认为100M,对于大集群,可设为200M。当buffer达到一定阈值,会启动一个后台线程来对buffer的内容进行排序,然后写入本地磁盘(一个spill文件)。
11. io.sort.spill.percent
这个值就是上述buffer的阈值,默认是0.8,即80%,当buffer中的数据达到这个阈值,后台线程会起来对buffer中已有的数据进行排序,然后写入磁盘。
12. io.sort.record
Io.sort.mb中分配给元数据的内存百分比,默认是0.05。这个需要根据应用程序进行调整。
13. mapred.compress.map.output/Mapred.output.compress
中间结果和最终结果是否要进行压缩,如果是,指定压缩方式(Mapred.compress.map.output.codec/ Mapred.output.compress.codec)。推荐使用LZO压缩。Intel内部测试表明,相比未压缩,使用LZO压缩的TeraSort作业运行时间减少60%,且明显快于Zlib压缩。
14. Mapred.reduce.parallel
Reduce shuffle阶段copier线程数。默认是5,对于较大集群,可调整为16~25。