HBase作为分布式NoSQL
数据库系统,不单支持宽列表,并且对于随机读写来说也具有较高的性能。在高性能的随机读写事务的同时,HBase也能保持事务的一致性。目前HBase只支持行级别的事务一致性。本文主要探讨一下HBase的写请求流程,主要基于
0.98.8版本的实现。
客户端写请求
HBase提供的
Java client API是以HTable为主要接口,对应其中的HBase表。写请求API主要为HTable.put(write和update)、HTable.delete等。以HTable.put为例子,首先来看看客户端是怎么把请求发送到HRegionServer的。
每个put请求表示一个KeyValue数据,考虑到客户端有大量的数据需要写入到HBase表,HTable.put默认是会把每个put请求都放到本地缓存中去,当本地缓存大小超过阀值(默认为2MB)的时候,就要请求刷新,即把这些put请求发送到指定的HRegionServer中去,这里是利用线程池并发发送多个put请求到不同的HRegionServer。但如果多个请求都是同一个HRegionServer,甚至是同一个HRegion,则可能造成对服务端造成压力,为了避免发生这种情况,客户端API会对写请求做了并发数限制,主要是针对put请求需要发送到的HRegionServer和HRegion来进行限制,具体实现在AsyncProcess中。主要参数设定为:
hbase.client.max.total.tasks 客户端最大并发写请求数,默认为100
hbase.client.max.perserver.tasks 客户端每个HRegionServer的最大并发写请求数,默认为2
hbase.client.max.perregion.tasks 客户端每个HRegion最大并发写请求数,默认为1
为了提高I/O效率,AsyncProcess会合并同一个HRegion对应的put请求,然后再一次把这些相同HRegion的put请求发送到指定HRegionServer上去。另外AsyncProcess也提供了各种同步的方法,如waitUntilDone等,方便某些场景下必须对请求进行同步处理。每个put和读请求一样,都是要通过访问hbase:meta表来查找指定的HRegionServer和HRegion,这个流程和读请求一致,可以参考文章的描述。
服务端写请求
当客户端把写请求发送到服务端时,服务端就要开始执行写请求操作。HRegionServer把写请求转发到指定的HRegion执行,HRegion每次操作都是以批量写请求为单位进行处理的。主要流程实现在HRegion.doMiniBatchMutation,大致如下:
获取写请求里指定行的行锁。由于这些批量写请求之间是不保证一致性(只保证行一致性),因此每次只会尝试阻塞获取至少一个写请求的行锁,其它已被获取的行锁则跳过这次更新,等待下次迭代的继续尝试获取更新已经获得行锁的写请求的时间戳为当前时间获取HRegion的updatesLock的读锁。获取MVCC(Multi-Version Concurrency Control)的最新写序号,和写请求KeyValue数据一起写入到MemStore。构造WAL(Write-Ahead Logging) edit对象把WAL edit对象异步添加到HLog中,获取txid号释放第3步中的updatesLock的读锁以及第1步中获得的行锁按照第6步中txid号同步HLog提交事务,把MVCC的读序号前移到第4步中获取到的写序号如果以上步骤出现失败,则回滚已经写入MemStore的数据如果MemStore缓存的大小超过阀值,则请求当前HRegion的MemStore刷新操作。
经过以上步骤后,写请求就属于被提交的事务,后面的读请求就能读取到写请求的数据。这些步骤里面都包含了HBase的各种特性,主要是为了保证可观的写请求的性能的同时,也确保行级别的事务ACID特性。接下来就具体分析一下一些主要步骤的具体情况。
HRegion的updatesLock
步骤3中获取HRegion的updatesLock,是为了防止MemStore在flush过程中和写请求事务发生线程冲突。
首先要知道MemStore在写请求的作用。HBase为了提高读性能,因此保证存储在HDFS上的数据必须是有序的,这样就能使用各种特性,如二分查找,提升读性能。但由于HDFS不支持修改,因此必须采用一种措施把随机写变为顺序写。MemStore就是为了解决这个问题。随机写的数据写如MemStore中就能够在内存中进行排序,当MemStore大小超过阀值就需要flush到HDFS上,以HFile格式进行存储,显然这个HFile的数据就是有序的,这样就把随机写变为顺序写。另外,MemStore也是HBase的LSM树(Log-Structured Merge Tree)的实现部分之一。
在MemStore进行flush的时候,为了避免对读请求的影响,MemStore会对当前内存数据kvset创建snapshot,并清空kvset的内容,读请求在查询KeyValue的时候也会同时查询snapshot,这样就不会受到太大影响。但是要注意,写请求是把数据写入到kvset里面,因此必须加锁避免线程访问发生冲突。由于可能有多个写请求同时存在,因此写请求获取的是updatesLock的readLock,而snapshot同一时间只有一个,因此获取的是updatesLock的writeLock。
获取MVCC写序号
MVCC是HBase为了保证行级别的事务一致性的同时,提升读请求的一种并发事务控制的机制。MVCC的机制不难理解,可以参考这里。
MVCC的最大优势在于,读请求和写请求之间不会互相阻塞冲突,因此读请求一般不需要加锁(只有两个写同一行数据的写请求需要加锁),只有当写请求被提交了后,读请求才能看到写请求的数据,这样就可以避免发生“脏读”,保证了事务一致性。具体MVCC实现可以参考HBase的一位PMC Member的这篇文章。
WAL(Write-Ahead Logging) 与HLog
WAL是HBase为了避免遇到节点故障无法服务的情况下,能让其它节点进行数据恢复的机制。HBase进行写请求操作的时候,默认都会把KeyValue数据写入封装成WALEdit对象,然后序列化到HLog中,在0.98.8版本里采用ProtoBuf格式进行序列化WAL。HLog是记录HBase修改的日志文件,和数据文件HFile一样,也是存储于HDFS上,因此保证了HLog文件的可靠性。这样如果机器发生宕机,存储在MemStore的KeyValue数据就会丢失,HBase就可以利用HLog里面记录的修改日志进行数据恢复。
每个HRegionServer只有一个HLog对象,因此当前HRegionServer上所有的HRegion的修改都会记录到同一个日志文件中,在需要数据恢复的时候再慢慢按照HRegion分割HLog里的修改日志(Log Splitting)。
整个写请求里,WALEdit对象序列化写入到HLog是唯一会发生I