知识库

知识库

HBase Schema 设计

hbase过往记忆 发表了文1章 • 0 个评论 • 1067 次浏览 • 2018-10-08 11:39 • 来自相关话题

大多数开发小伙伴都熟悉“数据库设计”这个词,在关系数据库的概念中,范式是关键。然而在大数据时代,分布式,可扩展,类似HBase这种nonSQL数据库,打破了传统关系数据库中的某些范式规定。这篇PPT将从大概念上介绍HBase大schema设计理念,并给一些通用的模式和真实的schema设计案例。当然也会探讨一些HBase的基本概念包括rowkey,列族等等,通过理解这些概念而理解在Schema定义的时候我们必须要做的一些取舍。 查看全部
大多数开发小伙伴都熟悉“数据库设计”这个词,在关系数据库的概念中,范式是关键。然而在大数据时代,分布式,可扩展,类似HBase这种nonSQL数据库,打破了传统关系数据库中的某些范式规定。这篇PPT将从大概念上介绍HBase大schema设计理念,并给一些通用的模式和真实的schema设计案例。当然也会探讨一些HBase的基本概念包括rowkey,列族等等,通过理解这些概念而理解在Schema定义的时候我们必须要做的一些取舍。

HBase实战之MOB使用指南

hbase过往记忆 发表了文1章 • 0 个评论 • 4582 次浏览 • 2018-10-08 10:29 • 来自相关话题

HBase可以很方便的将图片、文本等文件以二进制的方式进行存储。虽然HBase一般可以处理从1字节到10MB大小的二进制对象,但是HBase通常对于读写路径的优化主要是针对小于100KB的值。当HBase处理数据为100KB~10MB时,由于分裂(split)和压缩(compaction)会引起写的放大,从而会降低HBase性能。所以在HBase2.0+引入了MOB特性,这样保持了HBase的高性能、强一致性和低开销。

若要启用MOB功能,需要在每个RegionServer进行配置,并在建表或者修改表时对指定列族启用MOB特性。在HBase尝鲜版中启用MOB功能,需要由admin用户设置定期进程,以重新优化MOB数据的分布。

启用和配置RegionServer上的MOB特性
 
增加或者修改hbase-site.xml文件中的某些配置

1 设置MOB文件的缓存配置<property>
<name>hbase.mob.file.cache.size</name>
<value>1000</value>
</property>

<property>
<name>hbase.mob.cache.evict.period</name>
<value>3600</value>
</property>

<property>
<name>hbase.mob.cache.evict.remain.ratio</name>
<value>0.5f</value>
</property>

说明:

1)hbase.mob.file.cache.size 打开的文件句柄缓存数,默认值是1000。通过增加文件句柄数可以提高读的性能,可以减少频繁的打开、关闭文件。若这个值设置过大,会导致“too many opened file handers”。

2)hbase.mob.cache.evict.period  MOB缓存淘汰缓存的MOB文件时间间隔(以秒为单位),默认值为3600秒。

3)hbase.mob.cache.evict.remain.ratio   当缓存的MOB文件数目超过hbase.mob.file.cache.size设置的数目后,会触发MOB缓存淘汰机制(eviction),0.5f为剩余的MOB缓存比率(0~1),默认的比率为0.5f。


2  配置MobMasterObserver作为协处理器的master,主要用于表在删除后,MOB文件的归档。<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.hadoop.hbase.coprocessor.MobMasterObserver </value>
</property>

MOB的管理MOB特性为HBase引入新的读写路径,此时我们采用外部工具对其进行优化处理,一个是expiredMobFileCleanerTTL处理TTL和时间的过期数据,另一个是清理工具用来合并小的MOB文件或者多次更新、删除的MOB文件。

  a . 清理过期的MOB数据(expiredMobFileCleaner)

org.apache.hadoop.hbase.mob.compactions.expiredMobFileCleaner tableName familyName

设置清理延时<property>
<name>hbase.mob.cleaner.delay</name>
<value>60 * 60 * 1000</value>
</property>

 b.  清理工具

org.apache.hadoop.hbase.mob.compactions.Sweeper tableName familyName

属性值设置如下:<property>
<name>hbase.mob.compaction.invalid.file.ratio</name>
<value>0.3f</value>
</property>

<property>
<name>hbase.mob.compaction.small.file.threshold</name>
<value>67108864</value>
</property>

<property>
<name>hbase.mob.compaction.memstore.flush.size</name>
<value>134217728</value>
</property>

说明:

1)hbase.mob.compaction.invalid.file.ratio 如果在MOB文件中删除了太多的单元格,则被视为作为无效文件,需要重新写入或者合并。当MOB文件(mobFileSize)大小减去存在的单元格(existingCellsSize)大小之差除以MOB文件(mobFileSize)的比率小于设定的值时,我们就认为其为无效文件。默认值为0.3f。

2)hbase.mob.compaction.small.file.threshold 如果MOB的大小小于阈值,则视为小文件,需要合并。默认值为64MB。

3) hbase.mob.compaction.memstore.flush.size  MOB里memstore大小,超过此大小就会flush,并且每个sweep reducer拥有各自memstore。


警告:

使用清理工具最坏的情况:MOB文件压缩合并成功,但是相关的(put)更新失败。这意味着新的MOB文件已经创建但未能将新的MOB文件路径存入HBase中,因此HBase不会指向这些MOB文件。




