本篇继续介绍HBase写数据put的核心原理
写数据流程简述
- Client先访问zookeepr,获取hbase:meta表位于哪个RegionServer哪个Region中并且缓存到metaCache;
- 访问hbase:meta表所在的RegionServer,根据该请求的table/rowkey,查询出目标数据位于哪个RegionServer中的哪个Region中,并将该Region信息缓存到客户端的MetaCache,方便下次访问
- 与目标RegionServer进行通信
- 将数据顺序写入(追加)到WAL
- 将数据写入对应的MemStore,数据会在MemStore进行排序
- 向客户端发送ack
- 等达到MemStore的刷写时机后,将数据刷新写入到HFile
写数据的几种方式
Single Put
单条记录的随机put操作,Single Put所对应的接口定义如下:
在Table接口中的定义:
public void put(Put put) throws IOException {
FutureUtils.get(table.put(put));
}
基于RowKey和列定义信息,就可以组建HBase的Put对象,一个Put对象用来描述待写入的一行数据,一个Put可以理解为与某个Rowkey关联的1个或多个KeyValue的集合。
Batch Put
汇聚了几十条甚至几百上千条记录之后的小批次随机put操作
在Table接口中的定义:
@Override
public void put(List<Put> puts) throws IOException {
FutureUtils.get(table.putAll(puts));
}
Bulkload
基于MapReduce API提供的数据批量导入能力,导入数据量通常在GB级别以上,Bulkload能够绕过java client API直接生成HBase底层数据文件(HFile)。
写入流程的三个阶段
客户端处理阶段
meta表详解
Hbase一张表的数据是由多个Region构成,而这些Region是分布在整个集群的RegionServer上的。那么客户端在做任何数据操作时,都要先确定数据在哪个Region上,然后再根据Region的RegionServer信息,去对应的RegionServer上读取信息。因此,Hbase系统内部设计了一张特殊的表--hbase:meta表,专门用来存放整个集群所有的Region信息。
hbase:meta表的结构非常简单,整个表只有一个名为info的ColumnFamily,而且Hbase保证只有一个Region,具体存放了如下信息:

总体来说,hbase:meta的一个rowkey就对应一个Region,rowkey主要由TableName(业务表名)、StartRow(业务表Region区间的起始rowkey)、Timestamp(Region创建的时间戳)、EncodeName(上面三个字段的MD5 Hex值)4个字段拼接而成。每一行数据又分为4列,分别是info:regioninfo、info:seqnumDuringOpen、info:serve、info:serverstartcode。
定位meta表
在提交之前,hbase会在元数据表hbase:meta中根据rowkey找到他们归属的RegionServer,这个定位的过程是通过Hconnection的locationRegion方法完成的。如果是批量请求,还会把这些rowkey按照HRegionLocation分组,不同分组的请求意味着发送到不同的RegionServer,因此每个分组对应一次RPC请求。

- 客户端根据写入表以及rowkey在元数据缓存中查找,如果能查找出该rowkey所在RegionServer以及Region,就可以直接发送写入请求(携带region信息)到目标RegionServer
- 如果客户端缓存中没有查到对应的rowkey信息,需要首先到zookeeper上/hbase/meta-region-server节点查找Hbase元数据所在的RegionServer。向hbase:meta所在的regionServer发送查询请求,在元数据表中查找rowkey所在的RegionServer以及Region信息(通过Reversed Scen实现)。客户端收到返回结果之后会将结果缓存到本地,以备下次使用。

- 客户端根据rowkey相关元数据信息将写入请求发送给目标RegionServer,Region Server接收请求之后会解析出具体的Region信息,查找对应的Region对象,并将数据写入目标Region的MemStore中。
客户端侧的数据分组打包
如果这条待写入的数据采用Single Put的方式,那么,该步骤可以略过(事实上,单条put操作的流程相对简答,就是先定位该RowKey所对应的Region以及RegionServer信息后,Client直接发送写请求到RegionServer侧即可)
但如果这条数据被混杂在其他的数据列表中,采用Batch Put的方式,那么客户端将所有的数据写入对应的RegionServer之前,会先对分组"打包",流程如下:
- 按Region分组遍历每一条数据的RowKey,然后,依据meta表中记录的Region信息,确定每一条数据所属的Region。此步骤可以获取到Region到RowKey列表的映射关系。
- 按照RegionServer"打包":因为Region一定归属于某一个RegionServer,那属于同一个RegionServer的多个Region的写入,被打包成一个MultiAction对象,这样可以一并发送到每一个RegionServer中。

Client发送写数据请求到RegionServer
类似于Client发送建表到Master的流程,CLient发送写数据请求到RegionServer,也是通过RPC的方式。只是Client到Master以及CLient到RegionServer,采用了不同的RPC服务接口,服务端具体的IPC实现是RSPpcServices

