Google的三篇论文,Google File System,MapReduce以及Big Table能够说是整个大数据领域的三驾马车,这里,咱们简单介绍下这三驾马车基本都是干哈的,重点解读下Bigtable: A Distributed Storage System for Structured Data。c++
2003年的GFS:GFS是一个可扩展的分布式文件系统,主要解决传统单机文件系统中磁盘小,数据存储无冗余等问题;web
2004年的MapReduce:MapReduce是一个基于分布式文件系统(例如,GFS)的分布式计算框架,主要用来处理大规模数据集;算法
2006年的BigTable:BigTable是一个用来管理结构化数据的分布式存储系统,其本质上一个分布式KV存储系统。shell
BigTable是Google内部用来管理结构化数据的分布式存储系统,BigTable能够轻易地扩展到上千台机器,BigTable具备如下优势:数据库
与关系型数据库不一样,BigTable并不支持完整的关系数据模型,也就是说,BigTable是一个NoSQL数据库,BigTable为用户提供了一个简单的数据模型,该模型主要有如下两个特色:api
BigTable中的数据是经过行(row)和列(column)进行索引的,行和列能够是任意字符串。数组
客户端能够将各类形式的结构化和半结构数据序列化字符串,但BigTable只是将数据(Data)看作是未解释字符串(字节数组)进行处理。缓存
客户端能够经过模式(Schema)参数来控制底层数据存储的位置,BigTable的模式参数容许客户端动态控制是从内存仍是磁盘提供数据。服务器
让咱们先来看看BigTable论文中是如何对BigTable
定义的吧~网络
A BigTable is a Sparse, Distributed, Persistent Multi-Dimensional Sorted Map
. The Map
is indexed by a row key, column key, and a timestamp
; each value in the map is an uninterpreted array of bytes
.
BigTable是一个Map,即Key/Value键值对,这个Map有啥特色呢?稀疏的,分布式的,持久化存储的且多维度排序的。
对于Map数据结构来讲,最经常使用的操做就是经过Key检索Value,BigTable中的Value是经过行,列,时间戳进行索引的。
# BigTable/Map的索引结构 (row:string, column:string, time:int64) ==> string
上图是为Web页面建立的BigTable表Webtable:
注意:BigTable中,每一个Cell都有多个Version,每一个Version对应一个时间戳。
BigTable中的row key
能够是任意字符串(大部分场景下,用户使用的row key大小为10-100字节,但BigTable当前分配大小为64KB)。
客户端每次读取/写入数据时,指定row key
时,不管有多少列同时被读取/写入,该读写操做都是原子操做的。
BigTable底层是按row key
的字典顺序存储的,给定BigTable表,其row key range
是动态分区的,每一个分区称为一个Tablet
。
Tips:较小范围的row key
数据读取会更高效,缘由在于,这些数据读取时,只须要与不多的机器通讯便可,效率较高。
Tips:客户端能够充分利用上述属性,从而加快其数据访问,获取更高的吞吐量。例如,在Webtable中,使用url的逆序做为row key,这样作的好处是,访问同一网站的页面时,这些页面一般会对应同一台机器,不须要与集群中多个节点通讯,读取效率更高。
多个Column Key
构成的集合称为列族Column Families
,Column Key
是最基本的访问控制单元。
同一列族中的数据一般具备相同的数据类型(通常状况下,同一列族的数据会放在一块儿进行压缩存储)。
数据以列族(Column Families**)中某个列(**
Column Key`)进行存储以前,必须先建立该列族才行。
Tips:一般状况下,一张BitTable表的列族得数量可能比较小(最多几百个),并且在使用过程当中,列族一般是不变的,相反的,每一个表能够拥有无数个列,也就是说,每一个列族均可以拥有无数个列,并且列是不须要提早定义的。
Column Key
一般使用语法family:qualifier
进行命名,列族必须由可打印字符构成,而列名能够由任意字符构成。
BigTable表中每一个Cell包含同一数据的多个版本,这些版本经过时间戳进行索引,BigTable中的时间戳是64位整数。
时间戳能够由服务端生成,也能够由客户端生成,须要避免冲突的应用程序必须由自身生成相应的时间戳。
不一样版本的Cell以时间戳降序的方式进行存储,以致于时间戳最近的版本最早会读取到。
为了不Cell的数据版本过多,提供列族级别的配置项,以便BigTable自动删除旧的数据版本,一种是只保留最近的几个版本,另外一种是只保留足够新的版本数据(例如,保留最近7天写入的数据版本)。
BigTable API提供建立/删除表和列族的方法。
BigTable API提供修改集群,表,列族元数据方法,例如,修改访问控制权限等。
客户端应用程序能够执行写入/删除BigTable中的值,根据row key查询值,迭表明中部分数据集等操做。
// Open the table Table *t = OpenOrDie("/bigtable/web/wetable"); // Write a new anchor and delete an old anchor RowMutation r1(T, "com.cnn.www"); r1.Set("anchor:www.c-span.org", "CNN"); r1.Delete("anchor:www.abc.com"); Operation op; // 应用原子操做到Webtable中的r1上 Apply(&op, &r1);
客户端能够在多个列族上进行迭代操做,同时,BigTable提供了几种row, columns, timestamps构建方法来生成Scan实例。
Scanner scanner(T); ScanStream *stream; stream = scanner.FetchColumnFamily("anchor"); stream->SetReturnAllVersions(); scanner.Lookup("com.cnn.www"); for (; !stream->Done(); stream->Next()) { printf("%s %s %lld %s\n", scanner.RowName(), stream->ColumnName(), stream->MicroTimestamp(), stream->Value()); }
另外,BigTable支持其余更复杂地操做数据的方式:
row key
时执行原子性的读-改-写操做。Sawzall
语言(懵逼)。BigTable是基于Google的一些基础组件构建而成的。
BigTable使用GFS(Google File System)来存储日志(log)和数据(data)文件。
BigTable集群一般运行在一个共享的服务器集群中,BigTable的进程一般与其余分布式应用程序进程共享同一台服务器。
BigTable依赖集群管理系统来实现做业调度,管理共享机器上的资源,处理机器故障以及监视机器状态。
BigTable使用Google SSTable
文件格式来存储内部数据,SSTable
提供了从keys
到values
的持久化的,顺序的,不可变的映射,另外,SSTable
中keys和values均是任意字节数组,另外,SSTable
提供了根据key检索value以及根据key范围检索Value的功能。
SSTable
由不少Block
(每一个Block默认大小为64KB,可配置的)组成,Block Index
(存储在Block尾部)用来定位Blocks,当客户端打开SSTable
时,会将Block Index
加载到内存的。
从SSTable
中检索指定key
的values
时能够经过Single Disk Seek
实现:
首先加载Block Index
到内存中,而后经过二分检索到key
所在的Block,最后将磁盘中合适的Block
加载到内存检索便可。
BigTable依赖高可用且可持久化的分布式锁服务Chubby,Chubby服务包含4个活跃的副本(节点),其中一个节点选举为Master并处理用户请求,当大多数副本副本正常运行且能够互相通讯时,Chubby被认为是正常运行的。Chubby使用Paxos算法实现副本数据一致。
Chubby提供了包含目录和小文件的命名空间,每一个目录或文件能够当成一个锁来使用,读取和写入文件时原子操做。
Chubby客户端会同步缓存Chubby文件,每一个Chubby客户端会自动维护一个与Chubby服务的会话,在会话到期时,若是客户端没法经过Chubby服务更新到期时间,则会话会被中断,会话到期时,客户端会丢失全部全部锁且没法执行open操做。
Chubby客户端能够在Chubby文件/目录上注册回调方法,当会话到期或文件/目录改变是回调该方法。
BigTable使用Chubby完成各类各样的任务:
BigTable实现主要包括三部分:
Tips:与其余单Master分布式存储系统相似,客户端数据不会路由到Master,而是直接与Tablet Server通讯,进而实现数据的读写。
Tips:不少BigTable客户端不须要依赖于Master定位Tablet信息,也就是说,大部分场景下客户端不须要与Master通讯。
BigTable使用相似B+树的三层结构来存储Tablet位置信息。
第一层:一个Chubby文件,该文件存储了root tablet的位置信息,因为该文件是Chubby文件,也就意味着,一旦Chubby服务不可用,整个BigTable就丢失了root tablet的位置,整个服务也就不可用了。
第二层:root tablet,root tablet其实就是元数据表METADATA Table
的第一个Tablet,该Tablet中保存着元数据表其余Tablet的位置信息,root tablet很特殊,为了保证整个树的深度不变,root tablet从不分裂。
注意:对于元数据表METADATA Table
来讲,除了第一个特殊的Tablet来讲,其他每一个Tablet包含一组用户Tablet位置信息集合。
注意:METADATA Table
存储Tablet位置信息时,Row Key
是经过对Tablet Table Identifier
和该Tablet的End Row
生成的。
注意:每一个METADATA Table
的Row Key
大约占用1KB的内存,通常状况下,配置METADATA Table
的大小限制为128MB,也就是说,三层的定位模式大约能够寻址2^34个Tablets。
第三层:其余元数据表的Tablet,这些Tablet与root tablet共同构成整个元数据表。注意:元数据表虽然特殊,但仍然服从前面介绍的数据模型,每一个Tablet也由专门的Tablet Server负责,这就是为何不须要Master Server提供位置信息的缘由,客户端会缓存Tablet的位置信息,若是在缓存中找不到指定Tablet的位置信息,则须要查询该三层结构了,一次访问Chubby服务,两次Tablet Server访问。
每一个Tablet只能分配给某个Tablet Server。
Master Server维护当前哪些Tablet Server是活跃的,哪些Tablet分配给了哪些Tablet Server,哪些Tablet还未分配,当某个Tablet还未被分配、且恰好存在Tablet Server有足够的空间装载该Tablet时,Master Server会向该Tablet Server发送装载请求。
BigTable使用Chubby服务来检测Tablet Server是否存活,当Tablet Server启动时,会在特定的Chubby目录下建立排它锁,BigTable会监控该目录来发现哪些Tablet Server存活,当Tablet Server丢失其排它锁时(例如,网络缘由致使Tablet Server丢失Chubby会话)。
Chubby服务提供了很是高效地检测会话是否持有锁的机制,且不会致使网络拥塞。
当Tablet Server的排它锁文件存在时,Tablet Server可能会从新获取该锁,也就是,该锁是可重入的;排它锁文件不存在,则Tablet Server不会再次请求该锁,而是自杀。
Tablet Server进程终止是,会尝试释放锁,以便Master Server能够尽快地将其维护的Tablet分配到其余节点上。
Master负责检测Tablet Server是否还在为其余Tablet提供服务,并尽快从新分配其负责的Tablet到其余Tablet Server上。
问题是,Master是如何检测的呢?
Master会按期向每一个Tablet Server询问其锁的状态,若是Tablet Server向其报告锁已丢失,或者Master最后几回尝试都没法访问服务器,则Master将尝试获取该Tablet Server对应的排他锁文件,若是能够获取,则说明Chubby处于活跃状态,而Tablet Server已死或者没法访问Chubby,Master能够经过删除其服务器文件来确保Tablet Server再也不提供服务。一旦Tablet Server对应的排它锁文件被删除后,Master Server能够将先前分配给该Tablet SErver的全部Tablet移动到其余未分配的Tablet Server中。
为了确保Bigtablet集群不受Master Server与Chubby服务之间网络问题影响,若是Master的Chubbby会话到期,则Master会自动杀死本身,如上所述,Master Server设备故障不会更改Tablet分配到其余Tablet Server上。
当Master Server启动时,在其能够修改Tablet分配以前,须要先感知到当前Tablet分布才行,启动流程以下:
METADATA
表获取Tablets集合,在扫描的过程当中,当Master发现了还未分配的Tablet时,Master将该Tablet加入未分配的Tablet集合等待合适的时机分配。在第4不扫描METADATA
表时可能会遇到一种复杂的状况:METADATA
表的Tablet还未分配以前是不可以扫描它的。
步骤3扫描过程当中,若是发现Root Tablet尚未分配,Master就把Root Tablet加入到未分配的Tablet集合。
上面这个附加操做确保了Root Tablet会被分配。Root Tablet包括了 全部METADATA
的Tablet的名字,意味着Master扫描完Root Tablet后就获得了全部METADATA
表的Tablet的名字了。
现有的Tablet集合只有在建立新表或者删除了旧表、两个 Tablet被合并了或Tablet被分割成两个小的Tablet时才会发生改变。
Master能够跟踪记录全部这些事件, 除了Tablet分割外的事件都是Master发起的的。
Tablet分割事件须要特殊处理,由于该事件是由Tablet 服务器发起的。
Tablet分割结束后,Tablet Server经过在METADATA
表添加Tablet的信息来提交这 个操做;分割结束后,Tablet Server会通知Master。
若是分割操信息已提交,却没有通知到Master(可能两个服务器中有一个宕机了),Master在要求Tablet服务器装载已经被分割 的子表的时候会发现一个新的Tablet。对比METADATA
表中Tablet的信息,Tablet Server会发现 Master要求其装载的Tablet并不完整,就会从新向Master发送通知信息,从而更新METADATA
表。
Tablet的数据持久化存储在GFS中,具体持久化流程以下图所示。
Updates操做会先提交到log(WAL)中,log主要用来进行数据恢复的。全部的Updates中,最近提交的那部分会存放在排序的缓存中,这个缓存称为MemTable
,更早的Updates会存放在一系列SSTable
中,这些SSTable
本质上就是MemTablet
刷盘生成的。
为了恢复Tablet
,Tablet Server首先从MEMTABLE
中读取元数据信息,元数据信息包含组成该Tablet的SSTable列表及一系列重启点,这些重启点指向包含该Tablet数据的已提交日志记录,Tablet Server会把SSTable的索引读入内存,根据重启点恢复MemTable。
Tablet Server接收到数据写入请求时,Tablet Server首先要检查操做格式是否正确、操做发起者是否有执行这个操做的权限。权限验证的方法是根据从Chubby文件里读取出来的具备写权限的操做者列表来进行验证(这个文件几乎必定会存放在Chubby客户缓存里)。成功的修改操做会记录在提交日志里。能够采用批量提交的方式来提升大量小的修改操做的应用程序的吞吐量。
数据写如操做提交后,数据最终会被插入到MemTable
里。
Tablet Server接收到数据读取请求时,Tablet Server会做相似的完整性和权限检查。一个有效的读操做在一个由一系列SSTable和memtable合并的视图里执行的。因为SSTable和memtable是按字典排序的数据结构,所以能够高效生成合并视图。
Tablet合并和分割时,正在进行的读写操做可以继续进行。
随着数据的不断写入,MemTable
占用的内存会不断增长。当MemTable
占用的内存超过必定阈值时,内存中的MemTable
会被冻结,切换为只读状态,同时建立一个新的MemTable
,新的数据写入请求会写入到新的MemTable
中,只读的MemTable
会被转换为SSTable
并最终写入底层存储系统(GFS)中,这个过程被称做小合并(Minor Compaction
)。
小合并的做用主要有两个:
每次小合并都会生成SSTable
,若是只有小合并,一直这么持续下去,那么,在Tablet Server接收到数据读取操做时,就须要充全部可能存在待检索row key
的SSTable
检索,而后合并全部更新操做,才能最终获得最新的value值。
为了不上述这种状况发生,咱们在后台周期性地执行大合并(Major Compaction)
,大合并会读取几个SSTable
,而后进行数据合并,合并结束后,便可将原先的SSTable
文件删除。
前面一个章节描述了BigTable的底层实现原理,不过,为了知足用户所需的高性能,高可用和可靠性。在具体实现时须要各类优化才行。
客户端能够将多个列族组成Locality Graph
。每一个Tablet会为Locality Group中的数据单独生成SSTable。
将一般不会一切访问的列族分离到单独的Locality Group
中,能够实现更高效的数据读取。
例如,能够将Webtable中的页面元数据(例如,语言和检验和)放在同一Locality Group
中,叶绵绵的内容在不一样组中,当应用程序想要读取页面的元数据信息时,不须要读取全部的页面内容便可完成。
此外,还能够针对每一个Locality Grou
作相应的参数优化,例如,能够声明将某个Locality Group
放到内存中。
内存中Locality Group
对应的SSTable会被延迟加载到Tablet Server中,一旦加载完成,在不访问磁盘的状况下,实现Locality Group
数据访问,此功能对于频繁访问的小块数据十分有用,Google内部用来存储元数据表。
客户端能够控制是否压缩Locality Group
的SSTables以及使用哪一种方式进行压缩。
用户指定的压缩格式应用于每一个SSTable Block(其大小可经过特定于局部性组的调整参数进行控制)。
经过单独压缩每一个块会损失一些空间,但好处是能够读取SSTable的小部分,而无需对整个文件进行解压缩。
许多客户端使用两遍自定义压缩方案。第一遍压缩过程使用了Bentley和McIlroy的方案[6],在一个大范围内使用前缀压缩算法对普通的长字符串进行压缩。第二遍压缩使用了一个快速压缩算法,该算法在一个16kb的小窗口中寻找重复数据。两种压缩过程都很是快,现代机器上,它们的编码速度为100-200 MB/s,解码速度为400-1000 MB/s。尽管咱们在选择压缩算法时强调的是速度而不是空间缩减,但这种两遍压缩方案作得很是好。
为了提高数据读取性能,Tablet Server使用两级缓存。
Scan Cache
是Higher-Level缓存,该缓存会缓存SSTablet接口返回的Key-Value键值对。
Block Cache
是Lower-Level缓存,该缓存会缓存从GFS中读取的SSTable Block。
Scan Cache
对于须要频繁访问相同数据的应用程序来讲是很是有用的。
Block Cache
则对须要访问临近数据的应用程序来讲很是有用。
如上个章节中对BigTable底层实现原理描述的同样,数据读取操做最终是从Tablet的SSTable中读取的。若是SSTable再也不内存中,最终就须要经过读取磁盘来实现数据读取了。经过为特定Locality Group
的SSTable建立布隆过滤器,能够减小磁盘的访问次数。
布隆过滤器使得咱们经过查询布隆过滤器来判断SSTable是否包含指定行/列的数据,对于某些应用程序,经过使用布隆过滤器能够显著下降Tablet Server的磁盘寻道次数。另外,使用布隆过滤器意味着对于不存在的行/列数据能够避免大量没必要要的磁盘读取。
若是未每一个Tablet都生成相应的WAL文件,那么GFS就须要同时写入不少文件,并发量很高,这种场景下,底层每一个GFS服务器为了将日志数据写入不一样的物理文件,会致使大量磁盘寻道,效率极低。此外,每一个Tablet提交单独的日志文件也会下降批量提交优化性能,缘由在与,因为分Tablet进行提交,对应的批量数据就回比较少。为了解决这些问题,咱们将每一个Talbet Server中全部的Tablet的数据写入日志追加到同一日志文件中,从而下降GFS的并发量,也下降底层GFS的物理磁盘寻道。
每一个Tablet Server仅建立一个提交日志(WAL)文件,在正常操做场景下,能够大幅度提升性能,但数据恢复时却很复杂。
一台Tablet Server宕机时,其维护的全部Tablets将被转移到其余Tablet Server上,每台Tablet Server仅会装载少许原始Tablet Server的Tablet。为了恢复Tablet Server的状态,新的Tablet Server须要从原始Tablet写入的提交日志中从新应用该平板电脑的更新。然而,该宕机Tablet Server上的全部的Tablet的更新操做混合在同一个物理日志文件中。
一种方法是让每一个新的Tablet Server读取完整的提交日志文件,并只应用它须要恢复的Tablet所需的条目。然而,在这种方案下,若是当前集群有100台机器从一台故障的Tablet Server上分别分配Tablet,那么日志文件将被读取100次(每台服务器一次)。
为了不日志的重复读取,首先按照<table; row name; log sequence number>
对提交日志条目进行排序。在已排序的输出中,特定Tablet的全部更新操做都是连续的,所以,可经过一次磁盘搜索和顺序读取有效地读取。为了并行排序,将日志文件按64MB切分,在不一样的Tablet Server上并行排序。排序过程由Master协调,并指示每台Tablet Server须要从某些提交日志文件中恢复更新日志时启动。
WAL日志写入GFS有时会因为各类缘由致使性能中断(例如,涉及写操做的GFS服务器计算机,或为到达三个GFS服务器的特定集合而穿越的网络路径遇到网络拥塞或负载太重)。
为了保护更新操做不受GFS延迟峰值的影响,每一个Tablet Server实际上有两个日志写入线程,每一个线程都写入本身的日志文件;这两个线程一次只有一个处于活动状态。若是对WAL日志文件的写入执行得不好,则日志文件写入将切换到另外一个线程,提交日志队列中的更新操做将由新活动的日志写入线程写入。日志条目包含序列号,以便在恢复过程消除此日志线程切换过程当中产生的重复条目。
若是Master将Tablet从一个Tablet Server移动到另外一个Tablet Server,则源Tablet Server首先对该Tablet Server进行一次小合并。这种合并经过减小Tablet Server提交日志中未压缩状态的数量来缩短恢复时间。完成压缩后,Tablet Server中止为该Tablet提供服务。
在实际卸载Tablet以前,Tablet Server会执行另外一次(一般很是快速)小合并,以消除执行第一次小合并时到达的Tablet Server日志中的任何剩余未压缩状态。完成第二次小压缩后,能够将Tablet加载到另外一台Tablet Server上,而无需恢复任何日志条目。
之因此写这篇博客,其实,是为了引出LevelDB,LevelDB又是啥呢?
LevelDB一个单机的开源的高效的KV存储系统,该单机KV存储系统,能够说是高度复刻了BigTable中的Tablet,而BigTable毕竟不是开源哒,咱们经过Google的这篇论文,也只是能了解到BigTable的总体架构,可是具体细节就只能YY,或者去看HBase(参考BigTable实现的一个开源分布式列式数据库)的源码了。
不过呢,出于工做须要呢,目前对HBase需求不大,更须要弄懂单机KV系统是如何实现的,因此呢,我就屁颠屁颠地区看LevelDB了,相比于BigTable/HBase,仅仅是单机和分布式的区别了,并且,LevelDB代码量更小,更容易学习和掌控,接下来,我会经过一系列笔记来记录和分享本身学习LevelDB设计原理及底层细节的过程,但愿你们多多关注呀。
欢迎关注个人我的公众号,博客会同步更新哟~