小贴士:请检查yarn-site.xml的配置,在yarn.application.classpath中添加hbase的安装路径:$HBASE_HOME/* 和hbase的lib路径: $HBASE_HOME/lib/*

  <property>
<description>Classpath for typical applications.</description>
<name>yarn.application.classpath</name>
<value>
$HADOOP_CONF_DIR
$HADOOP_COMMON_HOME/*,$HADOOP_COMMON_HOME/lib/*
$HADOOP_HDFS_HOME/*,$HADOOP_HDFS_HOME/lib/*,
$HADOOP_MAPRED_HOME/*,$HADOOP_MAPRED_HOME/lib/*
$HADOOP_YARN_HOME/*,$HADOOP_YARN_HOME/lib/*
$HBASE_HOME/*, $HBASE_HOME/lib/*
</value>
</property>
在表中开启MOB特性a 将列族设置为MOBHColumnDescriptor hcd = new HColumnDescriptor(“f”);
hcd.setValue(MobConstants.IS_MOB, Bytes.toBytes(Boolean.TRUE));
b 设置MOB单元格的阈值,默认为102400HColumnDescriptor hcd;
hcd.setValue(MobConstants.MOB_THRESHOLD, Bytes.toBytes(102400L);

对于客户端而言,MOB单元格操作和普通单元格类似。

c 插入MOB值KeyValue kv = new KeyValue(row1, family, qf1, ts, KeyValue.Type.Put, value );
Put put = new Put(row1);
put.add(kv);
region.put(put);


d 获取MOB值Scan scan = new Scan();
InternalScanner scanner = (InternalScanner) region.getScanner(scan);
scanner.next(result, limit);
e 获取MOB 所有数据(raw =true)Scan scan = new Scan();
scan.setAttribute(MobConstants.MOB_SCAN_RAW, Bytes.toBytes(Boolean.TRUE));
InternalScanner scanner = (InternalScanner) region.getScanner(scan);
scanner.next(result, limit);
运行一个MOB示例sudo -u hbase hbase org.apache.hadoop.hbase.IntegrationTestIngestMOB


  查看全部
HBase可以很方便的将图片、文本等文件以二进制的方式进行存储。虽然HBase一般可以处理从1字节到10MB大小的二进制对象,但是HBase通常对于读写路径的优化主要是针对小于100KB的值。当HBase处理数据为100KB~10MB时,由于分裂(split)和压缩(compaction)会引起写的放大,从而会降低HBase性能。所以在HBase2.0+引入了MOB特性,这样保持了HBase的高性能、强一致性和低开销。

若要启用MOB功能,需要在每个RegionServer进行配置,并在建表或者修改表时对指定列族启用MOB特性。在HBase尝鲜版中启用MOB功能,需要由admin用户设置定期进程,以重新优化MOB数据的分布。

启用和配置RegionServer上的MOB特性
 
增加或者修改hbase-site.xml文件中的某些配置

1 设置MOB文件的缓存配置
<property>
<name>hbase.mob.file.cache.size</name>
<value>1000</value>
</property>

<property>
<name>hbase.mob.cache.evict.period</name>
<value>3600</value>
</property>

<property>
<name>hbase.mob.cache.evict.remain.ratio</name>
<value>0.5f</value>
</property>


说明:

1)hbase.mob.file.cache.size 打开的文件句柄缓存数,默认值是1000。通过增加文件句柄数可以提高读的性能,可以减少频繁的打开、关闭文件。若这个值设置过大,会导致“too many opened file handers”。

2)hbase.mob.cache.evict.period  MOB缓存淘汰缓存的MOB文件时间间隔(以秒为单位),默认值为3600秒。

3)hbase.mob.cache.evict.remain.ratio   当缓存的MOB文件数目超过hbase.mob.file.cache.size设置的数目后,会触发MOB缓存淘汰机制(eviction),0.5f为剩余的MOB缓存比率(0~1),默认的比率为0.5f。


2  配置MobMasterObserver作为协处理器的master,主要用于表在删除后,MOB文件的归档。
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.hadoop.hbase.coprocessor.MobMasterObserver </value>
</property>


MOB的管理MOB特性为HBase引入新的读写路径,此时我们采用外部工具对其进行优化处理,一个是expiredMobFileCleanerTTL处理TTL和时间的过期数据,另一个是清理工具用来合并小的MOB文件或者多次更新、删除的MOB文件。

  a . 清理过期的MOB数据(expiredMobFileCleaner)

org.apache.hadoop.hbase.mob.compactions.expiredMobFileCleaner tableName familyName

设置清理延时
<property>
<name>hbase.mob.cleaner.delay</name>
<value>60 * 60 * 1000</value>
</property>


 b.  清理工具

org.apache.hadoop.hbase.mob.compactions.Sweeper tableName familyName

属性值设置如下:
<property>
<name>hbase.mob.compaction.invalid.file.ratio</name>
<value>0.3f</value>
</property>

<property>
<name>hbase.mob.compaction.small.file.threshold</name>
<value>67108864</value>
</property>

<property>
<name>hbase.mob.compaction.memstore.flush.size</name>
<value>134217728</value>
</property>

说明:

1)hbase.mob.compaction.invalid.file.ratio 如果在MOB文件中删除了太多的单元格,则被视为作为无效文件,需要重新写入或者合并。当MOB文件(mobFileSize)大小减去存在的单元格(existingCellsSize)大小之差除以MOB文件(mobFileSize)的比率小于设定的值时,我们就认为其为无效文件。默认值为0.3f。

2)hbase.mob.compaction.small.file.threshold 如果MOB的大小小于阈值,则视为小文件,需要合并。默认值为64MB。

3) hbase.mob.compaction.memstore.flush.size  MOB里memstore大小,超过此大小就会flush,并且每个sweep reducer拥有各自memstore。


警告:

使用清理工具最坏的情况:MOB文件压缩合并成功,但是相关的(put)更新失败。这意味着新的MOB文件已经创建但未能将新的MOB文件路径存入HBase中,因此HBase不会指向这些MOB文件。




小贴士:请检查yarn-site.xml的配置,在yarn.application.classpath中添加hbase的安装路径:$HBASE_HOME/* 和hbase的lib路径: $HBASE_HOME/lib/*

  
<property>
<description>Classpath for typical applications.</description>
<name>yarn.application.classpath</name>
<value>
$HADOOP_CONF_DIR
$HADOOP_COMMON_HOME/*,$HADOOP_COMMON_HOME/lib/*
$HADOOP_HDFS_HOME/*,$HADOOP_HDFS_HOME/lib/*,
$HADOOP_MAPRED_HOME/*,$HADOOP_MAPRED_HOME/lib/*
$HADOOP_YARN_HOME/*,$HADOOP_YARN_HOME/lib/*
$HBASE_HOME/*, $HBASE_HOME/lib/*
</value>
</property>

在表中开启MOB特性a 将列族设置为MOB
HColumnDescriptor hcd = new HColumnDescriptor(“f”);
hcd.setValue(MobConstants.IS_MOB, Bytes.toBytes(Boolean.TRUE));

b 设置MOB单元格的阈值,默认为102400
HColumnDescriptor hcd;
hcd.setValue(MobConstants.MOB_THRESHOLD, Bytes.toBytes(102400L);


对于客户端而言,MOB单元格操作和普通单元格类似。

c 插入MOB值
KeyValue kv = new KeyValue(row1, family, qf1, ts, KeyValue.Type.Put, value );
Put put = new Put(row1);
put.add(kv);
region.put(put);


d 获取MOB值
Scan scan = new Scan();
InternalScanner scanner = (InternalScanner) region.getScanner(scan);
scanner.next(result, limit);

e 获取MOB 所有数据(raw =true)
Scan scan = new Scan();
scan.setAttribute(MobConstants.MOB_SCAN_RAW, Bytes.toBytes(Boolean.TRUE));
InternalScanner scanner = (InternalScanner) region.getScanner(scan);
scanner.next(result, limit);

运行一个MOB示例sudo -u hbase hbase org.apache.hadoop.hbase.IntegrationTestIngestMOB


 

连接HBase的正确姿势

hbasehbasegroup 发表了文1章 • 1 个评论 • 3504 次浏览 • 2018-10-01 17:42 • 来自相关话题

在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;每个线程一个Connection对象。每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。
从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。HBase RegionServer:用于读、写数据。
下图简单示意了客户端与服务器交互的步骤:



HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:



HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
} 查看全部
在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
  1. 自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;
  2. 每个线程一个Connection对象。
  3. 每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。

从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
  1. Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。
  2. HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。
  3. HBase RegionServer:用于读、写数据。

下图简单示意了客户端与服务器交互的步骤:
客户端与服务器交互的步骤.png
HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:
Connection对象与Socket连接之间的对应关系.png
HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。
//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}
HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:
/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}
RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。
protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}
当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:
/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}
连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:
protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
}

HBase版本进化史

hbase过往记忆 发表了文1章 • 1 个评论 • 684 次浏览 • 2018-09-04 20:16 • 来自相关话题

HBase诞生于美国加州旧金山的Powerset公司,从起初作为Hadoop项目的一部分,到后来成为apache软件基金会的顶级项目,再到逐渐被越来越多的互联网公司在很多业务场景中,重点使用。2006年至今,HBase经历了10多年的演化迭代:
2006年12月,Google发布了论文《Bigtable: A Distributed Storage System for Structured Data》,基于该文论的设计思想,HBase原型实现后来才得以诞生。2007年02月,作为Hadoop项目的分支,第一版HBase诞生。2007年10月,第一个可用的HBase版(Hadoop0.15.0)诞生。2008年01月,Hadoop成为Apache的顶级项目,HBase为其子项目。2008年10月,HBase 0.18.0版本发布。2009年01月,HBase 0.19.0版本发布。2009年10月,HBase 0.20.0版本发布。2010年05月,HBase晋升为Apache的顶级项目。2010年06月,HBase 首个开发者版本(0.89.20100621)发布。2011年01月,Hbase 首个可持续的,稳定版(0.90.0)版本发布。2012年01月,Hbase 0.92.0版本发布,支持事物(coprocessor)和安全(security)作为其版本标签。2012年05月,HBase 0.94.0版本发布,性能作为其版本标签。2013年01月,HBase 0.96.0版本发布。2014年02月,0.98.0版本发布,该版本也是很多国内公司最早使用的版本。2015年02月,HBase 1.0.0版本发布。2016年02月,HBase 1.2.0版本发布。2017年05月,HBase 1.2.6版本发布,作为其1.2版本的稳定版标签。2018年04月,HBase 期待已久的2.0.0版本正式发布。2018年06月,Hbase Hbase2.0.1版本发布。
HBase0.98.0、HBase1.2.6版本、HBase1.4+版本是目前很多互联网公司主流版本,系统稳定性、可靠性等都经受了很多考验。另外,对于想紧跟社区步伐,体验新功能的同学,可以使用最新的2.0版本。 查看全部
HBase诞生于美国加州旧金山的Powerset公司,从起初作为Hadoop项目的一部分,到后来成为apache软件基金会的顶级项目,再到逐渐被越来越多的互联网公司在很多业务场景中,重点使用。2006年至今,HBase经历了10多年的演化迭代:
  • 2006年12月,Google发布了论文《Bigtable: A Distributed Storage System for Structured Data》,基于该文论的设计思想,HBase原型实现后来才得以诞生。
  • 2007年02月,作为Hadoop项目的分支,第一版HBase诞生。
  • 2007年10月,第一个可用的HBase版(Hadoop0.15.0)诞生。
  • 2008年01月,Hadoop成为Apache的顶级项目,HBase为其子项目。
  • 2008年10月,HBase 0.18.0版本发布。
  • 2009年01月,HBase 0.19.0版本发布。
  • 2009年10月,HBase 0.20.0版本发布。
  • 2010年05月,HBase晋升为Apache的顶级项目。
  • 2010年06月,HBase 首个开发者版本(0.89.20100621)发布。
  • 2011年01月,Hbase 首个可持续的,稳定版(0.90.0)版本发布。
  • 2012年01月,Hbase 0.92.0版本发布,支持事物(coprocessor)和安全(security)作为其版本标签。
  • 2012年05月,HBase 0.94.0版本发布,性能作为其版本标签。
  • 2013年01月,HBase 0.96.0版本发布。
  • 2014年02月,0.98.0版本发布,该版本也是很多国内公司最早使用的版本。
  • 2015年02月,HBase 1.0.0版本发布。
  • 2016年02月,HBase 1.2.0版本发布。
  • 2017年05月,HBase 1.2.6版本发布,作为其1.2版本的稳定版标签。
  • 2018年04月,HBase 期待已久的2.0.0版本正式发布。
  • 2018年06月,Hbase Hbase2.0.1版本发布。

HBase0.98.0、HBase1.2.6版本、HBase1.4+版本是目前很多互联网公司主流版本,系统稳定性、可靠性等都经受了很多考验。另外,对于想紧跟社区步伐,体验新功能的同学,可以使用最新的2.0版本。

HBase 中分页过滤器详细解析

hbase过往记忆 发表了文1章 • 0 个评论 • 904 次浏览 • 2018-08-20 09:57 • 来自相关话题

分页过滤器:对habse中的数据,按照所设置的一页的行数,进行分页。
 
前提
 
首先我们要先了解数据在hbase中的存储格式,数据是以十六进制的格式存储在hbase中的。还有个我们需要知道的就是在hbase中,各行是按照什么标准进行排序的?在hbase根据各行之间通过行键按照字典序进行排序的,字典序就是如果是字母则对应其在字母表中的顺序进行排序,比如a(在1号位)就大于在3号位的c;1<10<2<20<200<3,先比较第一位,再比较二号位依次类推;具有相同前缀,短的小(400>4)。最后我们要了解分页过滤的机制,通过分页过滤,它会保证每页会存在所设置的行数(我们假设为3),也就是说如果把首行设置为一个不存在的行键也没关系,这样就会将该行键后的3行放到页中,而如果首行存在,这时就会将首行和该行后面的两行放到一页中。
 
设计思路
 
第一页比较好做,因为我们知道第一行的第一个行键(就是表的第一行的行键),然后我们根据设置的一页的行数,就可以做出第一页。 

第二页和后面的页就没这么好做了,因为我们不知道这些页的首行行键是什么,因为在hbase中各行的行键并没有规律,这时我们就需要些小技巧,对上一页的最后一个行键后面加个’0’,这样hbase会自动将加’0’的行键当作上一页最后一行的下一行的行键,再加上上面前提中的最后一条,我们就可以完成,对非第一页的分页了,是不是很巧妙?

样例所用的表






代码实现public class HighLevelApi {
Connection conn;
@Before
public void init() {
//配置参数信息
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "oracle");
conf.set("hbase.rootdir", "hdfs://oracle:9000/hbase");
//建立链接,这里需要注意的是我已经在电脑中的host文件添加了集群的主机名对应的IP
//若没有添加,"oralce"的位置还是应该写成IP地址
try {
conn = ConnectionFactory.createConnection(conf);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
@After
public void close() {
try {
conn.close();//关闭链接
} catch (IOException e) {
e.printStackTrace();
}
}
@Test//分页过滤器,按行输出比如说有五行,pageSize=3,就会输出两页
public void testPageFilter(){
Table table;
try {
table = conn.getTable(TableName.valueOf("school"));
byte POSTFIX = new byte { 0x00 };//长度为零的字节数组
Filter filter = new PageFilter(3);//设置一页所含的行数

//我们要了解分页过滤的机制
//通过分页过滤,它会保证每页会存在所设置的行数(我们假设为3),也就是说如果把首行设置为一个不存在的行键也没关系
//这样就会将该行键后的3行放到页中,而如果首行存在,这时就会将首行和该行后面的两行放到一页中
int totalRows = 0;//总行数
byte lastRow = null;//该页的最后一行
while (true) {
Scan scan = new Scan();
scan.setFilter(filter);
//如果不是第一页
if (lastRow != null) {
// 因为不是第一页,所以我们需要设置其实位置,我们在上一页最后一个行键后面加了一个零,来充当上一页最后一个行键的下一个
byte startRow = Bytes.add(lastRow, POSTFIX);
System.out.println("start row: "
+ Bytes.toStringBinary(startRow));
scan.setStartRow(startRow);
}
ResultScanner scanner = table.getScanner(scan);
int localRows = 0;
Result result;
while ((result = scanner.next()) != null) {
System.out.println(localRows++ + ": " + result);
totalRows++;
lastRow = result.getRow();//获取最后一行的行键
}
scanner.close();
//最后一页,查询结束
if (localRows == 0)
break;
}
System.out.println("total rows: " + totalRows);
table.close();

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}输出结果





  查看全部
分页过滤器:对habse中的数据,按照所设置的一页的行数,进行分页。
 
前提
 
首先我们要先了解数据在hbase中的存储格式,数据是以十六进制的格式存储在hbase中的。还有个我们需要知道的就是在hbase中,各行是按照什么标准进行排序的?在hbase根据各行之间通过行键按照字典序进行排序的,字典序就是如果是字母则对应其在字母表中的顺序进行排序,比如a(在1号位)就大于在3号位的c;1<10<2<20<200<3,先比较第一位,再比较二号位依次类推;具有相同前缀,短的小(400>4)。最后我们要了解分页过滤的机制,通过分页过滤,它会保证每页会存在所设置的行数(我们假设为3),也就是说如果把首行设置为一个不存在的行键也没关系,这样就会将该行键后的3行放到页中,而如果首行存在,这时就会将首行和该行后面的两行放到一页中。
 
设计思路
 
第一页比较好做,因为我们知道第一行的第一个行键(就是表的第一行的行键),然后我们根据设置的一页的行数,就可以做出第一页。 

第二页和后面的页就没这么好做了,因为我们不知道这些页的首行行键是什么,因为在hbase中各行的行键并没有规律,这时我们就需要些小技巧,对上一页的最后一个行键后面加个’0’,这样hbase会自动将加’0’的行键当作上一页最后一行的下一行的行键,再加上上面前提中的最后一条,我们就可以完成,对非第一页的分页了,是不是很巧妙?

样例所用的表

20170810004617542.png


代码实现
public class HighLevelApi {
Connection conn;
@Before
public void init() {
//配置参数信息
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "oracle");
conf.set("hbase.rootdir", "hdfs://oracle:9000/hbase");
//建立链接,这里需要注意的是我已经在电脑中的host文件添加了集群的主机名对应的IP
//若没有添加,"oralce"的位置还是应该写成IP地址
try {
conn = ConnectionFactory.createConnection(conf);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
@After
public void close() {
try {
conn.close();//关闭链接
} catch (IOException e) {
e.printStackTrace();
}
}
@Test//分页过滤器,按行输出比如说有五行,pageSize=3,就会输出两页
public void testPageFilter(){
Table table;
try {
table = conn.getTable(TableName.valueOf("school"));
byte POSTFIX = new byte { 0x00 };//长度为零的字节数组
Filter filter = new PageFilter(3);//设置一页所含的行数

//我们要了解分页过滤的机制
//通过分页过滤,它会保证每页会存在所设置的行数(我们假设为3),也就是说如果把首行设置为一个不存在的行键也没关系
//这样就会将该行键后的3行放到页中,而如果首行存在,这时就会将首行和该行后面的两行放到一页中
int totalRows = 0;//总行数
byte lastRow = null;//该页的最后一行
while (true) {
Scan scan = new Scan();
scan.setFilter(filter);
//如果不是第一页
if (lastRow != null) {
// 因为不是第一页,所以我们需要设置其实位置,我们在上一页最后一个行键后面加了一个零,来充当上一页最后一个行键的下一个
byte startRow = Bytes.add(lastRow, POSTFIX);
System.out.println("start row: "
+ Bytes.toStringBinary(startRow));
scan.setStartRow(startRow);
}
ResultScanner scanner = table.getScanner(scan);
int localRows = 0;
Result result;
while ((result = scanner.next()) != null) {
System.out.println(localRows++ + ": " + result);
totalRows++;
lastRow = result.getRow();//获取最后一行的行键
}
scanner.close();
//最后一页,查询结束
if (localRows == 0)
break;
}
System.out.println("total rows: " + totalRows);
table.close();

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
输出结果

20170810004643745.png

 

HBase实现分析:HFile

hbase过往记忆 发表了文1章 • 1 个评论 • 1071 次浏览 • 2018-08-16 11:40 • 来自相关话题

在这里主要分析一下HFile V2的各个组成部分的一些细节,重点分析了HFile V2的多级索引的机制,接下去有时间的话会分析源码中对HFile的读写扫描操作。HFile和流程:
如下图,HFile的组成分成四部分,分别是Scanned Block(数据block)、Non-Scanned block(元数据block)、Load-on-open(在hbase运行时,HFile需要加载到内存中的索引、bloom filter和文件信息)以及trailer(文件尾)。

在HFile中根据一个key搜索一个data的过程:
1、先内存中对HFile的root index进行二分查找。如果支持多级索引的话,则定位到的是leaf/intermediate index,如果是单级索引,则定位到的是data block
2、如果支持多级索引,则会从缓存/hdfs(分布式文件系统)中读取leaf/intermediate index chunk,在leaf/intermediate chunk根据key值进行二分查找(leaf/intermediate index chunk支持二分查找),找到对应的data block。
3、从缓存/hdfs中读取data block
4、在data block中遍历查找key。
接下去我们分析一个HFile的各个组成部分的详细细节,重点会分析一下HFile V2的多级索引。1 Hbase的KeyValue结构
KeyValue结构是hbase存储的核心,每个数据都是以keyValue结构在hbase中进行存储。KeyValue结构是一个有固定格式的byte数组,其结构在内存和磁盘中的格式如下:

The KeyValue格式:
Keylengthvaluelengthkeyvalue
其中keylength和valuelength都是整型,表示长度。
而key和value都是byte数据,key是有固定的数据,而value是raw data。Key的格式如下。
The Key format:
rowlengthrow (i.e., the rowkey)columnfamilylengthcolumnfamilycolumnqualifiertimestampkeytype
keytype有四种类型,分别是Put、Delete、 DeleteColumn和DeleteFamily。
特别说明:在key的所有组成成员中,columnquallfier的长度不固定,不需要用qualifier_len字段来标示其长度,因为可以通过key_len - ((key固定长度) + row_len + columnFamily_len)获得,其中Key固定长度为 sizeof(Row_len) + sizeof(columnFamily_len) + sizeof(timestamp) + sizeof(keytype)。KeyValue是字节流形式,所以不需要考虑字节对齐。2 File Trailer
fixedFileTrailer记录了HFile的基本信息、各个部分的偏移值和寻址信息。fileTailer拥有固定的长度,下图是HFile V1和HFile V2的差别。在FileTrailer中存储着加载一个HFile的所有信息。FileTrailer在磁盘中的分布如下图所示:

HFile V2见图右边。下面列举一下各个字段的含义和作用
BlockType:block类型。
FileInfoOffset:fileInfo的起始偏移地址。
LoadOnOpenDataOffset:需要被加载到内存中的Hfile部分的起始地址。
DataIndexEntriesNum:data index的root index chunk包含的index entry数目。
UncompressedDataIndexSize:所有的未经压缩的data index的大小 。
TmetaIndexEntriesNum:meta index entry的数目。
totalUncompressedBytes:key value对象未经压缩的总大小。
numEntries:key value对象的数目。
compressionCodec:编解码算法。
numDataIndexLeves:data block的index level。
firstDataBlockOffset:第一个data block的起始偏移地址。Scan操作的起始。
lastDataBlockOffset:最后一个data block的之后的第一个byte地址。记录scan的边界。
version:版本号。读取一个HFile的流程如下:
1、 首先读取文件尾的4字节Version信息(FileTrailer的version字段)。
2、 根据Version信息得到Trailer的长度(不同版本有不同的长度),然后根据trailer长度,加载FileTrailer。
3、 加载load-on-open部分到内存中,起始的文件偏移地址是trailer中的loadOnOpenDataOffset,load-on-open部分长度等于(HFile文件长度 - HFileTrailer长度)
如下图所示:

Load-on-open各个部分的加载顺序如下:
依次加载各部分的HFileBlock(load-on-open所有部分都是以HFileBlock格式存储):data index block、meta index block、FileInfo block、generate bloom filter index、和delete bloom filter。HFileBlock的格式会在下面介绍。3 Load on open
这部分数据在HBase的region server启动时,需要加载到内存中。包括FileInfo、Bloom filter block、data block index和meta block index。3.1 FileInfo
FileInfo中保存一些HFile的基本信息,并以PB格式写入到磁盘中。在0.96中是以PB格式进行保存。3.2 HFileBlock
在hfile中,所有的索引和数据都是以HFileBlock的格式存在在hdfs中,
HFile version2的Block格式如下两图所示,有两种类型,第一种类型是没有checksum;第二种是包含checksum。对于block,下图中的绿色和浅绿色的内存是block header;深红部分是block data;粉红部分是checksum。
第一种block的header长度= 8 + 2 * 4 + 8;
第二种block的header长度=8 + 2 * 4 + 8 + 1 + 4 * 2;

                      图3.1 不支持checksum的block

                    图 3.2 支持checksum的block
BlockType:8个字节的magic,表示不同的block 类型。
CompressedBlockSize:表示压缩的block 数据大小(也就是在HDFS中的HFileBlock数据长度),不包括header长度。
UncompressedBlockSize:表示未经压缩的block数据大小,不包括header长度。
PreBlockOffset:前一个block的在hfile中的偏移地址;用于访问前一个block而不用跳到前一个block中,实现类似于链表的功能。
CheckSumType:在支持block checksum中,表示checksum的类型。
bytePerCheckSum:在支持checksum的block中,记录了在checksumChunk中的字节数;records the number of bytes in a checksum chunk。
SizeDataOnDisk:在支持checksum的block中,记录了block在disk中的数据大小,不包括checksumChunk。DataBlock
DataBlock是用于存储具体kv数据的block,相对于索引和meta(这里的meta是指bloom filter)DataBlock的格式比较简单。
在DataBlock中,KeyValue的分布如下图,在KeyValue后面跟一个timestamp。
3.3 HFileIndex
HFile中的index level是不固定的,根据不同的数据类型和数据大小有不同的选择,主要有两类,一类是single-level(单级索引),另一类是multi-level(多级索引,索引block无法在内存中存放,所以采用多级索引)。
HFile中的index chunk有两大类,分别是root index chunk、nonRoot index chunk。而nonRoot index chunk又分为interMetadiate index chunk和leaf index chunk,但intermetadiate index chunk和leaf index chunk在内存中的分布是一样的。
对于meta block和bloom block,采用的索引是single-level形式,采用single-level时,只用root index chunk来保存指向block的索引信息(root_index-->xxx_block)。
而对于data,当HFile的data block数量较少时,采用的是single level(root_index-->data_block)。当data block数量较多时,采用的是multi-level,一般情况下是两级索引,使用root index chunk和leaf index chunk来保存索引信息(root_index-->leaf_index-->data_block);但当data block数量很多时,采用的是三级索引,使用root index chunk、intermetadiate index chunk和leaf index chunk来保存指向数据的索引(root_index-->intermediate_index-->leaf_index-->data_block)。
所有的index chunk都是以HFileBlock格式进行存放的,首先是一个HFileBlock Header,然后才是index chunk的内容。Root Index
Root index适用于两种情况:
1、作为data索引的根索引。
2、作为meta和bloom的索引。
在Hfile Version2中,Meta index和bloom index都是single-level,也都采用root索引的格式。Data index可以single-level和multi-level的这形式。Root index可以表示single-level index也可以表示multi-level的first level。但这两种表示方式在内存中的存储方式是由一定差别,见图3.3和3.4。

                         图3.3 single-level root index

                              图3.4 multi-level root indexSingle-level
root索引是会被加载到内存中。在磁盘的格式见图3.4。
index entry的组成含义如下:
1、Offset (long):表示索引对应的block在Hfile文件中的偏移值。
2、On-disk size (int):表示索引对应的block在disk(Hfile文件)中的长度。
3、Key:Key是在内存中存储的byte array,分成两部分,其中一部分是key长度,另一部分是key数据。 Key值应该是index entry对应的data block的first row key。不论这个block是leaf index chunk还是data block或者是meta block。Mid-key and multi-level
对于multi-level root index,除了上面index entry数组之外还带有格外的数据mid-key的信息,这个mid-key是用于在对hfile进行split时,快速定位HFile的中间位置所使用。Multi-level root index在硬盘中的格式见图3.4。
Mid-key的含义:如果HFile总共有n个data block,那么mid-key就是能定位到第(n - 1)/2个data block的信息。
Mid-key的信息组成如下:
1、Offset:所在的leaf index chunk的起始偏移量
2、On-disk size:所在的leaf index chunk的长度
3、Key:在leaf index chunk中的位置。
如下图所示:第(n – 1)/2个data block位于第i个LeafIndexChunk,如果LeafIndexChunk的第一个data block的序号为k,那么offset、on-disk size以及key的值如下:
Offset为 LeafIndexChunk[i] 的offset
On-disk size为LeafIndexChunk[i] 的size
Key为(n – 1)/2 – k

                                              图 3.6 mid-key示意图NonRoot index
当HFile以multi-level来索引数据block时,会引入nonRoot index与root index一起构建整个索引。Nonroot索引包括Intermediate index和leaf index这两种类型,这两种索引在disk中的格式一致,都统一使用NonRoot格式进行存放,但用途和存放的位置不同。
Intermediate index是在当HFile的数据block太多或内存存在限制时,使用两级数据索引时导致root index chunk超过其最大值,所以通过增加索引的级数,将intermediate index作为second level,以此来保证root index chunk的大小在一定限制内,减少加载到内存中时的内存消耗。
intermediate index chunk中的每个index Entry都指向一个leaf index chunk。Intermediate index chunk在加载时不会被加载到内存中。Intermediate index chunk在HFile中存储的位置是紧挨着root index chunk。在写入root index chunk时,会检查root index chunk的容量是否超过最大值,如果超过,那么将root index chunk划分成多个intermediate index chunk,然后重新生成一个root index entry,新root index entry中的每一个index entry都是指向intermediate index chunk。先将各个intermediate index chunk写入到disk中,然后再写入root index chunk,如下图所示。

HFile使用multi-level index来索引data block时,Leaf index chunk是作为最末一级,leaf index chunk中的index entry是保存指向datablock的数据。Leaf index chunk也是以nonRoot格式来进行存储的,见图3.4,与intermediate index chunk一样,都样不会在加载hfile时被加载到内存中。
nonRoot索引增加了secondIndexOffset,作为二级索引,用于实现二分查找;而而nonRoot索引不会加载到内存中。增加nonRoot索引的目的就是解决在存储数据过大时导致索引的数量量也增加,无法加载到内存中,从而增加了seek和read时的开销。NonRoot index在磁盘中的格式如下图:

                                             图 non Root index
1、BlockNumber:索引条目的数目。
2、secondaryIndexOffset:每一个secondaryIndexOffset都是表示index entry在leaf索引block中的相对偏移值(相对于第一个index entry),它是作为index entry的二级索引,用于实现快速搜索(二分法查找)。如下图所示,第一个secondaryIndexOffset的偏移值为0,往后都是index entry在disk中的长度相加。
3、curTotalNonRootEntrySize:leaf索引块中所有index entry在disk中总的大小。
4、Index Entries:每一个条目都包含三个部分
Offset:entry引用的block在文件中的偏移地址
On-disk size:block在硬盘中的大小
Key:block中的first row key. key不需要像在root索引中按照key length和keyvalue进行保存,因为有secondaryIndexOffset的存在,已经不需要通过key length来识别各个index entry的边界。NonRoot索引的二分查找实现
1、首先NonRoot索引中的Index_entry需要按照顺序排列,这个顺序是通过key值的大小来决定的。key值应该就是row的key值。
2、使用secondaryIndexOffset实现二分查找。二分查找的原理
如果要查找一个InputKey所处的位置
    首先将位置初始化,row为0,high为BlockNumber - 1
   循环:
       mid= (low + high) / 2
       定位到中间位置的index Entry
       将index Entry的key值与InputKey进行比较
           如果 key > inputKey
               Row = mid + 1
           如果 key == inputKey
               找到正确对象,返回
           如果 key < inputKey
               High = mid – 1快速定位

                          3.9 indexEntry偏移值计算
每一个secondaryIndexOffset是四个字节,secondaryIndexOffset的值是index Entry的相对偏移。见上图,对于一个序号为i的index entry,其在leaf索引chunk中的绝对偏移值为
“( BlockNumber + 2 ) * sizeof( int ) + secondaryIndexOffset[i]”

       图 3.10 key值相对于index Entry的偏移
见上图,那么key的长度等于indexEntryOffset[i + 1] - (indexEntryOffset[i] + 4 * 8)Bloom filter
在HFile中,bloom filter的meta index也是作为load-on-open的一部分保存,bloom fiter有两种类型,一种是generate bloom filter,用于快速确定key是否存储在hbase中;另一种是delete bloom filter,用于快速确定key是否已经被删除。
Bloom filter meta index在硬盘中的格式如下:

Bloom meta index在磁盘中的格式如上图所示。
Version:表示版本;
totalByteSize:表示bloom filter的位组的bit数。
HashCount:表示一个key在位组中用几个bit位来进行定位。
HashType:表示hash函数的类型。
totalKeyCount:表示bloom filter当前已经包含的key的数目.
totalKeyMaxs:表示bloom filter当前最多包含的key的数目.
numChunks:表示bloom filter中包含的bloom filter block的数目.
comparatorName:表示比较器的名字.
接下去是每一个Bloom filter block的索引条目。[/i][/i][/i][/i] 查看全部
在这里主要分析一下HFile V2的各个组成部分的一些细节,重点分析了HFile V2的多级索引的机制,接下去有时间的话会分析源码中对HFile的读写扫描操作。HFile和流程:
如下图,HFile的组成分成四部分,分别是Scanned Block(数据block)、Non-Scanned block(元数据block)、Load-on-open(在hbase运行时,HFile需要加载到内存中的索引、bloom filter和文件信息)以及trailer(文件尾)。

在HFile中根据一个key搜索一个data的过程
1、先内存中对HFile的root index进行二分查找。如果支持多级索引的话,则定位到的是leaf/intermediate index,如果是单级索引,则定位到的是data block
2、如果支持多级索引,则会从缓存/hdfs(分布式文件系统)中读取leaf/intermediate index chunk,在leaf/intermediate chunk根据key值进行二分查找(leaf/intermediate index chunk支持二分查找),找到对应的data block。
3、从缓存/hdfs中读取data block
4、在data block中遍历查找key。
接下去我们分析一个HFile的各个组成部分的详细细节,重点会分析一下HFile V2的多级索引。1 Hbase的KeyValue结构
KeyValue结构是hbase存储的核心,每个数据都是以keyValue结构在hbase中进行存储。KeyValue结构是一个有固定格式的byte数组,其结构在内存和磁盘中的格式如下:

The KeyValue格式:
  • Keylength
  • valuelength
  • key
  • value

其中keylength和valuelength都是整型,表示长度。
而key和value都是byte数据,key是有固定的数据,而value是raw data。Key的格式如下。
The Key format:
  • rowlength
  • row (i.e., the rowkey)
  • columnfamilylength
  • columnfamily
  • columnqualifier
  • timestamp
  • keytype

keytype有四种类型,分别是Put、Delete、 DeleteColumn和DeleteFamily。
特别说明:在key的所有组成成员中,columnquallfier的长度不固定,不需要用qualifier_len字段来标示其长度,因为可以通过key_len - ((key固定长度) + row_len + columnFamily_len)获得,其中Key固定长度为 sizeof(Row_len) + sizeof(columnFamily_len) + sizeof(timestamp) + sizeof(keytype)。KeyValue是字节流形式,所以不需要考虑字节对齐。2 File Trailer
fixedFileTrailer记录了HFile的基本信息、各个部分的偏移值和寻址信息。fileTailer拥有固定的长度,下图是HFile V1和HFile V2的差别。在FileTrailer中存储着加载一个HFile的所有信息。FileTrailer在磁盘中的分布如下图所示:

HFile V2见图右边。下面列举一下各个字段的含义和作用
BlockType:block类型。
FileInfoOffset:fileInfo的起始偏移地址。
LoadOnOpenDataOffset:需要被加载到内存中的Hfile部分的起始地址。
DataIndexEntriesNum:data index的root index chunk包含的index entry数目。
UncompressedDataIndexSize:所有的未经压缩的data index的大小 。
TmetaIndexEntriesNum:meta index entry的数目。
totalUncompressedBytes:key value对象未经压缩的总大小。
numEntries:key value对象的数目。
compressionCodec:编解码算法。
numDataIndexLeves:data block的index level。
firstDataBlockOffset:第一个data block的起始偏移地址。Scan操作的起始。
lastDataBlockOffset:最后一个data block的之后的第一个byte地址。记录scan的边界。
version:版本号。读取一个HFile的流程如下:
1、 首先读取文件尾的4字节Version信息(FileTrailer的version字段)。
2、 根据Version信息得到Trailer的长度(不同版本有不同的长度),然后根据trailer长度,加载FileTrailer。
3、 加载load-on-open部分到内存中,起始的文件偏移地址是trailer中的loadOnOpenDataOffset,load-on-open部分长度等于(HFile文件长度 - HFileTrailer长度)
如下图所示:

Load-on-open各个部分的加载顺序如下:
依次加载各部分的HFileBlock(load-on-open所有部分都是以HFileBlock格式存储):data index block、meta index block、FileInfo block、generate bloom filter index、和delete bloom filter。HFileBlock的格式会在下面介绍。3 Load on open
这部分数据在HBase的region server启动时,需要加载到内存中。包括FileInfo、Bloom filter block、data block index和meta block index。3.1 FileInfo
FileInfo中保存一些HFile的基本信息,并以PB格式写入到磁盘中。在0.96中是以PB格式进行保存。3.2 HFileBlock
在hfile中,所有的索引和数据都是以HFileBlock的格式存在在hdfs中,
HFile version2的Block格式如下两图所示,有两种类型,第一种类型是没有checksum;第二种是包含checksum。对于block,下图中的绿色和浅绿色的内存是block header;深红部分是block data;粉红部分是checksum。
第一种block的header长度= 8 + 2 * 4 + 8;
第二种block的header长度=8 + 2 * 4 + 8 + 1 + 4 * 2;

                      图3.1 不支持checksum的block

                    图 3.2 支持checksum的block
BlockType:8个字节的magic,表示不同的block 类型。
CompressedBlockSize:表示压缩的block 数据大小(也就是在HDFS中的HFileBlock数据长度),不包括header长度。
UncompressedBlockSize:表示未经压缩的block数据大小,不包括header长度。
PreBlockOffset:前一个block的在hfile中的偏移地址;用于访问前一个block而不用跳到前一个block中,实现类似于链表的功能。
CheckSumType:在支持block checksum中,表示checksum的类型。
bytePerCheckSum:在支持checksum的block中,记录了在checksumChunk中的字节数;records the number of bytes in a checksum chunk。
SizeDataOnDisk:在支持checksum的block中,记录了block在disk中的数据大小,不包括checksumChunk。DataBlock
DataBlock是用于存储具体kv数据的block,相对于索引和meta(这里的meta是指bloom filter)DataBlock的格式比较简单。
在DataBlock中,KeyValue的分布如下图,在KeyValue后面跟一个timestamp。
3.3 HFileIndex
HFile中的index level是不固定的,根据不同的数据类型和数据大小有不同的选择,主要有两类,一类是single-level(单级索引),另一类是multi-level(多级索引,索引block无法在内存中存放,所以采用多级索引)。
HFile中的index chunk有两大类,分别是root index chunk、nonRoot index chunk。而nonRoot index chunk又分为interMetadiate index chunk和leaf index chunk,但intermetadiate index chunk和leaf index chunk在内存中的分布是一样的。
对于meta block和bloom block,采用的索引是single-level形式,采用single-level时,只用root index chunk来保存指向block的索引信息(root_index-->xxx_block)。
而对于data,当HFile的data block数量较少时,采用的是single level(root_index-->data_block)。当data block数量较多时,采用的是multi-level,一般情况下是两级索引,使用root index chunk和leaf index chunk来保存索引信息(root_index-->leaf_index-->data_block);但当data block数量很多时,采用的是三级索引,使用root index chunk、intermetadiate index chunk和leaf index chunk来保存指向数据的索引(root_index-->intermediate_index-->leaf_index-->data_block)。
所有的index chunk都是以HFileBlock格式进行存放的,首先是一个HFileBlock Header,然后才是index chunk的内容。Root Index
Root index适用于两种情况:
1、作为data索引的根索引。
2、作为meta和bloom的索引。
在Hfile Version2中,Meta index和bloom index都是single-level,也都采用root索引的格式。Data index可以single-level和multi-level的这形式。Root index可以表示single-level index也可以表示multi-level的first level。但这两种表示方式在内存中的存储方式是由一定差别,见图3.3和3.4。

                         图3.3 single-level root index

                              图3.4 multi-level root indexSingle-level
root索引是会被加载到内存中。在磁盘的格式见图3.4。
index entry的组成含义如下:
1、Offset (long):表示索引对应的block在Hfile文件中的偏移值。
2、On-disk size (int):表示索引对应的block在disk(Hfile文件)中的长度。
3、Key:Key是在内存中存储的byte array,分成两部分,其中一部分是key长度,另一部分是key数据。 Key值应该是index entry对应的data block的first row key。不论这个block是leaf index chunk还是data block或者是meta block。Mid-key and multi-level
对于multi-level root index,除了上面index entry数组之外还带有格外的数据mid-key的信息,这个mid-key是用于在对hfile进行split时,快速定位HFile的中间位置所使用。Multi-level root index在硬盘中的格式见图3.4。
Mid-key的含义:如果HFile总共有n个data block,那么mid-key就是能定位到第(n - 1)/2个data block的信息。
Mid-key的信息组成如下:
1、Offset:所在的leaf index chunk的起始偏移量
2、On-disk size:所在的leaf index chunk的长度
3、Key:在leaf index chunk中的位置。
如下图所示:第(n – 1)/2个data block位于第i个LeafIndexChunk,如果LeafIndexChunk的第一个data block的序号为k,那么offset、on-disk size以及key的值如下:
Offset为 LeafIndexChunk[i] 的offset
On-disk size为LeafIndexChunk[i] 的size
Key为(n – 1)/2 – k

                                              图 3.6 mid-key示意图NonRoot index
当HFile以multi-level来索引数据block时,会引入nonRoot index与root index一起构建整个索引。Nonroot索引包括Intermediate index和leaf index这两种类型,这两种索引在disk中的格式一致,都统一使用NonRoot格式进行存放,但用途和存放的位置不同。
Intermediate index是在当HFile的数据block太多或内存存在限制时,使用两级数据索引时导致root index chunk超过其最大值,所以通过增加索引的级数,将intermediate index作为second level,以此来保证root index chunk的大小在一定限制内,减少加载到内存中时的内存消耗。
intermediate index chunk中的每个index Entry都指向一个leaf index chunk。Intermediate index chunk在加载时不会被加载到内存中。Intermediate index chunk在HFile中存储的位置是紧挨着root index chunk。在写入root index chunk时,会检查root index chunk的容量是否超过最大值,如果超过,那么将root index chunk划分成多个intermediate index chunk,然后重新生成一个root index entry,新root index entry中的每一个index entry都是指向intermediate index chunk。先将各个intermediate index chunk写入到disk中,然后再写入root index chunk,如下图所示。

HFile使用multi-level index来索引data block时,Leaf index chunk是作为最末一级,leaf index chunk中的index entry是保存指向datablock的数据。Leaf index chunk也是以nonRoot格式来进行存储的,见图3.4,与intermediate index chunk一样,都样不会在加载hfile时被加载到内存中。
nonRoot索引增加了secondIndexOffset,作为二级索引,用于实现二分查找;而而nonRoot索引不会加载到内存中。增加nonRoot索引的目的就是解决在存储数据过大时导致索引的数量量也增加,无法加载到内存中,从而增加了seek和read时的开销。NonRoot index在磁盘中的格式如下图:

                                             图 non Root index
1、BlockNumber:索引条目的数目。
2、secondaryIndexOffset:每一个secondaryIndexOffset都是表示index entry在leaf索引block中的相对偏移值(相对于第一个index entry),它是作为index entry的二级索引,用于实现快速搜索(二分法查找)。如下图所示,第一个secondaryIndexOffset的偏移值为0,往后都是index entry在disk中的长度相加。
3、curTotalNonRootEntrySize:leaf索引块中所有index entry在disk中总的大小。
4、Index Entries:每一个条目都包含三个部分
Offset:entry引用的block在文件中的偏移地址
On-disk size:block在硬盘中的大小
Key:block中的first row key. key不需要像在root索引中按照key length和keyvalue进行保存,因为有secondaryIndexOffset的存在,已经不需要通过key length来识别各个index entry的边界。NonRoot索引的二分查找实现
1、首先NonRoot索引中的Index_entry需要按照顺序排列,这个顺序是通过key值的大小来决定的。key值应该就是row的key值。
2、使用secondaryIndexOffset实现二分查找。二分查找的原理
如果要查找一个InputKey所处的位置
    首先将位置初始化,row为0,high为BlockNumber - 1
   循环:
       mid= (low + high) / 2
       定位到中间位置的index Entry
       将index Entry的key值与InputKey进行比较
           如果 key > inputKey
               Row = mid + 1
           如果 key == inputKey
               找到正确对象,返回
           如果 key < inputKey
               High = mid – 1快速定位

                          3.9 indexEntry偏移值计算
每一个secondaryIndexOffset是四个字节,secondaryIndexOffset的值是index Entry的相对偏移。见上图,对于一个序号为i的index entry,其在leaf索引chunk中的绝对偏移值为
“( BlockNumber + 2 ) * sizeof( int ) + secondaryIndexOffset[i]”

       图 3.10 key值相对于index Entry的偏移
见上图,那么key的长度等于indexEntryOffset[i + 1] - (indexEntryOffset[i] + 4 * 8)Bloom filter
在HFile中,bloom filter的meta index也是作为load-on-open的一部分保存,bloom fiter有两种类型,一种是generate bloom filter,用于快速确定key是否存储在hbase中;另一种是delete bloom filter,用于快速确定key是否已经被删除。
Bloom filter meta index在硬盘中的格式如下:

Bloom meta index在磁盘中的格式如上图所示。
Version:表示版本;
totalByteSize:表示bloom filter的位组的bit数。
HashCount:表示一个key在位组中用几个bit位来进行定位。
HashType:表示hash函数的类型。
totalKeyCount:表示bloom filter当前已经包含的key的数目.
totalKeyMaxs:表示bloom filter当前最多包含的key的数目.
numChunks:表示bloom filter中包含的bloom filter block的数目.
comparatorName:表示比较器的名字.
接下去是每一个Bloom filter block的索引条目。
[/i][/i][/i][/i]

HBase 高可用原理与实践

hbase过往记忆 发表了文1章 • 0 个评论 • 714 次浏览 • 2018-08-14 10:20 • 来自相关话题

前言

前段时间有套线上HBase出了点小问题,导致该套HBase集群服务停止了2个小时,从而造成使用该套HBase作为数据存储的应用也出现了服务异常。在排查问题之余,我们不禁也在思考,以后再出现类似的问题怎么办?这种问题该如何避免?用惯了MySQL,于是乎想到了HBase是否跟MySQL一样,也有其高可用方案?

答案当然是肯定的,几乎所有的数据库(无论是关系型还是分布式的),都采用WAL的方式来保障服务异常时候的数据恢复,HBase同样也是通过WAL来保障数据不丢失。HBase在写数据前会先写HLog,HLog中记录的是所有数据的变动, HBase的高可用也正是通过HLog来实现的。

进阶
 
HBase是一个没有单点故障的分布式系统,上层(HBase层)和底层(HDFS层)都通过一定的技术手段,保障了服务的可用性。上层HMaster一般都是高可用部署,而RegionServer如果出现宕机,region迁移的代价并不大,一般都在毫秒级别完成,所以对应用造成的影响也很有限;底层存储依赖于HDFS,数据本身默认也有3副本,数据存储上做到了多副本冗余,而且Hadoop 2.0以后NameNode的单点故障也被消除。所以,对于这样一个本身没有单点故障,数据又有多副本冗余的系统,再进行高可用的配置是否有这个必要?会不会造成资源的极大浪费?

高可用部署是否有必要,这个需要根据服务的重要性来定,这里先简单介绍下没有高可用的HBase服务会出现哪些问题:

1.      数据库管理人员失误,进行了不可逆的DDL操作

不管是什么数据库,DDL操作在执行的时候都需要慎之又慎,很可能一条简单的drop操作,会导致所有数据的丢失,并且无法恢复,对于HBase来说也是这样,如果管理员不小心drop了一个表,该表的数据将会被丢失。

2.      离线MR消耗过多的资源,造成线上服务受到影响
 
HBase经过这么多年的发展,已经不再是只适合离线业务的数据存储分析平台,许多公司的线上业务也相继迁移到了HBase上,比较典型的如:facebook的iMessage系统、360的搜索业务、小米米聊的历史数据等等。但不可避免在这些数据上做些统计分析类操作,大型MR跑起来,会有很大的资源消耗,可能会影响线上业务。

3.      不可预计的另外一些情况

比如核心交换机故障,机房停电等等情况都会造成HBase服务中断

 对于上述的那些问题,可以通过配置HBase的高可用来解决:

1.     不可逆DDL问题

HBase的高可用不支持DDL操作,换句话说,在master上的DDL操作,不会影响到slave上的数据,所以即使在master上进行了DDL操作,slave上的数据依然没有变化。这个跟MySQL有很大不同,MySQL的DDL可以通过statement格式的Binlog进行复制。

2.     离线MR影响线上业务问题

高可用的最大好处就是可以进行读写分离,离线MR可以直接跑在slave上,master继续对外提供写服务,这样也就不会影响到线上的业务,当然HBase的高可用复制是异步进行的,在slave上进行MR分析,数据可能会有稍微延迟。

3.     意外情况

对于像核心交换机故障、断电等意外情况,slave跨机架或者跨机房部署都能解决该种情况。
 
基于以上原因,如果是核心服务,对于可用性要求非常高,可以搭建HBase的高可用来保障服务较高的可用性,在HBase的Master出现异常时,只需简单把流量切换到Slave上,即可完成故障转移,保证服务正常运行。

原理
 
HBase高可用保证在出现异常时,快速进行故障转移。下面让我们先来看看HBase高可用的实现,首先看下官方的一张图:





HBase Replication
 
需要声明的是,HBase的replication是以Column Family为单位的,每个Column Family都可以设置是否进行replication。

 上图中,一个Master对应了3个Slave,Master上每个RegionServer都有一份HLog,在开启Replication的情况下,每个RegionServer都会开启一个线程用于读取该RegionServer上的HLog,并且发送到各个Slave,Zookeeper用于保存当前已经发送的HLog的位置。Master与Slave之间采用异步通信的方式,保障Master上的性能不会受到Slave的影响。用Zookeeper保存已经发送HLog的位置,主要考虑在Slave复制过程中如果出现问题后重新建立复制,可以找到上次复制的位置。





HBase Replication步骤

 

1.         HBase Client向Master写入数据

 

2.         对应RegionServer写完HLog后返回Client请求

 

3.         同时replication线程轮询HLog发现有新的数据,发送给Slave

 

4.         Slave处理完数据后返回给Master

 

5.         Master收到Slave的返回信息,在Zookeeper中标记已经发送到Slave的HLog位置

 

注:在进行replication时,Master与Slave的配置并不一定相同,比如Master上可以有3台RegionServer,Slave上并不一定是3台,Slave上的RegionServer数量可以不一样,数据如何分布这个HBase内部会处理。

种类

 

HBase通过HLog进行数据复制,那么HBase支持哪些不同种类的复制关系?

 

从复制模式上来讲,HBase支持主从、主主两种复制模式,也就是经常说的Master-Slave、Master-Master复制。

 

1.         Master-Slave

 

Master-Slave复制比较简单,所有在Master集群上写入的数据都会被同步到Slave上。

 

2.         Master-Master

 

Master-Master复制与Master-Slave类似,主要的不同在于,在Master-Master复制中,两个Master地位相同,都可以进行读取和写入。

 

既然Master-Master两个Master都可以进行写入,万一出现一种情况:两个Master上都进行了对同一表的相同Column Family的同一个rowkey进行写入,会出现什么情况?

 

create  ‘t’,  {NAME=>’cf’, REPLICATION_SCOPE=>’1’}

 

Master1                                                        Master2

 

put ‘t’, ‘r1’, ‘cf’, ‘aaaaaaaaaaaaaaa’                       put ‘t’, ‘r1’, ‘cf’, ‘bbbbbbbbbbbbbbb’

 

如上操作,Master1上对t的cf列簇写入rowkey为r1,value为aaaaaaaaaaaaaaa的数据,Master2上同时对t的cf列簇写入rowkey为r1, value为bbbbbbbbbbbbbbb的数据,由于是Master-Master复制,Master1和Master2上在写入数据的同时都会把更新发送给对方,这样最终的数据就变成了:

 






从上述表格中可以看到,最终Master1和Master2上cf列簇rowkey为r1的数据两边不一致。

 

所以,在做Master-Master高可用时,确保两边写入的表都是不同的,这样能防止上述数据不一致问题。

 

异常

 

HBase复制时,都是通过RegionServer开启复制线程进行HLog的发送,那么当其中某个RegionServer出现异常时,HBase是如何处理的?这里需要区别两种不同的情况,即Master上RegionServer异常和Slave上RegionServer异常。

 

1.     Slave上RegionServer异常

 

对于该种异常HBase处理比较简单,Slave上出现某个RegionServer异常,该RegionServer直接会被标记为异常状态,后续所有的更新都不会被发送到该台RegionServer,Slave会重新选取一台RegionServer来接收这部分数据。

 

2.   Master上RegionServer异常

 

Master上RegionServer出现异常,由于HLog都是通过RegionServer开启复制线程进行发送,如果RegionServer出现异常,这个时候,属于该台RegionServer的HLog就没有相关处理线程,这个时候,这部分数据又该如何处理?

 

Master上某台RegionServer异常,其他RegionServer会对该台RegionServer在zookeeper中的信息尝试加锁操作,当然这个操作是互斥的,同一时间只有一台RegionServer能获取到锁,然后,会把HLog信息拷贝到自己的目录下,这样就完成了异常RegionServer的HLog信息的转移,通过新的RegionServer把HLog的信息发送到Slave。

Master regionserver crash

操作

 

上面介绍的都是HBase高可用的理论实现和异常处理等问题,下面就动手实践下,如何配置一个HBase的Replication(假设已经部署好了两套HBase系统,并且在配置文件中已经开启了replication配置),首先尝试配置下Master-Slave模式的高可用:

 

1.         选取一套系统作为Master,另外一套作为Slave

 

2.         在Master上通过add_peer 命令添加复制关系,如下

 

add_peer ‘1’, “db-xxx.photo.163.org:2181:/hbase”

 

3.         在Master上新建表t,该表拥有一个列簇名为cf,并且该列簇开启replication,如下:

 

create ‘t’, {NAME=>’cf’, REPLICATION_SCOPE=>’1’}

 

上面REPLICATION_SCOPE的值需要跟步骤2中的对应

 

4.         在slave建立相同的表(HBase不支持DDL的复制),在master-slave模式中,slave不需要开启复制,如下:

 

create ‘t’, {NAME=>’cf’ }

 

这样,我们就完成了整个master-slave模式高可用的搭建,后续可以在master上通过put操作插入一条记录,查看slave上是否会复制该记录,最终结果如下:

Master上操作

Slave上结果

上述结果显示,在添加完复制关系后,Master上插入rowkey=r1, value=’aaaaaaaaa’的记录,slave上可以获取该记录,Master-Slave模式数据复制成功。

接下来我们再看下Master-Master模式的复制,配置的时候与Master-Slave模式不同的是,在Master上添加完复制关系后,需要在另外一台Master也添加复制关系,而且两边的cluster_id必须相同,并且在另外一台Master上建表的时候,需要加上列簇的REPLICATION_SCOPE=>’1’配置,最终结果如下:

Master1上操作

Master2上操作

 

上述结果显示,添加完了Master-Master复制关系,在Master1上插入一条记录rowkey=r1, value=“aaaaaaaaaa”,Master2上通过scan操作发现该记录已经被复制到Master2上,接着我们在Master2上添加一条记录rowkey=r2, value=’bbbbbbbbbbbb’,查看Master1上的数据,该条记录也已经被复制到Master2上,Master-Master模式的replication验证成功。 查看全部
前言

前段时间有套线上HBase出了点小问题,导致该套HBase集群服务停止了2个小时,从而造成使用该套HBase作为数据存储的应用也出现了服务异常。在排查问题之余,我们不禁也在思考,以后再出现类似的问题怎么办?这种问题该如何避免?用惯了MySQL,于是乎想到了HBase是否跟MySQL一样,也有其高可用方案?

答案当然是肯定的,几乎所有的数据库(无论是关系型还是分布式的),都采用WAL的方式来保障服务异常时候的数据恢复,HBase同样也是通过WAL来保障数据不丢失。HBase在写数据前会先写HLog,HLog中记录的是所有数据的变动, HBase的高可用也正是通过HLog来实现的。

进阶
 
HBase是一个没有单点故障的分布式系统,上层(HBase层)和底层(HDFS层)都通过一定的技术手段,保障了服务的可用性。上层HMaster一般都是高可用部署,而RegionServer如果出现宕机,region迁移的代价并不大,一般都在毫秒级别完成,所以对应用造成的影响也很有限;底层存储依赖于HDFS,数据本身默认也有3副本,数据存储上做到了多副本冗余,而且Hadoop 2.0以后NameNode的单点故障也被消除。所以,对于这样一个本身没有单点故障,数据又有多副本冗余的系统,再进行高可用的配置是否有这个必要?会不会造成资源的极大浪费?

高可用部署是否有必要,这个需要根据服务的重要性来定,这里先简单介绍下没有高可用的HBase服务会出现哪些问题:

1.      数据库管理人员失误,进行了不可逆的DDL操作

不管是什么数据库,DDL操作在执行的时候都需要慎之又慎,很可能一条简单的drop操作,会导致所有数据的丢失,并且无法恢复,对于HBase来说也是这样,如果管理员不小心drop了一个表,该表的数据将会被丢失。

2.      离线MR消耗过多的资源,造成线上服务受到影响
 
HBase经过这么多年的发展,已经不再是只适合离线业务的数据存储分析平台,许多公司的线上业务也相继迁移到了HBase上,比较典型的如:facebook的iMessage系统、360的搜索业务、小米米聊的历史数据等等。但不可避免在这些数据上做些统计分析类操作,大型MR跑起来,会有很大的资源消耗,可能会影响线上业务。

3.      不可预计的另外一些情况

比如核心交换机故障,机房停电等等情况都会造成HBase服务中断

 对于上述的那些问题,可以通过配置HBase的高可用来解决:

1.     不可逆DDL问题

HBase的高可用不支持DDL操作,换句话说,在master上的DDL操作,不会影响到slave上的数据,所以即使在master上进行了DDL操作,slave上的数据依然没有变化。这个跟MySQL有很大不同,MySQL的DDL可以通过statement格式的Binlog进行复制。

2.     离线MR影响线上业务问题

高可用的最大好处就是可以进行读写分离,离线MR可以直接跑在slave上,master继续对外提供写服务,这样也就不会影响到线上的业务,当然HBase的高可用复制是异步进行的,在slave上进行MR分析,数据可能会有稍微延迟。

3.     意外情况

对于像核心交换机故障、断电等意外情况,slave跨机架或者跨机房部署都能解决该种情况。
 
基于以上原因,如果是核心服务,对于可用性要求非常高,可以搭建HBase的高可用来保障服务较高的可用性,在HBase的Master出现异常时,只需简单把流量切换到Slave上,即可完成故障转移,保证服务正常运行。

原理
 
HBase高可用保证在出现异常时,快速进行故障转移。下面让我们先来看看HBase高可用的实现,首先看下官方的一张图:

20180705104611cfc33072-8bdb-4916-bc02-6c8a4db752f6.jpg

HBase Replication
 
需要声明的是,HBase的replication是以Column Family为单位的,每个Column Family都可以设置是否进行replication。

 上图中,一个Master对应了3个Slave,Master上每个RegionServer都有一份HLog,在开启Replication的情况下,每个RegionServer都会开启一个线程用于读取该RegionServer上的HLog,并且发送到各个Slave,Zookeeper用于保存当前已经发送的HLog的位置。Master与Slave之间采用异步通信的方式,保障Master上的性能不会受到Slave的影响。用Zookeeper保存已经发送HLog的位置,主要考虑在Slave复制过程中如果出现问题后重新建立复制,可以找到上次复制的位置。

201807051046218dd436b9-d42d-4243-ae58-b88d533e9910.jpg

HBase Replication步骤

 

1.         HBase Client向Master写入数据

 

2.         对应RegionServer写完HLog后返回Client请求

 

3.         同时replication线程轮询HLog发现有新的数据,发送给Slave

 

4.         Slave处理完数据后返回给Master

 

5.         Master收到Slave的返回信息,在Zookeeper中标记已经发送到Slave的HLog位置

 

注:在进行replication时,Master与Slave的配置并不一定相同,比如Master上可以有3台RegionServer,Slave上并不一定是3台,Slave上的RegionServer数量可以不一样,数据如何分布这个HBase内部会处理。

种类

 

HBase通过HLog进行数据复制,那么HBase支持哪些不同种类的复制关系?

 

从复制模式上来讲,HBase支持主从、主主两种复制模式,也就是经常说的Master-Slave、Master-Master复制。

 

1.         Master-Slave

 

Master-Slave复制比较简单,所有在Master集群上写入的数据都会被同步到Slave上。

 

2.         Master-Master

 

Master-Master复制与Master-Slave类似,主要的不同在于,在Master-Master复制中,两个Master地位相同,都可以进行读取和写入。

 

既然Master-Master两个Master都可以进行写入,万一出现一种情况:两个Master上都进行了对同一表的相同Column Family的同一个rowkey进行写入,会出现什么情况?

 

create  ‘t’,  {NAME=>’cf’, REPLICATION_SCOPE=>’1’}

 

Master1                                                        Master2

 

put ‘t’, ‘r1’, ‘cf’, ‘aaaaaaaaaaaaaaa’                       put ‘t’, ‘r1’, ‘cf’, ‘bbbbbbbbbbbbbbb’

 

如上操作,Master1上对t的cf列簇写入rowkey为r1,value为aaaaaaaaaaaaaaa的数据,Master2上同时对t的cf列簇写入rowkey为r1, value为bbbbbbbbbbbbbbb的数据,由于是Master-Master复制,Master1和Master2上在写入数据的同时都会把更新发送给对方,这样最终的数据就变成了:

 
menu.saveimg_.savepath20180814101838_.jpg



从上述表格中可以看到,最终Master1和Master2上cf列簇rowkey为r1的数据两边不一致。

 

所以,在做Master-Master高可用时,确保两边写入的表都是不同的,这样能防止上述数据不一致问题。

 

异常

 

HBase复制时,都是通过RegionServer开启复制线程进行HLog的发送,那么当其中某个RegionServer出现异常时,HBase是如何处理的?这里需要区别两种不同的情况,即Master上RegionServer异常和Slave上RegionServer异常。

 

1.     Slave上RegionServer异常

 

对于该种异常HBase处理比较简单,Slave上出现某个RegionServer异常,该RegionServer直接会被标记为异常状态,后续所有的更新都不会被发送到该台RegionServer,Slave会重新选取一台RegionServer来接收这部分数据。

 

2.   Master上RegionServer异常

 

Master上RegionServer出现异常,由于HLog都是通过RegionServer开启复制线程进行发送,如果RegionServer出现异常,这个时候,属于该台RegionServer的HLog就没有相关处理线程,这个时候,这部分数据又该如何处理?

 

Master上某台RegionServer异常,其他RegionServer会对该台RegionServer在zookeeper中的信息尝试加锁操作,当然这个操作是互斥的,同一时间只有一台RegionServer能获取到锁,然后,会把HLog信息拷贝到自己的目录下,这样就完成了异常RegionServer的HLog信息的转移,通过新的RegionServer把HLog的信息发送到Slave。

Master regionserver crash

操作

 

上面介绍的都是HBase高可用的理论实现和异常处理等问题,下面就动手实践下,如何配置一个HBase的Replication(假设已经部署好了两套HBase系统,并且在配置文件中已经开启了replication配置),首先尝试配置下Master-Slave模式的高可用:

 

1.         选取一套系统作为Master,另外一套作为Slave

 

2.         在Master上通过add_peer 命令添加复制关系,如下

 

add_peer ‘1’, “db-xxx.photo.163.org:2181:/hbase”

 

3.         在Master上新建表t,该表拥有一个列簇名为cf,并且该列簇开启replication,如下:

 

create ‘t’, {NAME=>’cf’, REPLICATION_SCOPE=>’1’}

 

上面REPLICATION_SCOPE的值需要跟步骤2中的对应

 

4.         在slave建立相同的表(HBase不支持DDL的复制),在master-slave模式中,slave不需要开启复制,如下:

 

create ‘t’, {NAME=>’cf’ }

 

这样,我们就完成了整个master-slave模式高可用的搭建,后续可以在master上通过put操作插入一条记录,查看slave上是否会复制该记录,最终结果如下:

Master上操作

Slave上结果

上述结果显示,在添加完复制关系后,Master上插入rowkey=r1, value=’aaaaaaaaa’的记录,slave上可以获取该记录,Master-Slave模式数据复制成功。

接下来我们再看下Master-Master模式的复制,配置的时候与Master-Slave模式不同的是,在Master上添加完复制关系后,需要在另外一台Master也添加复制关系,而且两边的cluster_id必须相同,并且在另外一台Master上建表的时候,需要加上列簇的REPLICATION_SCOPE=>’1’配置,最终结果如下:

Master1上操作

Master2上操作

 

上述结果显示,添加完了Master-Master复制关系,在Master1上插入一条记录rowkey=r1, value=“aaaaaaaaaa”,Master2上通过scan操作发现该记录已经被复制到Master2上,接着我们在Master2上添加一条记录rowkey=r2, value=’bbbbbbbbbbbb’,查看Master1上的数据,该条记录也已经被复制到Master2上,Master-Master模式的replication验证成功。

Hbase 统计表行数的3种方式

hbase过往记忆 发表了文1章 • 4 个评论 • 1659 次浏览 • 2018-08-13 19:24 • 来自相关话题

有些时候需要我们去统计某一个hbase表的行数,由于hbase本身不支持SQL语言,只能通过其他方式实现。可以通过一下几种方式实现hbase表的行数统计工作:

1.count命令

最直接的方式是在hbase shell中执行count的命令可以统计行数。hbase> count ‘t1′
hbase> count ‘t1′, INTERVAL => 100000
hbase> count ‘t1′, CACHE => 1000
hbase> count ‘t1′, INTERVAL => 10, CACHE => 1000
其中,INTERVAL为统计的行数间隔,默认为1000,CACHE为统计的数据缓存。这种方式效率很低,如果表行数很大的话不建议采用这种方式。
2. 调用Mapreduce$HBASE_HOME/bin/hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'tablename'
这种方式效率比上一种要高很多,调用的hbase jar中自带的统计行数的类。
3.hive over hbase
如果已经见了hive和hbase的关联表的话,可以直接在hive中执行sql语句统计hbase表的行数。
hive over hbase 表的建表语句为:
/*创建hive与hbase的关联表*/
CREATE TABLE hive_hbase_1(key INT,value STRING)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping"=":key,cf:val")
TBLPROPERTIES("hbase.table.name"="t_hive","hbase.table.default.storage.type"="binary");

