1.1 介绍
HBase架构从一开始就保证了强一致性,所有的读写都是通过一个region server,保证所有的写按顺序发生,所有的读都会看到最近提交的数据。
然而,由于在单一的位置读取,如果服务器不可用,在region中的表在某些时间是不可用的。Region恢复进程需要三个阶段:检测、分配和恢复。这些检测通常是耗时最长的,根据Zookeeper会话超时目前在20-30秒。在此期间和完成恢复之前,客户端不能读取到region数据。
然而对于某些使用案例来说,数据可能是只读的,或者可以接受读取陈旧的数据,HBase可以用于这些延迟的使用案例(应用程序期望有一个读完成的时间约束)。
为实现高可用的读取,HBase提供了一个特性叫做region副本。在这个模型中,每个region的一个表会在不同的RegionServer中有多个副本。默认情况下为1,所以只有一个region副本被部署,从原来的模型中不会有任何的变化,如果region副本被设置为2或者更多,master就会分配副本。负载均衡器会保证region副本不在相同的region server上但在相同的机架上(如果可能的话)。
所有的副本在单一的region server上都会有一个唯一的replica_id,从0开始。Region副本有replica_id==0被称为主region,其他的为从region。客户端的写只有主region可以接收,主region包含着最新的变化。所有的写都会先通过主region,从一定意义上讲这不会是高可用的表现。
1.2 时间轴一致性
public enum Consistency {
STRONG,
TIMELINE
}
Consistency.STRONG是默认的。如果region复制=1,或者region副本只在一个表中,但是读取的一致性是这么做的,读取总是会从主region来执行,所以不会有任何变化,客户端总是能观察到最新的数据。
如果执行读操作是通过Consistency.TIMELINE,然后读RPC将会首先发送到主region服务器上,在短时间内(hbase.client.primaryCallTimeout.get默认为10ms),如果主region没有响应RPC会被发送到从region。
之后结果会从第一个完成RPC的返回。如果响应是来自主region副本,我们就会知道数据是最新的,Result.isStale() API是检查过期数据,如果结果是 从region返回,那么Result.isStale()为true,然后用户就可以检查关于过期数据可能的原因。
时间轴一致性的实现方式:
- 单宿主和有序的更新:在写方面,只有一个主region副本被定义可以接收写操作,此副本负责有序的编辑和防止冲突。这能够保证两个不同的写操作通过不同的副本和数据发散的情况下不能在相同的时间提交。这样就没有必要维护读或者解决时间戳这种冲突。
- 从region包含主region上的数据镜像,类似于关系型数据库的复制,不管HBase在多数据中心还是在单个集群中。
- 在读取端,客户端可以发现读取的是来自最新的数据或者是陈旧的数据。此外,客户端在每个操作客户端仍然可以观察到编辑的顺序,可以追溯到过去的时间点。
上图可以帮助我们更好的理解TIMELINE的语义。
- 有两个客户端,最先的写是x=1,然后是x=2,最后是x=3,可以看出所有的写操作都是通过replica_id=0的主region来完成。写会被保存到WAL中,并且会被异步复制到其他的副本中。在上图可以注意到replica_id=1的接收到了两个更新操作,它显示为x=2,而replica_id=2的则只接收到1个更新操作,它显示为x=1。
- 如果Client1是通过STRONG的一致性进行读取,它只会从replica_id=0读取从而保证观察到最新的值x=3。如果客户端使用TIMELINE方式读取,那么RPC会被发送到所有的副本中(主region会超时),结果会从其中的第一个响应返回。因为客户端可以看到1,2或者3之中的任意一个值。主region失败了日志复制不能持续一段时间。如果客户端通过TIMELINE方式做了多次读取操作,她会首先观察到X=2然后是X=1等等。
1.3 权衡取舍
对于每个应用案例来说都应该权衡其副本的可用性。
优点:
- 只读表的高可用性。
- 旧数据读取的高可用性。
- 对于旧数据能够做到非常低的延迟读取非常高的百分比(99.9%+)
缺点:
- 表的region复制>1会有两倍/三倍的MemStore的使用(取决于region复制的数量)
- 增加block缓存的使用
- 对于log复制会有额外的网络传输
- RPC额外的副本备份。
1.4 存储文件的TTL
我们知道了compact的原理,如果主region上的文件被compact掉了,从region可能还会去读取并参考那些文件,就导致文件已经被compact掉而读取不到,虽然用HFileLinks特性但还是不能保证文件被删除。因此,可以配置属性hbase.master.hfilecleaner.ttl为较大的值,像配置为1小时而保证请求副本时不会接收到IOExceptions。
1.5 元数据表的副本复制
目前META表的WAL没有异步WAL复制。其副本仍然是通过自己的持久化存储文件来恢复。因此我们可以设置hbase.regionserver.meta.storefile.refresh.period属性为非0值,刷新元数据存储文件。注意这个配置和hbase.regionserver.storefile.refresh.period配置不同。
1.6 内存处理
主region的memstore达到阀值时会将数据刷新到磁盘,但是从region在memstore达到压力时无法刷新。他们有他们自己的memstores,只有主region做刷新操作时他们才能释放memstore内存。为允许这种刷新可以通过文件系统的列表操作从主region上面得到新的文件并且可能删除掉它自己的memstore,这种方式只会在hbase.region.replica.storefile.refresh.memstore.multiplier(默认为4)至少比主副本的memstore大。官方推荐不应该经常做这种操作,可以设置一个很大的值而禁用这个特性但是可能会导致永久的复制到block。
1.7 从副本的故障转移
当一个从region副本第一次上线或者失败时,它可能有过一些来自它的memstore的编辑。由于恢复是处理不同的从副本,必须确认它在无法上线之前,开始服务请求之后可以进行请求分配。从region会一直等到它观察到了一个完整的刷新周期(启动刷新,提交刷新)或者来自主region的“region启动事件”。直到这一切发生的时候,从region副本将会通过抛出IOException信息“The region’s reads are disabled ”拒绝所有的读请求。然后其他的副本仍然可能还会读取到,因此不会对TIMELINE方式的RPC造成任何影响。为了促进更快的恢复,从region将会触发一个来自主region的刷新请求。通过配置属性hbase.region.replica.wait.for.primary.flush(默认启用)可禁用此功能。
1.8 创建基于region副本数量的表
HTableDescriptor htd = new HTableDescriptor(TableName.valueOf(“test_table”));
htd.setRegionReplication(2);
...
admin.createTable(htd);
也可以通过setRegionReplication()方法增加或减少复制的副本个数。
1.9 读API使用
可以用Gets和Scans设置一致性:
Get get = new Get(row);
get.setConsistency(Consistency.TIMELINE);
...
Result result = table.get(get);
Scan scan = new Scan();
scan.setConsistency(Consistency.TIMELINE);
...
ResultScanner scanner = table.getScanner(scan);
也可以通过多个gets:
Get get1 = new Get(row);
get1.setConsistency(Consistency.TIMELINE);
...
ArrayList<Get> gets = new ArrayList<Get>();
gets.add(get1);
...
Result[] results = table.get(gets);
通过调用Result.isStale()方法可以检查结果是否来自主region:
Result result = table.get(get);
if (result.isStale()) {
...
}