HBase的取舍
放弃对类似RDBMS复杂查询(核心是join)的支持,采用简单的API进行简单的CRUD,再加上一个扫描函数实现全表扫描。
再次明确:HBase不支持表关联。为了实现类似关联操作,HBase可接受采取反范式设计,即冗余存储
表,行,列族,列,单元格
行键 | 列族1 | 列族2 | 列族n | |||
row_key_1 | 列1 | 列2 | 列n1 | |||
值1,版本1 值1,版本2 值1,版本3 | ||||||
row_key_2 | 列1' | 列2' | 列n2 | |||
值1',版本1 |
从上图中可以看出。逻辑上HBase表有如下特点:
1、HBase表由两个要素组成:行键和列族
2、行键是唯一的,行键按照字典序排序存储
3、列族由不定个数的列组成。即使是同一个列族,不同行的数据列也是允许不一致的。例如“row_key_1”和“row_key_2”两行数据的列族“列族1”可以包含完全不同的列,可以有不同数目的列,也可以有不用的列名。
4、某个值可以在HBase里面存储多个版本,最新的版本在最前面。
总结上述几点,唯一确定一个值的要素有5个:表名,行键,列族,列名,版本
命名空间
用于将表分组,便于管理。一般RDBMS支持分库(即同一台服务器上,有多个数据库),在HBase中可以用命名空间满足这个需求。
两个特殊的命名空间:
Hbase:系统表空间
default:没有显式指定命名空间的表所属空间,默认命名空间
列族
正如上文提到的,同一个列族里面不同行数据的列也是不一致的。列不用在创表时就指定,但是列族必须在创表时就定义好。如果需要中途修改列族(包括增、删、改)必须将表先下线。
NULL值
NULL在HBase中将被忽略,不会占用任何存储空间
值版本
用户为每个值单独指定最大存储版本数。当新值插入时,若超过了最大存储版本数就会将最旧的值抹掉。默认最大存储版本数是1。
数据操作的原子性
行数据的读写(不论写多少列)是原子的,HBase不支持跨行、跨表事务。
HFile
HFile是HBase在文件系统上存储的实际数据文件(独立的文件),是存储的最小单位。
HFile里面完成地存储一个列族的全部列。
这句话这样理解:
1、同一个列族的数据不会跨HFile存储。同一行,同一个列族的所有数据必定存储在一个HFile里面。
2、一个HFile里面只会存储某一个特定表的某一个特定列族。一个HFile不会同时存储两个及以上列族的数据。
准确完整的表述应该是:
HFile只会存储特定某个表中的某些行的特定某个列族
HFile的读写
HFile是按照行键有序存储的。HFile文件内部由连续的块组成,每个块默认大小是64KB(可配置),块的索引信息(行键起始信息)存储在文件尾部。
读取数据时,先将HFile的块索引信息加载到内存中,由于行键有序,可以现在内存中对行键进行二分查找确定要查询的行键所在的块,然后再在块中查找。
WAL
数据在从内存刷写(刷先前,内存数据会整理成LSM树结构,然后按照LSM书结构刷写)到HFile的过程中,新数据仍然可以写入WAL,不必为了刷写阻塞阻塞I/O。
HFile数据一旦写入,就不能更改。所以所有的删除操作和修改操作实际上是在通过追加记录的方式实现的。
问:前文有讲,HFile是按照行键有序存储的,那么如果发生了修改操作,修改操作是通过追加记录的方式实现的。那么有序存储与随机修改的追加记录如何做到统一?
答:
客户端在读取数据时,可读域(即数据库的即时状态)=memstore中尚未存盘(写入HFile)的数据+HFile中的数据。读数据用不着WAL。(当然,如果BlockCache开启的话,实际上是先读取BlockCache,未命中才读取memstore+HFile)
WAL用于服务器崩溃后,凡是在WAL中没有被废弃的数据都将重新加入内存,从而让整个系统错错误中恢复。可以将WAL看作memstore写入方式的保险。
WAL默认是开启的,而且是同步写入(即先写WAL,后写内存,由先后顺序),java api支持将其关闭。关闭WAL后,写入速度会变快,但是会有风险:如果意外宕机,那些仅写入memstore而没来得及刷写的数据将会丢失。
java api也支持异步写如WAL,即先写内存,然后会按照一定的时间间隔将数据从内存写入WAL(即WAL同步内存数据)。这是同步写与完全关闭WAL的折中方案。异步写方式会一定程度上加快写速度,但是如果WAL与memstore同步过程中出现问题,数据还是会部分丢失。
由于(默认情况下)数据需要先写入WAL,然后再写入memstore,最后写入HFile,memstore只是一个保证数据顺序写入磁盘的机构,所以单纯增加memstore并不能加快写入速度!
HFile合并
前面说过,对HBase中数据的增、删、改落实到HDFS其实都是新增操作。对HBase操作产生的数据不断刷写产生HFile,随着HFile不断产生,HBase内部有一个合并机制将多个小HFile合并成一个较大的文件。合并有两种:
minor合并:多路归并,将小HFile快速合并成较大的HFile。
major合并:压缩合并,扫描HFile里面的键值对,对诸如失效、已删除数据进行过滤,只将有效数据顺序写入文件系统。
Region
region是HBase中扩展个负载均衡的基本单元。是基于有序行键对HBase进行水平分割(即分区)。
表的分片存储结构如下图:
RegionServer内部结构如:
region由多个store组成,例如每行数据包含n个列族,那么该表的每个region里面都包含n个store。这n个store与n个列族一一对应。每个store又由一个memstore和一组HFile组成,数据不断从WAL进入memstore,然后刷写到HFile中。这里需要注意,每个store在不断刷写的过程中会不断生成HFile,所以HFile更像碎片化的存储文件。
store结果如下图:
HBase的region动态拆分与合并是自动的。可以在配置文件里面指定每个region的最大存储size。在插入新数据前,系统会检查当前region的大小,如果超过了限制就会取这个region的中间键进行分裂,将其拆分成两个大致等size的两个region。相反,如果系统检测到region太小也会自动根据有序行键原则进行合并以减少region的数量(region的合并一般是离线进行的,拆分是在线的)。起始时,region的数量是1。
每种底层文件系统备份原则不同,region也会有多副本,但是每个region只有一个副本生效,即:
每个region只能由唯一的一台region服务器加载(注意是加载,不是存储)
一个region服务器可以同时加载多个不同的region(同表或不同表)。
问:既然“每个region只能由一台region服务器加载”。而region是表的分区,意味着region存储的是完整的一行一行数据,否则就是混合分片,而不是水平分片。(相关内容见《HBase权威指南 中文版》Lar George著,2013年10月版,1.4.3节)
所以,问题是:HBase里的一行数据可能分布在不同的服务器上吗?(相关内容见《HBase不睡觉书》杨曦著,2017版,5.1节,·Row)
答:一般情况下一行数据会被完整地存储在一个region里,加载到一台region server中。除非一行数据非常大(真的要非常大),HBase也会将这个region按照列族做垂直切分,存储到不同服务器上。