/*hive关联已经存在的hbase*/
CREATE EXTERNAL TABLE hive_hbase_1(key INT,value STRING)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping"=":key,cf:val")
TBLPROPERTIES("hbase.table.name"="t_hive","hbase.table.default.storage.type"="binary");
  查看全部
有些时候需要我们去统计某一个hbase表的行数,由于hbase本身不支持SQL语言,只能通过其他方式实现。可以通过一下几种方式实现hbase表的行数统计工作:

1.count命令

最直接的方式是在hbase shell中执行count的命令可以统计行数。
hbase> count ‘t1′
hbase> count ‘t1′, INTERVAL => 100000
hbase> count ‘t1′, CACHE => 1000
hbase> count ‘t1′, INTERVAL => 10, CACHE => 1000

其中,INTERVAL为统计的行数间隔,默认为1000,CACHE为统计的数据缓存。这种方式效率很低,如果表行数很大的话不建议采用这种方式。
2. 调用Mapreduce
$HBASE_HOME/bin/hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'tablename'

这种方式效率比上一种要高很多,调用的hbase jar中自带的统计行数的类。
3.hive over hbase
如果已经见了hive和hbase的关联表的话,可以直接在hive中执行sql语句统计hbase表的行数。
hive over hbase 表的建表语句为:
/*创建hive与hbase的关联表*/
CREATE TABLE hive_hbase_1(key INT,value STRING)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping"=":key,cf:val")
TBLPROPERTIES("hbase.table.name"="t_hive","hbase.table.default.storage.type"="binary");

