HBase写数据和存数据的过程:
HBase数据的写入过程:
1、Client访问zookeeper,获取元数据存储所在的regionserver
2、拿到对应的表存储的regionserver,通过刚刚获取的地址访问对应的regionserver,
3、去表所在的regionserver进行数据的添加
4、查找对应的region,在region中寻找列族,先向memstore中写入数据
5、当memstore写入的值变多,触发溢写操作(flush),进行文件的溢写,成为一个StoreFile
6、当溢写的文件过多时,会触发文件的合并(Compact)操作,合并有两种方式(major,minor)
多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除
minor compaction:小范围合并,默认是3-10个文件进行合并,不会删除其他版本的数据。
major compaction:将当前目录下的所有文件全部合并,一般手动触发,会删除其他版本的数据(不同时间戳的)
7、当region中的数据逐渐变大之后,达到某一个阈值,会进行裂变(一个region等分为两个region,并分配到不同的regionserver),原本的Region会下线,新Split出来的两个Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。
由此可知HBase只是增加数据,所有的更新和删除操作,都是在Compact阶段做的,所以用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能读写。
补充:
(1).HStore存储是HBase存储的核心,其中由两部分组成,一部分是Memstore,一部分是StoreFile。
(2).HLog的的功能: 宕机数据恢复
在分布式系统环境中,我们是无法避免系统出错或者宕机的,一旦HRegionServer意外退出,
MemStore中的内存数据就会丢失,而引入HLog就是为了防止这种情况。
工作机制:每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件中,HLog文件定期会滚动出新,并删除旧的文件(已持久化到Storefile中的数据),当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的HLog文件,将不同region的log数据拆分,分别放在相应region目录下,然后再将失效的region(带有刚刚拆分的log)重新分配,领取到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到Memstore中,然后flush到StoreFile,完成数据恢复。
(3).Region就是StoreFiles,StoreFiles里由HFile构成,HFile里由hbase的data块构成,一个data块里面又有很多的keyvalue对,每个keyvalue里存了我们需要的值。
(4)
一 张表,有两个列族(红颜色的一个,黄颜色的一个),一个列族有两个列,从图中可以看出,这就是列式数据库的最大特点,同一个列族的数据在在一起的,我们还 发现如果是有多个版本,同时也会存多个版本。最后我们还发现里面存了这样的值:r1:键值,cf1:列族的名字,c1:列名。t1:版本号,value值 (最后一幅图说明的是value值可以存放的位置)。通过这样的看法,我们会发现如果在设计表的时候把这几个东西:r1:键值,cf1:列族的名 字,c1:列明的名字取短一点,我们就可以节省出好多存储的空间!
还有,我们从这一幅图中还应该得到这样的认识:
我 们看倒数第二张图,字段筛选的效率从左到右明显下降,所以在keyvalue的设计时用户可以考虑把一些重要的筛选信息左移到合适的位置,从而在不改变数 据量的情况下,提高查询性能。那么简单的说就是用户应当尽量把查询维度或信息存储在行健中,因为它筛选数据的效率最高。
得到上面的认识后,我们应该还要会有这样的觉悟:
HBase 的数据存储时会被有顺序的存储到一个特定的范围,因为我们存储的时候一般都是按顺序的,所以会一直存到同一个region上,由于一个region只能由 一个服务器管理,这样我们老是添加到同一个region上,会造成读写热点,从而使集群性能下降。
那么解决这个问题的办法还是有的,假如我们 有9台服务器,那么我们就会去当前时间,然后mod9,加到行键前缀,这样就会被平均的分到不同的region服务器上了,这样带来的好处是,因为相连的数据 都分布到不同的服务器上了,用户可以多线程并行的读取数据,这样查询的吞吐量会提高。关于版本的控制,我们要么就让多台服务器上的时间都同步,要么干脆就在put插入数据时,设置一个客户端的时间戳来代替。
(5).关于表的设计:
设计表的时候,有两种设计方式,一种是高表设计,一种是胖表设计。根据HBase的拆分规则,我们的高表设计更容易拆分(使用组合键),不过如果我们设计成胖表,而我们这个胖表里的数据需要经常修改,这样的设计是很合理的。以为HBase保证了行级原子性,如果设计成高表,反而就不合适了,因为不能保证跨行的原子性。
(6)写缓存:
每一个put的操作实际上是RPC的操作,它将客户端的数据传送到服务器然后返回,这只适合小数据量的操作,如果有个应用程序需要每秒存储上千行数据到HBase中,这样处理就不太合适了。HBase的API配备了一个客户端的写缓冲区,缓冲区负责收集put操作,然后调用RPC操作,一次性将put送往服务器。默认情况下,客户端缓冲区是禁止的。可以通过自动刷写设置为FALSE来激活缓冲区。
table.setAutoFlush(false);void flushCommits () throws IOException这个方法是强制 将数据写到服务器。用户还可以根据下面的方法来配置客户端写缓冲区的大小。 void setWritaeBufferSize(long writeBufferSize) throws IOException;默认大小是 2MB,这个也是适中的,一般用户插入的数据不大,不过如果你插入的数据大的话,可能要考虑增大这个值。从而允许客户端更高效地以一定数量的数据组成一组,然后通过一次RPC请求来执行。
在每一个客户端设置一个写缓冲区是一件麻烦的事,我们可以在Hbase-site.xml中给用户设置一个较大的预设值。
<property>
<name>hbase.client.write.buffer</name>
<value>20971520</value>
</property>
(7)压缩
hbase支持大量的算法,并且支持列族级别以上的压缩算法,除非有特殊原因,不然我们应该尽量使用压缩,压缩通常会带来较好的性能。通过一些测试,我们推荐使用SNAPPY这种算法来进行我们hbase的压缩。
HBase数据的读取流程:
1.Client访问zookeeper,获取元数据存储所在的regionserver
2.通过刚刚获取的地址访问对应的regionserver,拿到对应的表存储的regionserver
3.去表所在的regionserver进行数据的读取
4.查找对应的region,在region中寻找列族,先找到memstore,找不到去blockcache中寻找,再找不到就进行storefile的遍历
5.找到数据之后会先缓存到blockcache中,再将结果返回
blockcache逐渐满了之后,会采用LRU的淘汰策略。
补充:
(1)在HBase中,所有的存储文件都被划分成了若干个小存储块,这些存储块在get或scan操作时会加载到内存中,他们类似与RDBMS中的存储单元页,这个参数的默认大小是64k,HBase会顺序的读取一个数据块到内存缓存中,其读取相邻的数据时就可以在内存中读取而不需要从磁盘中再次读取,有效的减少了磁盘的I/O的次数。这个参数默认为TRUE,这意味着每次读取的块都会缓存到内存中。
但是:如果用户顺序读写某个特定的列族,这个时候,这个机制就会把其他我们不需要的列族的数据也加载到内存中,增加了我们的负担,那么1就需要将其关闭。void setBlockCacheEnable(boolean blockCacheEnable);
(2)禁止自动刷写
1.我们有大批数据要插入时,如果我们没有禁止,Put实例会被逐个的传送到region服务器,(一条一条的往磁盘中写,害怕不?)如果用户禁止了自动刷写的功能,put操作会在写缓冲区被填满时才会被送出。
2.使用扫描缓存,如果HBase被用作一个mapreduce作业的输入源,请最好将作为mapreduce作业输入扫描器实例的缓存用setCaching()方法设置为默认值1更大的数。使用默认值意味着map任务会在处理每条记录时都请求region服务器,不过,这个值是500的话,则一次可传送500条数据到客户端进行处理,当然了这数据也是根据具体情况而定。
(3)限定扫描范围
比如我们要处理大量行(特别是作为mapreduce的输入源),其中用到scan的时候我们有Scan.addFamily();的方法,这个时候我们如果只是需要到这几个列族中的几个列,那么我们一定要精确,因为过多的列会导致效率的损失。
(4)块缓存的用法
首先我们的块缓存是通过Scan.setCacheBlocks();启动的,那么被频繁访问的行,我们应该使用缓存块,但是MapReduce作业使用扫描大量的行,我们就不该使用这个了。
一、hbase的读操作:
ZooKeeper---meta--regionserver--region--memstore--storefile
1、首先从zookerper找到meta表的region的位置,然后读取meta表中的数据。而meta中又存储了用户表的region信息
2、根据namespace、表名和rowkey根据meta表中的数据找到写入数据对应的region信息
3、然后找到对应的regionserver
4、查找对应的region
5、先从Memstore找数据,如果没有,再到StoreFile上读
二、hbase的写操作:
ZooKeeper---meta--regionserver--1、Hlog 1、MemStore--storefile
1、首先从zookerper找到meta表的region的位置,然后读取meta表中的数据。而meta中又存储了用户表的region信息
2、根据namespace、表名和rowkey根据meta表中的数据找到写入数据对应的region信息
3、然后找到对应的regionserver
4、把数据分别写到Hlog和memstore各一份
4、1当memstore达到阈值后把数据刷成一个storefile文件,当compact后,逐渐形成越来越大的storefile后触发spilt,
把当前的StoreFile分成两个,这里相当于把一个大的region分割成两个region
4、1若MemStore中的数据有丢失,则可以从HLog上恢复,当多个StoreFile文件达到一定的大小后,会触发Compact合并操作,
合并为一个StoreFile,这里同时进行版本的合并和数据删除
第二篇:
HBase写数据和存数据的过程:
HBase数据的写入过程:
1、Client访问zookeeper,获取元数据存储所在的regionserver
2、通过刚刚获取的地址访问对应的regionserver,拿到对应的表存储的regionserver
3、去表所在的regionserver进行数据的添加
4、查找对应的region,在region中寻找列族,先向memstore中写入数据
5、当memstore写入的值变多,触发溢写操作(flush),进行文件的溢写,成为一个StoreFile
6、当溢写的文件过多时,会触发文件的合并(Compact)操作,合并有两种方式(major,minor)
多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除
minor compaction:小范围合并,默认是3-10个文件进行合并,不会删除其他版本的数据。
major compaction:将当前目录下的所有文件全部合并,一般手动触发,会删除其他版本的数据(不同时间戳的)
7、当region中的数据逐渐变大之后,达到某一个阈值,会进行裂变(一个region等分为两个region,并分配到不同的regionserver),原本的Region会下线,新Split出来的两个Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。
由此可知HBase只是增加数据,所有的更新和删除操作,都是在Compact阶段做的,所以用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能读写。
补充:
(1).HStore存储是HBase存储的核心,其中由两部分组成,一部分是Memstore,一部分是StoreFile。
(2).HLog的的功能: 宕机数据恢复
在分布式系统环境中,我们是无法避免系统出错或者宕机的,一旦HRegionServer意外退出,
MemStore中的内存数据就会丢失,而引入HLog就是为了防止这种情况。
工作机制:每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件中,HLog文件定期会滚动出新,并删除旧的文件(已持久化到Storefile中的数据),当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的HLog文件,将不同region的log数据拆分,分别放在相应region目录下,然后再将失效的region(带有刚刚拆分的log)重新分配,领取到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到Memstore中,然后flush到StoreFile,完成数据恢复。
(3).Region就是StoreFiles,StoreFiles里由HFile构成,HFile里由hbase的data块构成,一个data块里面又有很多的keyvalue对,每个keyvalue里存了我们需要的值。
(4)
一 张表,有两个列族(红颜色的一个,黄颜色的一个),一个列族有两个列,从图中可以看出,这就是列式数据库的最大特点,同一个列族的数据在在一起的,我们还 发现如果是有多个版本,同时也会存多个版本。最后我们还发现里面存了这样的值:r1:键值,cf1:列族的名字,c1:列名。t1:版本号,value值 (最后一幅图说明的是value值可以存放的位置)。通过这样的看法,我们会发现如果在设计表的时候把这几个东西:r1:键值,cf1:列族的名 字,c1:列明的名字取短一点,我们就可以节省出好多存储的空间!
还有,我们从这一幅图中还应该得到这样的认识:
我 们看倒数第二张图,字段筛选的效率从左到右明显下降,所以在keyvalue的设计时用户可以考虑把一些重要的筛选信息左移到合适的位置,从而在不改变数 据量的情况下,提高查询性能。那么简单的说就是用户应当尽量把查询维度或信息存储在行健中,因为它筛选数据的效率最高。
得到上面的认识后,我们应该还要会有这样的觉悟:
HBase 的数据存储时会被有顺序的存储到一个特定的范围,因为我们存储的时候一般都是按顺序的,所以会一直存到同一个region上,由于一个region只能由 一个服务器管理,这样我们老是添加到同一个region上,会造成读写热点,从而使集群性能下降。
那么解决这个问题的办法还是有的,假如我们 有9台服务器,那么我们就会去当前时间,然后mod9,加到行键前缀,这样就会被平均的分到不同的region服务器上了,这样带来的好处是,因为相连的数据 都分布到不同的服务器上了,用户可以多线程并行的读取数据,这样查询的吞吐量会提高。关于版本的控制,我们要么就让多台服务器上的时间都同步,要么干脆就在put插入数据时,设置一个客户端的时间戳来代替。
(5).关于表的设计:
设计表的时候,有两种设计方式,一种是高表设计,一种是胖表设计。根据HBase的拆分规则,我们的高表设计更容易拆分(使用组合键),不过如果我们设计成胖表,而我们这个胖表里的数据需要经常修改,这样的设计是很合理的。以为HBase保证了行级原子性,如果设计成高表,反而就不合适了,因为不能保证跨行的原子性。
(6)写缓存:
每一个put的操作实际上是RPC的操作,它将客户端的数据传送到服务器然后返回,这只适合小数据量的操作,如果有个应用程序需要每秒存储上千行数据到HBase中,这样处理就不太合适了。HBase的API配备了一个客户端的写缓冲区,缓冲区负责收集put操作,然后调用RPC操作,一次性将put送往服务器。默认情况下,客户端缓冲区是禁止的。可以通过自动刷写设置为FALSE来激活缓冲区。
table.setAutoFlush(false);void flushCommits () throws IOException这个方法是强制 将数据写到服务器。用户还可以根据下面的方法来配置客户端写缓冲区的大小。 void setWritaeBufferSize(long writeBufferSize) throws IOException;默认大小是 2MB,这个也是适中的,一般用户插入的数据不大,不过如果你插入的数据大的话,可能要考虑增大这个值。从而允许客户端更高效地以一定数量的数据组成一组,然后通过一次RPC请求来执行。
在每一个客户端设置一个写缓冲区是一件麻烦的事,我们可以在Hbase-site.xml中给用户设置一个较大的预设值。
<property>
<name>hbase.client.write.buffer</name>
<value>20971520</value>
</property>
(7)压缩
hbase支持大量的算法,并且支持列族级别以上的压缩算法,除非有特殊原因,不然我们应该尽量使用压缩,压缩通常会带来较好的性能。通过一些测试,我们推荐使用SNAPPY这种算法来进行我们hbase的压缩。
HBase数据的读取流程:
1.Client访问zookeeper,获取元数据存储所在的regionserver
2.通过刚刚获取的地址访问对应的regionserver,拿到对应的表存储的regionserver
3.去表所在的regionserver进行数据的读取
4.查找对应的region,在region中寻找列族,先找到memstore,找不到去blockcache中寻找,再找不到就进行storefile的遍历
5.找到数据之后会先缓存到blockcache中,再将结果返回
blockcache逐渐满了之后,会采用LRU的淘汰策略。
补充:
(1)在HBase中,所有的存储文件都被划分成了若干个小存储块,这些存储块在get或scan操作时会加载到内存中,他们类似与RDBMS中的存储单元页,这个参数的默认大小是64k,HBase会顺序的读取一个数据块到内存缓存中,其读取相邻的数据时就可以在内存中读取而不需要从磁盘中再次读取,有效的减少了磁盘的I/O的次数。这个参数默认为TRUE,这意味着每次读取的块都会缓存到内存中。
但是:如果用户顺序读写某个特定的列族,这个时候,这个机制就会把其他我们不需要的列族的数据也加载到内存中,增加了我们的负担,那么1就需要将其关闭。void setBlockCacheEnable(boolean blockCacheEnable);
(2)禁止自动刷写
1.我们有大批数据要插入时,如果我们没有禁止,Put实例会被逐个的传送到region服务器,(一条一条的往磁盘中写,害怕不?)如果用户禁止了自动刷写的功能,put操作会在写缓冲区被填满时才会被送出。
2.使用扫描缓存,如果HBase被用作一个mapreduce作业的输入源,请最好将作为mapreduce作业输入扫描器实例的缓存用setCaching()方法设置为默认值1更大的数。使用默认值意味着map任务会在处理每条记录时都请求region服务器,不过,这个值是500的话,则一次可传送500条数据到客户端进行处理,当然了这数据也是根据具体情况而定。
(3)限定扫描范围
比如我们要处理大量行(特别是作为mapreduce的输入源),其中用到scan的时候我们有Scan.addFamily();的方法,这个时候我们如果只是需要到这几个列族中的几个列,那么我们一定要精确,因为过多的列会导致效率的损失。
(4)块缓存的用法
首先我们的块缓存是通过Scan.setCacheBlocks();启动的,那么被频繁访问的行,我们应该使用缓存块,但是MapReduce作业使用扫描大量的行,我们就不该使用这个了。
HBase 入门之数据刷写(Memstore Flush)详细说明
接触过 HBase 的同学应该对 HBase 写数据的过程比较熟悉(不熟悉也没关系)。HBase 写数据(比如 put、delete)的时候,都是写 WAL(假设 WAL 没有被关闭) ,然后将数据写到一个称为 MemStore 的内存结构里面的,如下图:
如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop
但是,MemStore 毕竟是内存里面的数据结构,写到这里面的数据最终还是需要持久化到磁盘的,生成 HFile。如下图:
如果想及时了解Spark、Hadoop或者Hbase相关的文章,欢迎关注微信公共帐号:iteblog_hadoop
理解 MemStore 的刷写对优化 MemStore 有很重要的意义,大部分人遇到的性能问题都是写操作被阻塞(Block)了,无法写入HBase。本文基于 HBase 2.0.2,并对 MemStore 的 Flush 进行说明,包括哪几种条件会触发 Memstore Flush 及目前常见的刷写策略(FlushPolicy
)。
什么时候触发 MemStore Flush
有很多情况会触发 MemStore 的 Flush 操作,所以我们最好需要了解每种情况在什么时候触发 Memstore Flush。总的来说,主要有以下几种情况会触发 Memstore Flush:
- Region 中所有 MemStore 占用的内存超过相关阈值
- 整个 RegionServer 的 MemStore 占用内存总和大于相关阈值
- WAL数量大于相关阈值
- 定期自动刷写
- 数据更新超过一定阈值
- 手动触发刷写
下面对这几种刷写进行简要说明。
Region 中所有 MemStore 占用的内存超过相关阈值
当一个 Region 中所有 MemStore 占用的内存(包括 OnHeap + OffHeap)大小超过刷写阈值的时候会触发一次刷写,这个阈值由 hbase.hregion.memstore.flush.size
参数控制,默认为128MB。我们每次调用 put、delete 等操作都会检查的这个条件的。
但是如果我们的数据增加得很快,达到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier
的大小,hbase.hregion.memstore.block.multiplier
默认值为4,也就是128*4=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞所有写入该 Store 的写请求!这时候如果你往对应的 Store 写数据,会出现 RegionTooBusyException
异常。
整个 RegionServer 的 MemStore 占用内存总和大于相关阈值
HBase 为 RegionServer 的 MemStore 分配了一定的写缓存,大小等于 hbase_heapsize(RegionServer 占用的堆内存大小)* hbase.regionserver.global.memstore.size
。hbase.regionserver.global.memstore.size
的默认值是 0.4,也就是说写缓存大概占用 RegionServer 整个 JVM 内存使用量的 40%。
如果整个 RegionServer 的 MemStore 占用内存总和大于 hbase.regionserver.global.memstore.size.lower.limit
* hbase.regionserver.global.memstore.size
* hbase_heapsize 的时候,将会触发 MemStore 的刷写。其中 hbase.regionserver.global.memstore.size.lower.limit
的默认值为 0.95。
举个例子,如果我们 HBase 堆内存总共是 32G,按照默认的比例,那么触发 RegionServer 级别的 Flush 是 RS 中所有的 MemStore 占用内存为:32 * 0.4 * 0.95 = 12.16G。
注意:0.99.0 之前 hbase.regionserver.global.memstore.size
是 hbase.regionserver.global.memstore.upperLimit
参数;hbase.regionserver.global.memstore.size.lower.limit
是 hbase.regionserver.global.memstore.lowerLimit
,参见 HBASE-5349
RegionServer 级别的 Flush 策略是每次找到 RS 中占用内存最大的 Region 对他进行刷写,这个操作是循环进行的,直到总体内存的占用低于全局 MemStore 刷写下
限(hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize)才会停止。
需要注意的是,如果达到了 RegionServer 级别的 Flush,那么当前 RegionServer 的所有写操作将会被阻塞,而且这个阻塞可能会持续到分钟级别。
WAL数量大于相关阈值
WAL(Write-ahead log,预写日志)用来解决宕机之后的操作恢复问题的。数据到达 Region 的时候是先写入 WAL,然后再被写到 Memstore 的。如果 WAL 的数量越来越大,这就意味着 MemStore 中未持久化到磁盘的数据越来越多。当 RS 挂掉的时候,恢复时间将会变成,所以有必要在 WAL 到达一定的数量时进行一次刷写操作。这个阈值(maxLogs
)的计算公式如下:
|
也就是说,如果设置了 hbase.regionserver.maxlogs
,那就是这个参数的值;否则是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)
。如果某个 RegionServer 的 WAL 数量大于 maxLogs
就会触发 MemStore 的刷写。
WAL 数量触发的刷写策略是,找到最旧的 un-archived WAL 文件,并找到这个 WAL 文件对应的 Regions, 然后对这些 Regions 进行刷写。
定期自动刷写
如果我们很久没有对 HBase 的数据进行更新,这时候就可以依赖定期刷写策略了。RegionServer 在启动的时候会启动一个线程 PeriodicMemStoreFlusher
每隔 hbase.server.thread.wakefrequency
时间去检查属于这个 RegionServer 的 Region 有没有超过一定时间都没有刷写,这个时间是由 hbase.regionserver.optionalcacheflushinterval
参数控制的,默认是 3600000,也就是1小时会进行一次刷写。如果设定为0,则意味着关闭定时自动刷写。
为了防止一次性有过多的 MemStore 刷写,定期自动刷写会有 0 ~ 5 分钟的延迟,具体参见 PeriodicMemStoreFlusher
类的实现。
数据更新超过一定阈值
如果 HBase 的某个 Region 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,比如超过 hbase.regionserver.flush.per.changes
参数配置,默认为30000000,那么也是会触发刷写的。
手动触发刷写
除了 HBase 内部一些条件触发的刷写之外,我们还可以通过执行相关命令或 API 来触发 MemStore 的刷写操作。比如调用可以调用 Admin
接口提供的方法:
|
分别对某张表、某个 Region 或者某个 RegionServer 进行刷写操作。也可以在 Shell 中通过执行 flush
命令:
|
需要注意的是,以上所有条件触发的刷写操作最后都会检查对应的 HStore 包含的 StoreFiles 文件超过 hbase.hstore.blockingStoreFiles
参数配置的个数,默认值是16。如果满足这个条件,那么当前刷写会被推迟到 hbase.hstore.blockingWaitTime
参数设置的时间后再刷写。在阻塞刷写的同时,HBase 还会请求 Split 或 Compaction 操作。
什么操作会触发 MemStore 刷写
我们常见的 put、delete、append、increment、调用 flush 命令、Region 分裂、Region Merge、bulkLoad HFiles 以及给表做快照操作都会对上面的相关条件做检查,以便判断要不要做刷写操作。
MemStore 刷写策略(FlushPolicy)
在 HBase 1.1 之前,MemStore 刷写是 Region 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的!这会造成一定的问题,比如小文件问题,具体参见 《为什么不建议在 HBase 中使用过多的列族》。针对这个问题,HBASE-10201/HBASE-3149引入列族级别的刷写。我们可以通过 hbase.regionserver.flush.policy
参数选择不同的刷写策略。
目前 HBase 2.0.2 的刷写策略全部都是实现 FlushPolicy
抽象类的。并且自带三种刷写策略:FlushAllLargeStoresPolicy
、FlushNonSloppyStoresFirstPolicy
以及 FlushAllStoresPolicy
。
FlushAllStoresPolicy
这种刷写策略实现最简单,直接返回当前 Region 对应的所有 MemStore。也就是每次刷写都是对 Region 里面所有的 MemStore 进行的,这个行为和 HBase 1.1 之前是一样的。
FlushAllLargeStoresPolicy
在 HBase 2.0 之前版本是 FlushLargeStoresPolicy
,后面被拆分成分 FlushAllLargeStoresPolicy
和FlushNonSloppyStoresFirstPolicy
,参见 HBASE-14920。
这种策略会先判断 Region 中每个 MemStore 的使用内存(OnHeap + OffHeap)是否大于某个阀值,大于这个阀值的 MemStore 将会被刷写。阀值的计算是由 hbase.hregion.percolumnfamilyflush.size.lower.bound
、hbase.hregion.percolumnfamilyflush.size.lower.bound.min
以及 hbase.hregion.memstore.flush.size
参数决定的。计算逻辑如下:
|
计算逻辑上面已经很清晰的描述了。hbase.hregion.percolumnfamilyflush.size.lower.bound.min
默认值为 16MB,而 hbase.hregion.percolumnfamilyflush.size.lower.bound
没有设置。
比如当前表有3个列族,其他用默认的值,那么 flushSizeLowerBound = max((long)128 / 3, 16) = 42
。
如果当前 Region 中没有 MemStore 的使用内存大于上面的阀值,FlushAllLargeStoresPolicy
策略就退化成 FlushAllStoresPolicy
策略了,也就是会对 Region 里面所有的 MemStore 进行 Flush。
FlushNonSloppyStoresFirstPolicy
HBase 2.0 引入了 in-memory compaction,参见 HBASE-13408。如果我们对相关列族 hbase.hregion.compacting.memstore.type
参数的值不是 NONE
,那么这个 MemStore 的 isSloppyMemStore
值就是 true,否则就是 false。
FlushNonSloppyStoresFirstPolicy
策略将 Region 中的 MemStore 按照 isSloppyMemStore
分到两个 HashSet 里面(sloppyStores
和 regularStores
)。然后
- 判断
regularStores
里面是否有 MemStore 内存占用大于相关阀值的 MemStore ,有的话就会对这些 MemStore 进行刷写,其他的不做处理,这个阀值计算和FlushAllLargeStoresPolicy
的阀值计算逻辑一致。 - 如果
regularStores
里面没有 MemStore 内存占用大于相关阀值的 MemStore,这时候就开始在sloppyStores
里面寻找是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore 进行刷写,其他的不做处理。 - 如果上面
sloppyStores
和regularStores
都没有满足条件的 MemStore 需要刷写,这时候就FlushNonSloppyStoresFirstPolicy
策略久退化成FlushAllStoresPolicy
策略了。
刷写的过程
MemStore 的刷写过程很复杂,很多操作都可能触发,但是这些条件触发的刷写最终都是调用 HRegion
类中的 internalFlushcache
方法。
|
从上面的实现可以看出,Flush 操作主要分以下几步做的
- prepareFlush 阶段:刷写的第一步是对 MemStore 做 snapshot,为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难,所以在刷写期间需要持有 updateLock 。持有了 updateLock 之后,这将阻塞客户端的写操作。所以只在创建 snapshot 期间持有 updateLock,而且 snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。对 MemStore 做 snapshot 是
internalPrepareFlushCache
里面进行的。 - flushCache 阶段:如果创建快照没问题,那么返回的
result.result
将为 null。这时候我们就可以进行下一步internalFlushCacheAndCommit
。其实internalFlushCacheAndCommit
里面包含两个步骤:flushCache
和commit
阶段。flushCache 阶段其实就是将prepareFlush
阶段创建好的快照写到临时文件里面,临时文件是存放在对应 Region 文件夹下面的.tmp
目录里面。 - commit 阶段:将
flushCache
阶段生产的临时文件移到(rename
)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 snapshot。
HBase的读写数据流程
写流程:
具体流程:
Client进行写操作的时候,会先查询Meta缓存中是否含有目标table的region信息以及Meta表位置信息,如果有就不再去访问zookeeper,而是直接进行下一步的操作。如果没有则会去访问zookeeper,获取hbase:meta表位于哪个Region Server。Meta表主要用于存储用户表和系统表的所在位置。在低版本的时,会有一个-ROOT-表,用于存储meta表的位置信息,这个操作主要是为了预防meta表过大而需要对meta表进行切分,切分之后就会造成有多个meta表,这就需要一个表准们存储meta表的位置信息;
获取到meta的位置信息以后,会去访问对应的Region Server,根据读请求的信息namespace:table/rowkey,查询处目标数据位于哪个Region Server中的哪个Region。并将该table的region信息以及meta表的位置信息缓存到客户端的meta cache,方便下次访问;
然后与得到的RegionServer通讯,将数据顺序写入(append)到WAL,此时并不进行同步操作,即并不将wal写到hdfs
将数据写入对应Region的memstore中,数据会在MemStore中进行排序;
同步wal,将wal写到hdfs,如果不能同步成功则会进行回滚操作。wal和数据写入到memstore是一个整体的事务,要么都成功要么都失败;
上面成功后,向客户端发送ack;
等达到MemStore的刷写时机后,将数据刷写到HFile中。
读流程:
具体步骤:
1)Client 先访问zookeeper,获取hbase:meta 表位于哪个Region Server。
2)访问对应的Region Server,获取hbase:meta 表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
3)与目标Region Server进行通讯;
4)分别在Block Cache(读缓存),MemStore和Store File(HFile)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)。
5) 将从文件中查询到的数据块(Block,HFile 数据存储单元,默认大小为64KB)缓存到Block Cache。
6)将合并后的最终结果返回给客户端。
需要注意的是,读数据的时候,block cache、memstore、storefile中的数据要一起读,读出来之后要做merge(合并),merge的过程中要比较所有读出的数据的时间戳,谁的时间戳大,就返回哪一条数据。此时,磁盘和内存是一起读的,磁盘中的数据读出来之后会放入block cache中,所以,读流程无论如何都会扫描磁盘,也就造成了HBase的读流程要慢于写流程。
从读写流程的两幅流程图可以看出,HMaster好像并没有参与整个的读写流程,其实Master可以完全不参与读写流程,因为读写数据所需的meta表位置信息是存储在zookeeper的,zookeeper担任了一部分Master与客户端的交互的功能,所以即使Master挂掉了,用户也可以在客户端进行读写。但是如果Master一直处于挂掉的状态,对于HBase集群来说是非常不健康的,比如集群中某个RegionServer出故障挂掉了,那么就无法及时将该RegionServer上的Region转移到其他健康的RegionServer上面。
HBase 数据读写流程
2016-10-18 杜亦舒
读数据
HBase的表是按行拆分为一个个 region 块儿,这些块儿被放置在各个 regionserver 中
假设现在想在用户表中获取 row key 为 row0001 的用户信息
要想取得这条数据,就需要先找到含有此条记录的 region
HBase 是如何定位到具体 regionserver 中的具体 region 的呢?
HBase 中有一个内置的 hbase:meta
表,其中记录了所有表的所有 region 的详细信息
例如 region 的 开始KEY、结束KEY、所在server的地址……
hbase:meta
表就像一个目录,通过他可以快速定位数据的实际位置
hbase:meta
表是存储在 ZooKeeper 中的,所以客户端就需要先访问 ZooKeeper,获取到 hbase:meta,从中查询出目标数据是在哪个 regionserver 中的哪个 region 中,然后到 region 中进行读取
我们可能会感觉这个寻址路径有点长,所以客户端会将查询过的位置信息保存缓存起来,方便以后快速读取
写数据
写操作会被分配到对应的 regionserver 进行处理,先回顾一下 regionserver 的结构
从客户端来看,写操作比较简单,写请求到达 regionserver 后,这些修改会先被写到 MemStore和 HLog 中,成功写入后便会通知客户端写入完成了
MemStore 是内存缓存,保存最近更新的数据
HLog 是日志文件,记录着所有的更新操作
对于系统来说,写操作还没完,系统会定期调用刷新缓存的方法,把MemStore中的内容写入文件,生成一个新的 StoreFile,然后把缓存清空,并在HLog中做一个标记,表明上面的内容已经写入文件
这样,数据就真正落地了,但写操作会引发一些后续问题,例如HLog日志文件越来越大了、StoreFile越来越多了、当前region越来越大了,所以,系统有还有更多的工作需要做
-
系统会定期清理HLog日志文件,把其中已经写入文件的记录删除
-
当 StoreFile 文件数量超过设定值时,会触发合并操作,合并成一个大文件,如果这个大文件超过了设定值,会再被分割开
-
当region的大小达到阈值时,会被切分开,生成一个新的region,HMaster会对其进行管理,分配到合适的 regionserver
-
region的变化后,系统还需要对
hbase:meta
表进行维护