一. HBase的通用优化
1 高可用
在 HBase 中 Hmaster 负责监控 RegionServer 的生命周期,均衡 RegionServer 的负载,如果 Hmaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以 HBase 支持对 Hmaster 的高可用配置。
HBase的高可用集群搭建参考: CentOS7.5搭建HBase1.2.6HA集群
2 Hadoop 的通用性优化
1) NameNode 元数据备份使用 SSD
2) 定时备份 NameNode 上的元数据
每小时或者每天备份,如果数据极其重要,可以 5~10 分钟备份一次。备份可以通过定时任务复制元数据目录即可。
3) 为 NameNode 指定多个元数据目录
使用 dfs.name.dir 或者 dfs.namenode.name.dir 指定。这样可以提供元数据的冗余和健壮性, 以免发生故障。
4) NameNode 的 dir 自恢复
设置 dfs.namenode.name.dir.restore 为 true,允许尝试恢复之前失败的 dfs.namenode.name.dir
目录,在创建 checkpoint 时做此尝试,如果设置了多个磁盘,建议允许。
5) HDFS 保证 RPC 调用会有较多的线程数
复制代码
属性:dfs.namenode.handler.count
解释:该属性是 NameNode 服务默认线程数,默认值是 10,根据机器的可用内存可以调整为 50~100
属性:dfs.datanode.handler.count
解释:该属性默认值为 10,是 DataNode 的处理线程数,如果 HDFS 客户端程序读写请求比较多,可以调高到 15~20,设置的值越大,内存消耗越多,不要调整的过高,一般业务中,
5~10 即可。
复制代码
hdfs-site.xml
6) HDFS 副本数的调整
复制代码
属性:dfs.replication
解释:如果数据量巨大,且不是非常之重要,可以调整为 2~3,如果数据非常之重要,可以调整为 3~5。
复制代码
hdfs-site.xml
7) HDFS 文件块大小的调整
复制代码
属性:dfs.blocksize
解释:块大小定义,该属性应该根据存储的大量的单个文件大小来设置,如果大量的单个文件都小于 100M,
建议设置成 64M 块大小,对于大于 100M 或者达到 GB 的这种情况,建议设置成 256M,一般设置范围波动在 64M~256M 之间。
复制代码
hdfs-site.xml
8) MapReduce Job 任务服务线程数调整
复制代码
属性:mapreduce.jobtracker.handler.count
解释:该属性是 Job 任务线程数,默认值是 10,根据机器的可用内存可以调整为 50~100
复制代码
mapred-site.xml
9) Http 服务器工作线程数
mapred-site.xml
复制代码
属性:mapreduce.tasktracker.http.threads
解释:定义 HTTP 服务器工作线程数,默认值为 40,对于大集群可以调整到 80~100
复制代码
10) 文件排序合并优化
mapred-site.xml
复制代码
属性:mapreduce.task.io.sort.factor
解释:文件排序时同时合并的数据流的数量,这也定义了同时打开文件的个数,默认值为
10,如果调高该参数,可以明显减少磁盘 IO,即减少文件读取的次数。
复制代码
11) 设置任务并发
mapred-site.xml
复制代码
属性:mapreduce.map.speculative
解释:该属性可以设置任务是否可以并发执行,如果任务多而小,该属性设置为 true 可以明显加快任务执行效率,但是对于延迟非常高的任务,建议改为 false,这就类似于迅雷下载。
复制代码
12) MR 输出数据的压缩
mapred-site.xml
复制代码
属性:mapreduce.map.output.compress、mapreduce.output.fileoutputformat.compress
解释:对于大集群而言,建议设置 Map-Reduce 的输出为压缩的数据,而对于小集群,则不需要。
复制代码
13) 优化 Mapper 和 Reducer 的个数
mapred-site.xml
复制代码
属性:mapreduce.tasktracker.map.tasks.maximum mapreduce.tasktracker.reduce.tasks.maximum
解释:以上两个属性分别为一个单独的 Job 任务可以同时运行的 Map 和 Reduce 的数量。
设置上面两个参数时,需要考虑 CPU 核数、磁盘和内存容量。假设一个 8 核的 CPU,业务内容非常消耗 CPU,那么可以设置 map 数量为 4,如果该业务不是特别消耗 CPU 类型的,
那么可以设置 map 数量为 40,reduce 数量为 20。这些参数的值修改完成之后,一定要观察是否有较长等待的任务,如果有的话,可以减少数量以加快任务执行,
如果设置一个很大的值,会引起大量的上下文切换,以及内存与磁盘之间的数据交换,这里没有标准的配置数值,
需要根据业务和硬件配置以及经验来做出选择。
在同一时刻,不要同时运行太多的 MapReduce,这样会消耗过多的内存,任务会执行的非常缓慢,我们需要根据 CPU 核数,内存容量设置一个 MR 任务并发的最大值,
使固定数据量的任务完全加载到内存中,避免频繁的内存和磁盘数据交换,从而降低磁盘 IO,提高性能。
复制代码
大概估算公式:
map = 2 + ⅔cpu_core, reduce = 2 + ⅓cpu_core
3 Linux 优化
1) 开启文件系统的预读缓存可以提高读取速度
$ sudo blockdev --setra 32768 /dev/sda
尖叫提示:ra 是 readahead 的缩写
2) 关闭进程睡眠池
即不允许后台进程进入睡眠状态,如果进程空闲,则直接 kill 掉释放资源
$ sudo sysctl -w vm.swappiness=0
3) 调整 ulimit 上限,默认值为比较小的数字
复制代码
$ ulimit -n 查看允许最大进程数
$ ulimit -u 查看允许打开最大文件数
复制代码
优化修改:
末尾添加:
*
soft
nofile
1024000
*
hard
nofile
1024000
Hive
-
nofile
1024000
hive
-
nproc
1024000
4) 开启集群的时间同步 NTP
集群中某台机器同步网络时间服务器的时间,集群中其他机器则同步这台机器的时间。
5) 更新系统补丁
更新补丁前,请先测试新版本补丁对集群节点的兼容性。
4 Zookeeper 优化
1) 优化 Zookeeper 会话超时时间
hbase-site.xml
复制代码
参数:zookeeper.session.timeout
解 释 :In hbase-site.xml, set zookeeper.session.timeout to 30 seconds or less to bound failure detection (20-30 seconds is a good start).
该值会直接关系到 master 发现服务器宕机的最大周期,默认值为 30 秒,如果该值过小,会在 HBase 在写入大量数据发生而 GC 时,导致RegionServer 短暂的不可用,
从而没有向 ZK 发送心跳包,最终导致认为从节点 shutdown。一般 20 台左右的集群需要配置 5 台 zookeeper。
复制代码
二. HBase的个性优化
1 预分区及RowKey设计
详细请看:HBase表以及Rowkey的设计原则
2 内存优化
HBase 操作过程中需要大量的内存开销,毕竟 Table 是可以缓存在内存中的,一般会分配整个可用内存的 70%给 HBase 的 Java 堆。但是不建议分配非常大的堆内存,因为 GC 过程持续太久会导致 RegionServer 处于长期不可用状态,一般 16~48G 内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。
3 基础优化
1) 允许在 HDFS 的文件中追加内容
不是不允许追加内容么?没错,请看背景故事:http://blog.cloudera.com/blog/2009/07/file-appends-in-hdfs/
hdfs-site.xml、hbase-site.xml
复制代码
属性:dfs.support.append
解释:开启 HDFS 追加同步,可以优秀的配合 HBase 的数据同步和持久化。默认值为 true.
复制代码
2) 优化 DataNode 允许的最大文件打开数
hdfs-site.xml
复制代码
属性:dfs.datanode.max.transfer.threads
解释:HBase 一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为 4096 或者更高。默认值:4096
复制代码
3) 优化延迟高的数据操作的等待时间
hdfs-site.xml
复制代码
属性:dfs.image.transfer.timeout
解释:如果对于某一次数据操作来讲,延迟非常高,socket 需要等待更长的时间,建议把该值设置为更大的值(默认 60000 毫秒),以确保 socket 不会被 timeout 掉。
复制代码
4) 优化数据的写入效率
mapred-site.xml
复制代码
属性:
mapreduce.map.output.compress mapreduce.map.output.compress.codec
解释:开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为true,第二个属性值修改为:org.apache.hadoop.io.compress.GzipCodec 或者其他压缩方式
复制代码
5) 优化 DataNode 存储
复制代码
属性:dfs.datanode.failed.volumes.tolerated
解释:默认为 0,意思是当 DataNode 中有一个磁盘出现故障,则会认为该 DataNode shutdown 了。
如果修改为 1,则一个磁盘出现故障时,数据会被复制到其他正常的 DataNode 上,当前的 DataNode 继续工作。
复制代码
6) 设置 RPC 监听数量
hbase-site.xml
复制代码
属性:hbase.regionserver.handler.count
解释:默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。
复制代码
7) 优化 HStore 文件大小
hbase-site.xml
复制代码
属性:hbase.hregion.max.filesize
解释:默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值, 因为一个 region 对应一个 map 任务,
如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两个 Hfile。
复制代码
8) 优化 hbase 客户端缓存
hbase-site.xml
复制代码
属性:hbase.client.write.buffer
解释:用于指定 HBase 客户端缓存,增大该值可以减少 RPC 调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少 RPC 次数的目的。
复制代码
9) 指定 scan.next 扫描 HBase 所获取的行数
hbase-site.xml
复制代码
属性:hbase.client.scanner.caching
解释:用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。
复制代码
10) flush、compact、split 机制
当 MemStore 达到阈值,将 Memstore 中的数据 Flush 进 Storefile;compact 机制则是把 flush 出来的小文件合并成大的 Storefile 文件。split 则是当 Region 达到阈值,会把过大的 Region 一分为二。
涉及属性:
即:128M 就是 Memstore 的默认阈值
hbase.hregion.memstore.flush.size:134217728
即:这个参数的作用是当单个 HRegion 内所有的 Memstore 大小总和超过指定值时,flush
该 HRegion 的所有 memstore。RegionServer 的 flush 是通过将请求添加一个队列,模拟生产消费模型来异步处理的。那这里就有一个问题,当队列来不及消费,产生大量积压请求时,可能会导致内存陡增,最坏的情况是触发 OOM。
复制代码
hbase.regionserver.global.memstore.upperLimit:0.4
hbase.regionserver.global.memstore.lowerLimit:0.38
复制代码
即:当 MemStore 使用内存总量达到 hbase.regionserver.global.memstore.upperLimit 指定值时,将会有多个 MemStores flush 到文件中,MemStore flush 顺序是按照大小降序执行的,直到刷新到 MemStore 使用内存略小于 lowerLimit
三. HBase的写表优化
1 多HTable并发写
创建多个HTable客户端用于写操作,提高写数据的吞吐量,一个例子:
复制代码
static final Configuration conf = HBaseConfiguration.create();
static final String table_log_name = “user_log”;
wTableLog = new HTable[tableN];
for (int i = 0; i < tableN; i++) {
wTableLog[i] = new HTable(conf, table_log_name);
wTableLog[i].setWriteBufferSize(5 * 1024 * 1024); //5MB
wTableLog[i].setAutoFlush(false);
}
复制代码
2 HTable参数设置
Auto Flush
通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。
Write Buffer
通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。
WAL Flag
在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。
因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。
值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
3 批量写
通过调用HTable.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方法:通过调用HTable.put(List
)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升。
4 多线程并发写
在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。下面给个具体的例子:
复制代码
for (int i = 0; i < threadN; i++) {
Thread th = new Thread() {
public void run() {
while (true) {
try {
sleep(1000); //1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (wTableLog[i]) {
try {
wTableLog[i].flushCommits();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
};
th.setDaemon(true);
th.start();
}
复制代码
四. HBase的读表优化
1 多HTable并发读
创建多个HTable客户端用于读操作,提高读数据的吞吐量,一个例子:
复制代码
static final Configuration conf = HBaseConfiguration.create();
static final String table_log_name = “user_log”;
rTableLog = new HTable[tableN];
for (int i = 0; i < tableN; i++) {
rTableLog[i] = new HTable(conf, table_log_name);
rTableLog[i].setScannerCaching(50);
}
复制代码
2 HTable参数设置
Scanner Caching
hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务端抓取的数据条数,默认情况下一次一条。通过将其设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录。
有三个地方可以进行配置:三者的优先级越来越高。
1)在HBase的conf配置文件中进行配置;
2)通过调用HTable.setScannerCaching(int scannerCaching)进行配置;
3)通过调用Scan.setCaching(int caching)进行配置。
Scan Attribute Selection
scan时指定需要的Column Family,可以减少网络传输数据量,否则默认scan操作会返回整行所有Column Family的数据。
Close ResultScanner
通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。
3 批量读
通过调用HTable.get(Get)方法可以根据一个指定的row key获取一行记录,同样HBase提供了另一个方法:通过调用HTable.get(List)方法可以根据一个指定的row key列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高而且网络传输RTT高的情景下可能带来明显的性能提升。
4 多线程并发读
在客户端开启多个HTable读线程,每个读线程负责通过HTable对象进行get操作。下面是一个多线程并发读取HBase,获取店铺一天内各分钟PV值的例子:
复制代码
public class DataReaderServer {
//获取店铺一天内各分钟PV值的入口函数
public static ConcurrentHashMap getUnitMinutePV(long uid, long startStamp, long endStamp){
long min = startStamp;
int count = (int)((endStamp - startStamp) / (60*1000));
List lst = new ArrayList();
for (int i = 0; i <= count; i++) {
min = startStamp + i * 60 * 1000;
lst.add(uid + "_" + min);
}
return parallelBatchMinutePV(lst);
}
//多线程并发查询,获取分钟PV值
private static ConcurrentHashMap parallelBatchMinutePV(List lstKeys){
ConcurrentHashMap hashRet = new ConcurrentHashMap();
int parallel = 3;
List> lstBatchKeys = null;
if (lstKeys.size() < parallel ){
lstBatchKeys = new ArrayList>(1);
lstBatchKeys.add(lstKeys);
}
else{
lstBatchKeys = new ArrayList>(parallel);
for(int i = 0; i < parallel; i++ ){
List lst = new ArrayList();
lstBatchKeys.add(lst);
}
for(int i = 0 ; i < lstKeys.size() ; i ++ ){
lstBatchKeys.get(i%parallel).add(lstKeys.get(i));
}
}
List >> futures = new ArrayList >>(5);
ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
builder.setNameFormat("ParallelBatchQuery");
ThreadFactory factory = builder.build();
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(lstBatchKeys.size(), factory);
for(List keys : lstBatchKeys){
Callable< ConcurrentHashMap > callable = new BatchMinutePVCallable(keys);
FutureTask< ConcurrentHashMap > future = (FutureTask< ConcurrentHashMap >) executor.submit(callable);
futures.add(future);
}
executor.shutdown();
// Wait for all the tasks to finish
try {
boolean stillRunning = !executor.awaitTermination(
5000000, TimeUnit.MILLISECONDS);
if (stillRunning) {
try {
executor.shutdownNow();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (InterruptedException e) {
try {
Thread.currentThread().interrupt();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
// Look for any exception
for (Future f : futures) {
try {
if(f.get() != null)
{
hashRet.putAll((ConcurrentHashMap)f.get());
}
} catch (InterruptedException e) {
try {
Thread.currentThread().interrupt();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
} catch (ExecutionException e) {
e.printStackTrace();
}
}
return hashRet;
}
//一个线程批量查询,获取分钟PV值
protected static ConcurrentHashMap getBatchMinutePV(List lstKeys){
ConcurrentHashMap hashRet = null;
List lstGet = new ArrayList();
String[] splitValue = null;
for (String s : lstKeys) {
splitValue = s.split("_");
long uid = Long.parseLong(splitValue[0]);
long min = Long.parseLong(splitValue[1]);
byte[] key = new byte[16];
Bytes.putLong(key, 0, uid);
Bytes.putLong(key, 8, min);
Get g = new Get(key);
g.addFamily(fp);
lstGet.add(g);
}
Result[] res = null;
try {
res = tableMinutePV[rand.nextInt(tableN)].get(lstGet);