/*hive关联已经存在的hbase*/
CREATE EXTERNAL TABLE hive_hbase_1(key INT,value STRING)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping"=":key,cf:val")
TBLPROPERTIES("hbase.table.name"="t_hive","hbase.table.default.storage.type"="binary");

 

HBase读写设计与实践

hbase过往记忆 发表了文1章 • 0 个评论 • 663 次浏览 • 2018-08-13 17:28 • 来自相关话题

背景介绍
本项目主要解决 check 和 opinion2 张历史数据表(历史数据是指当业务发生过程中的完整中间流程和结果数据)的在线查询。原实现基于 Oracle 提供存储查询服务,随着数据量的不断增加,在写入和读取过程中面临性能问题,且历史数据仅供业务查询参考,并不影响实际流程,从系统结构上来说,放在业务链条上游比较重。本项目将其置于下游数据处理 Hadoop 分布式平台来实现此需求。下面列一些具体的需求指标:

数据量:目前 check 表的累计数据量为 5000w+ 行,11GB;opinion 表的累计数据量为 3 亿 +,约 100GB。每日增量约为每张表 50 万 + 行,只做 insert,不做 update。

查询要求:check 表的主键为 id(Oracle 全局 id),查询键为 check_id,一个 check_id 对应多条记录,所以需返回对应记录的 list; opinion 表的主键也是 id,查询键是 bussiness_no 和 buss_type,同理返回 list。单笔查询返回 List 大小约 50 条以下,查询频率为 100 笔 / 天左右,查询响应时间 2s。
技术选型
从数据量及查询要求来看,分布式平台上具备大数据量存储,且提供实时查询能力的组件首选 HBase。根据需求做了初步的调研和评估后,大致确定 HBase 作为主要存储组件。将需求拆解为写入和读取 HBase 两部分。

读取 HBase 相对来说方案比较确定,基本根据需求设计 RowKey,然后根据 HBase 提供的丰富 API(get,scan 等)来读取数据,满足性能要求即可。

写入 HBase 的方法大致有以下几种:

1、 Java 调用 HBase 原生 API,HTable.add(List(Put))。
2、 MapReduce 作业,使用 TableOutputFormat 作为输出。
3、 Bulk Load,先将数据按照 HBase 的内部数据格式生成持久化的 HFile 文件,然后复制到合适的位置并通知 RegionServer ,即完成海量数据的入库。其中生成 Hfile 这一步可以选择 MapReduce 或 Spark。

本文采用第 3 种方式,Spark + Bulk Load 写入 HBase。该方法相对其他 2 种方式有以下优势:

① BulkLoad 不会写 WAL,也不会产生 flush 以及 split。
②如果我们大量调用 PUT 接口插入数据,可能会导致大量的 GC 操作。除了影响性能之外,严重时甚至可能会对 HBase 节点的稳定性造成影响,采用 BulkLoad 无此顾虑。
③过程中没有大量的接口调用消耗性能。
④可以利用 Spark 强大的计算能力。

图示如下:





设计
环境信息
Hadoop 2.5-2.7
HBase 0.98.6
Spark 2.0.0-2.1.1
Sqoop 1.4.6
表设计

本段的重点在于讨论 HBase 表的设计,其中 RowKey 是最重要的部分。为了方便说明问题,我们先来看看数据格式。以下以 check 举例,opinion 同理。

check 表(原表字段有 18 个,为方便描述,本文截选 5 个字段示意)





如上图所示,主键为 id,32 位字母和数字随机组成,业务查询字段 check_id 为不定长字段(不超过 32 位),字母和数字组成,同一 check_id 可能对应多条记录,其他为相关业务字段。众所周知,HBase 是基于 RowKey 提供查询,且要求 RowKey 是唯一的。RowKey 的设计主要考虑的是数据将怎样被访问。初步来看,我们有 2 种设计方法。

① 拆成 2 张表,一张表 id 作为 RowKey,列为 check 表对应的各列;另一张表为索引表,RowKey 为 check_id,每一列对应一个 id。查询时,先找到 check_id 对应的 id list,然后根据 id 找到对应的记录。均为 HBase 的 get 操作。

②将本需求可看成是一个范围查询,而不是单条查询。将 check_id 作为 RowKey 的前缀,后面跟 id。查询时设置 Scan 的 startRow 和 stopRow,找到对应的记录 list。

第一种方法优点是表结构简单,RowKey 容易设计,缺点为 1)数据写入时,一行原始数据需要写入到 2 张表,且索引表写入前需要先扫描该 RowKey 是否存在,如果存在,则加入一列,否则新建一行,2)读取的时候,即便是采用 List, 也至少需要读取 2 次表。第二种设计方法,RowKey 设计较为复杂,但是写入和读取都是一次性的。综合考虑,我们采用第二种设计方法。
 RowKey 设计
 
热点问题

HBase 中的行是以 RowKey 的字典序排序的,其热点问题通常发生在大量的客户端直接访问集群的一个或极少数节点。默认情况下,在开始建表时,表只会有一个 region,并随着 region 增大而拆分成更多的 region,这些 region 才能分布在多个 regionserver 上从而使负载均分。对于我们的业务需求,存量数据已经较大,因此有必要在一开始就将 HBase 的负载均摊到每个 regionserver,即做 pre-split。常见的防治热点的方法为加盐,hash 散列,自增部分(如时间戳)翻转等。

RowKey 设计

Step1:确定预分区数目,创建 HBase Table

不同的业务场景及数据特点确定数目的方式不一样,我个人认为应该综合考虑数据量大小和集群大小等因素。比如 check 表大小约为 11G,测试集群大小为 10 台机器,hbase.hregion.max.filesize=3G(当 region 的大小超过这个数时,将拆分为 2 个),所以初始化时尽量使得一个 region 的大小为 1~2G(不会一上来就 split),region 数据分到 11G/2G=6 个,但为了充分利用集群资源,本文中 check 表划分为 10 个分区。如果数据量为 100G,且不断增长,集群情况不变,则 region 数目增大到 100G/2G=50 个左右较合适。Hbase check 表建表语句如下: 
create 'tinawang:check',
{ NAME => 'f', COMPRESSION => 'SNAPPY',DATA_BLOCK_ENCODING => 'FAST_DIFF',BLOOMFILTER=>'ROW'},
{SPLITS => [ '1','2','3', '4','5','6','7','8','9']}其中,Column Family =‘f’,越短越好。

COMPRESSION => 'SNAPPY',HBase 支持 3 种压缩 LZO, GZIP and Snappy。GZIP 压缩率高,但是耗 CPU。后两者差不多,Snappy 稍微胜出一点,cpu 消耗的比 GZIP 少。一般在 IO 和 CPU 均衡下,选择 Snappy。

DATA_BLOCK_ENCODING => 'FAST_DIFF',本案例中 RowKey 较为接近,通过以下命令查看 key 长度相对 value 较长。
./hbase org.apache.hadoop.hbase.io.hfile.HFile -m -f /apps/hbase/data/data/tinawang/check/a661f0f95598662a53b3d8b1ae469fdf/f/a5fefc880f87492d908672e1634f2eed_SeqId_2_




Step2:RowKey 组成

Salt

让数据均衡的分布到各个 Region 上,结合 pre-split,我们对查询键即 check 表的 check_id 求 hashcode 值,然后 modulus(numRegions) 作为前缀,注意补齐数据。
StringUtils.leftPad(Integer.toString(Math.abs(check_id.hashCode() % numRegion)),1,’0’)
说明:如果数据量达上百 G 以上,则 numRegions 自然到 2 位数,则 salt 也为 2 位。

Hash 散列

因为 check_id 本身是不定长的字符数字串,为使数据散列化,方便 RowKey 查询和比较,我们对 check_id 采用 SHA1 散列化,并使之 32 位定长化。
MD5Hash.getMD5AsHex(Bytes.toBytes(check_id))
唯一性

以上 salt+hash 作为 RowKey 前缀,加上 check 表的主键 id 来保障 RowKey 唯一性。综上,check 表的 RowKey 设计如下:(check_id=A208849559)





为增强可读性,中间还可以加上自定义的分割符,如’+’,’|’等。
7+7c9498b4a83974da56b252122b9752bf+56B63AB98C2E00B4E053C501380709AD
以上设计能保证对每次查询而言,其 salt+hash 前缀值是确定的,并且落在同一个 region 中。需要说明的是 HBase 中 check 表的各列同数据源 Oracle 中 check 表的各列存储。

WEB 查询设计

RowKey 设计与查询息息相关,查询方式决定 RowKey 设计,反之基于以上 RowKey 设计,查询时通过设置 Scan 的 [startRow,stopRow], 即可完成扫描。以查询 check_id=A208849559 为例,根据 RowKey 的设计原则,对其进行 salt+hash 计算,得前缀。
startRow = 7+7c9498b4a83974da56b252122b9752bf
stopRow = 7+7c9498b4a83974da56b252122b9752bg
代码实现关键流程
Spark write to HBase

Step0: prepare work

因为是从上游系统承接的业务数据,存量数据采用 sqoop 抽到 hdfs;增量数据每日以文件的形式从 ftp 站点获取。因为业务数据字段中包含一些换行符,且 sqoop1.4.6 目前只支持单字节,所以本文选择’0x01’作为列分隔符,’0x10’作为行分隔符。

Step1: Spark read hdfs text file




SparkContext.textfile() 默认行分隔符为”\n”,此处我们用“0x10”,需要在 Configuration 中配置。应用配置,我们调用 newAPIHadoopFile 方法来读取 hdfs 文件,返回 JavaPairRDD,其中 LongWritable 和 Text 分别为 Hadoop 中的 Long 类型和 String 类型(所有 Hadoop 数据类型和 java 的数据类型都很相像,除了它们是针对网络序列化而做的特殊优化)。我们需要的数据文件放在 pairRDD 的 value 中,即 Text 指代。为后续处理方便,可将 JavaPairRDD转换为 JavaRDD< String >。

Step2: Transfer and sort RDD

① 将 avaRDD< String>转换成 JavaPairRDD<tuple2,String>,其中参数依次表示为,RowKey,col,value。做这样转换是因为 HBase 的基本原理是基于 RowKey 排序的,并且当采用 bulk load 方式将数据写入多个预分区(region)时,要求 Spark 各 partition 的数据是有序的,RowKey,column family(cf),col name 均需要有序。在本案例中因为只有一个列簇,所以将 RowKey 和 col name 组织出来为 Tuple2格式的 key。请注意原本数据库中的一行记录(n 个字段),此时会被拆成 n 行。

② 基于 JavaPairRDD<tuple2,String>进行 RowKey,col 的二次排序。如果不做排序,会报以下异常:
java.io.IOException: Added a key notlexically larger than previous key
③ 将数据组织成 HFile 要求的 JavaPairRDDhfileRDD。

Step3:create hfile and bulk load to HBase

①主要调用 saveAsNewAPIHadoopFile 方法:
hfileRdd.saveAsNewAPIHadoopFile(hfilePath,ImmutableBytesWritable.class,
KeyValue.class,HFileOutputFormat2.class,config);② hfilebulk load to HBase
final Job job = Job.getInstance();
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setMapOutputValueClass(KeyValue.class);
HFileOutputFormat2.configureIncrementalLoad(job,htable);
LoadIncrementalHFiles bulkLoader = newLoadIncrementalHFiles(config);
bulkLoader.doBulkLoad(newPath(hfilePath),htable);注:如果集群开启了 kerberos,step4 需要放置在 ugi.doAs()方法中,在进行如下验证后实现UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(keyUser,keytabPath);
UserGroupInformation.setLoginUser(ugi);访问 HBase 集群的 60010 端口 web,可以看到 region 分布情况。





Read from HBase

本文基于 spring boot 框架来开发 web 端访问 HBase 内数据。

use connection pool(使用连接池)

创建连接是一个比较重的操作,在实际 HBase 工程中,我们引入连接池来共享 zk 连接,meta 信息缓存,region server 和 master 的连接。
HConnection connection = HConnectionManager.createConnection(config);
HTableInterface table = connection.getTable("table1");
try {
// Use the table as needed, for a single operation and a single thread
} finally {
table.close();
}也可以通过以下方法,覆盖默认线程池。
HConnection createConnection(org.apache.hadoop.conf.Configuration conf,ExecutorService pool);
process query

Step1: 根据查询条件,确定 RowKey 前缀

根据 3.3 RowKey 设计介绍,HBase 的写和读都遵循该设计规则。此处我们采用相同的方法,将 web 调用方传入的查询条件,转化成对应的 RowKey 前缀。例如,查询 check 表传递过来的 check_id=A208849559,生成前缀 7+7c9498b4a83974da56b252122b9752bf。

Step2:确定 scan 范围

A208849559 对应的查询结果数据即在 RowKey 前缀为 7+7c9498b4a83974da56b252122b9752bf 对应的 RowKey 及 value 中。
scan.setStartRow(Bytes.toBytes(rowkey_pre)); //scan, 7+7c9498b4a83974da56b252122b9752bf
byte[] stopRow = Bytes.toBytes(rowkey_pre);
stopRow[stopRow.length-1]++;
scan.setStopRow(stopRow);// 7+7c9498b4a83974da56b252122b9752bgStep3:查询结果组成返回对象

遍历 ResultScanner 对象,将每一行对应的数据封装成 table entity,组成 list 返回。

测试

从原始数据中随机抓取 1000 个 check_id,用于模拟测试,连续发起 3 次请求数为 2000(200 个线程并发,循环 10 次),平均响应时间为 51ms,错误率为 0。










如上图,经历 N 次累计测试后,各个 region 上的 Requests 数较为接近,符合负载均衡设计之初。

踩坑记录
1、kerberos 认证问题

如果集群开启了安全认证,那么在进行 Spark 提交作业以及访问 HBase 时,均需要进行 kerberos 认证。

本文采用 yarn cluster 模式,像提交普通作业一样,可能会报以下错误。
ERROR StartApp: job failure,
java.lang.NullPointerException
at com.tinawang.spark.hbase.utils.HbaseKerberos.<init>(HbaseKerberos.java:18)
at com.tinawang.spark.hbase.job.SparkWriteHbaseJob.run(SparkWriteHbaseJob.java:60)定位到 HbaseKerberos.java:18,代码如下:
this.keytabPath = (Thread.currentThread().getContextClassLoader().getResource(prop.getProperty("hbase.keytab"))).getPath();
这是因为 executor 在进行 HBase 连接时,需要重新认证,通过 --keytab 上传的 tina.keytab 并未被 HBase 认证程序块获取到,所以认证的 keytab 文件需要另外通过 --files 上传。示意如下
--keytab /path/tina.keytab \
--principal tina@GNUHPC.ORG \
--files "/path/tina.keytab.hbase"其中 tina.keytab.hbase 是将 tina.keytab 复制并重命名而得。因为 Spark 不允许同一个文件重复上传。

2、序列化
 
org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)
at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:370)
at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:369)
...
org.apache.spark.deploy.yarn.ApplicationMaster$$anon$2.run(ApplicationMaster.scala:637)
Caused by: java.io.NotSerializableException: org.apache.spark.api.java.JavaSparkContext
Serialization stack:
- object not serializable (class: org.apache.spark.api.java.JavaSparkContext, value: org.apache.spark.api.java.JavaSparkContext@24a16d8c)
- field (class: com.tinawang.spark.hbase.processor.SparkReadFileRDD, name: sc, type: class org.apache.spark.api.java.JavaSparkContext)
...解决方法一:

如果 sc 作为类的成员变量,在方法中被引用,则加 transient 关键字,使其不被序列化。
private transient JavaSparkContext sc;
解决方法二:

将 sc 作为方法参数传递,同时使涉及 RDD 操作的类 implements Serializable。 代码中采用第二种方法。详见代码。

3、批量请求测试
Exception in thread "http-nio-8091-Acceptor-0" java.lang.NoClassDefFoundError: org/apache/tomcat/util/ExceptionUtils
或者
Exception in thread "http-nio-8091-exec-34" java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy

查看下面 issue 以及一次排查问题的过程,可能是 open file 超过限制。

https://github.com/spring-proj ... /1106

http://mp.weixin.qq.com/s/34GVlaYDOdY1OQ9eZs-iXg

使用 ulimit-a 查看每个用户默认打开的文件数为 1024。

在系统文件 /etc/security/limits.conf 中修改这个数量限制,在文件中加入以下内容, 即可解决问题。

soft nofile 65536
hard nofile 65536

作者介绍

汪婷,中国民生银行大数据开发工程师,专注于 Spark 大规模数据处理和 Hbase 系统设计。
 
参考文献

http://hbase.apache.org/book.html#perf.writing
http://www.opencore.com/blog/2 ... park/
http://hbasefly.com/2016/03/23/hbase_writer/
https://github.com/spring-proj ... /1106
http://mp.weixin.qq.com/s/34GVlaYDOdY1OQ9eZs-iXg 查看全部
背景介绍
本项目主要解决 check 和 opinion2 张历史数据表(历史数据是指当业务发生过程中的完整中间流程和结果数据)的在线查询。原实现基于 Oracle 提供存储查询服务,随着数据量的不断增加,在写入和读取过程中面临性能问题,且历史数据仅供业务查询参考,并不影响实际流程,从系统结构上来说,放在业务链条上游比较重。本项目将其置于下游数据处理 Hadoop 分布式平台来实现此需求。下面列一些具体的需求指标:

数据量:目前 check 表的累计数据量为 5000w+ 行,11GB;opinion 表的累计数据量为 3 亿 +,约 100GB。每日增量约为每张表 50 万 + 行,只做 insert,不做 update。

查询要求:check 表的主键为 id(Oracle 全局 id),查询键为 check_id,一个 check_id 对应多条记录,所以需返回对应记录的 list; opinion 表的主键也是 id,查询键是 bussiness_no 和 buss_type,同理返回 list。单笔查询返回 List 大小约 50 条以下,查询频率为 100 笔 / 天左右,查询响应时间 2s。
技术选型
从数据量及查询要求来看,分布式平台上具备大数据量存储,且提供实时查询能力的组件首选 HBase。根据需求做了初步的调研和评估后,大致确定 HBase 作为主要存储组件。将需求拆解为写入和读取 HBase 两部分。

读取 HBase 相对来说方案比较确定,基本根据需求设计 RowKey,然后根据 HBase 提供的丰富 API(get,scan 等)来读取数据,满足性能要求即可。

写入 HBase 的方法大致有以下几种:

1、 Java 调用 HBase 原生 API,HTable.add(List(Put))。
2、 MapReduce 作业,使用 TableOutputFormat 作为输出。
3、 Bulk Load,先将数据按照 HBase 的内部数据格式生成持久化的 HFile 文件,然后复制到合适的位置并通知 RegionServer ,即完成海量数据的入库。其中生成 Hfile 这一步可以选择 MapReduce 或 Spark。

本文采用第 3 种方式,Spark + Bulk Load 写入 HBase。该方法相对其他 2 种方式有以下优势:

① BulkLoad 不会写 WAL,也不会产生 flush 以及 split。
②如果我们大量调用 PUT 接口插入数据,可能会导致大量的 GC 操作。除了影响性能之外,严重时甚至可能会对 HBase 节点的稳定性造成影响,采用 BulkLoad 无此顾虑。
③过程中没有大量的接口调用消耗性能。
④可以利用 Spark 强大的计算能力。

图示如下:

671-1513698266272.jpg

设计
环境信息
Hadoop 2.5-2.7
HBase 0.98.6
Spark 2.0.0-2.1.1
Sqoop 1.4.6

表设计

本段的重点在于讨论 HBase 表的设计,其中 RowKey 是最重要的部分。为了方便说明问题,我们先来看看数据格式。以下以 check 举例,opinion 同理。

check 表(原表字段有 18 个,为方便描述,本文截选 5 个字段示意)

下载.jpg

如上图所示,主键为 id,32 位字母和数字随机组成,业务查询字段 check_id 为不定长字段(不超过 32 位),字母和数字组成,同一 check_id 可能对应多条记录,其他为相关业务字段。众所周知,HBase 是基于 RowKey 提供查询,且要求 RowKey 是唯一的。RowKey 的设计主要考虑的是数据将怎样被访问。初步来看,我们有 2 种设计方法。

① 拆成 2 张表,一张表 id 作为 RowKey,列为 check 表对应的各列;另一张表为索引表,RowKey 为 check_id,每一列对应一个 id。查询时,先找到 check_id 对应的 id list,然后根据 id 找到对应的记录。均为 HBase 的 get 操作。

②将本需求可看成是一个范围查询,而不是单条查询。将 check_id 作为 RowKey 的前缀,后面跟 id。查询时设置 Scan 的 startRow 和 stopRow,找到对应的记录 list。

第一种方法优点是表结构简单,RowKey 容易设计,缺点为 1)数据写入时,一行原始数据需要写入到 2 张表,且索引表写入前需要先扫描该 RowKey 是否存在,如果存在,则加入一列,否则新建一行,2)读取的时候,即便是采用 List, 也至少需要读取 2 次表。第二种设计方法,RowKey 设计较为复杂,但是写入和读取都是一次性的。综合考虑,我们采用第二种设计方法。
 RowKey 设计
 
热点问题

HBase 中的行是以 RowKey 的字典序排序的,其热点问题通常发生在大量的客户端直接访问集群的一个或极少数节点。默认情况下,在开始建表时,表只会有一个 region,并随着 region 增大而拆分成更多的 region,这些 region 才能分布在多个 regionserver 上从而使负载均分。对于我们的业务需求,存量数据已经较大,因此有必要在一开始就将 HBase 的负载均摊到每个 regionserver,即做 pre-split。常见的防治热点的方法为加盐,hash 散列,自增部分(如时间戳)翻转等。

RowKey 设计

Step1:确定预分区数目,创建 HBase Table

不同的业务场景及数据特点确定数目的方式不一样,我个人认为应该综合考虑数据量大小和集群大小等因素。比如 check 表大小约为 11G,测试集群大小为 10 台机器,hbase.hregion.max.filesize=3G(当 region 的大小超过这个数时,将拆分为 2 个),所以初始化时尽量使得一个 region 的大小为 1~2G(不会一上来就 split),region 数据分到 11G/2G=6 个,但为了充分利用集群资源,本文中 check 表划分为 10 个分区。如果数据量为 100G,且不断增长,集群情况不变,则 region 数目增大到 100G/2G=50 个左右较合适。Hbase check 表建表语句如下: 
create 'tinawang:check',
{ NAME => 'f', COMPRESSION => 'SNAPPY',DATA_BLOCK_ENCODING => 'FAST_DIFF',BLOOMFILTER=>'ROW'},
{SPLITS => [ '1','2','3', '4','5','6','7','8','9']}
其中,Column Family =‘f’,越短越好。

COMPRESSION => 'SNAPPY',HBase 支持 3 种压缩 LZO, GZIP and Snappy。GZIP 压缩率高,但是耗 CPU。后两者差不多,Snappy 稍微胜出一点,cpu 消耗的比 GZIP 少。一般在 IO 和 CPU 均衡下,选择 Snappy。

DATA_BLOCK_ENCODING => 'FAST_DIFF',本案例中 RowKey 较为接近,通过以下命令查看 key 长度相对 value 较长。
./hbase org.apache.hadoop.hbase.io.hfile.HFile -m -f /apps/hbase/data/data/tinawang/check/a661f0f95598662a53b3d8b1ae469fdf/f/a5fefc880f87492d908672e1634f2eed_SeqId_2_
下载.png

