前言
本文从数据存储、数据检索、高可用、高并发、高性能、集群方案这几个维度来对 HBase 进行抽象总结,不适合想要对 hbase 入门的小伙伴
数据存储
数据定位
客户端请求 zookeeper 获取到当前 hbase:meta 所在的节点地址,然后根据 rowkey 检索 hbase:meta 表所在的区间对应的 Region Server 后缓存到本地
数据存储
不论是增删改都是作为一个数据插入进去,不同的是 type 会记录操作类型,timestamp 会记录操作时间
region server 收到请求后先将数据写入到 hlog 缓冲中,然后写入到 memstore 中,然后执行 sync 将 hlog 缓冲数据刷入到 hdfs 中(默认存储在 os cache)
系统会根据一些策略将 memstore 刷入到一个 hfile 中
策略:memstore 达到阈值、每隔 1 小时、region server memstore 数量达到阈值等等
关键数据结构
LSM 树:内存部分一般采用跳跃表来维护一个有序的 KeyValue 集合(memstore),磁盘部分一般由多个内部 KeyValue 有序文件组成(hfile)
memstore 是通过 ConcurrentSkipListMap 实现的,底层依赖的数据结构为跳表,按照从小到达的顺序排序
key 由 rowkey、column family、qualifier、type、timestamp 组成
value:插入的值
hfile
每个 memstore 会维护一个列簇的数据,当写满会就会 flush 到磁盘中,需要记录数据、索引、布隆过滤器这 3 部分数据
数据就是存储的 KeyValue 值
布隆过滤器会在生成 hfile 的时候去构建,用于快速判定 rowkey 是否在文件中
索引有 root data index block 这部分索引会在打开 hfile 直接加载到内存中,当 hdfile 数据增多的时候回分裂出来 Intermediate Level Data Index Block 和 Leaf Index Block 本质上是多叉树、稀疏索引用于根据 rowkey 快速定位数据
合并 hfile
memstore 持续生成 hfile 会导致产生大量的 hfile,对于写入性能来说都是顺序写而且是写入到 hdfs os cache 并且本地化率越高性能就越高
但是查询的时候回 seek 多个 hfile 导致性能底下,所以需要对 hfile 进行合并
合并采用多路归并算法,对每个文件设计一个指针,取出K个指针中数值最小的一个,然后把最小的那个指针后移,接着继续找K个指针中数值最小的一个,继续后移指针……直到N个文件全部读完为止
合并的方式,一次性全部合并和少量合并,合并的时机:
- MemStore Flush
- 后台线程周期性检查
- 手动触发
合并的核心作用:
- 减少文件数,稳定随机读延迟
- 提高数据本地化率
- 清楚无效数据,减少数据存储量(清除删除数据,清除需要保留的版本数以上的数据)
合并 hfile 会带来较大的带宽压力,以及短时间的 IO 压力,所以需要进行合理的控制
- 独立的线程池资源进行 compaction
- 可以关闭全部合并,然后在业务低峰期手动触发
检索数据
构建 Scanner Iterator 体系
由于 Hbase 增删改都是插入数据,所以查询的时候会去查询多个 hfile 文件获取到最后的数据
这个时候就需要 Scanner 体系来进行检索了
一个 RegionScanner 由多个 StoreScanner 构成,一个列簇对应一个 StoreScanner
一个 StoreScanner 由一个 MemStoreScanner 和多个 StoreFileScanner 组成,Store 会为每个 HFile 构造一个 StoreFileScanner,会为 MemStore 构造一个 MemStoreScanner,他们分别负责对应的数据的读取
根据 rowkey 定位数据
先检索本地缓存寻找 rowkey 所在的区间对应的 region server,如果没有招到去 zookeeper 中定位到 hbase:meata 所在的地址,然后请求查询 hbase:meata 找到对应的 region server
(1)构建 Scanner 体系,通过 MemStoreScanner 去招到对应的 rowkey 其中记录的是最近变更的,查到了直接返回
(2)没有查到,根据每个 hfile 的布隆过滤器快速过滤出对应的 StoreFileScanners,通过 StoreFileScanner 检索数据,通过索引 root index、intermediate index、leaf index 最终定位到目标 Block
(3)查看如果 Block 在 BlockCache 就直接使用,否则加载 hdfs 中的 Block 到 BlockCache 中
(4)合并对应 Block 的数据可能有删除和修改记录,返回返回
(2)指定范围 scan 数据
(1)切分区间
根据 [startKey, stopKey) 进行切分出去区间对应的 Region 区间,客户端将每个自取件请求发送给对应的 Region 处理
(2)构建 Scanner 体系,通过执行 next 获取多行数据
(3)基于过滤器过滤出对应的 StoreFileScanner,seek startKey
(4)基于 StoreFileScanner 和 MemStoreScanner 构建最小堆,每次 next 就通过指针检索出对应的数据
scan 可能会查询到非常多的数据,所以不能一次性全部返回,返回的是一个 Iterator 对象,每次调用 next 返回一部分数据,客户端调用 next 先取缓存,缓存没有再去 Region Server 中获取
集群方案
数据集群
hbase 底层数据都是存储在 hdfs 中的,hfile 会拆分成一个个的 hdfs block(128M)向 NameNode 申请存储在哪些机器中
hbase 的 hfile 一个 block 为 64K 主要是因为要缓存到 BlockCache 中满足随机读的性能,不能太大了
数据基于 hdfs 存储所以非常容易扩展,增加 hdfs 机器后,namenode 会自动将数据按照权重分配到新的机器
数据存储关系集群
KeyValue 存储在哪一台 Region Server 机器,这个关系由 Hbase 来进行维护,在 Hbase:meta 表(只会在一个 Region)中维护存储关系
在 zookeeper 中存储 Hbase:meta 所在的 Region Server 地址
当 Region Server 需要进行扩容的时候数据是无须移动的,因为数据存储在 hdfs 中,不过要重新维护数据与 Region Server 的存储关系,这个操作是比较快的,扩容是比较简单的
整体集群架构
一个表的数据分布在一个或者多个 Region 中
表中的数据被分片存储,每个 Region 包含了完整的列,每个列簇的数据存储在一起对应一个 Store
Master 节点
(1)负责表的创建、修改
(2)管理集群所有 RegionServer,负载均衡、当即恢复、Region 迁移
高可用
RegionServer 都向 Zookeeper 注册,当 Master 节点宕机后,ZooKeeper 感知到后通知 RegionServer 然后会通过选举机制选举出新的 Master
当 RegionServer 宕机后,Master 会感知到,然后会将其负责的范围区间的数据交给其它的 RegionServer
数据的高可用,数据存储在 hdfs 中至少都是 3 个副本,2个写入同一个机架,另外一个处于不同机架,他们默认写入 os cache,除非写入 os cache 后没有来得及写入磁盘并且同时几个机架同时掉电,不然数据不会丢失
高性能
HBase 作为一个列簇式存储的数据库,底层数据结构采用了 LSM 树
(1)写入性能极高,随机写也当做了顺序写入,当采用默认实现方式的时候只有一个 region 位于一个 region server 存在热点问题,此时可以进行预分区,提前分配指定多个区间对应不同的 region server
(2)get 性能较好,因为可以通过 MemStore 缓存命中最近写入的数据,通过布隆过滤器过滤无效 HFile 之后通过索引快速命中对应的 Block,通过 BlockCache 命中缓存减少 HFile Block 文件读取进一步提升性能,通过提高本地化率来减少网络消耗
(3)scan 性能底下,因为增删改都是插入新数据到 hfile 中,所以 scan 的时候需要扫描所有相关联的 hfile 然后维护一个最小堆的值,不停的读取各个文件中的数据,通过客户端调用 next 返回
(4)单点 hbase:meata 所在的 server 也不会有热点问题,客户端都会缓存 region 区间和 server 的映射关系在本地
并发处理
并发写入问题,在每次执行写入动作的时候需要先申请当前行锁,最后在数据写入到 MemStore 之后释放行锁,调用 sync 将 hlog 缓冲区数据写入到 hdfs 中
zookeeper 用来获取分布式行锁
对一张表进行 alter 操作的时候需要先加分布式表锁
MemStore 采用的数据结构为跳跃表,需要支持多线程访问安全问题,采用了 ConcurrentSkipListMap