深入HBase——Bigtable
引入
在引入篇,我们提到Bigtable通过以下三个方案,去解决支撑业务海量数据的随机读写的问题:
- 将整个系统的存储层,搭建在GFS上。然后通过单Master调度多Tablets的方式,使得整个集群非常容易伸缩和维护。
- 通过MemTable+SSTable这样一个底层文件格式,解决高速随机读写数据的问题。
- 通过Chubby这个高可用的分布式锁服务解决一致性的挑战。
我们从以下几个问题出发,去深入了解Bigtable的设计与实现思路:
- Bigtable是如何进行数据分区,使得整个集群灵活可扩展的?
- Bigtable是如何设计,解决单点故障以及单点性能瓶颈的?
- Bigtable的整体架构和组件由哪些东西组成?
- Bigtable的底层存储SSTable究竟是怎么回事?
Bigtable是如何进行数据分区,使得整个集群灵活可扩展的?
在解答这个问题之前,我们先来弄清楚Bigtable基本的数据模型。
基本数据模型
基于对MySQL之类关系型数据库的了解,我们知道,一旦开始分库分表了,就很难使用关系数据库的一系列的特性了。比如SQL里面的Join功能,或者是跨行的事务。因为这些功能在分库分表的场景下,都要涉及到多台服务器,不是说做不到,但是问题一下子就复杂了。
Bigtable或许正是基于这一点,在设计的时候,一开始就不考虑事务、Join等高级的功能,而是把核心放在了“可伸缩性”上。
因此,Bigtable自己的数据模型也特别简单,是一个很宽的稀疏表。
每一张Bigtable的表都特别简单,每一行就是一条数据:
-
一条数据里面,有一个行键(Row Key),也就是这条数据的主键,Bigtable提供了通过这个行键随机读写这条记录的接口。因为总是通过行键来读写数据,所以很多人也把这样的数据库叫做KV数据库。
-
每一行里的数据呢,你需要指定一些列簇(Column Family),每个列簇下,你不需要指定列(Column)。 每一条数据都可以有属于自己的列,每一行数据的列也可以完全不一样,因为列不是固定的。这个所谓不是固定的,其实就是列下面没有值。(因为Bigtable在底层存储数据的时候,如果一条记录的某个列没有值,那么对应的这一行就没有这个列。)这也是为什么说Bigtable是一个“稀疏”的表。
-
列下面如果有值的话,可以存储多个版本,不同版本都会存上对应版本的时间戳(Timestamp),可以指定保留最近的N个版本(比如N=3,就是保留时间戳最近的三个版本),也可以指定保留某一个时间点之后的版本。
这里列簇的命名容易让人误解,这个名字很容易让人误解Bigtable是一个基于列存储的数据库。但事实完全不是这样,我觉得对于列簇,更合理的解读是,它是一张“物理表”,同一个列簇下的数据会在物理上存储在一起。而整个表则是一张“逻辑表”。比如HBase实现的时候,就是把每一个列簇的数据存储在同一个HFile文件里。在Bigtable的论文中,Google定义了一个叫做本地组(Locality Group)的概念,我们可以把多个列簇放在同一个本地组中,而同一个本地组的所有列的数据,都会存储在同一个SSTable文件里。
这个设计,就使得我们不需要针对字段多的数据表,像MySQL那样,进行纵向拆表了。
Bigtable的这个数据模型,使得我们能很容易地去增加列,而且增加列并不算是修改Bigtable里一张表的Schema,而是在某些这个列需要有值的行里面,直接写入数据就好了。这里的列和值,其实是直接以key-value键值对的形式直接存储下来的。
这个灵活、稀疏而又宽的表,特别适合早期的互联网业务。虽然数据量很大,但是数据本身的Schema我们可能没有想清楚,加减字段都不需要停机或者锁表。在关系型数据库中,如果要修改表结构,就需要将整张表锁住,并且在锁住这张表的时候,是不能往表里写数据的。对于一张数据量很大的表来说,这会让整张表有很长一段时间不能写入数据。
Bigtable这个稀疏列的设计,就为我们带来了很大的灵活性,如同《架构整洁之道》的作者 Uncle Bob 说的那样:“架构师的工作不是作出决策,而是尽可能久地推迟决策,在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策。”
数据分区
搞清楚了Bigtable的数据模型,我们再来一起看一看,Bigtable是怎么通过数据分区,实现水平分库问题的。
把一个数据表,根据主键的不同,拆分到多个不同的服务器上,在分布式数据库里被称之为数据分区( Paritioning)。分区之后的每一片数据,在不同的分布式系统里有不同的名字,在MySQL里呢,我们一般叫做Shard,Bigtable里则叫做Tablet。
在传统的关系型数据库集群里,通常是通过取模函数来进行分区,也就是所谓的哈希分区。通过一个字段哈希取模,然后划分到预先定好N个分片里面。这里最大的问题,在于分区需要在一开始就设计好,而不是自动随我们的数据变化动态调整的。但是往往计划赶不上变化,当我们的业务变化和计划稍有不同,就会遇到需要搬运数据或者各个分片负载不均衡的情况。
在Bigtable里,采用了动态区间分区的方式。不再是一开始就定义好需要多少个机器,应该怎么分区,而是采用了一种自动去“分裂”(split)的方式来动态地进行分区。
核心思路就是:整个数据表,会按照行键排好序,然后按照连续的行键一段段地分区。如果某一段行键的区间里,写的数据越来越多,占用的存储空间越来越大,那么整个系统会自动地将这个分区一分为二,变成两个分区。而如果某一个区间段的数据被删掉了很多,占用的空间越来越小了,也会自动把这个分区和它旁边的分区合并到一起。
采用这样的方式,可以动态地调整数据分区,并且每个分区在数据量上,都会相对比较均匀。而且,在分区发生变化的时候,需要调整的只有一个分区,就没有需要大量搬运数据的问题了。
实现思路
Bigtable实现数据分区的方案是动态区间分区,其核心思路如下:
-
按照行键排序和分区:Bigtable根据行键对整个数据表进行排序,然后按照连续的行键区间进行分区。每个分区称为一个Tablet。
-
自动分裂和合并:当某个分区的数据量增加到一定程度时,系统会自动将该分区分裂成两个更小的分区;当某个分区的数据量减少到一定程度时,系统会将该分区与相邻的分区合并。
-
动态调整:Bigtable的分区数量和具体的分区方式不需要在一开始就确定,而是可以根据数据的变化自动调整。
而传统的关系型数据库集群实现数据分区通常是通过哈希分区,其核心思路如下:
-
基于哈希函数:哈希分区通过计算分片键的哈希值来确定数据存储的位置。通常会对哈希值进行取模运算,将数据均匀地分配到预先设定的多个分片中。
-
固定分区数量:在创建表时,需要预先指定分区的数量和具体的分区方式,一旦确定后,分区数量和方式不易更改。
优缺点
Bigtable的动态区间分区
-
优点:
-
自动调整:能够根据数据量的变化自动分裂和合并分区,无需人工干预,适应性强。
-
数据分布均匀:通过自动分裂和合并,可以使每个分区的数据量相对均匀,避免数据倾斜。
-
扩展性好:当数据量增加时,可以通过自动分裂分区来扩展存储容量和计算能力。
-
对应用透明:分区的分裂和合并对应用是透明的,应用无需关心分区的具体变化。
-
-
缺点:
-
可能出现热点:如果数据的访问模式不均匀,可能会导致某些分区的访问压力较大,出现热点问题。
-
分裂和合并有开销:分区的分裂和合并过程可能会带来一定的系统开销,尤其是在数据量较大的情况下。
-
不适合复杂查询:Bigtable更适合简单的读写操作,对于复杂的查询和事务处理支持较弱。
-
传统关系型数据库的哈希分区
-
优点:
-
数据分布均匀:只要分片键选择得当,哈希函数能够确保数据在各分片间均匀分布,避免数据倾斜。
-
查询性能高:通过哈希计算可以快速定位数据所在的分片,查询性能较高。
-
扩展性较好:增加或减少分片时,只需重新计算部分数据的哈希值并迁移,对整体系统影响较小。
-
-
缺点:
-
分区数量固定:需要在创建表时预先指定分区的数量和方式,一旦确定后不易更改,不够灵活。
-
数据迁移成本高:当分片数量发生变化时,部分数据需要迁移,可能带来一定的系统开销和数据暂时不可用的风险。
-
哈希冲突:虽然概率较低,但理论上可能存在哈希冲突,导致原本应分散的数据被映射到同一分片。
-
分片键选择困难:分片键的选择对数据分布和查询性能影响很大,一旦选定后不易更改。
-
Bigtable是如何设计,解决单点故障以及单点性能瓶颈的?
Bigtable通过Master、Chubby和Tablet Server的协同设计,来解决了单点故障和单点性能瓶颈问题。
Master
解决单点故障:
-
依赖Chubby进行选举:Bigtable的Master服务器并非直接运行,而是依赖于Chubby进行选举。Chubby通过Paxos算法确保多个Master服务器副本之间的一致性。当主Master服务器出现故障时,Chubby会通过选举机制选出一个新的Master服务器来接管工作。
-
监控Tablet Server状态:Master服务器会监控Tablet Server的状态,当某个Tablet Server出现故障时,Master会将该Tablet Server上的Tablet迁移到其他正常的Tablet Server上,从而避免了单点故障。
解决单点性能瓶颈:
-
不直接参与数据读写:Master服务器主要负责管理元数据和协调Tablet Server的工作,而不直接参与数据的读写操作。这样可以减少Master服务器的负载,避免其成为性能瓶颈。
-
负载均衡:Master服务器负责调度Tablet的分配和负载均衡,以优化整体性能。它会根据各个Tablet Server的负载情况,动态调整Tablet的分布,确保资源的高效利用。
事实上,Master一共会负责5项工作:
分配Tablets给Tablet Server;
检测Tablet Server的新增和过期;
平衡Tablet Server的负载;
对于GFS上的数据进行垃圾回收(GC);
管理表(Table)和列族的Schema变更,比如表和列族的创建与删除。
Chubby
解决单点故障:
-
分布式锁和目录服务:Chubby作为分布式锁和目录服务,确保了系统的协调和一致性。它通过Paxos算法保持多个副本之间的一致性,即使某个副本出现故障,其他副本仍然可以继续提供服务。
-
Master选举:Chubby负责选举出一个Master服务器来处理请求,其他服务器作为副本同步数据。当Master服务器出现故障时,Chubby会通过选举机制选出新的Master服务器。
解决单点性能瓶颈:
-
高效的数据同步:Chubby通过Paxos算法确保副本之间的一致性,数据同步效率高,不会因为数据同步而成为性能瓶颈。
-
分布式架构:Chubby本身采用分布式架构,多个副本可以并行处理请求,提高了系统的性能。
Bigtable需要Chubby的地方:
确保我们只有一个Master;
存储Bigtable数据的引导位置(Bootstrap Location);
发现Tablet Servers以及在它们终止之后完成清理工作;
存储Bigtable的Schema信息;
存储ACL,也就是Bigtable的访问权限。
Tablet Server
解决单点故障:
-
数据冗余:每个Tablet会被复制到多个Tablet Server上,通常至少有三个副本。这样,即使某个Tablet Server出现故障,其他副本仍然可以继续提供服务。
-
故障恢复:当某个Tablet Server出现故障时,Master会将该Tablet Server上的Tablet迁移到其他正常的Tablet Server上,从而快速恢复服务。
解决单点性能瓶颈:
-
分布式存储和处理:Bigtable将数据划分为多个Tablet,每个Tablet由一个或多个Tablet Server存储和管理。客户端的读写请求直接发送到相应的Tablet Server,而不是通过Master服务器中转。这种分布式架构使得数据的读写操作可以并行处理,大大提高了系统的性能。
-
负载均衡:Tablet Server会根据数据量自动分裂大的Tablet或合并小的Tablet,以保持数据分布的均匀性,避免某些Tablet Server的负载过高。
为什么数据读写不需要Master?
Chubby帮我们保障了只有一个Master,那么我们再来看看分区和Tablets的分配信息,这些信息也没有放在Master。Bigtable在这里用了一个很巧妙的方法,就是直接把这个信息,存成了Bigtable的一张METADATA表,而这张表在哪里呢,它是直接存放在Bigtable集群里面的,其实METADATA表自己就是一张Bigtable的数据表。
这其实有点像MySQL里面的information_schema表,也就是数据库定义了一张特殊的表,用来存放自己的元数据。不过Bigtable是一个分布式数据库,那这个元数据究竟存放在哪个Tablet Server里,这个就需要通过Chubby来告诉我们了。
-
Bigtable在Chubby里的一个指定的文件里,存放了一个叫做 Root Tablet 的分区所在的位置。
-
然后,这个Root Tablet的分区,是METADATA表的第一个分区, 这个分区永远不会分裂。它里面存的,是METADATA里其他Tablets所在的位置。
-
而METADATA剩下的这些Tablets,每一个Tablet中,都存放了用户创建的那些数据表,所包含的Tablets所在的位置,也就是所谓的User Tablets的位置。
数据写入流程
-
客户端发起写入请求:客户端程序向Bigtable发送写入请求,请求中包含要写入的行键、列族、列以及对应的值。
-
定位Tablet Server:客户端通过与Master Server进行交互,获取目标行键所在的Tablet位置信息。Master Server维护了整个系统的表结构和Tablet分布信息,能够根据行键快速定位到对应的Tablet Server。
-
写入WAL(Write-Ahead Log):一旦确定了目标Tablet Server,客户端将写入请求发送到该Tablet Server。Tablet Server首先将写入的请求记录到WAL文件中。WAL文件是一个FIFO(先进先出)的日志文件,用于记录所有的写入操作。写入WAL文件的目的是为了在Tablet Server发生故障时,能够通过WAL文件恢复数据,确保数据的持久性和一致性。
-
写入MemTable:在写入WAL文件之后,数据会被写入到内存中的一个名为MemTable的数据结构。MemTable是一个按行键排序的有序结构,通常是一个Skip List或者类似的数据结构。数据在MemTable中以较少的代价插入,并且可以快速进行查询。
-
确认写入完成:当数据成功写入WAL文件和MemTable后,Tablet Server会向客户端发送写入成功的确认消息。
-
MemTable的刷盘(Flush):MemTable中的数据会定期或当达到一定阈值后被刷写到磁盘中,形成不可变的SSTable文件。刷盘操作是为了防止MemTable中的数据量过大,占用过多内存资源。
-
WAL的清理:当MemTable中的数据成功刷写到SSTable文件后,对应的WAL记录可以被清理,以释放磁盘空间。
数据读取流程
-
客户端发起读取请求:客户端程序向Bigtable发送读取请求,请求中包含要读取的行键、列簇、列等信息。
-
定位Tablet Server:与写入流程类似,客户端通过与Master Server交互,获取目标行键所在的Tablet位置信息,从而确定目标Tablet Server。
-
查询Bloom Filter(布隆过滤器):Tablet Server在接收到读取请求后,首先会查询Bloom Filter。Bloom Filter是一个用于快速判断一个元素是否可能存在于集合中的数据结构。如果Bloom Filter返回不存在,那么说明目标数据不在当前Tablet对应的MemTable或SSTable文件中,Tablet Server可以直接返回“未找到”的响应。如果Bloom Filter返回可能存在,那么需要进一步查询。
-
查询MemTable:如果Bloom Filter返回可能存在,Tablet Server会先查询MemTable。由于MemTable是按行键排序的,可以通过二分查找等高效算法快速定位到目标行键对应的记录。如果在MemTable中找到数据,则直接返回该数据。
-
查询SSTable:如果MemTable中没有找到数据,Tablet Server将查询磁盘上的SSTable文件。SSTable是不可变的有序存储文件,每个SSTable文件的索引信息可以帮助快速定位到目标行键所在的物理位置。Tablet Server会遍历可能包含目标行键的SSTable文件,并在这些文件中查找数据。
-
合并读取结果:由于数据可能存在于多个SSTable文件中(例如,数据经过多次刷盘),Tablet Server需要将这些SSTable文件中的记录按照时间顺序进行合并。通常,最新的数据会被优先返回。
-
返回读取结果:一旦找到目标数据,Tablet Server会将数据返回给客户端。如果在所有可能的位置都没有找到数据,Tablet Server会返回“未找到”的响应。
Bigtable通过将数据读写操作直接交给Tablet Server处理,避免了Master成为性能瓶颈,提高了系统的性能和可扩展性。Master主要负责元数据管理和协调Tablet的分配,而Tablet Server负责存储和管理数据,直接处理客户端的读写请求。这种设计使得Bigtable能够高效地处理大规模数据的读写操作,同时保证系统的高可用性和可靠性。
Bigtable的整体架构和组件由哪些东西组成?
整个Bigtable是由4个组件组成的,分别是:
-
负责存储数据的GFS;
-
负责作为分布式锁和目录服务的Chubby;
-
负责实际提供在线服务的Tablet Server;
-
负责调度Tablet和调整负载的Master。
注意:
通过动态区域分区的方式,Bigtable的分区策略需要的数据搬运工作量会很小。在Bigtable里,Master并不负责保存分区信息,也不负责为分区信息提供查询服务。
Bigtable是通过把分区信息直接做成了三层树状结构的Bigtable表,来让查询分区位置的请求分散到了整个Bigtable集群里,并且通过把查询的引导位置放在Chubby中,解决了和操作系统类似的“如何启动”问题。而整个系统的分区分配工作,由Master完成。通过对于Chubby锁的使用,就解决了Master、Tablet Server进出整个集群的问题。
Bigtable的底层存储SSTable究竟是怎么回事?
在前面深入HDFS里面,我们有提到GFS对随机读写是没有任何一致性保障的。而Bigtable实际的数据存储是放在GFS上的。这就引出一个重要的问题——为什么一个对随机读写没有一致性保障的文件系统,可以拿来存储随机读写的数据库的数据呢?
Bigtable为了做到高性能的随机读写,采用了下面的方案来解决这个问题:
- 首先是将硬盘随机写,转化成了顺序写,也就是把Bigtable里面的提交日志(Commit Log)以及将内存表(MemTable)输出到磁盘的Minor Compaction机制。
- 其次是利用“ 局部性原理”,最近写入的数据,会保留在内存表里。最近被读取到的数据,会存放到缓存(Cache)里,而不存在的行键,也会以一个在内存里的布隆过滤器(BloomFilter)进行快速过滤,尽一切可能减少真正需要随机访问硬盘的次数。
SSTable(Sorted String Table)是一种持久化、有序且不可变的键值存储结构,其中键和值均可以是任意的字节字符串序列。
核心设计原理
数据的排序和索引:在SSTable中,数据按照键(key)进行排序存储,这样可以有效地提高区间查询(Range Query)的性能。同时,为了加快查找速度,SSTable引入了稀疏索引(Sparse Index)来进行数据的快速定位。通过这种方式,SSTable在进行查询操作时能够有效地减少磁盘的随机读取,提高读取效率。
SSTable的不可变性:SSTable设计为不可变的数据结构,一旦数据写入SSTable后,数据将不再发生变化。这种特性为SSTable的查询和读取提供了很大的便利,同时,不可变性也为SSTable的合并和压缩提供了可能性。
SSTable的压缩和合并策略:为了减少存储空间和提高查询效率,SSTable引入了压缩和合并策略。当SSTable的大小达到一定阈值时,会触发SSTable的合并和压缩操作,通过合并相邻的SSTable文件并进行数据的压缩,以减少磁盘占用和提高读取性能。
索引结构:SSTable的索引结构是其中的关键部分,它用于快速定位和检索数据。一般而言,SSTable的索引采用稀疏索引的方式,即在固定间隔内记录索引信息,以减少索引占用的空间同时保证检索效率。索引通常包含键的位置信息,用于在数据文件中快速定位对应键值对的位置。
数据存储格式:SSTable采用有序存储的方式来存储键值对,通常在写入时将数据按照键的顺序写入文件,这样就可以实现区间查找和范围查询的高效性能。数据存储格式一般包含数据块、数据文件头部信息等内容,以便在读取时能够快速定位数据。
数据写入SSTable的过程
写入缓存(Write Buffer):新写入的数据首先会被存储在内存中的写入缓存中,形成一个待写入SSTable的队列。
内存排序(Memory Sorting):当写入缓存中的数据量达到一定阈值时,会触发内存排序操作,将数据按照键进行排序。
生成新SSTable文件:排序后的数据将被写入到一个新的SSTable数据文件中,同时更新索引结构以记录新数据文件的位置。
后台合并(Background Compaction):定期或触发条件下,系统会执行后台合并操作,将多个SSTable文件合并成一个更大的文件,以减少文件个数和提高读取效率。
数据读取SSTable的过程:
查询Bloom Filter(布隆过滤器):Tablet Server在接收到读取请求后,首先会查询Bloom Filter。Bloom Filter是一个用于快速判断一个元素是否可能存在于集合中的数据结构。如果Bloom Filter返回不存在,那么说明目标数据不在当前Tablet对应的MemTable或SSTable文件中,Tablet Server可以直接返回“未找到”的响应。如果Bloom Filter返回可能存在,那么需要进一步查询。
查询MemTable:如果Bloom Filter返回可能存在,Tablet Server会先查询MemTable。由于MemTable是按行键排序的,可以通过二分查找等高效算法快速定位到目标行键对应的记录。如果在MemTable中找到数据,则直接返回该数据。
查询SSTable:如果MemTable中没有找到数据,Tablet Server将查询磁盘上的SSTable文件。SSTable是不可变的有序存储文件,每个SSTable文件的索引信息可以帮助快速定位到目标行键所在的物理位置。Tablet Server会遍历可能包含目标行键的SSTable文件,并在这些文件中查找数据。
合并读取结果:由于数据可能存在于多个SSTable文件中(例如,数据经过多次刷盘),Tablet Server需要将这些SSTable文件中的记录按照时间顺序进行合并。通常,最新的数据会被优先返回。
SSTable与LSM-Tree的关系
SSTable是LSM-Tree(Log-Structured Merge-Tree)结构中的一个重要组成部分。LSM-Tree是一种能够将批量随机写转换为顺序写的数据结构,其本质是不断产生SSTable结构的Log文件,然后不断Merge以提高文件效率。具体到实现上,LSM-Tree的树节点可以分为两种,保存在内存中的称之为MemTable,保存在磁盘上的称之为SSTable。
总结
我们通过几个问题梳理了Bigtable的核心内容。
Bigtable的数据,本质是由内存里的MemTable和GFS上的SSTable共同组成的。在MemTable里,它是通过跳表实现了O(logN)时间复杂度的单条数据随机读写,以及O(N)时间复杂度的数据顺序遍历。而SSTable里,则是把数据按照行键进行排序,并分成一个个固定大小的block存储。而对应指向block的索引等元数据,也一样存成了一个个block。所以Bigtable的数据模型,其实本质就是一系列的内存+数据文件+日志文件组合下封装出来的一个逻辑视图。
感兴趣的小伙伴可以深入相关论文去拓展了解一下自己感兴趣的地方。