Step2:RowKey 组成

Salt

让数据均衡的分布到各个 Region 上,结合 pre-split,我们对查询键即 check 表的 check_id 求 hashcode 值,然后 modulus(numRegions) 作为前缀,注意补齐数据。
StringUtils.leftPad(Integer.toString(Math.abs(check_id.hashCode() % numRegion)),1,’0’) 
说明:如果数据量达上百 G 以上,则 numRegions 自然到 2 位数,则 salt 也为 2 位。

Hash 散列

因为 check_id 本身是不定长的字符数字串,为使数据散列化,方便 RowKey 查询和比较,我们对 check_id 采用 SHA1 散列化,并使之 32 位定长化。
MD5Hash.getMD5AsHex(Bytes.toBytes(check_id))
唯一性

以上 salt+hash 作为 RowKey 前缀,加上 check 表的主键 id 来保障 RowKey 唯一性。综上,check 表的 RowKey 设计如下:(check_id=A208849559)

1084-1513698265991.png

为增强可读性,中间还可以加上自定义的分割符,如’+’,’|’等。
7+7c9498b4a83974da56b252122b9752bf+56B63AB98C2E00B4E053C501380709AD

以上设计能保证对每次查询而言,其 salt+hash 前缀值是确定的,并且落在同一个 region 中。需要说明的是 HBase 中 check 表的各列同数据源 Oracle 中 check 表的各列存储。

WEB 查询设计

RowKey 设计与查询息息相关,查询方式决定 RowKey 设计,反之基于以上 RowKey 设计,查询时通过设置 Scan 的 [startRow,stopRow], 即可完成扫描。以查询 check_id=A208849559 为例,根据 RowKey 的设计原则,对其进行 salt+hash 计算,得前缀。
startRow = 7+7c9498b4a83974da56b252122b9752bf 
stopRow = 7+7c9498b4a83974da56b252122b9752bg

代码实现关键流程
Spark write to HBase

Step0: prepare work

因为是从上游系统承接的业务数据,存量数据采用 sqoop 抽到 hdfs;增量数据每日以文件的形式从 ftp 站点获取。因为业务数据字段中包含一些换行符,且 sqoop1.4.6 目前只支持单字节,所以本文选择’0x01’作为列分隔符,’0x10’作为行分隔符。

Step1: Spark read hdfs text file
下载_(1).jpg

SparkContext.textfile() 默认行分隔符为”\n”,此处我们用“0x10”,需要在 Configuration 中配置。应用配置,我们调用 newAPIHadoopFile 方法来读取 hdfs 文件,返回 JavaPairRDD,其中 LongWritable 和 Text 分别为 Hadoop 中的 Long 类型和 String 类型(所有 Hadoop 数据类型和 java 的数据类型都很相像,除了它们是针对网络序列化而做的特殊优化)。我们需要的数据文件放在 pairRDD 的 value 中,即 Text 指代。为后续处理方便,可将 JavaPairRDD转换为 JavaRDD< String >。

Step2: Transfer and sort RDD

① 将 avaRDD< String>转换成 JavaPairRDD<tuple2,String>,其中参数依次表示为,RowKey,col,value。做这样转换是因为 HBase 的基本原理是基于 RowKey 排序的,并且当采用 bulk load 方式将数据写入多个预分区(region)时,要求 Spark 各 partition 的数据是有序的,RowKey,column family(cf),col name 均需要有序。在本案例中因为只有一个列簇,所以将 RowKey 和 col name 组织出来为 Tuple2格式的 key。请注意原本数据库中的一行记录(n 个字段),此时会被拆成 n 行。

② 基于 JavaPairRDD<tuple2,String>进行 RowKey,col 的二次排序。如果不做排序,会报以下异常:
java.io.IOException: Added a key notlexically larger than previous key
③ 将数据组织成 HFile 要求的 JavaPairRDDhfileRDD。

Step3:create hfile and bulk load to HBase

①主要调用 saveAsNewAPIHadoopFile 方法:
hfileRdd.saveAsNewAPIHadoopFile(hfilePath,ImmutableBytesWritable.class,
KeyValue.class,HFileOutputFormat2.class,config);
② hfilebulk load to HBase
final Job job = Job.getInstance();
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setMapOutputValueClass(KeyValue.class);
HFileOutputFormat2.configureIncrementalLoad(job,htable);
LoadIncrementalHFiles bulkLoader = newLoadIncrementalHFiles(config);
bulkLoader.doBulkLoad(newPath(hfilePath),htable);
注:如果集群开启了 kerberos,step4 需要放置在 ugi.doAs()方法中,在进行如下验证后实现
UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(keyUser,keytabPath);
UserGroupInformation.setLoginUser(ugi);
访问 HBase 集群的 60010 端口 web,可以看到 region 分布情况。

896-1513698266412.png

Read from HBase

本文基于 spring boot 框架来开发 web 端访问 HBase 内数据。

use connection pool(使用连接池)

创建连接是一个比较重的操作,在实际 HBase 工程中,我们引入连接池来共享 zk 连接,meta 信息缓存,region server 和 master 的连接。
HConnection connection = HConnectionManager.createConnection(config);
HTableInterface table = connection.getTable("table1");
try {
// Use the table as needed, for a single operation and a single thread
} finally {
table.close();
}
也可以通过以下方法,覆盖默认线程池。
HConnection createConnection(org.apache.hadoop.conf.Configuration conf,ExecutorService pool);
process query

Step1: 根据查询条件,确定 RowKey 前缀

根据 3.3 RowKey 设计介绍,HBase 的写和读都遵循该设计规则。此处我们采用相同的方法,将 web 调用方传入的查询条件,转化成对应的 RowKey 前缀。例如,查询 check 表传递过来的 check_id=A208849559,生成前缀 7+7c9498b4a83974da56b252122b9752bf。

Step2:确定 scan 范围

A208849559 对应的查询结果数据即在 RowKey 前缀为 7+7c9498b4a83974da56b252122b9752bf 对应的 RowKey 及 value 中。
scan.setStartRow(Bytes.toBytes(rowkey_pre)); //scan, 7+7c9498b4a83974da56b252122b9752bf
byte[] stopRow = Bytes.toBytes(rowkey_pre);
stopRow[stopRow.length-1]++;
scan.setStopRow(stopRow);// 7+7c9498b4a83974da56b252122b9752bg
Step3:查询结果组成返回对象

遍历 ResultScanner 对象,将每一行对应的数据封装成 table entity,组成 list 返回。

测试

从原始数据中随机抓取 1000 个 check_id,用于模拟测试,连续发起 3 次请求数为 2000(200 个线程并发,循环 10 次),平均响应时间为 51ms,错误率为 0。

667-1513699401621.png


558-1513699401302.png

如上图,经历 N 次累计测试后,各个 region 上的 Requests 数较为接近,符合负载均衡设计之初。

踩坑记录
1、kerberos 认证问题

如果集群开启了安全认证,那么在进行 Spark 提交作业以及访问 HBase 时,均需要进行 kerberos 认证。

本文采用 yarn cluster 模式,像提交普通作业一样,可能会报以下错误。
ERROR StartApp: job failure,
java.lang.NullPointerException
at com.tinawang.spark.hbase.utils.HbaseKerberos.<init>(HbaseKerberos.java:18)
at com.tinawang.spark.hbase.job.SparkWriteHbaseJob.run(SparkWriteHbaseJob.java:60)
定位到 HbaseKerberos.java:18,代码如下:
this.keytabPath = (Thread.currentThread().getContextClassLoader().getResource(prop.getProperty("hbase.keytab"))).getPath();
这是因为 executor 在进行 HBase 连接时,需要重新认证,通过 --keytab 上传的 tina.keytab 并未被 HBase 认证程序块获取到,所以认证的 keytab 文件需要另外通过 --files 上传。示意如下
--keytab /path/tina.keytab \
--principal tina@GNUHPC.ORG \
--files "/path/tina.keytab.hbase"
其中 tina.keytab.hbase 是将 tina.keytab 复制并重命名而得。因为 Spark 不允许同一个文件重复上传。

2、序列化
 
org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)
at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:370)
at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:369)
...
org.apache.spark.deploy.yarn.ApplicationMaster$$anon$2.run(ApplicationMaster.scala:637)
Caused by: java.io.NotSerializableException: org.apache.spark.api.java.JavaSparkContext
Serialization stack:
- object not serializable (class: org.apache.spark.api.java.JavaSparkContext, value: org.apache.spark.api.java.JavaSparkContext@24a16d8c)
- field (class: com.tinawang.spark.hbase.processor.SparkReadFileRDD, name: sc, type: class org.apache.spark.api.java.JavaSparkContext)
...
解决方法一:

如果 sc 作为类的成员变量,在方法中被引用,则加 transient 关键字,使其不被序列化。
private transient JavaSparkContext sc;

解决方法二:

将 sc 作为方法参数传递,同时使涉及 RDD 操作的类 implements Serializable。 代码中采用第二种方法。详见代码。

3、批量请求测试
Exception in thread "http-nio-8091-Acceptor-0" java.lang.NoClassDefFoundError: org/apache/tomcat/util/ExceptionUtils
或者
Exception in thread "http-nio-8091-exec-34" java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy

查看下面 issue 以及一次排查问题的过程,可能是 open file 超过限制。

https://github.com/spring-proj ... /1106

http://mp.weixin.qq.com/s/34GVlaYDOdY1OQ9eZs-iXg

使用 ulimit-a 查看每个用户默认打开的文件数为 1024。

在系统文件 /etc/security/limits.conf 中修改这个数量限制,在文件中加入以下内容, 即可解决问题。

soft nofile 65536
hard nofile 65536

作者介绍

汪婷,中国民生银行大数据开发工程师,专注于 Spark 大规模数据处理和 Hbase 系统设计。
 
参考文献

http://hbase.apache.org/book.html#perf.writing
http://www.opencore.com/blog/2 ... park/
http://hbasefly.com/2016/03/23/hbase_writer/
https://github.com/spring-proj ... /1106
http://mp.weixin.qq.com/s/34GVlaYDOdY1OQ9eZs-iXg

HBase 优化实战

hbase过往记忆 发表了文1章 • 0 个评论 • 1023 次浏览 • 2018-08-10 12:44 • 来自相关话题

背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):





应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:




HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:




Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:




HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。




HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:




网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:




HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?




从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区 查看全部
背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):

menu.saveimg_.savepath20180810124058_.jpg

应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:
201806281646495b8a5674-3a09-4c12-8f81-5ff43ebc0fa9.jpg

HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:
20180628164659ff3e72b7-9be5-4671-ba17-6a37aafd0cdd.jpg

Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:
201806281647298f5c069c-bc91-4706-bb78-ad825e4a15d4.jpg

HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。
20180628164752abef950e-8d45-42ca-a5fe-0b1396a96b5f.jpg

HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:
20180628164811ef38f3c7-dcc1-492f-83f8-fe7bb8ba94fa.jpg

网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:
20180628164824ce396dad-ae37-49b5-8fa7-de67ad187b5c.jpg

HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?
201806281648395c7bae34-f068-41f5-9c8d-a865f5f8f70c.jpg

从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区

HBase查询优化

hbase过往记忆 发表了文1章 • 2 个评论 • 1546 次浏览 • 2018-08-06 14:35 • 来自相关话题

1.概述

HBase是一个实时的非关系型数据库,用来存储海量数据。但是,在实际使用场景中,在使用HBase API查询HBase中的数据时,有时会发现数据查询会很慢。本篇博客将从客户端优化和服务端优化两个方面来介绍,如何提高查询HBase的效率。

2.内容

这里,我们先给大家介绍如何从客户端优化查询速度。

2.1 客户端优化

客户端查询HBase,均通过HBase API的来获取数据,如果在实现代码逻辑时使用API不当,也会造成读取耗时严重的情况。

2.1.1 Scan优化

在使用HBase的Scan接口时,一次Scan会返回大量数据。客户端向HBase发送一次Scan请求,实际上并不会将所有数据加载到本地,而是通过多次RPC请求进行加载。这样设计的好处在于避免大量数据请求会导致网络带宽负载过高影响其他业务使用HBase,另外从客户端的角度来说可以避免数据量太大,从而本地机器发送OOM(内存溢出)。

默认情况下,HBase每次Scan会缓存100条,可以通过属性hbase.client.scanner.caching来设置。另外,最大值默认为-1,表示没有限制,具体实现见源代码:/**
* @return the maximum result size in bytes. See {@link #setMaxResultSize(long)}
*/
public long getMaxResultSize() {
return maxResultSize;
}

/**
* Set the maximum result size. The default is -1; this means that no specific
* maximum result size will be set for this scan, and the global configured
* value will be used instead. (Defaults to unlimited).
*
* @param maxResultSize The maximum result size in bytes.
*/
public Scan setMaxResultSize(long maxResultSize) {
this.maxResultSize = maxResultSize;
return this;
}
一般情况下,默认缓存100就可以满足,如果数据量过大,可以适当增大缓存值,来减少RPC次数,从而降低Scan的总体耗时。另外,在做报表曾显时,建议使用HBase分页来返回Scan的数据。

2.1.2 Get优化

HBase系统提供了单条get数据和批量get数据,单条get通常是通过请求表名+rowkey,批量get通常是通过请求表名+rowkey集合来实现。客户端在读取HBase的数据时,实际是与RegionServer进行数据交互。在使用批量get时可以有效的较少客户端到各个RegionServer之间RPC连接数,从而来间接的提高读取性能。批量get实现代码见org.apache.hadoop.hbase.client.HTable类:public Result get(List<Get> gets) throws IOException {
if (gets.size() == 1) {
return new Result{get(gets.get(0))};
}
try {
Object r1 = new Object[gets.size()];
batch((List<? extends Row>)gets, r1, readRpcTimeoutMs);
// Translate.
Result results = new Result[r1.length];
int i = 0;
for (Object obj: r1) {
// Batch ensures if there is a failure we get an exception instead
results[i++] = (Result)obj;
}
return results;
} catch (InterruptedException e) {
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
}
}
从实现的源代码分析可知,批量get请求的结果,要么全部返回,要么抛出异常。

2.1.3 列簇和列优化

通常情况下,HBase表设计我们一个指定一个列簇就可以满足需求,但也不排除特殊情况,需要指定多个列簇(官方建议最多不超过3个),其实官方这样建议也是有原因的,HBase是基于列簇的非关系型数据库,意味着相同的列簇数据会存放在一起,而不同的列簇的数据会分开存储在不同的目录下。如果一个表设计多个列簇,在使用rowkey查询而不限制列簇,这样在检索不同列簇的数据时,需要独立进行检索,查询效率固然是比指定列簇查询要低的,列簇越多,这样影响越大。

而同一列簇下,可能涉及到多个列,在实际查询数据时,如果一个表的列簇有上1000+的列,这样一个大表,如果不指定列,这样查询效率也是会很低。通常情况下,在查询的时候,可以查询指定我们需要返回结果的列,对于不需要的列,可以不需要指定,这样能够有效地的提高查询效率,降低延时。

2.1.4 禁止缓存优化

批量读取数据时会全表扫描一次业务表,这种提现在Scan操作场景。在Scan时,客户端与RegionServer进行数据交互(RegionServer的实际数据时存储在HDFS上),将数据加载到缓存,如果加载很大的数据到缓存时,会对缓存中的实时业务热数据有影响,由于缓存大小有限,加载的数据量过大,会将这些热数据“挤压”出去,这样当其他业务从缓存请求这些数据时,会从HDFS上重新加载数据,导致耗时严重。

在批量读取(T+1)场景时,建议客户端在请求是,在业务代码中调用setCacheBlocks(false)函数来禁止缓存,默认情况下,HBase是开启这部分缓存的。源代码实现为:/**
* Set whether blocks should be cached for this Get.
* <p>
* This is true by default. When true, default settings of the table and
* family are used (this will never override caching blocks if the block
* cache is disabled for that family or entirely).
*
* @param cacheBlocks if false, default settings are overridden and blocks
* will not be cached
*/
public Get setCacheBlocks(boolean cacheBlocks) {
this.cacheBlocks = cacheBlocks;
return this;
}

/**
* Get whether blocks should be cached for this Get.
* @return true if default caching should be used, false if blocks should not
* be cached
*/
public boolean getCacheBlocks() {
return cacheBlocks;
}
2.2 服务端优化

HBase服务端配置或集群有问题,也会导致客户端读取耗时较大,集群出现问题,影响的是整个集群的业务应用。

2.2.1 负载均衡优化

客户端的请求实际上是与HBase集群的每个RegionServer进行数据交互,在细分一下,就是与每个RegionServer上的某些Region进行数据交互,每个RegionServer上的Region个数上的情况下,可能这种耗时情况影响不大,体现不够明显。但是,如果每个RegionServer上的Region个数较大的话,这种影响就会很严重。笔者这里做过统计的数据统计,当每个RegionServer上的Region个数超过800+,如果发生负载不均衡,这样的影响就会很严重。

可能有同学会有疑问,为什么会发送负载不均衡?负载不均衡为什么会造成这样耗时严重的影响?

1.为什么会发送负载不均衡?

负载不均衡的影响通常由以下几个因素造成:

没有开启自动负载均衡
集群维护,扩容或者缩减RegionServer节点
集群有RegionServer节点发生宕机或者进程停止,随后守护进程又自动拉起宕机的RegionServer进程

针对这些因素,可以通过以下解决方案来解决:

开启自动负载均衡,执行命令:echo "balance_switch true" | hbase shell
在维护集群,或者守护进程拉起停止的RegionServer进程时,定时调度执行负载均衡命令:echo "balancer" | hbase shell

2.负载不均衡为什么会造成这样耗时严重的影响?

这里笔者用一个例子来说,集群每个RegionServer包含由800+的Region数,但是,由于集群维护,有几台RegionServer节点的Region全部集中到一台RegionServer,分布如下图所示:






这样之前请求在RegionServer2和RegionServer3上的,都会集中到RegionServer1上去请求。这样就不能发挥整个集群的并发处理能力,另外,RegionServer1上的资源使用将会翻倍(比如网络、磁盘IO、HBase RPC的Handle数等)。而原先其他正常业务到RegionServer1的请求也会因此受到很大的影响。因此,读取请求不均衡不仅会造成本身业务性能很长,还会严重影响其他正常业务的查询。同理,写请求不均衡,也会造成类似的影响。故HBase负载均衡是HBase集群性能的重要体现。

2.2.2 BlockCache优化

BlockCache作为读缓存,合理设置对于提高读性能非常重要。默认情况下,BlockCache和Memstore的配置各站40%,可以通过在hbase-site.xml配置以下属性来实现:

hfile.block.cache.size,默认0.4,用来提高读性能
hbase.regionserver.global.memstore.size,默认0.4,用来提高写性能

本篇博客主要介绍提高读性能,这里我们可以将BlockCache的占比设置大一些,Memstore的占比设置小一些(总占比保持在0.8即可)。另外,BlockCache的策略选择也是很重要的,不同的策略对于读性能来说影响不大,但是对于GC的影响却比较明显,在设置hbase.bucketcache.ioengine属性为offheap时,GC表现的很优秀。缓存结构如下图所示:






设置BlockCache可以在hbase-site.xml文件中,配置如下属性:<!-- 分配的内存大小尽可能的多些,前提是不能超过 (机器实际物理内存-JVM内存) -->
<property>
<name>hbase.bucketcache.size</name>
<value>16384</value>
</property>
<property>
<name>hbase.bucketcache.ioengine</name>
<value>offheap</value>
</property>设置块内存大小,可以参考入下表格:






另外,BlockCache策略,能够有效的提高缓存命中率,这样能够间接的提高热数据覆盖率,从而提升读取性能。

2.2.3 HFile优化

HBase读取数据时会先从BlockCache中进行检索(热数据),如果查询不到,才会到HDFS上去检索。而HBase存储在HDFS上的数据以HFile的形式存在的,文件如果越多,检索所花费的IO次数也就必然增加,对应的读取耗时也就增加了。文件数量取决于Compaction的执行策略,有以下2个属性有关系:

hbase.hstore.compactionThreshold,默认为3,表示store中文件数超过3个就开始进行合并操作
hbase.hstore.compaction.max.size,默认为9223372036854775807,合并的文件最大阀值,超过这个阀值的文件不能进行合并

 另外,hbase.hstore.compaction.max.size值可以通过实际的Region总数来计算,公式如下:hbase.hstore.compaction.max.size = RegionTotal / hbase.hstore.compactionThreshold
 
2.2.4 Compaction优化

Compaction操作是将小文件合并为大文件,提高后续业务随机读取的性能,但是在执行Compaction操作期间,节点IO、网络带宽等资源会占用较多,那么什么时候执行Compaction才最好?什么时候需要执行Compaction操作?

1.什么时候执行Compaction才最好?

实际应用场景中,会关闭Compaction自动执行策略,通过属性hbase.hregion.majorcompaction来控制,将hbase.hregion.majorcompaction=0,就可以禁止HBase自动执行Compaction操作。一般情况下,选择集群负载较低,资源空闲的时间段来定时调度执行Compaction。

如果合并的文件较多,可以通过设置如下属性来提生Compaction的执行速度,配置如下:<property>
<name>hbase.regionserver.thread.compaction.large</name>
<value>8</value>
<description></description>
</property>
<property>
<name>hbase.regionserver.thread.compaction.small</name>
<value>5</value>
<description></description>
</property>
2.什么时候需要执行Compaction操作?

一般维护HBase集群后,由于集群发生过重启,HBase数据本地性较低,通过HBase页面可以观察,此时如果不执行Compaction操作,那么客户端查询的时候,需要跨副本节点去查询,这样来回需要经过网络带宽,对比正常情况下,从本地节点读取数据,耗时是比较大的。在执行Compaction操作后,HBase数据本地性为1,这样能够有效的提高查询效率。

3.总结

本篇博客HBase查询优化从客户端和服务端角度,列举一些常见有效地额优化手段。当然,优化还需要从自己实际应用场景出发,例如代码实现逻辑、物理机的实际配置等方面来设置相关参数。大家可以根据实际情况来参考本篇博客进行优化。 查看全部
1.概述

HBase是一个实时的非关系型数据库,用来存储海量数据。但是,在实际使用场景中,在使用HBase API查询HBase中的数据时,有时会发现数据查询会很慢。本篇博客将从客户端优化和服务端优化两个方面来介绍,如何提高查询HBase的效率。

2.内容

这里,我们先给大家介绍如何从客户端优化查询速度。

2.1 客户端优化

客户端查询HBase,均通过HBase API的来获取数据,如果在实现代码逻辑时使用API不当,也会造成读取耗时严重的情况。

2.1.1 Scan优化

在使用HBase的Scan接口时,一次Scan会返回大量数据。客户端向HBase发送一次Scan请求,实际上并不会将所有数据加载到本地,而是通过多次RPC请求进行加载。这样设计的好处在于避免大量数据请求会导致网络带宽负载过高影响其他业务使用HBase,另外从客户端的角度来说可以避免数据量太大,从而本地机器发送OOM(内存溢出)。

默认情况下,HBase每次Scan会缓存100条,可以通过属性hbase.client.scanner.caching来设置。另外,最大值默认为-1,表示没有限制,具体实现见源代码:
/**
* @return the maximum result size in bytes. See {@link #setMaxResultSize(long)}
*/
public long getMaxResultSize() {
return maxResultSize;
}

/**
* Set the maximum result size. The default is -1; this means that no specific
* maximum result size will be set for this scan, and the global configured
* value will be used instead. (Defaults to unlimited).
*
* @param maxResultSize The maximum result size in bytes.
*/
public Scan setMaxResultSize(long maxResultSize) {
this.maxResultSize = maxResultSize;
return this;
}

一般情况下,默认缓存100就可以满足,如果数据量过大,可以适当增大缓存值,来减少RPC次数,从而降低Scan的总体耗时。另外,在做报表曾显时,建议使用HBase分页来返回Scan的数据。

2.1.2 Get优化

HBase系统提供了单条get数据和批量get数据,单条get通常是通过请求表名+rowkey,批量get通常是通过请求表名+rowkey集合来实现。客户端在读取HBase的数据时,实际是与RegionServer进行数据交互。在使用批量get时可以有效的较少客户端到各个RegionServer之间RPC连接数,从而来间接的提高读取性能。批量get实现代码见org.apache.hadoop.hbase.client.HTable类:
public Result get(List<Get> gets) throws IOException {
if (gets.size() == 1) {
return new Result{get(gets.get(0))};
}
try {
Object r1 = new Object[gets.size()];
batch((List<? extends Row>)gets, r1, readRpcTimeoutMs);
// Translate.
Result results = new Result[r1.length];
int i = 0;
for (Object obj: r1) {
// Batch ensures if there is a failure we get an exception instead
results[i++] = (Result)obj;
}
return results;
} catch (InterruptedException e) {
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
}
}

