《HBase原理与实践》
HBase
概念
HBase是一个稀疏的、分布式的、多维排序的Map。
特征
多维:这个特征是相对于普通Map
而言,HBase
的Map
中的Key
是多维(四元组)的,(<rowkey, column family: qualifier, type, timestamp>, value
)。
稀疏:即不是所有的列都需要有值,可以为空(不需要占用存储空间),这也是为了可以随时扩充列(否则每添加一个列都需要向其他列插入空值)。
排序:构成HBase的KV在同一个文件中都是有序的,排序的规则是,RowKey
> Column family:Qualifier
> timestamp
(timestamp大的排在前面)。这个特征对于HBase的读取性能有很大的提升。
分布式:HBase中的数据分布在整个集群中。
体系结构
HBase中的重要组件
Zookeeper
- 实现
Master
的高可用性:系统中同一时间内只能有一个Master处于工作状态,当Master挂掉时,需要快速选出另一台机器当选Master,以保证集群正常运行。 - 管理系统核心元数据:比如存储当前系统的所有RegionServer集合,保存系统元数据表
hbase:meta
表所在的RegionServer地址。 - 参与RegionServer的宕机恢复:Zookeeper通过心跳可以感知RegionServer是否宕机,如果宕机,需要通知Master采取措施。
- 实现分布式锁:HBase中对一张表进行各种管理操作(
alter
)需要先加表锁,防止其他用户对同一张表进行管理操作。
Master
- 处理用户的各种请求:包括建表、修改表、权限操作、切分表、合并数据分区以及Compaction。
- 管理集群中所有的RegionServer:包括RegionServer中的Region的负载均衡、RegionServer的宕机恢复以及Region的迁移等。
- 清理过期的日志和文件:Master每隔一段时间都会检查HDFS中的HLog是否过期、HFile是否已经被删除,并在过期之后将其删除。
RegionServer
- WAL(HLog):HLog在HBase中有两个核心作用:①用于实现数据的高可用性:HBase写数据时,先将数据写入HLog中,然后在合适的时机将数据flush到磁盘。②实现HBase集群间的主从复制,通过回放主集群推送过来的HLog日志实现主从复制。
- BlockCache:HBase系统中的读缓存。客户端从磁盘读取数据后会将一块数据缓存的内存中。一个Block默认为64K,由物理上的多个KV数据组成,BlockCache利用了时间局部性和空间局部性原理,前者表示最近将读取的KV数据很可能与当前读取的KV在地址上是临近的。后者表示一个KV数据现在被访问,那么近期它有可能会再次被访问,主要由两种实现(LRUBlockCache和BucketCache)。
- Region:数据表的一个横向切片,当数据表大小超过一定阈值时会水平切割,分裂为多个Region,Region是集群负载均衡的基本单位,通常一个表的多个Region会被分散在集群的多个机器上。
- Store:一个Region由多个Store组成,每一个Column family对应一个Store。每一个Store又由一个Memstore和多个HFile组成。Memstore被称为写缓存,用户写入的数据会被先写入到Memstore,当MemStore写满之后,系统会异步的将数据flush成一个HFile文件。随着HFile文件越来越多,系统会执行Compact操作,将这些HFile文件合并成一个或多个HFile文件。
HDFS
- HBase实际存储数据的地方,用户数据文件、HLog日志文件最终都会落盘到HDFS,并且HBase内部封装了一个名为DFSClient的HDFS客户端组件,负责对HDFS的实际数据进行读写访问。
缺点
- 无法进行复杂的聚合操作(Join、Group By),如果业务中需要使用聚合运算,可以在HBase中假设Phoenix组件或者Spark组件,前者适用于小规模的OLTP场景,后者适用于大规模的OLAP场景。
- 没有实现二级索引功能,即只能指定RowKey的查找范围,而无法指定其他的列。一般用Phoenix实现。
- HBase原始不支持全局跨行操作,只支持单行事务模型。一般用Phoenix弥补此缺陷。
设计一个类似HBase的KV数据库
核心设计
MiniBase是一个标志的LSM树索引结构,分为内存部分和磁盘部分。
内存部分
内存部分为MenStore,客户端不断写入数据,当MemStore的内存超过一定阈值时,会把MemStore flush到磁盘形成一个DiskStore。并且MemStore分成MutableMemstore和ImmutableMemstore两个部分,这两个都是由ConcurrentSkipListMap组成。初始时,MutableMemstore = new ConcurrentSkipListMap(); ImmutableMemstore = null;
,当需要flush时,令ImmutableMemstore = MutableMemstore;MutableMemstore = new ConcurrentSkipListMap();
磁盘部分
磁盘部分分为多个DiskStore,由多个DiskFile组成。并且每执行一次flush操作,都会生成一个新的DiskFile,并且当DiskStore达到一定数量时,会进行Compact操作,将多个DiskFile合并成一个DiskStore。
由于磁盘空间很大,内存空间相对较小,所以DiskFile必须分成多个块,一次IO操作只读取一小部分的数据,通过读取多个数据块来完成一次区间的扫描。
DataBlock:用来存储有序的KeyValue集合,一个Block大约为64KB。
IndexBlock:一个DiskFile中仅有一个IndexBlock。主要用来存储多个DataBlock的索引数据,每个DataBlock都应该包含4个字段。
- lastKV:该DataBlock的最后一个KV。
- offset:该DataBlock在DIskFile中的偏移量。查找时,用offset和size直接去文件中读取DataBlock的数据。
- size:该DataBlock占用的字节长度。
- bloomFilter:该DataBlock内所有KeyValue计算出的布隆过滤器字节数据。即输入多个字节数组,通过过滤器生产算法生成过滤器,然后可以通过过滤器判断某个字节数据可能存在或者一定不存在。
MetaBlock:一个DiskFile中仅有一个MetaBlock,并且MetaBlock是定长的,主要用MetaBlock来存储DiskFile级别的元数据信息,主要包括以下4种信息。
- fileSize:该DIskFile的文件总字节数,可以用来判断文件是否损坏。
- blockCount:该DiskFile中拥有的Block数量。
- blockIndexOffset:该DiskFile中IndexBlock的偏移位置。
- blockIndexSize:IndexBlock的字节长度。
如何使用
当用户需要读取DiskFile中key='abc’的数据时,可以按照以下流程进行IO读取:因为MetaBlock长度是确定的,所以可以很容易定位到MetaBlock的位置,并且读取数据,然后获取到IndexBlock的偏移和长度,通过偏移和长度获得IndexBlock中的数据,这样就获取了这个DIskFile中所有的DataBlock索引,可以通过二分查找来确定对应DataBlock的offset和size,就能顺利完成DataBlock的IO读取。
KeyValue的设计
public class KeyValue {
private byte[] key;
private byte[] value;
private Op op;
private long sequenceId;
}
/*
sequenceId字段:每次Put/Delete都会被分配一个自增的唯一sequenceId,
这样没一个操作都对应一个sequenceId,读取的时候只能得到小于等于当前sequanceId
的Put/Delete操作,这样保证了本次读取不会得到未来某个时间点的数据,
实现了最简单的Read Commited的事务隔离级别。
*/
LSM树种存放的是操作记录,而不是真实数据(因为要保持顺序写,所以不直接修改数据)。
读写流程
写入流程:当写入MemStore之后,需要考虑进行flush操作时,需要令ImmutableMemstore = MutableMemstore;MutableMemstore = new ConcurrentSkipListMap();
执行此代码时需要确保没有写入操作,所以应该用一个读写锁来控制写入操作和flush的切换操作的互斥,当需要执行写入操作时,先拿到读锁,写完再释放;当需要执行切换操作时,需要拿到写锁,切换完之后再释放锁。
读取操作:因为数据可能会存在MutableMemstore或者ImmutableMemStore或者多个DIskFile的DataBLock中,并且三者都是有序的KV集合,所以可以对三者进行多路归并排序,然后过滤掉不符合条件的版本,将正确的KV返回给用户。