传统的关系型,按行存储。
行结构是固定的,即使你不用,也必须空到那里,而不能没有。
此非关系型数据库,是按列来存储的。
不会造成空间浪费。
HBase的目标是管理超级大表-数十亿行 * 数百万列。模仿谷歌的BigTable。底层使用HDFS。
Hbase的表在放入数据前需要确定下来的东西,那就是列族。
人员基本信息,教育经历信息,工作经历信息,这三个类别其实就相当于三个列族。
人员基本信息里有姓名、电话、出生年月等,在Hbase中叫做列修饰符。
在Hbase中一个列族和一个列修饰符组合起来才叫一个列,使用冒号(:)分割,列族:列修饰符
在传统数据库中每一行的唯一标识符叫做主键,在Hbase中叫做row key(行键)。
数据在进入Hbase时都会被打上一个时间戳,这个时间戳可以作为版本号来使用。
在t1时间我存入一个人的基本信息在t2时间又更新了姓名,并不会去更新原来的那条数据,而是又插入了一条新数据且打上新的时间戳。
此时去查询获取的是新数据,仿佛是更新了,但其实只是默认返回了最新版本的数据而已。
一个行键、列族、列修饰符、数据和时间戳组合起来叫做一个单元格(Cell)。
一个行键、一到多列(包括数据)组合起来叫做一行(Row)。下图中所有1001的数据合起来相当于Hbase中的一行,1002的相当于另一行:
在Hbase中,只要确定了列族(具体列不用管),表就确定了。
HBase以表的形式存储数据。
存储时,数据按照Row key的 字典序 排序存储。设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
列族
hbase表中的每个列,都归属与某个列族。列族是表的schema的一部分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如courses:history , courses:math 都属于 courses 这个列族。
时间戳 不同版本的数据按照时间倒序排序,即最新的数据排在最前面。
hbase提供了两种数据版本回收方式。一是保存数据的最后n个版本,二是保存最近一段时间内的版本。
表在行的方向上分割为多个Hregion。
region按大小分割的,每个表一开始只有一个region,随着数据不断插入表,region到一个阀值的时候,Hregion就会等分会两个新的Hregion。
Hregion是Hbase中分布式存储和负载均衡的最小单元。最小单元就表示不同的Hregion可以分布在不同的HRegion server上。但一个Hregion是不会拆分到多个server上的。HRegion并不是存储的最小单元。
HRegion由一个或者多个Store组成,重点 每个store保存一个列族 重点,每个Strore又由一个memStore和0至多个StoreFile组成。StoreFile以HFile格式保存在HDFS上,里面有:datablocks段(很多datablock,里面存的是kv)fileinfo段 dataindex段(每个datablock的index) trailer段(存了fileinfo 的offset dataindex的offset )
Data Block 段–保存表中的数据,这部分可以被压缩
Trailer–这一段是定长的。保存了每一段的偏移量,读取一个HFile时,会首先 读取Trailer,Trailer保存了每个段的起始位置,DataBlock Index会被读取到内存中,当检索某个key时,不需要扫描整个HFile,而只需从内存中找到key所在的block,通过一次磁盘io将整个block读取到内存中,再找到需要的key。
HLog(WAL log)Write ahead log
所有写入、更新、删除操作都会把数据先写入HLog,再写入MemStore。大多数情况下,HLog并不会被读取。但是在HBase的RegionServer故障,MemStore中数据尚未flush到磁盘,这时就需要回放HLog进行数据恢复。此外,HBase主从集群数据复制也是通过将HLog日志发送给从集群,然后从集群再执行回放来完成。
一个region server 里面有很多个region 和一个Hlog
每个Region Server维护一个Hlog,而不是每个Region一个。不断追加单个文件。带来的麻烦是,如果一台region server下线,为了恢复其上的region,需要将region server上的log进行拆分,然后分发到其它region server上进行恢复。
系统架构:zookeeper集群 client master regionserver集群
Client
包含访问hbase的接口,client维护着一些cache来加快对hbase的访问,比如region的位置信息。
Zookeeper
1 保证任何时候,集群中只有一个master
2 -存-所有Region的寻址入口。
3 实时监控RegionServer的状态,将Regionserver的上线和下线信息实时通知给Master
4 -存-Hbase的schema,包括有哪些table,每个table有哪些列族
Master
1 为Region server分配region
2 负责region server的负载均衡
3 发现失效的region server并重新分配其上的region
4 处理schema更新请求
Region Server
1 Region server维护Master分配给它的region,处理对这些region的IO请求
2 Region server负责切分在运行过程中变得过大的region
client访问hbase上数据的过程并不需要master参与(寻址访问zookeeper和region server,数据读写访问regione server),master仅仅维护table和region的元数据信息,负载很低。
region定位
系统如何找到某个row key所在的region:
使用三层树状结构来保存region位置。
第一层是保存zookeeper里面的文件,它持有root region的位置。
第二层root region是.META.表的第一个region其中保存了.META.表其它region的位置。通过root region,我们就可以访问.META.表的数据。
.META.是第三层,它是一个特殊的表,保存了hbase中所有数据表的region 位置信息。
root region不会被split,保证了三次跳转,就能定位到任意region 。
META.表每行保存一个region的位置信息,有start key (end key存疑)等信息
为了加快访问,.META.表的全部region都保存在内存中。
假设,.META.表的一行在内存中大约占用1KB。并且每个region限制为128MB。
那么上面的三层结构可以保存的region数目为:
(128MB/1KB) * (128MB/1KB) = = 2(34)个region
client会将查询过的位置信息保存缓存起来,缓存不会主动失效,因此如果client上的缓存全部失效,则需要进行6次网络来回,才能定位到正确的region(其中三次用来发现缓存失效,另外三次用来获取位置信息
hbase使用MemStore和StoreFile存储对表的更新。
数据在更新时首先写入WAL 和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时,系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了。
当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用Log(WAL log)来恢复checkpoint之后的数据。
前面提到过StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更 新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并,将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行split,等分为两个StoreFile。
由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有索引,合并过程比较快。
client向region server提交写请求
region server找到目标region
region检查数据是否与schema一致
如果客户端没有指定版本,则获取当前系统时间作为数据版本
将更新写入WAL log
将更新写入Memstore
判断Memstore的是否需要flush为Store文件。
任何时刻,一个region只能分配给一个region server。master记录了当前有哪些可用的region server。以及当前哪些region分配给了哪些region server,哪些region还没有分配。当存在未分配的region,并且有一个region server上有可用空间时,master就给这个region server发送一个装载请求,把region分配给这个region server。region server得到请求后,就开始对此region提供服务。
master使用zookeeper来跟踪region server状态。当某个region server启动时,会首先在zookeeper上的server目录下建立代表自己的文件,并获得该文件的独占锁。由于master订阅了server 目录上的变更消息,当server目录下的文件出现新增或删除操作时,master可以得到来自zookeeper的实时通知。因此一旦region server上线,master能马上得到消息。
当region server下线时,它和zookeeper的会话断开,zookeeper而自动释放代表这台server的文件上的独占锁。而master不断轮询server目录下文件的锁状态。如果master发现某个regionserver丢失了它自己的独占锁,(或者master连续几次和region server通信都无法成功),master就是尝试去获取代表这个region server的读写锁,一旦获取成功,就可以确定:
region server和zookeeper之间的网络断开或region server挂了。
region server无法继续为它的region提供服务了,此时master会删除server目录下代表这台region server的文件,并将这台region server的region分配给其它还活着的。
如果网络短暂出现问题导致region server丢失了它的锁,那么region server重新连接到zookeeper之后,只要代表它的文件还在,它就会不断尝试获取这个文件上的锁,一旦获取到了,就可以继续提供服务。
master上线
1 从zookeeper上获取唯一一个代码master的锁,用来阻止其它master成为master。
2 扫描zookeeper上的server目录,获得当前可用的region server列表。
3 和2中的每个region server通信,获得当前已分配的region和region server的对应关系。
4 扫描.META.region的集合,计算得到当前还未分配的region,将他们放入待分配region列表。
master下线
由于master只维护表和region的元数据,而不参与表数据IO的过 程,master下线仅导致所有元数据的修改被冻结(无法创建删除表,无法修改表的schema,无法进行region的负载均衡,无法处理region 上下线,无法进行region的合并,唯一例外的是region的split可以正常进行,因为只有region server参与),表的数据读写还可以正常进行。因此master下线短时间内对整个hbase集群没有影响。从上线过程可以看到,master保存的 信息全是可以冗余信息(都可以从系统其它地方收集到或者计算出来),一般hbase集群中总是有一个master在提供服务,还有一个以上 的’master’在等待时机抢占它的位置。
尽管已经有许多数据存储和访问的策略和实现方法,但事实上大多数解决方案,特别是一些关系类型的,在构建时并没有考虑超大规模和分布式的特点。许多商家通过复制和分区的方法来扩充数据库使其突破单个节点的界限,但这些功能通常都是事后增加的,安装和维护都和复杂。同时,也会影响RDBMS的特定功能,例如联接、复杂的查询、触发器、视图和外键约束这些操作在大型的RDBMS上的代价相当高,甚至根本无法实现。HBase从另一个角度处理伸缩性问题。它通过线性方式从下到上增加节点来进行扩展。
面向列:面向列簇的存储和权限控制,列簇独立检索。
HBase的高并发和实时处理数据
Hadoop是一个高容错、高延时的分布式文件系统和高并发的批处理系统,不适用于提供实时计算;HBase是可以提供实时计算的分布式数据库,数据被保存在HDFS分布式文件系统上,由HDFS保证期高容错性,但是再生产环境中,HBase是如何基于hadoop提供实时性呢? HBase上的数据是以StoreFile(HFile)二进制流的形式存储在HDFS上block块儿中;但是HDFS并不知道的hbase存的是什么,它只把存储文件是为二进制文件,也就是说,hbase的存储数据对于HDFS文件系统是透明的。
HBase HRegion servers集群中的所有的region的数据在服务器启动时都是被打开的,并且在内冲初始化一些memstore,相应的这就在一定程度上加快系统响 应;而Hadoop中的block中的数据文件默认是关闭的,只有在需要的时候才打开,处理完数据后就关闭,这在一定程度上就增加了响应时间。
从根本上说,HBase能提供实时计算服务主要原因是由其架构和底层的数据结构决定的,即由LSM-Tree + HTable(region分区) + Cache决定——客户端可以直接定位到要查数据所在的HRegion server服务器,然后直接在服务器的一个region上查找要匹配的数据,并且这些数据部分是经过cache缓存的。
如果在BlockCache中能查到则返回结果,否则就读去相应的StoreFile文件中读取一block的数据,如果还没有读到要查的 数据,就将该数据block放到HRegion Server的blockcache中,然后接着读下一block块儿的数据,一直到这样循环的block数据直到找到要请求的数据并返回结果;当然blockcache会在其大小大于一的阀值后启动基于LRU算法的淘汰机制。
client访问hbase上的数据时不需要master的参与,因为数据寻址访问zookeeper和region server,而数据读写访问region server。master仅仅维护(更新)table和region的元数据信息,而table的元数据信息保存在zookeeper上,因此master负载很低。
Client会通过内部缓存的相关的-ROOT-中的信息和.META.中的信息直接连接与请求数据匹配的HRegion server
client会将查询的位置信息保存缓存起来,缓存不会主动失效,因此如果client上的缓存全部失效,则需要进行6次网络来回,才能定位到正确的region,其中三次用来发现缓存失效,另外三次用来获取位置信息。
LogFlusher
前面提到,数据以KeyValue形式到达HRegionServer,将写入WAL之后,写入一个SequenceFile。看过去没问题,但是因为数据流在写入文件系统时,经常会缓存以提高性能。这样,有些本以为在日志文件中的数据实际在内存中。
这里,我们提供了一个LogFlusher的类。它调用 HLog.optionalSync(),后者根据 hbase.regionserver.optionallogflushinterval (默认是10秒),定期调用Hlog.sync()。另外,HLog.doWrite()也会根据
hbase.regionserver.flushlogentries (默认100秒)定期调用Hlog.sync()。Sync() 本身调用HLog.Writer.sync(),它由SequenceFileLogWriter实现。
LogRoller
Log的大小通过$HBASE_HOME/conf/hbase-site.xml 的 hbase.regionserver.logroll.period 限制,默认是一个小时。所以每60分钟,会打开一个新的log文件。久而久之,会有一大堆的文件需要维护。首先,LogRoller调用 HLog.rollWriter(),定时滚动日志,之后,利用HLog.cleanOldLogs()可以清除旧的日志。它首先取得存储文件中的最大的 sequence number,之后检查是否存在一个log所有的条目的“sequence number”均低于这个值,如果存在,将删除这个log。 每个region server维护一个HLog,而不是每一个region一个,这样不同region(来自不同的table)的日志会混在一起,这样做的目的是不断追加 单个文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高table的写性能。带来麻烦的时,如果一个region server下线,为了恢复其上的region,需要将region server上的log进行拆分,然后分发到其他region server上进行恢复。
当用户需要进行Table和Region的管理工作时,就需要和HMaster进行通信。HBase中可以启动多个HMaster,通过Zookeeper的Master Eletion机制保证总有一个Master运行。
当StoreFile文件数量增长到一定阀值,就会触发Compact合并操作,并将多个StoreFile合并成一个StoreFile,合并过程中会进行版本合并和数据删除,因此可以看出HBase其实只有增加数据,所有的更新和删除操作都是在后续的compact过程中进行的,这使得用户的读写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。
当StoreFile文件数量增长到一定阀值,就会触发Compact合并操作,并将多个StoreFile合并成一个StoreFile,当这个StoreFile大小超过一定阀值后,会触发Split操作,同时把当前Region Split成2个Region,这是旧的Region会下线,新Split出的2个Region会被HMaster分配到相应的HregionServer上,使得原先1个Region的压力得以分散到2个Region上。
如下图四个Storefile文件(从memstore文件经过flush而得到,默认64M的storefile文件)经过Compact合并成一个大的256M storefile文件,当设定的Region阀值为128M时,就会Split为两个128M的Storefile文件,然后HMaster再把这两个storefile文件分配到不停地Regionserver上。
Data Block是hbase io的基本单元,为了提高效率,HRegionServer中有基于LRU的block cache机制。
开始是两个固定长度的数值,分别表示key的长度和alue的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。
每个HRegionServer中都有一个HLog对象,HLog是一个实现Write Ahead Log的类,在每一次用户操作写入MemStore的同时,也会写一份数据到HLog文件中,HLog文件定期(当文件已持久化到StoreFile中的数据)会滚出新的,并且删除旧的文件。当HRegionServer意外终止 后,HMaster会通过Zookeeper感知到,HMaster首先会处理遗留的Hlog文件,将其中不同Region的Log数据进行拆分,分别放到相应Region的目录下,然后再将失效的Region重新分配,领取到这些Region的Regionserver在Load Region的过程中,会发现历史HLog需要处理,因此Replay HLog中的数据到MemStore中,然后flush到StoreFiles,完成数据恢复。
WAL在处理插入和删除过程中,用来记录操作内容的日志,只有日志写入成功,才会通知客户端操作成功。
HBase的高可用
HDFS机架识别策略:当数据文件损坏时,会找相同机架上备份的数据文件,如果相同机架上的数据文件也损坏会找不同机架备份数据文件。
HBase的Region快速恢复:当节点损坏时,节点上的丢失region,会在其他节点上均匀快速恢复。
Master节点的HA机制:Master为一主多备。当Master主节点宕机后,剩下的备节点通过选举,产生主节点。
对于随机读密集型工作负载,高效利用缓存和更好地索引会给HBase系统带来更高的性能。
(2)顺序读密集型
对于顺序读密集型工作负载,读缓存不会带来太多好处;除非顺序读的规模很小并且限定在一个特定的行键范围内,否则很可能使用缓存会比不使用缓存需要更频繁地访问硬盘。
(3)写密集型
写密集型工作负载的优化方法需要有别于读密集型负载。缓存不再起到重要作用。写操作总是进入MemStore,然后被刷写生成新的Hfile,以后再被合并。
(4)混合型
对于完全混合型工作负载,优化方法会变得复杂些。优化时,需要混合调整多个参数来得到一个最优的组合。
其它角度来优化HBase性能
(5)Region预先加载优化
LSM树(Log-Structured Merge Tree)存储引擎和B树存储引擎一样,同样支持增、删、读、改、顺序扫描操作。而且通过批量存储技术规避磁盘随机写入问题。当然凡事有利有弊,LSM树和B+树相比,LSM树牺牲了部分读性能,用来大幅提高写性能。
通过以上的分析,应该知道LSM树的由来了,LSM树的设计思想非常朴素:将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘,不过读取的时候稍微麻烦,需要合并磁盘中历史数据和内存中最近修改操作,所以写入性能大大提升,读取时可能需要先看是否命中内存,否则需要访问较多的磁盘文件。极端的说,基于LSM树实现的HBase的写性能比Mysql高了一个数量级,读性能低了一个数量级。
LSM树原理把一棵大树拆分成N棵小树,它首先写入内存中,随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树定期可以做merge操作,合并成一棵大树,以优化读性能。
因为小树先写到内存中,为了防止内存数据丢失,写内存的同时需要暂时持久化到磁盘,对应了HBase的MemStore和HLog
MemStore上的树达到一定大小之后,需要flush到HRegion磁盘中(一般是Hadoop DataNode),这样MemStore就变成了DataNode上的磁盘文件StoreFile,定期HRegionServer对DataNode的数据做merge操作,彻底删除无效空间,多棵小树在这个时机合并成大树,来增强读性能。
关于LSM Tree,对于最简单的二层LSM Tree而言,内存中的数据和磁盘你中的数据merge操作,如下图
lsm tree,理论上,可以是内存中树的一部分和磁盘中第一层树做merge,对于磁盘中的树直接做update操作有可能会破坏物理block的连续性,但是实际应用中,一般lsm有多层,当磁盘中的小树合并成一个大树的时候,可以重新排好顺序,使得block连续,优化读性能。
hbase在实现中,是把整个内存在一定阈值后,flush到disk中,形成一个file,这个file的存储也就是一个小的B+树,因为hbase一般是部署在hdfs上,hdfs不支持对文件的update操作,所以hbase这么整体内存flush,而不是和磁盘中的小树merge update,这个设计也就能讲通了。内存flush到磁盘上的小树,定期也会合并成一个大树。整体上hbase就是用了lsm tree的思路。