从实现的源代码分析可知,批量get请求的结果,要么全部返回,要么抛出异常。

2.1.3 列簇和列优化

通常情况下,HBase表设计我们一个指定一个列簇就可以满足需求,但也不排除特殊情况,需要指定多个列簇(官方建议最多不超过3个),其实官方这样建议也是有原因的,HBase是基于列簇的非关系型数据库,意味着相同的列簇数据会存放在一起,而不同的列簇的数据会分开存储在不同的目录下。如果一个表设计多个列簇,在使用rowkey查询而不限制列簇,这样在检索不同列簇的数据时,需要独立进行检索,查询效率固然是比指定列簇查询要低的,列簇越多,这样影响越大。

而同一列簇下,可能涉及到多个列,在实际查询数据时,如果一个表的列簇有上1000+的列,这样一个大表,如果不指定列,这样查询效率也是会很低。通常情况下,在查询的时候,可以查询指定我们需要返回结果的列,对于不需要的列,可以不需要指定,这样能够有效地的提高查询效率,降低延时。

2.1.4 禁止缓存优化

批量读取数据时会全表扫描一次业务表,这种提现在Scan操作场景。在Scan时,客户端与RegionServer进行数据交互(RegionServer的实际数据时存储在HDFS上),将数据加载到缓存,如果加载很大的数据到缓存时,会对缓存中的实时业务热数据有影响,由于缓存大小有限,加载的数据量过大,会将这些热数据“挤压”出去,这样当其他业务从缓存请求这些数据时,会从HDFS上重新加载数据,导致耗时严重。

在批量读取(T+1)场景时,建议客户端在请求是,在业务代码中调用setCacheBlocks(false)函数来禁止缓存,默认情况下,HBase是开启这部分缓存的。源代码实现为:
/**
* Set whether blocks should be cached for this Get.
* <p>
* This is true by default. When true, default settings of the table and
* family are used (this will never override caching blocks if the block
* cache is disabled for that family or entirely).
*
* @param cacheBlocks if false, default settings are overridden and blocks
* will not be cached
*/
public Get setCacheBlocks(boolean cacheBlocks) {
this.cacheBlocks = cacheBlocks;
return this;
}

/**
* Get whether blocks should be cached for this Get.
* @return true if default caching should be used, false if blocks should not
* be cached
*/
public boolean getCacheBlocks() {
return cacheBlocks;
}

2.2 服务端优化

HBase服务端配置或集群有问题,也会导致客户端读取耗时较大,集群出现问题,影响的是整个集群的业务应用。

2.2.1 负载均衡优化

客户端的请求实际上是与HBase集群的每个RegionServer进行数据交互,在细分一下,就是与每个RegionServer上的某些Region进行数据交互,每个RegionServer上的Region个数上的情况下,可能这种耗时情况影响不大,体现不够明显。但是,如果每个RegionServer上的Region个数较大的话,这种影响就会很严重。笔者这里做过统计的数据统计,当每个RegionServer上的Region个数超过800+,如果发生负载不均衡,这样的影响就会很严重。

可能有同学会有疑问,为什么会发送负载不均衡?负载不均衡为什么会造成这样耗时严重的影响?

1.为什么会发送负载不均衡?

负载不均衡的影响通常由以下几个因素造成:

没有开启自动负载均衡
集群维护,扩容或者缩减RegionServer节点
集群有RegionServer节点发生宕机或者进程停止,随后守护进程又自动拉起宕机的RegionServer进程

针对这些因素,可以通过以下解决方案来解决:

开启自动负载均衡,执行命令:echo "balance_switch true" | hbase shell
在维护集群,或者守护进程拉起停止的RegionServer进程时,定时调度执行负载均衡命令:echo "balancer" | hbase shell

2.负载不均衡为什么会造成这样耗时严重的影响?

这里笔者用一个例子来说,集群每个RegionServer包含由800+的Region数,但是,由于集群维护,有几台RegionServer节点的Region全部集中到一台RegionServer,分布如下图所示:

666745-20180805123042543-512818204.png


这样之前请求在RegionServer2和RegionServer3上的,都会集中到RegionServer1上去请求。这样就不能发挥整个集群的并发处理能力,另外,RegionServer1上的资源使用将会翻倍(比如网络、磁盘IO、HBase RPC的Handle数等)。而原先其他正常业务到RegionServer1的请求也会因此受到很大的影响。因此,读取请求不均衡不仅会造成本身业务性能很长,还会严重影响其他正常业务的查询。同理,写请求不均衡,也会造成类似的影响。故HBase负载均衡是HBase集群性能的重要体现。

2.2.2 BlockCache优化

BlockCache作为读缓存,合理设置对于提高读性能非常重要。默认情况下,BlockCache和Memstore的配置各站40%,可以通过在hbase-site.xml配置以下属性来实现:

hfile.block.cache.size,默认0.4,用来提高读性能
hbase.regionserver.global.memstore.size,默认0.4,用来提高写性能

本篇博客主要介绍提高读性能,这里我们可以将BlockCache的占比设置大一些,Memstore的占比设置小一些(总占比保持在0.8即可)。另外,BlockCache的策略选择也是很重要的,不同的策略对于读性能来说影响不大,但是对于GC的影响却比较明显,在设置hbase.bucketcache.ioengine属性为offheap时,GC表现的很优秀。缓存结构如下图所示:

666745-20180805125621644-552171040.png


设置BlockCache可以在hbase-site.xml文件中,配置如下属性:
<!-- 分配的内存大小尽可能的多些,前提是不能超过 (机器实际物理内存-JVM内存) -->
<property>
<name>hbase.bucketcache.size</name>
<value>16384</value>
</property>
<property>
<name>hbase.bucketcache.ioengine</name>
<value>offheap</value>
</property>
设置块内存大小,可以参考入下表格:

搜狗截图20180806142247.png


另外,BlockCache策略,能够有效的提高缓存命中率,这样能够间接的提高热数据覆盖率,从而提升读取性能。

2.2.3 HFile优化

HBase读取数据时会先从BlockCache中进行检索(热数据),如果查询不到,才会到HDFS上去检索。而HBase存储在HDFS上的数据以HFile的形式存在的,文件如果越多,检索所花费的IO次数也就必然增加,对应的读取耗时也就增加了。文件数量取决于Compaction的执行策略,有以下2个属性有关系:

hbase.hstore.compactionThreshold,默认为3,表示store中文件数超过3个就开始进行合并操作
hbase.hstore.compaction.max.size,默认为9223372036854775807,合并的文件最大阀值,超过这个阀值的文件不能进行合并

 另外,hbase.hstore.compaction.max.size值可以通过实际的Region总数来计算,公式如下:hbase.hstore.compaction.max.size = RegionTotal / hbase.hstore.compactionThreshold
 
2.2.4 Compaction优化

Compaction操作是将小文件合并为大文件,提高后续业务随机读取的性能,但是在执行Compaction操作期间,节点IO、网络带宽等资源会占用较多,那么什么时候执行Compaction才最好?什么时候需要执行Compaction操作?

1.什么时候执行Compaction才最好?

实际应用场景中,会关闭Compaction自动执行策略,通过属性hbase.hregion.majorcompaction来控制,将hbase.hregion.majorcompaction=0,就可以禁止HBase自动执行Compaction操作。一般情况下,选择集群负载较低,资源空闲的时间段来定时调度执行Compaction。

如果合并的文件较多,可以通过设置如下属性来提生Compaction的执行速度,配置如下:
<property>
<name>hbase.regionserver.thread.compaction.large</name>
<value>8</value>
<description></description>
</property>
<property>
<name>hbase.regionserver.thread.compaction.small</name>
<value>5</value>
<description></description>
</property>

2.什么时候需要执行Compaction操作?

一般维护HBase集群后,由于集群发生过重启,HBase数据本地性较低,通过HBase页面可以观察,此时如果不执行Compaction操作,那么客户端查询的时候,需要跨副本节点去查询,这样来回需要经过网络带宽,对比正常情况下,从本地节点读取数据,耗时是比较大的。在执行Compaction操作后,HBase数据本地性为1,这样能够有效的提高查询效率。

3.总结

本篇博客HBase查询优化从客户端和服务端角度,列举一些常见有效地额优化手段。当然,优化还需要从自己实际应用场景出发,例如代码实现逻辑、物理机的实际配置等方面来设置相关参数。大家可以根据实际情况来参考本篇博客进行优化。

HBase数据模型解析和基本的表设计分析

hbase过往记忆 发表了文1章 • 0 个评论 • 868 次浏览 • 2018-08-06 11:02 • 来自相关话题

HBase是一个开源可伸缩的针对海量数据存储的分布式nosql数据库,它根据Google Bigtable数据模型来建模并构建在hadoop的hdfs存储系统之上。它和关系型数据库Mysql, Oracle等有明显的区别,HBase的数据模型牺牲了关系型数据库的一些特性但是却换来了极大的可伸缩性和对表结构的灵活操作。

在一定程度上,Hbase又可以看成是以行键(Row Key),列标识(column qualifier),时间戳(timestamp)标识的有序Map数据结构的数据库,具有稀疏,分布式,持久化,多维度等特点。

Base的数据模型介绍
HBase的数据模型也是由一张张的表组成,每一张表里也有数据行和列,但是在HBase数据库中的行和列又和关系型数据库的稍有不同。下面统一介绍HBase数据模型中一些名词的概念:
      
表(Table): HBase会将数据组织进一张张的表里面,但是需要注意的是表名必须是能用在文件路径里的合法名字,因为HBase的表是映射成hdfs上面的文件。行(Row): 在表里面,每一行代表着一个数据对象,每一行都是以一个行键(Row Key)来进行唯一标识的,行键并没有什么特定的数据类型,以二进制的字节来存储。 列族(Column Family): 在定义HBase表的时候需要提前设置好列族, 表中所有的列都需要组织在列族里面,列族一旦确定后,就不能轻易修改,因为它会影响到HBase真实的物理存储结构,但是列族中的列标识(Column Qualifier)以及其对应的值可以动态增删。表中的每一行都有相同的列族,但是不需要每一行的列族里都有一致的列标识(Column Qualifier)和值,所以说是一种稀疏的表结构,这样可以一定程度上避免数据的冗余。例如:{row1, userInfo: telephone —> 137XXXXX869 }{row2, userInfo: fax phone —> 0898-66XXXX } 行1和行2都有同一个列族userinfo,但是行1中的列族只有列标识(Column Qualifier):移动电话号码,而行2中的列族中只有列标识(Column Qualifier):传真号码。列标识(Column Qualifier): 列族中的数据通过列标识来进行映射,其实这里大家可以不用拘泥于“列”这个概念,也可以理解为一个键值对,Column Qualifier就是Key。列标识也没有特定的数据类型,以二进制字节来存储。单元(Cell): 每一个 行键,列族和列标识共同组成一个单元,存储在单元里的数据称为单元数据,单元和单元数据也没有特定的数据类型,以二进制字节来存储。时间戳(Timestamp): 默认下每一个单元中的数据插入时都会用时间戳来进行版本标识。读取单元数据时,如果时间戳没有被指定,则默认返回最新的数据,写入新的单元数据时,如果没有设置时间戳,默认使用当前时间。每一个列族的单元数据的版本数量都被HBase单独维护,默认情况下HBase保留3个版本数据。





        有时候,你也可以把HBase看成一个多维度的Map模型去理解它的数据模型。正如下图,一个行键映射一个列族数组,列族数组中的每个列族又映射一个列标识数组,列标识数组中的每一个列标识(Column Qualifier)又映射到一个时间戳数组,里面是不同时间戳映射下不同版本的值,但是默认取最近时间的值,所以可以看成是列标识(Column Qualifier)和它所对应的值的映射。用户也可以通过HBase的API去同时获取到多个版本的单元数据的值。Row Key在HBase中也就相当于关系型数据库的主键,并且Row Key在创建表的时候就已经设置好,用户无法指定某个列作为Row Key。






又有的时候,你也可以把HBase看成是一个类似Redis那样的Key-Value数据库。如下图,当你要查询某一行的所有数据时,Row Key就相当于Key,而Value就是单元中的数据(列族,列族里的列和列中时间戳所对应的不同版本的值);当深入到HBase底层的存储机制时,用户要查询指定行里某一条单元数据时,HBase会去读取一个数据块,里面除了有要查询的单元数据,可能同时也会获取到其它单元数据,因为这个数据块还包含着这个Row Key所对应的其它列族或其它的列信息,这些信息实际也代表着另一个单元数据,这也是HBase的API内部实际的工作原理






HBase提供了丰富的API接口让用户去操作这些数据。主要的API接口有3个,Put,Get,Scan。Put和Get是操作指定行的数据的,所以需要提供行键来进行操作。Scan是操作一定范围内的数据,通过指定开始行键和结束行键来获取范围,如果没有指定开始行键和结束行键,则默认获取所有行数据。


HBase的表设计中需要注意的问题


   当开始设计HBase中的表的时候需要考虑以下的几个问题:
        1. Row Key的结构该如何设置,而Row Key中又该包含什么样的信息(这个很重要,下面的例子会有说明)
        2. 表中应该有多少的列族
        3. 列族中应该存储什么样的数据
        4. 每个列族中存储多少列数据
        5. 列的名字分别是什么,因为操作API的时候需要这些信息
        6. 单元中(cell)应该存储什么样的信息
        7. 每个单元中存储多少个版本信息
     在HBase表设计中最重要的就是定义Row-Key的结构,要定义Row-Key的结构时就不得不考虑表的接入样本,也就是在真真实应用中会对这张表出现什么样的读写场景。

除此之外,在设计表的时候我们也应该要考虑HBase数据库的一些特性。

       1. HBase中表的索引是通过Key来实现的
       2. 在表中是通过Row Key的字典序来对一行行的数据来进行排序的,表中每一块区域的划分都是通过开始Row Key和结束Row Key来决定的。
       3. 所有存储在HBase表中的数据都是二进制的字节,并没有数据类型。
       4. 原子性只在行内保证,HBase表中并没有多行事务。
       5. 列族(Column Family)在表创建之前就要定义好
       6. 列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加。

        接下来我们考虑一个这样的场景,我们要设计一张表,用来保存微博上用户互粉的信息。所以设计表之前,我们要考虑业务中的读写场景。

读场景中我们要考虑:
1. 每个用户都关注了谁
2. 用户A有没有关注用户B
3. 谁关注了用户A

写场景中我们要考虑:
1. 用户关注了另一个用户
2. 用户取消关注某个用户

下面我们来看几种表结构的设计:





        第一种表结构设计中,在这种表结构设计中,每一行代表着某个用户和所有他所关注的其它用户。这个用户ID就是Row Key,而每一个列标识(Column Qualifier)就是这个用户所关注的其他用户在列族里的序号,单元数据就是这个用户所关注的其他用户的用户ID。在这种表结构的设计下,“每个用户都关注了谁”这个问题很好解决,但对于“用户A有没有关注用户B”这个问题在列很多的时候,需要遍历所有单元数据去找到用户B,这样的开销会十分大。并且当添加新的被关注用户时,因为不知道给这个新用户分配什么样的列族序号,需要遍历整个列族中的所有列找出最后一个列,并将最后一个列的序号+1给新的被关注用户作为列族内的序号,这样的开销也十分大。







所以衍生出了第二种表结构设计,如下图,添加一个counter记录列族中所有列的总数量,当添加新的被关注用户时,这个新用户的序号就是counter+1。但是当要取消关注某个用户时,一样得遍历所有的列数据,而且最大的问题是在于HBase不支持事务处理,这种通过counter来添加被关注用户的操作逻辑得写在客户端中。






回想一下,列标识(Column Qualifier)存储的时候是二进制的字节,所以列标识可以存储任何数据,而且列标识还是动态增添的,基于这个特性我们再改进表的设计,如下图。这次以被关注的用户ID做为列标识(Column Qualifier),然后单元数据可以是任意数字,比如全部统一成1。

在这种表结构的设计下,添加新的被关注者,以及取消关注都会变得很简单。但是对于读场景中,谁关注了用户A这个问题,因为HBase数据库的索引只建立在Row Key上,这里不得不扫描全表去统计所有关注了用户A的用户数量,所以下面的这个表结构设计也存在一定的性能问题。这里也引出一个思路,被关注者需要以某种方式添加索引。






针对上面的表结构有三种优化方案,第一种是新建另一张表,里面保存某个用户和所有关注他的用户。第二种解决方案就是在同一张表中也存储某个用户和所有关注他的用户的信息,并从Row Key中区分开来,比如:Row key为Jame_001_following的这行保存着所有Jame关注的人的信息,而Row_Key为Jame_001_followed的这行保存着所有关注Jame的人的信息。

最后一种优化方案就是,如下图,将Row Key设计成“followerID+followedID”的形式,比如:“Jame+Emma”,这里的Row Key值就代表着Jame关注了Emma(其实这里应该是“Jame的ID+Emma的ID”,只是为了解释方便而直接用名字),同时包含了关注者和被关注者两个信息;还需要注意的一点就是列族的名字被设计成只有一个字母f,这样设计的好处就是减少了HBase对数据的I/O操作压力,同时减少了返回到客户端的数据字节,提高响应速度,因为每一个返回给客户端的KeyValue对象都会包含列族名字。

同时将被关注人的用户名称也保存在了表中作为Column Qualifier,这样做的好处就是节省了去用户表查找用户名的资源。在这种表结构设计下,“用户A取消关注某个用户B”,“用户A有没有关注用户B?”的业务处理就会变得简单高效。






还有一个需要注意的问题,就是在实际的生产环境中,还需要将Row Key使用MD5加密,一方面是使Row Key的长度都一致,能提高数据的存取性能。这方面的优化不在本文的讨论范围内。





总结:
整篇文章概述了HBase的数据模型和基本的表设计思路。下面是HBase一些关键特性的总结:
   1. Row Key是HBase表结构设计中很重要的一环,它设计的好坏直接影响程序和HBase交互的效率和数据存储的性能。
   2. Base的表结构比传统关系型数据库更灵活,你能存储任何二进制数据在表中,而且无关数据类型。
   3. 在相同的列族中所有数据都具有相同的接入模式
   4. 主要是通过Row Key来建立索引
   5. 以纵向扩张为主设计的表结构能快速简单的获取数据,但牺牲了一定的原子性,就比如上文中最后一种表结构;而以横向扩张为主设计的表结构,也就是列族中有很多列,比如上文中第一种表结构,能在行里面保持一定的原子性。
   6. HBase并不支持事务,所有尽量在一次API请求操作中获取到结果
   7. 对Row Key的Hash优化能获得固定长度的Row Key并使数据分布更加均匀一些,而不是集中在一台服务器上,但是也牺牲了一定的数据排序和读取性能。
   8. 可以利用列标识(Column Qualifier)来存储数据。
   9. 列标识(Column Qualifier)名字的长度和列族名字的长度都会影响I/O的读写性能和发送给客户端的数据量,所以它们的命名应该简洁! 查看全部
HBase是一个开源可伸缩的针对海量数据存储的分布式nosql数据库,它根据Google Bigtable数据模型来建模并构建在hadoop的hdfs存储系统之上。它和关系型数据库Mysql, Oracle等有明显的区别,HBase的数据模型牺牲了关系型数据库的一些特性但是却换来了极大的可伸缩性和对表结构的灵活操作。

在一定程度上,Hbase又可以看成是以行键(Row Key),列标识(column qualifier),时间戳(timestamp)标识的有序Map数据结构的数据库,具有稀疏,分布式,持久化,多维度等特点。

Base的数据模型介绍
HBase的数据模型也是由一张张的表组成,每一张表里也有数据行和列,但是在HBase数据库中的行和列又和关系型数据库的稍有不同。下面统一介绍HBase数据模型中一些名词的概念:
      
  • 表(Table): HBase会将数据组织进一张张的表里面,但是需要注意的是表名必须是能用在文件路径里的合法名字,因为HBase的表是映射成hdfs上面的文件。
  • 行(Row): 在表里面,每一行代表着一个数据对象,每一行都是以一个行键(Row Key)来进行唯一标识的,行键并没有什么特定的数据类型,以二进制的字节来存储。
  •  列族(Column Family): 在定义HBase表的时候需要提前设置好列族, 表中所有的列都需要组织在列族里面,列族一旦确定后,就不能轻易修改,因为它会影响到HBase真实的物理存储结构,但是列族中的列标识(Column Qualifier)以及其对应的值可以动态增删。表中的每一行都有相同的列族,但是不需要每一行的列族里都有一致的列标识(Column Qualifier)和值,所以说是一种稀疏的表结构,这样可以一定程度上避免数据的冗余。例如:{row1, userInfo: telephone —> 137XXXXX869 }{row2, userInfo: fax phone —> 0898-66XXXX } 行1和行2都有同一个列族userinfo,但是行1中的列族只有列标识(Column Qualifier):移动电话号码,而行2中的列族中只有列标识(Column Qualifier):传真号码。
  • 列标识(Column Qualifier): 列族中的数据通过列标识来进行映射,其实这里大家可以不用拘泥于“列”这个概念,也可以理解为一个键值对,Column Qualifier就是Key。列标识也没有特定的数据类型,以二进制字节来存储。
  • 单元(Cell): 每一个 行键,列族和列标识共同组成一个单元,存储在单元里的数据称为单元数据,单元和单元数据也没有特定的数据类型,以二进制字节来存储。
  • 时间戳(Timestamp): 默认下每一个单元中的数据插入时都会用时间戳来进行版本标识。读取单元数据时,如果时间戳没有被指定,则默认返回最新的数据,写入新的单元数据时,如果没有设置时间戳,默认使用当前时间。每一个列族的单元数据的版本数量都被HBase单独维护,默认情况下HBase保留3个版本数据。


下载.jpg

        有时候,你也可以把HBase看成一个多维度的Map模型去理解它的数据模型。正如下图,一个行键映射一个列族数组,列族数组中的每个列族又映射一个列标识数组,列标识数组中的每一个列标识(Column Qualifier)又映射到一个时间戳数组,里面是不同时间戳映射下不同版本的值,但是默认取最近时间的值,所以可以看成是列标识(Column Qualifier)和它所对应的值的映射。用户也可以通过HBase的API去同时获取到多个版本的单元数据的值。Row Key在HBase中也就相当于关系型数据库的主键,并且Row Key在创建表的时候就已经设置好,用户无法指定某个列作为Row Key。

下载_(1).jpg


又有的时候,你也可以把HBase看成是一个类似Redis那样的Key-Value数据库。如下图,当你要查询某一行的所有数据时,Row Key就相当于Key,而Value就是单元中的数据(列族,列族里的列和列中时间戳所对应的不同版本的值);当深入到HBase底层的存储机制时,用户要查询指定行里某一条单元数据时,HBase会去读取一个数据块,里面除了有要查询的单元数据,可能同时也会获取到其它单元数据,因为这个数据块还包含着这个Row Key所对应的其它列族或其它的列信息,这些信息实际也代表着另一个单元数据,这也是HBase的API内部实际的工作原理

下载_(2).jpg


HBase提供了丰富的API接口让用户去操作这些数据。主要的API接口有3个,Put,Get,Scan。Put和Get是操作指定行的数据的,所以需要提供行键来进行操作。Scan是操作一定范围内的数据,通过指定开始行键和结束行键来获取范围,如果没有指定开始行键和结束行键,则默认获取所有行数据。


HBase的表设计中需要注意的问题


   当开始设计HBase中的表的时候需要考虑以下的几个问题:
        1. Row Key的结构该如何设置,而Row Key中又该包含什么样的信息(这个很重要,下面的例子会有说明)
        2. 表中应该有多少的列族
        3. 列族中应该存储什么样的数据
        4. 每个列族中存储多少列数据
        5. 列的名字分别是什么,因为操作API的时候需要这些信息
        6. 单元中(cell)应该存储什么样的信息
        7. 每个单元中存储多少个版本信息
     在HBase表设计中最重要的就是定义Row-Key的结构,要定义Row-Key的结构时就不得不考虑表的接入样本,也就是在真真实应用中会对这张表出现什么样的读写场景。