single put请求与batch put 请求,两者所调用的RPC服务接口是不同的,如下:
service ClientService {
// single Put请求所涉及的RPC服务接口方法
MutateResponse Mutate(MutateRequest);
// batch put请求所涉及的RPC服务接口方法
MultiResponse Multi(MultiRequest);
}
Region写入阶段
Region分法
RegionServer的RPC Server侧,接收到来自Client端的RPC请求以后,将该请求交给Handler线程处理
如果是single put,则该步骤比较简单,因为在发送过来的请求的参数MutateRequest中,已经携带了这条记录所关联的Region,那么直接将该请求转发给对应的Region即可
如果是batch put,则接收到的请求参数为MultiRequest,在MultiRequest中,混合了这个RegionServer所持用的多个Region写入请求,每个Region的写入请求都包装成了一个RegionAction对象(包含该region上的一组数据)。RegionServer接收到MultiRequest请求以后,遍历所有的RegionAction,而后写入到每一个Region中,此过程是串行的,所以从这里可以看出来,并不是batch越大越好,大的batch size甚至可能导致吞吐量下降。

Region写入
服务器端RegionServer接收客户端的写入请求后,首先会反序列化put对象,然后执行各种检查操作,比如检查Region是否只读、MemStore大小是否超过blockingMemstoreSize等。检查完成之后,执行一些列如下核心操作:

- Acquire locks:Hbase中使用行锁保证同一行数据的更新都是互斥操作,用以保证更新的原子性,要么更新成功,要么更新失败。
- Update LASTEST_TIMESTAMP timestamps:更新所有待写入(更新)KeyValue的时间戳为当前系统时间。
- Build WAL edit:Hbase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。该步骤就是在内存中构建WALEdit对象,为了保证Region级别事物的写入原子性,一次写入操作中所有KeyValue会构建一条WALEdit记录
- Append WALEdit To WAL:将步骤3中构造在内存中的WALEdit记录顺序写入HLog中,此时不需要执行sync操作。当前版本的Hbase使用discruptor实现了高效的生产消费队列,来实现WAL得追加写入操作。
- Write back to MemStore:写入WAL之后再将数据写入MemStore。
- Release row locks:释放行锁。
- Sync wal:HLog 真正sync到HDFS,在释放行锁之后执行sync操作是为了尽量减少持锁时间,提升写性能。如果synv失败,执行回滚操作将MemStore中已经写入的数据移除。
- 结束写事务:此时该线程的更新操作才会被其他读请求可见,更新才实际生效。
Region内部处理:写WAL
Hbase也采用了LSM树的架构设计:LSM树利用了传统机械硬盘的"顺序读写速度远高于随机读写速度"的特点。随机写入的数据,如果直接去改写每一个Region上的数据文件,那么吞吐量是非常差的。因此,每一给Region中随机写入的数据,都暂时先缓存在内存中(Hbase存放这部分内存数据的模块称之为MemStore,为了保障数据可靠性,将这些随机写入的数据顺序写入到一个称之为WAL(Write-Ahead-Log)的日志文件中,WAL中的数据按照时间顺序组织:

如果位于内存中的数据尚未持久化,而且突然遇到了突然机器断电,只需要将WAL中的数据回放到Region中即可

在Hbase中,默认一个RegionServer只有一个可写的WAL文件,WAL中写入的记录,以Entry为单元,而一个Entry中,包含:
- WALKey包含{Encodeed Region Name,Table Name,Sequence ID,Timestamp}等关键信息,其中,Sequence ID 在维持数据一致性方便起到了关键作用,可以理解为一个事物ID
- WALEdit 中直接保存待写入数据的所有KeyValue,而这些KeyValues可能来自一个Region中的多行数据
也就是说,通常一个Region中的一个batch put操作,会被组装成一个Entry,写入到WAL中

Region内部处理:写MemStore
每一个Column Family,在Region内部被抽象为了一个HStore对象,而每一个HStore拥有自身的MemStore,用来缓存一批最近被随机写入的数据,这是LSM树核心设计的一部分。
MemStore中用来存放所有的KeyValue的数据结构,称之为CellSet,而CellSet的核心是一个ConcurrentSkipListMap,ConcurrentSkipListMap是Java的跳表实现,优点是能够非常友好地支持大规模并发写入,同时跳跃表本身是有序存储的,这有利于数据有序落盘,并且有利于提升MemStore中的KeyValue查找性能。
因此,写MemStore的过程,事实上是将batch put提交过来的所有KeyValue列表,写入到MemStore的以ConcurrentSkipListMap为组成核心的CellSet中:

MemStore因为涉及大量的随机写入操作,会带来大量Java小对象的创建与消亡,会导致大量的内存碎片,给GC带来比较重的压力,Hbase为了优化这里的机制,借鉴了操作系统化的内存分页技术,增加了一个名为MSLab的特性,通过分配一些固定大小的Chunk,来存储MemStore中的数据,这样可以有效减少内存碎片问题,降低GC的压力。
至此,这条数据已经被同时成功写到了WAL以及MemStore中。

MemStore Flush 阶段
Flush阶段流程放在另一篇中Hbase原理解析—Flush与Compaction