1 HBase系统架构
Client职责
1、HBase有两张特殊表:
- .META.:记录用户所有表拆分出来的Region映射信息,.META可以有多个Region
- -ROOT-:记录了.META.表的Region信息,-ROOT-只有一个Region,无论如何都不会分裂split
2、Client访问用户数据前需要首先访问Zookeeper,找到-ROOT-表的Region所在的服务器位置,然后访问-ROOT-表,接着访问META表,最后才能找到用户数据的服务器位置去访问,中间需要多次网络操作,不过client端会做cache缓存
Zookeeper职责
- Zookeeper为HBase提供Failover机制,选举Master,避免单点Master故障问题
- 存储所有Region的寻址入口:-ROOT-表的位置信息
- 实时监控RegionServer的状态,将RegionServer的上线和下线信息实时通知给Master
- 存储HBase的Schema,包括有哪些Table,每个Table有哪些Column Family
HBase的元数据有两种:
- HBase中的表的相关信息,主要指表名、列簇的定义等,Master来管理
- 所有用户表的region的位置信息,这种数据也称之为元数据;存储在meta表,meta表由多个region组成,这些region也会分散到各个RegionServer去存储
Master职责
- 为RegionServer分配Region
- 负责RegionServer的负载均衡
- 发现失效的RegionServer并重新分配其上的Region
- HDFS上的垃圾文件(HBase)回收
- 处理Schema更新请示(表的创建、删除、修改、列簇的增加等等)
- 这些schema的数据都是存储在Zookeeper,但是Master负责更新
- 如果涉及表的创建、修改、删除等操作,Master宕机了就没法做,但是数据的插入和查询还是可以继续
RegionServer职责
- RegionServer维护Master分配给它的Region,处理对这些Region的IO请求
- 负责和底层的文件系统HDFS的交互,存储数据到HDFS;每个RegionServer内部有一个客户端(DataNode的代理),负责把数据写入HDFS
- 负责Store的HFile的合并Compact工作 + Split工作
- RegionSever负责Split在运行过程中变得过大的Region,负责Compact操作
总结
- Client访问HBase上数据 的过程中并不需要Master参与(寻址访问Zookeeper和RegionServer,数据读写访问RegionServer),Master仅仅维护着Table和Region的元数据 信息,负载很低
- META存的是所有的Region位置信息,那么RegionServer当中Regoin在进行分裂之后的新产生的Region,是由Master来决定发到哪个RegionServer,这就由Master来决定发到哪个RegionServer;这意味着,只有Master知道new Region的位置信息,由Master来管理.META.这个表当中的数据的CRUD
如果master宕机了,那些事情不能做:
1、创建,修改,删除,表
2、负责均衡不能做
3、split不能做了。
那些操作能继续做呢?
1、读数据
2、写数据
2 物理存储
2.1 整体物理结构
要点知识:
- Table中的所有行都按照Rowkey的字典序排列,hbase的一个rowkey就对应一行数据
- Table在行的方向上分割为多个HRegion
- HRegion按大小分割的(默认10G),每个表一开始只有一个HRegion,随着数据不断插入表,HRegion不断增大,当增大到一个阀值的时候,HRegion就会等分为两个新的HRegion。当表中的行不断增多,就会有起来越多的HRegion
- HRegion是HBase中分布式存储和负载均衡的最小单元。最小单元就表示不同的HRegion可以分布在不同的HRegionServer上,但一个HRegion是不会拆分到多个server上
- HRegion虽然是负载均衡的最小单元,但并不是物理存储的最小单元。事实上,HRegion由一个或者多个Store组成,每个Store保存一个Column Family。每个Store又由一个MemStore和0至多个StoreFile组成
2.2 StoreFile和HFile结构
StoreFile 以 HFile 格式保存在 HDFS 上,请看下图 HFile 的数据组织格式:
首先 HFile 文件是不定长的,长度固定的只有其中的两块:Trailer 和 FileInfo。
正如图中所示:
Trailer中有指针指向其他数据块的起始点。
FileInfo中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY,COMPARATOR, MAX_SEQ_ID_KEY等。
- Data Block 段–保存表中的数据,这部分可以被压缩。
- Meta Block 段 (可选的)–保存用户自定义的key-value对,可以被压缩。
- File Info 段–HFile的元信息,不被压缩,用户也可以在这一部分添加自己的元信息。
- Data Block Index 段–Data Block的索引。每条索引的key是被索引的block的第一条记录的key。
- Meta Block Index段 (可选的)–Meta Block的索引。
- Trailer段–这一段是定长的。保存了每一段的偏移量,读取一个HFile时,会首先读取Trailer,Trailer保存了每个段的起始位置(段的Magic Number用来做安全check),然后,DataBlock Index会被读取到内存中,这样,当检索某个key时,不需要扫描整个HFile,而只需从内存中找到key所在的block,通过一次磁盘IO将整个block读取到内存中,再找到需要的key。DataBlock Index采用LRU机制淘汰
HFile的Data Block,Meta Block通常采用压缩方式存储,压缩之后可以大大减少网络IO和磁盘IO,随之而来的开销是需要花费cpu进行压缩和解压缩;目标HFile的压缩支持两种方式:Gzip,LZO
Data Index和Meta Index块记录了每个Data块和Meta块的起始点。
Data Block是HBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRU的Block Cache机制。每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询。 每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成,Magic内容就是一些随机数字,目的是防止数据损坏。
HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构
rowkey, cf, qualifier, timestamp, value
这四个字段的长度 + Cell的类型(Put/Delete)
开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示TimeStamp和KeyType(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。
2.3 MemStore和StoreFile
一个HRegion由多个Store组成,每个Store包含一个列簇的所有数据。
Store包括位于内存的一个MemStore和位于硬盘的多个StoreFile组成
写操作先写入MemStore,当MemStore中的数据量达到某个阈值,HRegionServer启动flushcache进程进入StoreFile,每次写入形成单独一个HFile。
当总StoreFile大小超过一定阈值后,会把当前的Region分割成两个,并由HMaster分配给相应的RegionServer,实现负载均衡。
Client检索数据时,先在MemStore找,找不到再找StoreFile。
2.4 HLog & WAL
WAL意为Write Ahead Log,类似MySQL中的binlog,用来做灾难恢复之用,HLog记录数据的所有变更,一旦数据修改,就可以从Log中进行恢复。
HBase采用类LSM的架构体系,数据写入并没有直接写入数据 文件 ,而是会先写入缓存(MemStore),在满足一定条件下缓存数据再会异步flush到硬盘。为了防止数据写入缓存之后不会因为RegionServer进程发生异常导致数据丢失,在写入缓存之前会首先将数据 顺序写入HLog中。如果不幸一旦发生RegionServer宕机或者其它异常,这种设计可以从HLog中进行日志回话进行数据补救,保证数据不丢失。HBase故障恢复的最大看点就在于如何通过HLog回话补救丢失的数据。
WAL(Write-Ahead Logging)是一种高效的日志算法,几乎是所有非内存数据库提升写性能的不二法门,基本原理是在数据写入之前首先顺序写入日志,然后再写入缓存,等到缓存写满之后统一落盘。之所以能够提升写性能,是因为WAL将一次随机写转化为了一次顺序写加一次内存写。提升写性能的同时,WAL可以保证数据的可靠性,即在任何情况下数据不丢失。假如一次写入完成之后发生了宕机,即使所有缓存中的数据丢失,也可以通过恢复日志还原出丢失的数据。
每个Region Server维护一个HLog,而不是每个Region一个。这样不同region(来自不同table)的日志会混在一起,这样做的目的是不断追加单个文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高对table的写性能。带来的麻烦是,如果一台region server下线,为了恢复其上的Region,需要将RegionServer上的log进行拆分,然后分发到其它RegionServer上进行恢复。
- HLog Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequencenumber的起始值为0,或者是最近一次存入文件系统中sequence number。
- HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue
3 寻址机制
3.1 老的Region寻址机制
在HBase-0.96版本以前,HBase有两个特殊的表,分别是-ROOT-表和.META.表,其中-ROOT-的位置存储在ZooKeeper中,-ROOT-本身存储了.META. Table的RegionInfo信息,并且-ROOT-不会分裂,只有一个Region。而.META.表可以被切分成多个Region。读取的流程如下图所示:
- 第1步:Client请求ZooKeeper获得-ROOT-所在的RegionServer地址
- 第2步:Client请求-ROOT-所在的RS地址,获取.META.表的地址,Client会将-ROOT-的相关信息cache下来,以便下一次快速访问
- 第3步:Client请求.META.表的RegionServer地址,获取访问数据所在RegionServer的地址,Client会将.META.的相关信息cache下来,以便下一次快速访问
- 第4步:Client请求访问数据所在RegionServer的地址,获取对应的数据
从上面的路径我们可以看出,用户需要3次请求才能直到用户Table真正的位置,这在一定程序带来了性能的下降。在0.96之前使用3层设计的主要原因是考虑到元数据可能需要很大。但是真正集群运行,元数据的大小其实很容易计算出来。在BigTable的论文中,每行METADATA数据存储大小为1KB左右,如果按照一个Region为128M的计算,3层设计可以支持的Region个数为2^34个,采用2层设计可以支持2^17(131072)。那么2层设计的情况下一个集群可以存储4P的数据。这仅仅是一个Region只有128M的情况下。如果是10G呢? 因此,通过计算,其实2层设计就可以满足集群的需求。因此在0.96版本以后就去掉了-ROOT-表了。
3.2 新的Region寻址方式
如上面的计算,2层结构其实完全能满足业务的需求,因此0.96版本以后将-ROOT-表去掉了。如下图所示:
- 第1步:Client请求ZooKeeper获取.META.所在的RegionServer的地址。
- 第2步:Client请求.META.所在的RegionServer获取访问数据所在的RegionServer地址,Client会将.META.的相关信息cache下来,以便下一次快速访问。
- 第3步:Client请求数据所在的RegionServer,获取所需要的数据。
这里还有一个问题需要说明,那就是Client会缓存.META.的数据,用来加快访问,既然有缓存,那它什么时候更新?如果.META.更新了,比如Region1不在RerverServer2上了,被转移到了RerverServer3上。Client的缓存没有更新会有什么情况?
其实,Client的元数据缓存不更新,当.META.的数据发生更新。如上面的例子,由于Region1的位置发生了变化,Client再次根据缓存去访问的时候,会出现错误,当出现异常达到重试次数后就会去.META.所在的RegionServer获取最新的数据,如果.META.所在的RegionServer也变了,Client就会去ZooKeeper上获取.META.所在的RegionServer的最新地址。
4 读写过程
4.1 读请求过程
详细步骤:
- 客户端通过Zookeeper以及-ROOT表和.META.表找到目标数据所在的RegionServer(就是数据所在的Region的主机地址)
- 联系RegionServer查询目标数据
- RegionServer定位到目标数据所在的Region,发出查询请求
- Region先在MemStore中查找,命中则返回
- 如果在MemStore中找不到,则在StoreFile中扫描
- 先从MemStore找数据 ,如果没有找到
- 从blockcache找数据,如果也没有找到(布隆过滤器)
- 从HFile当中,HFile也是一种精妙设计的结果,扫描起来也不会特别慢
HBase的单个key的扫描速度和Table的数据规模没有关系
4.2 写请求过程
- Client先根据RowKey找到对应的Region所在的RegionServer
- Client向RegionServer提交写请求
- RegionServer找到目标Region
- Region检查数据 是否与Schema一致
- 如果客户端没有指定版本,则获取当前系统时间作为数据版本
- 将更新写入WAL Log
- 将更新写入MemStore
- 判断MemStore是否需要Flush为StoreFile文件
写入数据的前提:需要保证该张表是按照RowKey有序的
Hbase在做数据插入操作时,首先要找到RowKey所对应的的Region,怎么找到的?其实这个简单,因为.META.表存储了每张表每个Region的起始RowKey了。
4.3 Region的Split和Compact
数据在更新时首先写入HLog(WAL Log),再写入内存(MemStore)中,MemStore(ConcurrentSkipListMap,优点就是增删改查key-value效率都很高)中的数据是排序的当MemStore累计到一定阈值(默认是128M,局部控制)时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。与此同时,系统会在ZooKeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了。当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用HLog(WAL Log)来恢复checkpoint之后的数据。
MemStore执行flush操作的触发条件:
- 全局内存控制:当所有memstore占整个heap的最大比例的时候,会触发刷盘的操作。这个参数是hbase.regionserver.global.memstore.upperLimit,默认为整个heap内存的40%。这个全局的参数是控制内存整体的使用情况,但这并不意味着全局内存触发的刷盘操作会将所有的MemStore都进行输盘,而是通过另外一个参数hbase.regionserver.global.memstore.lowerLimit来控制,默认是整个heap内存的35%。当flush到所有memstore占整个heap内存的比率为35%的时候,就停止刷盘。这么做主要是为了减少刷盘对业务带来的影响,实现平滑系统负载的目的。
- 局部内存控制:当MemStore的大小达到hbase.hregion.memstore.flush.size大小的时候会触发刷盘,默认128M大小
- HLog的数量:前面说到HLog为了保证HBase数据的一致性,那么如果HLog太多的话,会导致故障恢复的时间太长,因此HBase会对HLog的最大个数做限制。当达到HLog的最大个数的时候,会强制刷盘。这个参数是hase.regionserver.max.logs,默认是32个
- 手动操作:可以通过HBase Shell或者Java API手工触发flush的操作
HBase的三种默认的Split策略:
- ConstantSizeRegionSplitPolicy 常数数量
- IncreasingToUpperBoundRegionSplitPolicy 递增上限
- SteppingSplitPolicy 步增上线
StoreFile是只读的,一旦创建后就不可以再修改。因此HBase的更新/修改其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(minor_compact, major_compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对StoreFile进行split,等分为两个StoreFile。由于对表的更新是不断追加的,compact时,需要访问Store中全部的StoreFile和MemStore,将它们按RowKey进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,合并的过程还是比较快。
Minor_Compact 和 Major_Compact 的区别:
- Minor操作只用来做部分文件的合并操作以及包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作。
- Major操作是对Region下的HStore下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件。
Client写入 -> 存入MemStore,一直到MemStore写满 -> Flush成一个StoreFile,直至增长到一定阈值 -> 触发Compact合并操作 -> 多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除 -> 当StoreFile Copact后,逐步形成越来越大的StoreFile -> 单个StoreFile大小超过一定阈值后,触发Split操作,把当前Region Split成2个Region,Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer上,使原先1个Region的压力得以分流到2个Region上。由此过程可知,HBase只是增加数据,所有的更新和删除操作,都晨Compact阶段做的,所以,用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能。
写入数据的过程补充:
工作机制:每个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到StoreFiles,完成数据恢复