除此之外,在设计表的时候我们也应该要考虑HBase数据库的一些特性。

       1. HBase中表的索引是通过Key来实现的
       2. 在表中是通过Row Key的字典序来对一行行的数据来进行排序的,表中每一块区域的划分都是通过开始Row Key和结束Row Key来决定的。
       3. 所有存储在HBase表中的数据都是二进制的字节,并没有数据类型。
       4. 原子性只在行内保证,HBase表中并没有多行事务。
       5. 列族(Column Family)在表创建之前就要定义好
       6. 列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加。

        接下来我们考虑一个这样的场景,我们要设计一张表,用来保存微博上用户互粉的信息。所以设计表之前,我们要考虑业务中的读写场景。

读场景中我们要考虑:
1. 每个用户都关注了谁
2. 用户A有没有关注用户B
3. 谁关注了用户A

写场景中我们要考虑:
1. 用户关注了另一个用户
2. 用户取消关注某个用户

下面我们来看几种表结构的设计:





        第一种表结构设计中,在这种表结构设计中,每一行代表着某个用户和所有他所关注的其它用户。这个用户ID就是Row Key,而每一个列标识(Column Qualifier)就是这个用户所关注的其他用户在列族里的序号,单元数据就是这个用户所关注的其他用户的用户ID。在这种表结构的设计下,“每个用户都关注了谁”这个问题很好解决,但对于“用户A有没有关注用户B”这个问题在列很多的时候,需要遍历所有单元数据去找到用户B,这样的开销会十分大。并且当添加新的被关注用户时,因为不知道给这个新用户分配什么样的列族序号,需要遍历整个列族中的所有列找出最后一个列,并将最后一个列的序号+1给新的被关注用户作为列族内的序号,这样的开销也十分大。

下载_(3).jpg



所以衍生出了第二种表结构设计,如下图,添加一个counter记录列族中所有列的总数量,当添加新的被关注用户时,这个新用户的序号就是counter+1。但是当要取消关注某个用户时,一样得遍历所有的列数据,而且最大的问题是在于HBase不支持事务处理,这种通过counter来添加被关注用户的操作逻辑得写在客户端中。

下载_(4).jpg


回想一下,列标识(Column Qualifier)存储的时候是二进制的字节,所以列标识可以存储任何数据,而且列标识还是动态增添的,基于这个特性我们再改进表的设计,如下图。这次以被关注的用户ID做为列标识(Column Qualifier),然后单元数据可以是任意数字,比如全部统一成1。

在这种表结构的设计下,添加新的被关注者,以及取消关注都会变得很简单。但是对于读场景中,谁关注了用户A这个问题,因为HBase数据库的索引只建立在Row Key上,这里不得不扫描全表去统计所有关注了用户A的用户数量,所以下面的这个表结构设计也存在一定的性能问题。这里也引出一个思路,被关注者需要以某种方式添加索引。

下载_(5).jpg


针对上面的表结构有三种优化方案,第一种是新建另一张表,里面保存某个用户和所有关注他的用户。第二种解决方案就是在同一张表中也存储某个用户和所有关注他的用户的信息,并从Row Key中区分开来,比如:Row key为Jame_001_following的这行保存着所有Jame关注的人的信息,而Row_Key为Jame_001_followed的这行保存着所有关注Jame的人的信息。

最后一种优化方案就是,如下图,将Row Key设计成“followerID+followedID”的形式,比如:“Jame+Emma”,这里的Row Key值就代表着Jame关注了Emma(其实这里应该是“Jame的ID+Emma的ID”,只是为了解释方便而直接用名字),同时包含了关注者和被关注者两个信息;还需要注意的一点就是列族的名字被设计成只有一个字母f,这样设计的好处就是减少了HBase对数据的I/O操作压力,同时减少了返回到客户端的数据字节,提高响应速度,因为每一个返回给客户端的KeyValue对象都会包含列族名字。

同时将被关注人的用户名称也保存在了表中作为Column Qualifier,这样做的好处就是节省了去用户表查找用户名的资源。在这种表结构设计下,“用户A取消关注某个用户B”,“用户A有没有关注用户B?”的业务处理就会变得简单高效。

下载_(6).jpg


还有一个需要注意的问题,就是在实际的生产环境中,还需要将Row Key使用MD5加密,一方面是使Row Key的长度都一致,能提高数据的存取性能。这方面的优化不在本文的讨论范围内。

下载_(7).jpg

总结:
整篇文章概述了HBase的数据模型和基本的表设计思路。下面是HBase一些关键特性的总结:
   1. Row Key是HBase表结构设计中很重要的一环,它设计的好坏直接影响程序和HBase交互的效率和数据存储的性能。
   2. Base的表结构比传统关系型数据库更灵活,你能存储任何二进制数据在表中,而且无关数据类型。
   3. 在相同的列族中所有数据都具有相同的接入模式
   4. 主要是通过Row Key来建立索引
   5. 以纵向扩张为主设计的表结构能快速简单的获取数据,但牺牲了一定的原子性,就比如上文中最后一种表结构;而以横向扩张为主设计的表结构,也就是列族中有很多列,比如上文中第一种表结构,能在行里面保持一定的原子性。
   6. HBase并不支持事务,所有尽量在一次API请求操作中获取到结果
   7. 对Row Key的Hash优化能获得固定长度的Row Key并使数据分布更加均匀一些,而不是集中在一台服务器上,但是也牺牲了一定的数据排序和读取性能。
   8. 可以利用列标识(Column Qualifier)来存储数据。
   9. 列标识(Column Qualifier)名字的长度和列族名字的长度都会影响I/O的读写性能和发送给客户端的数据量,所以它们的命名应该简洁!

HBase最佳实践-读性能优化策略

hbase过往记忆 发表了文1章 • 0 个评论 • 767 次浏览 • 2018-08-06 10:32 • 来自相关话题

本文作者范欣欣,其就职于网易杭州研究院后台技术中心数据库技术组,从事HBase开发、运维,对HBase相关技术有浓厚的兴趣。

任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题。HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少。总结起来,大家遇到的主要问题无非是Full GC异常导致宕机问题、RIT问题、写吞吐量太低以及读延迟较大。

Full GC问题之前在一些文章里面已经讲过它的来龙去脉,主要的解决方案目前主要有两方面需要注意,一方面需要查看GC日志确认是哪种Full GC,根据Full GC类型对JVM参数进行调优,另一方面需要确认是否开启了BucketCache的offheap模式,建议使用LRUBlockCache的童鞋尽快转移到BucketCache来。当然我们还是很期待官方2.0.0版本发布的更多offheap模块。

RIT问题,我相信更多是因为我们对其不了解,具体原理可以戳这里,解决方案目前有两个,优先是使用官方提供的HBCK进行修复(HBCK本人一直想拿出来分享,但是目前案例还不多,等后面有更多案例的话再拿出来说),使用之后还是解决不了的话就需要手动修复文件或者元数据表。

而对于写吞吐量太低以及读延迟太大的优化问题,笔者也和很多朋友进行过探讨,这篇文章就以读延迟优化为核心内容展开,具体分析HBase进行读延迟优化的那些套路,以及这些套路之后的具体原理。希望大家在看完之后能够结合这些套路剖析自己的系统。

一般情况下,读请求延迟较大通常存在三种场景,分别为:

1. 仅有某业务延迟较大,集群其他业务都正常

2. 整个集群所有业务都反映延迟较大

3. 某个业务起来之后集群其他部分业务延迟较大

这三种场景是表象,通常某业务反应延迟异常,首先需要明确具体是哪种场景,然后针对性解决问题。下图是对读优化思路的一点总结,主要分为四个方面:客户端优化、服务器端优化、列族设计优化以及HDFS相关优化,下面每一个小点都会按照场景分类,文章最后进行归纳总结。下面分别进行详细讲解:





HBase客户端优化

和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题:

1. scan缓存是否设置合理?

优化原理:在解释这个问题之前,首先需要解释什么是scan缓存,通常来讲一次scan会返回大量数据,因此客户端发起一次scan请求,实际并不会一次就将所有数据加载到本地,而是分成多次RPC请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生OOM。在这样的设计体系下用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在scan缓存中,默认100条数据大小。

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

优化建议:大scan场景下将scan缓存从100增大到500或者1000,用以减少RPC次数

2. get请求是否可以使用批量请求?


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

优化建议:使用批量get进行读取请求

3. 请求是否可以显示指定列族或者列?


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

优化建议:可以指定列族或者列进行精确查找的尽量指定查找

4. 离线批量读取请求是否设置禁止缓存?


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

优化建议:离线批量读取请求设置禁用缓存,scan.setBlockCache(false)

HBase服务器端优化

一般服务端端问题一旦导致业务读请求延迟较大的话,通常是集群级别的,即整个集群的业务都会反映读延迟较大。可以从4个方面入手:

5. 读请求是否均衡?


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

观察确认:观察所有RegionServer的读请求QPS曲线,确认是否存在读请求不均衡现象

优化建议:RowKey必须进行散列化处理(比如MD5散列),同时建表必须进行预分区处理

6. 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的到来!

7. HFile文件是否太多?


优化原理: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

8. 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尽早提供稳定服务

HBase列族设计优化

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

9. Bloomfilter是否设置?是否设置合理?

优化原理: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

HDFS相关优化

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

10. Short-Circuit Local Read功能是否开启?


优化原理:当前HDFS读取数据都需要经过DataNode,客户端会向DataNode发送读取数据的请求,DataNode接受到请求之后从硬盘中将文件读出来,再通过TPC发送给客户端。Short Circuit策略允许客户端绕过DataNode直接读取本地数据。(具体原理参考此处)

优化建议:开启Short Circuit Local Read功能,具体配置戳这里

11. Hedged Read功能是否开启?

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

优化建议:开启Hedged Read功能,具体配置参考这里

12. 数据本地率是否太低?


数据本地率: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读性能优化总结

性能优化是任何一个系统都会遇到的话题,每个系统也都有自己的优化方式。 HBase作为分布式KV数据库,优化点又格外不同,更多得融入了分布式特性以及存储系统优化特性。文中总结了读优化的基本突破点,有什么不对的地方还望指正,有补充的也可以一起探讨交流! 查看全部
本文作者范欣欣,其就职于网易杭州研究院后台技术中心数据库技术组,从事HBase开发、运维,对HBase相关技术有浓厚的兴趣。

任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题。HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少。总结起来,大家遇到的主要问题无非是Full GC异常导致宕机问题、RIT问题、写吞吐量太低以及读延迟较大。

Full GC问题之前在一些文章里面已经讲过它的来龙去脉,主要的解决方案目前主要有两方面需要注意,一方面需要查看GC日志确认是哪种Full GC,根据Full GC类型对JVM参数进行调优,另一方面需要确认是否开启了BucketCache的offheap模式,建议使用LRUBlockCache的童鞋尽快转移到BucketCache来。当然我们还是很期待官方2.0.0版本发布的更多offheap模块。

RIT问题,我相信更多是因为我们对其不了解,具体原理可以戳这里,解决方案目前有两个,优先是使用官方提供的HBCK进行修复(HBCK本人一直想拿出来分享,但是目前案例还不多,等后面有更多案例的话再拿出来说),使用之后还是解决不了的话就需要手动修复文件或者元数据表。

而对于写吞吐量太低以及读延迟太大的优化问题,笔者也和很多朋友进行过探讨,这篇文章就以读延迟优化为核心内容展开,具体分析HBase进行读延迟优化的那些套路,以及这些套路之后的具体原理。希望大家在看完之后能够结合这些套路剖析自己的系统。

一般情况下,读请求延迟较大通常存在三种场景,分别为:

1. 仅有某业务延迟较大,集群其他业务都正常

2. 整个集群所有业务都反映延迟较大

3. 某个业务起来之后集群其他部分业务延迟较大

这三种场景是表象,通常某业务反应延迟异常,首先需要明确具体是哪种场景,然后针对性解决问题。下图是对读优化思路的一点总结,主要分为四个方面:客户端优化、服务器端优化、列族设计优化以及HDFS相关优化,下面每一个小点都会按照场景分类,文章最后进行归纳总结。下面分别进行详细讲解:
hbase1.png


HBase客户端优化

和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题:

1. scan缓存是否设置合理?

优化原理:在解释这个问题之前,首先需要解释什么是scan缓存,通常来讲一次scan会返回大量数据,因此客户端发起一次scan请求,实际并不会一次就将所有数据加载到本地,而是分成多次RPC请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生OOM。在这样的设计体系下用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在scan缓存中,默认100条数据大小。

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

优化建议:大scan场景下将scan缓存从100增大到500或者1000,用以减少RPC次数

2. get请求是否可以使用批量请求?


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

优化建议:使用批量get进行读取请求

3. 请求是否可以显示指定列族或者列?


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

优化建议:可以指定列族或者列进行精确查找的尽量指定查找

4. 离线批量读取请求是否设置禁止缓存?


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

优化建议:离线批量读取请求设置禁用缓存,scan.setBlockCache(false)

HBase服务器端优化

一般服务端端问题一旦导致业务读请求延迟较大的话,通常是集群级别的,即整个集群的业务都会反映读延迟较大。可以从4个方面入手:

5. 读请求是否均衡?


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

观察确认:观察所有RegionServer的读请求QPS曲线,确认是否存在读请求不均衡现象

优化建议:RowKey必须进行散列化处理(比如MD5散列),同时建表必须进行预分区处理

6. 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的到来!

7. HFile文件是否太多?


优化原理: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

8. 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尽早提供稳定服务

HBase列族设计优化

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

9. Bloomfilter是否设置?是否设置合理?

优化原理: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

HDFS相关优化

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

10. Short-Circuit Local Read功能是否开启?


优化原理:当前HDFS读取数据都需要经过DataNode,客户端会向DataNode发送读取数据的请求,DataNode接受到请求之后从硬盘中将文件读出来,再通过TPC发送给客户端。Short Circuit策略允许客户端绕过DataNode直接读取本地数据。(具体原理参考此处)

优化建议:开启Short Circuit Local Read功能,具体配置戳这里

11. Hedged Read功能是否开启?

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

优化建议:开启Hedged Read功能,具体配置参考这里

12. 数据本地率是否太低?


数据本地率: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读性能优化归纳

在本文开始的时候提到读延迟较大无非三种常见的表象,单个业务慢、集群随机读慢以及某个业务随机读之后其他业务受到影响导致随机读延迟很大。了解完常见的可能导致读延迟较大的一些问题之后,我们将这些问题进行如下归类,读者可以在看到现象之后在对应的问题列表中进行具体定位:
hbase2.png

hbase3.png

hbase4.png


HBase读性能优化总结

性能优化是任何一个系统都会遇到的话题,每个系统也都有自己的优化方式。 HBase作为分布式KV数据库,优化点又格外不同,更多得融入了分布式特性以及存储系统优化特性。文中总结了读优化的基本突破点,有什么不对的地方还望指正,有补充的也可以一起探讨交流!

HBase最佳实践-读性能优化策略

hbase过往记忆 发表了文1章 • 0 个评论 • 600 次浏览 • 2018-08-01 09:57 • 来自相关话题

任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题。HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少。总结起来,大家遇到的主要问题无非是Full GC异常导致宕机问题、RIT问题、写吞吐量太低以及读延迟较大。

Full GC问题之前在一些文章里面已经讲过它的来龙去脉,主要的解决方案目前主要有两方面需要注意,一方面需要查看GC日志确认是哪种Full GC,根据Full GC类型对JVM参数进行调优,另一方面需要确认是否开启了BucketCache的offheap模式,建议使用LRUBlockCache的童鞋尽快转移到BucketCache来。当然我们还是很期待官方2.0.0版本发布的更多offheap模块。

RIT问题,我相信更多是因为我们对其不了解,具体原理可以戳这里,解决方案目前有两个,优先是使用官方提供的HBCK进行修复(HBCK本人一直想拿出来分享,但是目前案例还不多,等后面有更多案例的话再拿出来说),使用之后还是解决不了的话就需要手动修复文件或者元数据表。

而对于写吞吐量太低以及读延迟太大的优化问题,笔者也和很多朋友进行过探讨,这篇文章就以读延迟优化为核心内容展开,具体分析HBase进行读延迟优化的那些套路,以及这些套路之后的具体原理。希望大家在看完之后能够结合这些套路剖析自己的系统。

一般情况下,读请求延迟较大通常存在三种场景,分别为:

1. 仅有某业务延迟较大,集群其他业务都正常

2. 整个集群所有业务都反映延迟较大

3. 某个业务起来之后集群其他部分业务延迟较大

这三种场景是表象,通常某业务反应延迟异常,首先需要明确具体是哪种场景,然后针对性解决问题。下图是对读优化思路的一点总结,主要分为四个方面:客户端优化、服务器端优化、列族设计优化以及HDFS相关优化,下面每一个小点都会按照场景分类,文章最后进行归纳总结。下面分别进行详细讲解:




HBase客户端优化


和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题:

1. scan缓存是否设置合理?


优化原理:在解释这个问题之前,首先需要解释什么是scan缓存,通常来讲一次scan会返回大量数据,因此客户端发起一次scan请求,实际并不会一次就将所有数据加载到本地,而是分成多次RPC请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生OOM。在这样的设计体系下用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在scan缓存中,默认100条数据大小。

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

优化建议:大scan场景下将scan缓存从100增大到500或者1000,用以减少RPC次数

2. get请求是否可以使用批量请求?


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

优化建议:使用批量get进行读取请求

3. 请求是否可以显示指定列族或者列?


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

优化建议:可以指定列族或者列进行精确查找的尽量指定查找

4. 离线批量读取请求是否设置禁止缓存?


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

优化建议:离线批量读取请求设置禁用缓存,scan.setBlockCache(false)

HBase服务器端优化


一般服务端端问题一旦导致业务读请求延迟较大的话,通常是集群级别的,即整个集群的业务都会反映读延迟较大。可以从4个方面入手:

5. 读请求是否均衡?


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

观察确认:观察所有RegionServer的读请求QPS曲线,确认是否存在读请求不均衡现象

优化建议:RowKey必须进行散列化处理(比如MD5散列),同时建表必须进行预分区处理

6. 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的到来!

7. HFile文件是否太多?


优化原理: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

8. 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尽早提供稳定服务

HBase列族设计优化


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

9. Bloomfilter是否设置?是否设置合理?


优化原理: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

HDFS相关优化


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

10. Short-Circuit Local Read功能是否开启?


优化原理:当前HDFS读取数据都需要经过DataNode,客户端会向DataNode发送读取数据的请求,DataNode接受到请求之后从硬盘中将文件读出来,再通过TPC发送给客户端。Short Circuit策略允许客户端绕过DataNode直接读取本地数据。(具体原理参考此处)

优化建议:开启Short Circuit Local Read功能,具体配置戳这里

11. Hedged Read功能是否开启?


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

优化建议:开启Hedged Read功能,具体配置参考这里

12. 数据本地率是否太低?


数据本地率: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读性能优化总结

性能优化是任何一个系统都会遇到的话题,每个系统也都有自己的优化方式。 HBase作为分布式KV数据库,优化点又格外不同,更多得融入了分布式特性以及存储系统优化特性。文中总结了读优化的基本突破点,有什么不对的地方还望指正,有补充的也可以一起探讨交流! 查看全部
任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题。HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少。总结起来,大家遇到的主要问题无非是Full GC异常导致宕机问题、RIT问题、写吞吐量太低以及读延迟较大。

Full GC问题之前在一些文章里面已经讲过它的来龙去脉,主要的解决方案目前主要有两方面需要注意,一方面需要查看GC日志确认是哪种Full GC,根据Full GC类型对JVM参数进行调优,另一方面需要确认是否开启了BucketCache的offheap模式,建议使用LRUBlockCache的童鞋尽快转移到BucketCache来。当然我们还是很期待官方2.0.0版本发布的更多offheap模块。

RIT问题,我相信更多是因为我们对其不了解,具体原理可以戳这里,解决方案目前有两个,优先是使用官方提供的HBCK进行修复(HBCK本人一直想拿出来分享,但是目前案例还不多,等后面有更多案例的话再拿出来说),使用之后还是解决不了的话就需要手动修复文件或者元数据表。

而对于写吞吐量太低以及读延迟太大的优化问题,笔者也和很多朋友进行过探讨,这篇文章就以读延迟优化为核心内容展开,具体分析HBase进行读延迟优化的那些套路,以及这些套路之后的具体原理。希望大家在看完之后能够结合这些套路剖析自己的系统。

一般情况下,读请求延迟较大通常存在三种场景,分别为:

1. 仅有某业务延迟较大,集群其他业务都正常

2. 整个集群所有业务都反映延迟较大

3. 某个业务起来之后集群其他部分业务延迟较大

这三种场景是表象,通常某业务反应延迟异常,首先需要明确具体是哪种场景,然后针对性解决问题。下图是对读优化思路的一点总结,主要分为四个方面:客户端优化、服务器端优化、列族设计优化以及HDFS相关优化,下面每一个小点都会按照场景分类,文章最后进行归纳总结。下面分别进行详细讲解:
hbase1.png

HBase客户端优化


和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题:

1. scan缓存是否设置合理?


优化原理:在解释这个问题之前,首先需要解释什么是scan缓存,通常来讲一次scan会返回大量数据,因此客户端发起一次scan请求,实际并不会一次就将所有数据加载到本地,而是分成多次RPC请求进行加载,这样设计一方面是因为大量数据请求可能会导致网络带宽严重消耗进而影响其他业务,另一方面也有可能因为数据量太大导致本地客户端发生OOM。在这样的设计体系下用户会首先加载一部分数据到本地,然后遍历处理,再加载下一部分数据到本地处理,如此往复,直至所有数据都加载完成。数据加载到本地就存放在scan缓存中,默认100条数据大小。

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

优化建议:大scan场景下将scan缓存从100增大到500或者1000,用以减少RPC次数

2. get请求是否可以使用批量请求?


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

优化建议:使用批量get进行读取请求

3. 请求是否可以显示指定列族或者列?


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

优化建议:可以指定列族或者列进行精确查找的尽量指定查找

4. 离线批量读取请求是否设置禁止缓存?


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

优化建议:离线批量读取请求设置禁用缓存,scan.setBlockCache(false)

HBase服务器端优化


一般服务端端问题一旦导致业务读请求延迟较大的话,通常是集群级别的,即整个集群的业务都会反映读延迟较大。可以从4个方面入手:

5. 读请求是否均衡?


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

观察确认:观察所有RegionServer的读请求QPS曲线,确认是否存在读请求不均衡现象

优化建议:RowKey必须进行散列化处理(比如MD5散列),同时建表必须进行预分区处理

6. 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的到来!

7. HFile文件是否太多?


优化原理: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

8. 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尽早提供稳定服务

HBase列族设计优化


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

9. Bloomfilter是否设置?是否设置合理?


优化原理: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

HDFS相关优化


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

10. Short-Circuit Local Read功能是否开启?


优化原理:当前HDFS读取数据都需要经过DataNode,客户端会向DataNode发送读取数据的请求,DataNode接受到请求之后从硬盘中将文件读出来,再通过TPC发送给客户端。Short Circuit策略允许客户端绕过DataNode直接读取本地数据。(具体原理参考此处)

优化建议:开启Short Circuit Local Read功能,具体配置戳这里

11. Hedged Read功能是否开启?


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

优化建议:开启Hedged Read功能,具体配置参考这里

12. 数据本地率是否太低?


数据本地率: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读性能优化归纳


在本文开始的时候提到读延迟较大无非三种常见的表象,单个业务慢、集群随机读慢以及某个业务随机读之后其他业务受到影响导致随机读延迟很大。了解完常见的可能导致读延迟较大的一些问题之后,我们将这些问题进行如下归类,读者可以在看到现象之后在对应的问题列表中进行具体定位:
hbase2.png

hbase3.png

hbase4.png


HBase读性能优化总结

性能优化是任何一个系统都会遇到的话题,每个系统也都有自己的优化方式。 HBase作为分布式KV数据库,优化点又格外不同,更多得融入了分布式特性以及存储系统优化特性。文中总结了读优化的基本突破点,有什么不对的地方还望指正,有补充的也可以一起探讨交流!

阅读HBase源码的正确姿势建议

hbase过往记忆 发表了文1章 • 0 个评论 • 2426 次浏览 • 2018-07-31 10:37 • 来自相关话题

关于如何阅读开源社区源码,最近陆续有同学过来问我这个问题。前段时间,在HBase技术交流群里,大家也讨论过一些零散的方法,但都不系统。借着这个问题,我也认真回顾了一下自己所用过的一些方法,觉的有必要整理出来,供大家参考。

先选择合适的源码版本

