赞
踩
MVCC
写入流程中涉及到MVCC多版本协议控制协议,主要是hbase解决读写一致性的解决方案。MVCC变量是region级别的,每个region之间的mvcc是相互独立的。
Hbase每次Put都会指定一个唯一ID,该ID是Region级递增的。每个Region得MVCC维护两个point:
没有数据写入的时候,二者的位置是一样的,当有数据写入的时候,readpoint要比writepoint小,只有readpoint之前的数据能够读取到(只要成功写入HLog和Memstore的数据能够读取到,无需写入HFile中)
Nonce
Nonce机制,在网络不稳定的情况下,当客户端发送rpc请求给regionserver服务器的时候,如果服务器处理时间过长导致超时,会出现服务器处理完毕,而无法及时通知客户端,导致客户端重新发送写入请求,即多次发送append,会造成数据多次添加。为了防止类似的现象,Hbase引入了Nonce机制,ServerNonceManager负责管理该RegionServer的nonce。
客户端每次申请以及重复申请会使用同一个nonce,发送到服务端之后,服务端会判断该nonce是否存在,如果不存在则可以放心执行,否则会根据当前的nonce进行相应的回调处理:
初始化ZooKeeper Session
因为meta Region的路由信息存放于ZooKeeper中,在第一次从ZooKeeper中读取META Region的地址时,需要先初始化一个ZooKeeper Session。ZooKeeper Session是ZooKeeper Client与ZooKeeper Server端所建立的一个会话,通过心跳机制保持长连接。
获取Region路由信息
通过前面建立的连接,从ZooKeeper中读取meta Region所在的RegionServer,这个读取流程,当前已经是异步的。获取了meta Region的路由信息以后,再从meta Region中定位要读写的RowKey所关联的Region信息。如下图所示:
因为每一个用户表Region都是一个RowKey Range,meta Region中记录了每一个用户表Region的路由以及状态信息,以RegionName(包含表名,Region StartKey,Region ID,副本ID等信息)作为RowKey。基于一条用户数据RowKey,快速查询该RowKey所属的Region的方法其实很简单:只需要基于表名以及该用户数据RowKey,构建一个虚拟的Region Key,然后通过Reverse Scan的方式,读到的第一条Region记录就是该数据所关联的Region。
Region只要不被迁移,那么获取的该Region的路由信息就是一直有效的,因此,HBase Client有一个Cache机制来缓存Region的路由信息,避免每次读写都要去访问ZooKeeper或者meta Region。
客户端侧的数据分组“打包”
如果这条待写入的数据采用的是Single Put的方式,那么,该步骤可以略过(事实上,单条Put操作的流程相对简单,就是先定位该RowKey所对应的Region以及RegionServer信息后,Client直接发送写请求到RegionServer侧即可)。
但如果这条数据被混杂在其它的数据列表中,采用Batch Put的方式,那么,客户端在将所有的数据写到对应的RegionServer之前,会先分组”打包”,流程如下:
Client发送写数据请求到RegionServer
类似于Client发送建表到Master的流程,Client发送写数据请求到RegionServer,也是通过RPC的方式。只是,Client到Master以及Client到RegionServer,采用了不同的RPC服务接口。
RegionServer端处理:Region分发
RegionServer的RPC Server侧,接收到来自Client端的RPC请求以后,将该请求交给Handler线程处理。
如果是single put,则该步骤比较简单,因为在发送过来的请求参数中,已经携带了这条记录所关联的Region,那么直接将该请求转发给对应的Region即可。
如果是batch puts,则接收到的请求参数为混合了这个RegionServer所持有的多个Region的写入请求,每一个Region的写入请求都被包装成了一个RegionAction对象。RegionServer接收到请求以后,遍历所有的RegionAction,而后写入到每一个Region中,此过程是串行的。
从这里可以看出来,并不是一个batch越大越好,大的batch size甚至可能导致吞吐量下降。
Region内部处理:写WAL
HBase也采用了LSM-Tree的架构设计:LSM-Tree利用了传统机械硬盘的“顺序读写速度远高于随机读写速度”的特点。随机写入的数据,如果直接去改写每一个Region上的数据文件,那么吞吐量是非常差的。因此,每一个Region中随机写入的数据,都暂时先缓存在内存中(HBase中存放这部分内存数据的模块称之为MemStore),为了保障数据可靠性,将这些随机写入的数据顺序写入到一个称之为WAL(Write-Ahead-Log)的日志文件中,WAL中的数据按时间顺序组织:
在HBase中,默认一个RegionServer只有一个可写的WAL文件。
Region内部处理:写MemStore
每一个Column Family,在Region内部被抽象为了一个HStore对象,而每一个HStore拥有自身的MemStore,用来缓存一批最近被随机写入的数据,这是LSM-Tree核心设计的一部分。
MemStore中用来存放所有的KeyValue的数据结构,核心是一个ConcurrentSkipListMap,我们知道,ConcurrentSkipListMap是Java的跳表实现,数据按照Key值有序存放,而且在高并发写入时,性能远高于ConcurrentHashMap。
MemStore中的数据,达到一定的阈值,被Flush成HDFS中的HFile文件。
HBase Compaction可以将一些HFile文件合并成较大的HFile文件,也可以把所有的HFile文件合并成一个大的HFile文件,这个过程可以理解为:将多个HFile的“交错无序状态”,变成单个HFile的“有序状态”,降低读取时延。小范围的HFile文件合并,称之为Minor Compaction,一个列族中将所有的HFile文件合并,称之为Major Compaction。
MemStore由一个可写的Segment,以及一个或多个不可写的Segments构成。
MemStore中的数据先Flush成一个Immutable的Segment,多个Immutable Segments可以在内存中进行Compaction,当达到一定阈值以后才将内存中的数据持久化成HDFS中的HFile文件。
为什么不能调小MemStore的大小,多次写入HFile,减小内存开销?
如果MemStore中的数据被直接Flush成HFile,而多个HFile又被Compaction合并成了一个大HFile,随着一次次Compaction发生以后,一条数据往往被重写了多次,这带来显著的IO放大问题,另外,频繁的Compaction对IO资源的抢占,其实也是导致HBase查询时延大毛刺的罪魁祸首之一。
为何不直接调大MemStore的大小,减少Compaction的次数
ConcurrentSkipListMap在存储的数据量达到一定大小以后,写入性能将会出现显著的恶化。
目的
如果有多个HFiles文件,如果想基于RowKey读取一行数据,则需要查看多个文件,因为不同的HFile文件的RowKey Range可能是重叠的,此时,Compaction对于降低读取时延是非常必要的。
很多HBase用户在集群中关闭了自动Major Compaction,为了降低Compaction对IO资源的抢占,但出于清理数据的需要,又不得不在一些非繁忙时段手动触发Major Compaction,这样既可以有效降低存储空间,也可以有效降低读取时延。
弊端
Compaction会导致写入放大
在Facebook Messages系统中,业务读写比为99:1,而最终反映到磁盘中,读写比却变为了36:64。
WAL,HDFS Replication,Compaction以及Caching,共同导致了磁盘写IO的显著放大。
随着不断的执行Minor Compaction以及Major Compaction,可以看到,这条数据被反复读取/写入了多次,这是导致写放大的一个关键原因,这里的写放大,涉及到网络IO与磁盘IO,因为数据在HDFS中默认有三个副本。
而关于如何合理的执行Compaction,我们需要结合业务数据特点,不断的权衡如下两点:
Get
Get是指基于确切的RowKey去获取一行数据,通常被称之为随机点查,这正是HBase所擅长的读取模式。
发送Get请求的接口获取到的一行记录,被封装成一个Result对象:
也定义了Batch Get的接口,这样可以在一次网络请求中同时获取多行数据。获取到的Result列表中的结果的顺序,与给定的RowKey顺序是一致的。
Scan
HBase中的数据表通过划分成一个个的Region来实现数据的分片,每一个Region关联一个RowKey的范围区间,而每一个Region中的数据,按RowKey的字典顺序进行组织。
正是基于这种设计,使得HBase能够轻松应对这类查询:”指定一个RowKey的范围区间,获取该区间的所有记录”, 这类查询在HBase被称之为Scan。
从以上图片可以看出HFile主要分为四个部分:
Data Block
Data Block是HBase中数据存储的最小单元,它存储的是用户KeyValue数据,数据结构如图所示:
Key Type:存储Key类型Key Type,占1字节,Type分为Put、Delete、DeleteColumn、DeleteFamilyVersion、DeleteFamily等类型,标记这个KeyValue的类型
Bloom Block
BloomFilter对于HBase随机读的性能至关重要,他可以避免读取一些不会用到HFile,减少实际的IO次数,提高随机读的性能。
下图中集合S只有两个元素x和y,分别被3个hash函数进行映射,映射到的位置分别为(0,2,6)和(4,7,10),对应的位会被置为1:
现在假如要判断另一个元素是否是在此集合中,只需要被这3个hash函数进行映射,查看对应的位置是否有0存在,如果有的话,表示此元素肯定不存在于这个集合,否则有可能存在。下图所示就表示z肯定不在集合{x,y}中:
分层索引
无论是Data Block Index还是Bloom Filter,都采用了分层索引的设计。
Data Block的索引,在HFile V2中做多可支持三层索引:最底层的Data Block Index称之为Leaf Index Block,可直接索引到Data Block;中间层称之为Intermediate Index Block,最上层称之为Root Data Index,Root Data index存放在一个称之为”Load-on-open Section“区域,Region Open时会被加载到内存中。基本的索引逻辑为:由Root Data Index索引到Intermediate Block Index,再由Intermediate Block Index索引到Leaf Index Block,最后由Leaf Index Block查找到对应的Data Block。在实际场景中,Intermediate Block Index基本上不会存在,因此,索引逻辑被简化为:由Root Data Index直接索引到Leaf Index Block,再由Leaf Index Block查找到的对应的Data Block。
Bloom Filter也被拆成了多个Bloom Block,在”Load-on-open Section”区域中,同样存放了所有Bloom Block的索引数据。
交叉存放
在”Scanned Block Section“区域,Data Block(存放用户数据KeyValue)、存放Data Block索引的Leaf Index Block(存放Data Block的索引)与Bloom Block(Bloom Filter数据)交叉存在。
按需读取
无论是Data Block的索引数据,还是Bloom Filter数据,都被拆成了多个Block,基于这样的设计,无论是索引数据,还是Bloom Filter,都可以按需读取,避免在Region Open阶段或读取阶段一次读入大量的数据,有效降低时延。
Root Index Block、Leaf Index Block、Data Block所处的位置以及索引关系(忽略Bloom过滤器):
混合了BloomFilter Block以后的HFile构成如下图所示:
Hbase宕机处理
什么样的数据适合用HBase来存储?
关于行级别的ACID
LSM
LSM树原理把一棵大树拆分成N棵小树,它首先写入内存中,随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树定期可以做merge操作,合并成一棵大树,以优化读性能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。