因为不同的版本间的特性/流程方面存在较大的差异,阅读源码时选择合适的版本还是至关重要的。因此,需要先审视自己的需求:

“我阅读源码,是单纯的为了学习?还是希望在业务系统中更好的用好它?”

如果是前者,那完全可以选择最新发布或待发布的稳定版本。
如果是后者,则需要选择自己业务系统中正在使用的版本。

借助书籍或官方资料快速了解技术架构和关键特性

如果有介绍原理的书籍,可以先快速浏览一遍,粗略了解整体架构、关键特性。
这些信息也可以从官方资料中一探究竟,尤其是架构介绍相关的章节。

从快速试用开始加强自己对该项目的感性认识
先参考官方资料中的Quick Start章节,先学习如何使用,加强自己对于整体项目的感性认识。
这个过程,基本能摸清楚利用该项目"能做什么",以及"如何做"。当然,这里仅仅涉及了最基础的功能。
简单了解源码模块结构,而后从最基础的流程入手
快速了解源码的模块组成结构,以及每一个模块的主要作用。

这样有助于从源码结构上把握整体项目的轮廓,而后选择最基础的流程入手。
对于HBase而言,最基础的流程无非是如何建表以及如何写数据的流程。
学习一个特性要从了解配置和如何使用着手,同时建议阅读相关特性的设计文档或网上已有的源码解析文章

在学习一个特性时,也应该先从如何使用这个特性开始,接口如何被调用,关键配置有哪些,都是了解基础功能的基本起点。

接下来,可以先自己思考一下,这个特性如果由自己来设计,那整体思路应该是怎样的。

部分关键的特性/流程,在社区的问题单中,通常会有简洁的设计文档,这些文档能帮你理清方案的整体框架和思路。如果没有设计文档,那问题单中的Comments也是值得参考的。

当然,网上如果已经存在一些源码解析文章,也可以先参考一下,但好的文章往往是可遇不可求的。

如果在阅读源码之前,能够大致了解方案的思路,对自己会有很大的帮助,"瞎子摸象"式的阅读非常费时费力。

有一点需要强调一下:书籍或别人的文章中所描述的流程,在新版本中有可能已经发生了变化,因此,阅读时一定要带着辨证的思维。

摸清主线,避免过早陷入一些旁枝末节

刚开始阅读源码时,会遇到很多"好奇点":
这个算法居然实现的如此神奇?这个数据结构怎么没有见过?这个参数是干嘛的?

我自己也时常经不起这些"诱惑",陷于对这些细节的考究中,常常"离题"半天以后,才被拉回到主线中。

在阅读源码的时候,能遇到一些感兴趣的细节是好事,但建议先将这些细节点记录下来,等过完整体流程以后再回头看这些细节,避免过早陷入。

阅读源码过程中,通常需要动手做一些测试,此时,可以借助jstack工具(针对Java项目),它能为你提供如下有价值的信息:
线程模型调用栈

调用栈信息可以帮你理清整体调用流程(另外,在定位问题时,jstack打印出的信息也时常可以发挥重要作用)。

重视阅读日志信息

在进程启动或运行过程中,一些关键的操作或处理,都会记录日志信息,因此,阅读日志往往是一条有助于理清流程主线的捷径。

(PS: 感谢范欣欣同学补充的此条建议,笔者在实践中也时常用到)

阅读源码过程中,同步绘制时序图,固化对流程的理解

好不容易摸清的主线,建议及时用时序图的方式固化下来,这样可以帮助自己快速回顾整个流程。

当然,除了时序图,还建议附带简单的文字性总结。

阅读源码过程中,不断发现或提出疑问,并且记下来

当理清了主线流程以后,要继续深入探索这些细节疑问点,这些点决定了你对整个特性/流程的理解深度。

掌握一个特性/流程的基本前提,就是需要自己解答自己提出的所有疑问。

对于一些"莫名其妙"或"匪夷所思"的设计,请一定要对照参考社区问题单中的描述信息、设计文档或Comments信息。

阅读源码过程中,遇到晦涩难懂的细节,如何应对?

此时,建议开启Debug模式,详细跟踪每一步的调用流程,Debug可以分两种形式:
 
远程Debug本地Debug

对于HBase而言,相比于远程Debug,本地Debug似乎更难以理解了,因为我们所熟知的HBase部署形态就是分布式的,要对运行时的HBase集群进行Debug,自然采用远程Debug模式了。

其实,Debug也可以针对HBase提供的测试用例,大部分用例都是基于一个本地模拟的Mini Cluster运行的,这个Mini Cluster运行在一个进程中,使用线程模拟HBase的关键进程。

这个过程中,也可以动手小改一下源码,验证自己的想法,或者观察因为改动所带来的行为变化。

重视阅读测试用例源码

很多人并不习惯于阅读HBase的测试用例源码,其实,阅读测试用例的源码,可以帮你理解一些正确的行为应该是怎样的。

因为每一个被定义的正确行为,都以具体的测试用例固化下来了。

重视实际遇到的每一个Bug,每一个Bug都可以讲一个完整的故事

阅读源码过程中,自己提出的疑问,往往还不是最深刻的。最深刻的点,往往存在于所遇到的每一个Bug中。

对于Bug,很多人的态度往往是,能规避则规避之,集群只要能恢复正常,就不再有任何兴致去探究根因。

Bug往往是一些未考虑充足的边界场景,如果想探究Bug的根因,必然需要先摸清与之相关的所有流程,而后结合问题现象进行相关推理。一个Bug的前因后果,通常可以讲一个完整的故事。只有经过一个个Bug的历练,才能逐步成长为内核专家。

能力进阶:开始关注社区动态,或尝试为社区贡献Patch

关注社区动态,可以及时获知一些重要的Bugs或社区正在开发的大的Features。关注的方式包括但不限于:




订阅社区的Mail List

关注社区的问题单

如果感觉自己已经很好的掌握了源码,而且发现了部分设计不合理,或者是部分能力不完善(结合实际的业务需求),也可以主动为社区贡献Patch,对于大部分开源项目而言,都是非常鼓励大家贡献Patch的。

总结

本文主要从方法论的角度探讨了阅读开源项目源码的一些建议,供大家参考。这是第一个版本的内容,欢迎大家在评论中贡献自己的优秀实践方法,我可以继续完善它的内容,期望能够为更多人带来有价值的指导信息。 查看全部
关于如何阅读开源社区源码,最近陆续有同学过来问我这个问题。前段时间,在HBase技术交流群里,大家也讨论过一些零散的方法,但都不系统。借着这个问题,我也认真回顾了一下自己所用过的一些方法,觉的有必要整理出来,供大家参考。

先选择合适的源码版本

因为不同的版本间的特性/流程方面存在较大的差异,阅读源码时选择合适的版本还是至关重要的。因此,需要先审视自己的需求:

“我阅读源码,是单纯的为了学习?还是希望在业务系统中更好的用好它?”

如果是前者,那完全可以选择最新发布或待发布的稳定版本。
如果是后者,则需要选择自己业务系统中正在使用的版本。

借助书籍或官方资料快速了解技术架构和关键特性

如果有介绍原理的书籍,可以先快速浏览一遍,粗略了解整体架构、关键特性。
这些信息也可以从官方资料中一探究竟,尤其是架构介绍相关的章节。

从快速试用开始加强自己对该项目的感性认识
先参考官方资料中的Quick Start章节,先学习如何使用,加强自己对于整体项目的感性认识。
这个过程,基本能摸清楚利用该项目"能做什么",以及"如何做"。当然,这里仅仅涉及了最基础的功能。
简单了解源码模块结构,而后从最基础的流程入手
快速了解源码的模块组成结构,以及每一个模块的主要作用。

这样有助于从源码结构上把握整体项目的轮廓,而后选择最基础的流程入手。
对于HBase而言,最基础的流程无非是如何建表以及如何写数据的流程。
学习一个特性要从了解配置和如何使用着手,同时建议阅读相关特性的设计文档或网上已有的源码解析文章

在学习一个特性时,也应该先从如何使用这个特性开始,接口如何被调用,关键配置有哪些,都是了解基础功能的基本起点。

接下来,可以先自己思考一下,这个特性如果由自己来设计,那整体思路应该是怎样的。

部分关键的特性/流程,在社区的问题单中,通常会有简洁的设计文档,这些文档能帮你理清方案的整体框架和思路。如果没有设计文档,那问题单中的Comments也是值得参考的。

当然,网上如果已经存在一些源码解析文章,也可以先参考一下,但好的文章往往是可遇不可求的。

如果在阅读源码之前,能够大致了解方案的思路,对自己会有很大的帮助,"瞎子摸象"式的阅读非常费时费力。

有一点需要强调一下:书籍或别人的文章中所描述的流程,在新版本中有可能已经发生了变化,因此,阅读时一定要带着辨证的思维。

摸清主线,避免过早陷入一些旁枝末节

刚开始阅读源码时,会遇到很多"好奇点":
  • 这个算法居然实现的如此神奇?
  • 这个数据结构怎么没有见过?
  • 这个参数是干嘛的?


我自己也时常经不起这些"诱惑",陷于对这些细节的考究中,常常"离题"半天以后,才被拉回到主线中。

在阅读源码的时候,能遇到一些感兴趣的细节是好事,但建议先将这些细节点记录下来,等过完整体流程以后再回头看这些细节,避免过早陷入。

阅读源码过程中,通常需要动手做一些测试,此时,可以借助jstack工具(针对Java项目),它能为你提供如下有价值的信息:
  • 线程模型
  • 调用栈


调用栈信息可以帮你理清整体调用流程(另外,在定位问题时,jstack打印出的信息也时常可以发挥重要作用)。

重视阅读日志信息

在进程启动或运行过程中,一些关键的操作或处理,都会记录日志信息,因此,阅读日志往往是一条有助于理清流程主线的捷径。

(PS: 感谢范欣欣同学补充的此条建议,笔者在实践中也时常用到)

阅读源码过程中,同步绘制时序图,固化对流程的理解

好不容易摸清的主线,建议及时用时序图的方式固化下来,这样可以帮助自己快速回顾整个流程。

当然,除了时序图,还建议附带简单的文字性总结。

阅读源码过程中,不断发现或提出疑问,并且记下来

当理清了主线流程以后,要继续深入探索这些细节疑问点,这些点决定了你对整个特性/流程的理解深度。

掌握一个特性/流程的基本前提,就是需要自己解答自己提出的所有疑问。

对于一些"莫名其妙"或"匪夷所思"的设计,请一定要对照参考社区问题单中的描述信息、设计文档或Comments信息。

阅读源码过程中,遇到晦涩难懂的细节,如何应对?

此时,建议开启Debug模式,详细跟踪每一步的调用流程,Debug可以分两种形式:
 
  • 远程Debug
  • 本地Debug


对于HBase而言,相比于远程Debug,本地Debug似乎更难以理解了,因为我们所熟知的HBase部署形态就是分布式的,要对运行时的HBase集群进行Debug,自然采用远程Debug模式了。

其实,Debug也可以针对HBase提供的测试用例,大部分用例都是基于一个本地模拟的Mini Cluster运行的,这个Mini Cluster运行在一个进程中,使用线程模拟HBase的关键进程。

这个过程中,也可以动手小改一下源码,验证自己的想法,或者观察因为改动所带来的行为变化。

重视阅读测试用例源码

很多人并不习惯于阅读HBase的测试用例源码,其实,阅读测试用例的源码,可以帮你理解一些正确的行为应该是怎样的。

因为每一个被定义的正确行为,都以具体的测试用例固化下来了。

重视实际遇到的每一个Bug,每一个Bug都可以讲一个完整的故事

阅读源码过程中,自己提出的疑问,往往还不是最深刻的。最深刻的点,往往存在于所遇到的每一个Bug中。

对于Bug,很多人的态度往往是,能规避则规避之,集群只要能恢复正常,就不再有任何兴致去探究根因。

Bug往往是一些未考虑充足的边界场景,如果想探究Bug的根因,必然需要先摸清与之相关的所有流程,而后结合问题现象进行相关推理。一个Bug的前因后果,通常可以讲一个完整的故事。只有经过一个个Bug的历练,才能逐步成长为内核专家。

能力进阶:开始关注社区动态,或尝试为社区贡献Patch

关注社区动态,可以及时获知一些重要的Bugs或社区正在开发的大的Features。关注的方式包括但不限于:




订阅社区的Mail List

关注社区的问题单

如果感觉自己已经很好的掌握了源码,而且发现了部分设计不合理,或者是部分能力不完善(结合实际的业务需求),也可以主动为社区贡献Patch,对于大部分开源项目而言,都是非常鼓励大家贡献Patch的。

总结

本文主要从方法论的角度探讨了阅读开源项目源码的一些建议,供大家参考。这是第一个版本的内容,欢迎大家在评论中贡献自己的优秀实践方法,我可以继续完善它的内容,期望能够为更多人带来有价值的指导信息。

HBase全网最佳学习资料汇总

hbasehbase 发表了文1章 • 0 个评论 • 2374 次浏览 • 2018-02-05 21:55 • 来自相关话题

1、前言
 HBase这几年在国内使用的越来越广泛,在一定规模的企业中几乎是必备存储引擎,互联网企业阿里巴巴、京东、小米都有数千台的HBase集群,中国电信的话单、中国人寿的保单都是存储在HBase中。注意大公司有数十个数百个HBase集群,此点跟Hadoop集群很不相同。另外,数据需求,很多公司是mysql+hbase+hadoop(spark),满足关系型数据库需求,满足大规模结构化存储需求,满足复杂分析的需求。如此流行的原因来源于很多方面,如:
  - 开源繁荣的生态:1. 任何公司倒闭了,开源的HBase还在 2.几乎每家公司都可以去下载源码,改进她,再反馈给社区,就如阿里已经反馈了数百个patch了。加入的人越多,引擎就越好
  - 跟HADOOP深度结合:本就同根同源,在数据存储在HBase后,如果想复杂分析,则非常方便
  - 高扩展、高容量、高性能、低成本、低延迟、稀疏宽表、动态列、TTL、多版本等最为关键,起源google论文,发扬社区及广大互联网公司,设计之初就是为存储互联网,后经过多年的改进升级,如今已经是结构化存储的事实标准

以下资料会一直更新中......请大家关注!

2、书籍
最好买纸质书籍,集中时间看下
HBase权威指南(HBase: The Definitive Guide):理论多一些HBase实战:实践多一些
3、总结性
HBase2.0: HBase2.0 :预计今年会发布,hbase2.0是革命性的版本HBase Phoenix:Apache Phoenix与HBase:HBase之上SQL的过去,现在和未来 社区hbase博客:https://blogs.apache.org/hbase/
4、方法论
学术界关于HBase应用场景(物联网/车联网/交通/电力等)研究大全: HBase在互联网领域有广泛的应用,比如:互联网的消息系统的存储、订单的存储、搜索原材料的存储、用户画像数据的存储等。得益于HBase海量的存储量及超高并发写入读取量。HBase在09年就开始在工业界大范围使用,在学术界,也有非常多的高校、机构在研究HBase应用于不同的行业,本文主要梳理下这些资料(主要是中文资料,有一些是硕士论文\期刊),这些很多都在工业界使用了。HBase使用场景和成功案例  存储互联网的初心不变 一种基于物联网大数据的设备信息采集系统及方法:怎么使用HBase、sparkStreaming、redis处理物联网大数据一种基于HBase的智能电网时序大数据处理方方案:一种基于HBase的智能电网时序大数据处理方方案HBase配合GeoHash算法支持经纬度:此文主要讲GeoHash算法的基于HBase的海量GIS数据分布式处理实践:设计了一种基于分布式数据库HBase的GIS数据管理系统。系统优化了栅格数据的生成和存储过程,将海量栅格数据直接写入HBase存储、索引。同时,针对矢量空间数据的存储、索引与检索,提出了一种新的rowkey设计,既考虑经纬度,又考虑空间数据类型和属性,使得在按空间位置检索矢量地理信息时,能通过HBase的rowkey迅速定位需要返回的数据。在HBase的集群环境上用真实GIS数据对上述方法进行了验证,结果表明,提出的系统具有较高的海量数据存储和检索性能,实现了海量地理信息数据的高效存储和实时高速检索。基于HBase的金融时序数据存储系统:金融类时序数据的存储方案,写的还是结合实际场景的。
5、各大公司的实践
基本围绕在用户画像、安全风控、订单存储、交通轨迹、物理网、监控、大数据中间存储、搜索、推荐等方面:
阿里巴巴-大数据时代的结构化存储HBase在阿里的应用实践:讲述在阿里巴巴集团的实践,HBase在阿里集团已经10000台左右,主要在订单、监控、风控、消息、大数据计算等领域使用阿里巴巴搜索-Hbase在阿里巴巴搜索中的完美应用实践:讲述在搜索场景下hbase的应用及相关的改进日均采集1200亿数据点,腾讯千亿级服务器监控数据存储实践:本文将从当前存储架构存在的问题出发,介绍从尝试使用 Opentsdb 到自行设计 Hbase 存储方案来存储 TMP 服务器海量监控数据的实践历程。滴滴-HBase在滴滴出行的应用场景和最佳实践:统计结果、报表类数据、原始事实类数据、中间结果数据、线上系统的备份数据的一些应用HBase上搭建广告实时数据处理平台]:主要分享 1. 如何通过HBase实现数据流实时关联 2. 如何保证重要的计费数据不重不丢 3. HBase实战经验,优化负载均衡、读写缓存、批量读写等性能问题HBase在京东的实践 :跟阿里一样,京东各个业务线使用了HBase,如:风控、订单、商品评价等中国人寿基于HBase的企业级大数据平台:使用一个大跨表存储所有的保单,HBase宽表的实践HBase在Hulu的使用和实践:用户画像、订单存储系统、日志存储系统的使用Apache HBase at Netease:在报表、监控、日志类业务、消息类业务、推荐类业务、风控类业务有所使用,另外讲述了一些优化的点。10 Million Smart Meter Data with Apache HBase:讲述Hitachi为什么选择hbase及在HBase方面的应用G7:如何用云计算链接30万车辆--EMR&Hbase 在物联网领域的实践及解决方案 讲述了怎么使用spark及hbase来满足物联网的需求
6、结尾
  这些资料是笔者整理,以供有大规模结构化需求的用户及HBase爱好者学习交流,以使用HBase更好的解决实际的问题。欢迎传播,原文路径:http://www.hbase.group/hbase/?/article/1

7、声明
以上资料来自互联网,如果侵权,请联系我删除 查看全部
1、前言
 HBase这几年在国内使用的越来越广泛,在一定规模的企业中几乎是必备存储引擎,互联网企业阿里巴巴、京东、小米都有数千台的HBase集群,中国电信的话单、中国人寿的保单都是存储在HBase中。注意大公司有数十个数百个HBase集群,此点跟Hadoop集群很不相同。另外,数据需求,很多公司是mysql+hbase+hadoop(spark),满足关系型数据库需求,满足大规模结构化存储需求,满足复杂分析的需求。如此流行的原因来源于很多方面,如:
  - 开源繁荣的生态:1. 任何公司倒闭了,开源的HBase还在 2.几乎每家公司都可以去下载源码,改进她,再反馈给社区,就如阿里已经反馈了数百个patch了。加入的人越多,引擎就越好
  - 跟HADOOP深度结合:本就同根同源,在数据存储在HBase后,如果想复杂分析,则非常方便
  - 高扩展、高容量、高性能、低成本、低延迟、稀疏宽表、动态列、TTL、多版本等最为关键,起源google论文,发扬社区及广大互联网公司,设计之初就是为存储互联网,后经过多年的改进升级,如今已经是结构化存储的事实标准

以下资料会一直更新中......请大家关注!

2、书籍
最好买纸质书籍,集中时间看下
  • HBase权威指南(HBase: The Definitive Guide):理论多一些
  • HBase实战:实践多一些

3、总结性

4、方法论

5、各大公司的实践
基本围绕在用户画像、安全风控、订单存储、交通轨迹、物理网、监控、大数据中间存储、搜索、推荐等方面:

6、结尾
  这些资料是笔者整理,以供有大规模结构化需求的用户及HBase爱好者学习交流,以使用HBase更好的解决实际的问题。欢迎传播,原文路径:http://www.hbase.group/hbase/?/article/1

7、声明
以上资料来自互联网,如果侵权,请联系我删除

连接HBase的正确姿势

hbasehbasegroup 发表了文1章 • 1 个评论 • 3504 次浏览 • 2018-10-01 17:42 • 来自相关话题

在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;每个线程一个Connection对象。每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。
从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。HBase RegionServer:用于读、写数据。
下图简单示意了客户端与服务器交互的步骤:



HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:



HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
} 查看全部
在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
  1. 自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;
  2. 每个线程一个Connection对象。
  3. 每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。

从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
  1. Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。
  2. HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。
  3. HBase RegionServer:用于读、写数据。

下图简单示意了客户端与服务器交互的步骤:
客户端与服务器交互的步骤.png
HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:
Connection对象与Socket连接之间的对应关系.png
HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。
//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}
HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:
/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}
RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。
protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}
当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:
/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}
连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:
protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
}

HBase 优化实战

hbase过往记忆 发表了文1章 • 0 个评论 • 1023 次浏览 • 2018-08-10 12:44 • 来自相关话题

背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):





应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:




HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:




Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:




HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。




HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:




网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:




HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?




从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区 查看全部
背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):

menu.saveimg_.savepath20180810124058_.jpg

应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:
201806281646495b8a5674-3a09-4c12-8f81-5ff43ebc0fa9.jpg

HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:
20180628164659ff3e72b7-9be5-4671-ba17-6a37aafd0cdd.jpg

Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:
201806281647298f5c069c-bc91-4706-bb78-ad825e4a15d4.jpg

HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。
20180628164752abef950e-8d45-42ca-a5fe-0b1396a96b5f.jpg

HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:
20180628164811ef38f3c7-dcc1-492f-83f8-fe7bb8ba94fa.jpg

网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:
20180628164824ce396dad-ae37-49b5-8fa7-de67ad187b5c.jpg

HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?
201806281648395c7bae34-f068-41f5-9c8d-a865f5f8f70c.jpg

从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区

HBase全网最佳学习资料汇总

hbasehbase 发表了文1章 • 0 个评论 • 2374 次浏览 • 2018-02-05 21:55 • 来自相关话题

1、前言
 HBase这几年在国内使用的越来越广泛,在一定规模的企业中几乎是必备存储引擎,互联网企业阿里巴巴、京东、小米都有数千台的HBase集群,中国电信的话单、中国人寿的保单都是存储在HBase中。注意大公司有数十个数百个HBase集群,此点跟Hadoop集群很不相同。另外,数据需求,很多公司是mysql+hbase+hadoop(spark),满足关系型数据库需求,满足大规模结构化存储需求,满足复杂分析的需求。如此流行的原因来源于很多方面,如:
  - 开源繁荣的生态:1. 任何公司倒闭了,开源的HBase还在 2.几乎每家公司都可以去下载源码,改进她,再反馈给社区,就如阿里已经反馈了数百个patch了。加入的人越多,引擎就越好
  - 跟HADOOP深度结合:本就同根同源,在数据存储在HBase后,如果想复杂分析,则非常方便
  - 高扩展、高容量、高性能、低成本、低延迟、稀疏宽表、动态列、TTL、多版本等最为关键,起源google论文,发扬社区及广大互联网公司,设计之初就是为存储互联网,后经过多年的改进升级,如今已经是结构化存储的事实标准

以下资料会一直更新中......请大家关注!

2、书籍
最好买纸质书籍,集中时间看下
HBase权威指南(HBase: The Definitive Guide):理论多一些HBase实战:实践多一些
3、总结性
HBase2.0: HBase2.0 :预计今年会发布,hbase2.0是革命性的版本HBase Phoenix:Apache Phoenix与HBase:HBase之上SQL的过去,现在和未来 社区hbase博客:https://blogs.apache.org/hbase/
4、方法论
学术界关于HBase应用场景(物联网/车联网/交通/电力等)研究大全: HBase在互联网领域有广泛的应用,比如:互联网的消息系统的存储、订单的存储、搜索原材料的存储、用户画像数据的存储等。得益于HBase海量的存储量及超高并发写入读取量。HBase在09年就开始在工业界大范围使用,在学术界,也有非常多的高校、机构在研究HBase应用于不同的行业,本文主要梳理下这些资料(主要是中文资料,有一些是硕士论文\期刊),这些很多都在工业界使用了。HBase使用场景和成功案例  存储互联网的初心不变 一种基于物联网大数据的设备信息采集系统及方法:怎么使用HBase、sparkStreaming、r