Google Bigtable
Google Bigtable论文要点整理
摘要
本质及用途
- 分布式的结构化数据存储系统
- 被设计用来处理海量数据(通常是分布在数千台普通服务器上的 PB 级的数据)
应用
- Web索引
- Google Earth
- Google Finanace
本文所做的
本论文描述了 Bigtable 提供的简单的数据模型。利用这个模型,用户可以动态的控制数据的分布和格式。 我们还将描述 Bigtable 的设计和实现。
介绍
Bigtable的目标
- 适用性广泛
- 可扩展
- 高性能
- 高可用性
与传统数据库的异同
相同点:
- 使用很多数据库的实现策略
不同点:
- Bigtable 不支持完整的关系数据模型
- Bigtable 为客户提供了简单的数据模型,利用这个模型,客户可以动态控制数据的分布和格式,用户也可以自己推测底层存储数据的位置相关性
数据
- 数据的下标是行和列的名字
- 名字可以是任意字符串
- 存储的数据均视为字符串
- Bigtable本身不会去解析
- 客户存入结构化或半结构化数据,通过选择数据模式,用户可以控制数据位置的相关性
- 通过参数调节存放位置:内存 | 硬盘
数据模型
Bigtable 是一个稀疏的、分布式的、持久化存储的多维度排序 Map。Map 的索引是行关键字、列关键字 以及时间戳;Map 中的每个 value 都是一个未经解析的 byte 数组。
(row:string, column:string,time:int64)->string
Webtable
- 行关键字:URL
- 列名:网页的某些属性
- contents列:存储网页的某些属性
- 标识:网页的时间戳
- 行名:反向URL
- contents列族:网页的内容
- anchor列族:引用该网页的锚链接文本
行
行关键字:
- 任意字符串(最大支持64KB)
- 同一行关键字读写操作都为原子操作
Tablet
- Bigtable通过行关键字字典顺序组织数据
- 表中每行可以动态分区,每个分区叫Tablet,是数据分布和负载均衡调整的最小单位
列族
- 列关键字组成的集合叫做“列族“,是访问控制的基本单位
- 同一列族下的所有数据通常都属于同一个类型
- 列族在使用之前必须先创建,然后才能在列族中任何的列关键字下存放数据
- 创建后,其中的任何一个列关键字下都可以存放数据
- 一张表中列族不能太多(最多几百个)
- 但是一张表中可以有无限多个列
列关键字命名
语法:
列族:限定词
列组名字:可打印字符串
限定词:任意的字符串
比如,Webtable 有个列族 language,language 列族用来存放撰写网页的语言。我们在 language列族中只使用一个列关键字,用来存放每个网页的语言标识 ID。Webtable 中另一个有用的列族是 anchor;这个列族的每一个列关键字代表一个锚链接,如图一所示。Anchor 列族的限定词是引用该网页的站点名;Anchor列族每列的数据项存放的是链接文本。
控制权限
- 访问控制、磁盘和内存的使用统计都是在列族层面进行
在我们的 Webtable 的例子中,上述的控制权限能帮助我们管理不同类型的应用:
- 允许一些应用可以添加新的基本数据
- 一些应用可以读取基本数据并创建继承的列族
- 一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)
时间戳
在 Bigtable 中,表的每一个数据项都可以包含同一份数据的不同版本;不同版本的数据通过时间戳来索引。
时间戳:
- 64位整型
- Bigtable可以给时间戳赋值,精确到ms
- 用户可以给时间戳赋值
- 不同版本数据按照时间戳倒序排序,最新的数据在最前面
垃圾回收:
为了减轻多个版本数据的管理负担,我们对每一个列族配有两个设置参数,Bigtable 通过这两个参数可以对废弃版本的数据自动进行垃圾收集。用户可以指定只保存最后 n 个版本的数据,或者只保存“足够新”的版本的数据(比如,只保存最近 7 天的内容写入的数据)。 、
API
Bigtable提供的API
- 建立和删除表以及列族的API函数
- 修改集群、表和列族的元数据的API
例如修改访问权限:
// Open the table
Table *T = OpenOrDie(“/bigtable/web/webtable”);
// 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;
Apply(&op, &r1);
/*
*客户程序可以对 Bigtable 进行如下的操作:写入或者删除 Bigtable 中的值、从每个行中查找值、或者遍历表中
*的一个数据子集。图 2 中的C++代码使用 RowMutation 抽象对象进行了一系列的更新操作。(为了保持示例代码的
*简洁,我们忽略了一些细节相关代码)。调用 Apply 函数对Webtable 进行了一个原子修改操作:它为
*www.cnn.com 增加了一个锚点,同时删除了另外一个锚点。
*/
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());
}
/*
*C++代码使用 Scanner 抽象对象遍历一个行内的所有锚点。客户程序可以遍历多个列族,有几种方法可以对扫描输出
*的行、列和时间戳进行限制。例如,我们可以限制上面的扫描,让它只输出那些匹配正则表达式*.cnn.com 的锚点,
*或者那些时间戳在当前时间前 10 天的锚点
*/
支持的其他特性
- Bigtable 支持单行上的事务处理:利用这个功能,用户可以对存储在一个行关键字下的数据进行原子性的读-更新-写操作
- Bigtable 允许把数据项用做整数计数器
- Bigtable 允许用户在服务器的地址空间内执行脚本程序
Bigtable 可以和 MapReduce 一起使用,MapReduce 是 Google 开发的大规模并行计算框架。我们已经开发了一些 Wrapper 类,通过使用这些 Wrapper 类,Bigtable 可以作为 MapReduce 框架的输入和输出。
BigTable构建
GFS
- BigTable 使用 Google 的分布式文件系统(GFS)存储日志文件和数据文件
集群
- BigTable 集群通常运行在一个共享的机器池中,池中的机器还会运行其它的各种各样的分布式应用程序,BigTable 的进程经常要和其它应用的进程共享机器
- BigTable 依赖集群管理系统来调度任务、管理共享的机器上的资源、处理机器的故障、以及监视机器的状态
Google SSTable
- BigTable 内部存储数据的文件是 Google SSTable 格式的
SSTable 是一个持久化的、排序的、不可更改的Map 结构,而 Map 是一个 key-value 映射的数据结构,key 和 value 的值都是任意的 Byte 串。可以对 SSTable进行如下的操作:查询与一个 key 值相关的 value,或者遍历某个 key 值范围内的所有的 key-value 对。从内部看,SSTable 是一系列的数据块(通常每个块的大小是 64KB,这个大小是可以配置的)。SSTable 使用块索引(通常存储在 SSTable 的最后)来定位数据块;在打开 SSTable 的时候,索引被加载到内存。每次查找都可以通过一次磁盘搜索完成:首先使用二分查找法在内存中的索引里找到数据块的位置,然后再从硬盘读取相应的数据块。也可以选择把整个 SSTable 都放在内存中,这样就不必访问硬盘了。
Chubby
- BigTable 还依赖一个高可用的、序列化的分布式锁服务组件,叫做 Chubby
一个 Chubby 服务包括了 5 个活动的副本,其中的一个副本被选为 Master,并且处理请求。只有在大多数副本都是正常运行的,并且彼此之间能够互相通信的情况下,Chubby 服务才是可用的。当有副本失效的时候,Chubby 使用 Paxos 算法来保证副本的一致性。Chubby 提供了一个名字空间,里面包括了目录和小文件。每个目录或者文件可以当成一个锁,读写文件的操作都是原子的。Chubby 客户程序库提供对 Chubby 文件的一致性缓存。每个 Chubby 客户程序都维护一个与 Chubby 服务的会话。如果客户程序不能在租约到期的时间内重新签订会话的租约,这个会话就过期失效了9。当一个会话失效时,它拥有的锁和打开的文件句柄都失效了。Chubby 客户程序可以在文件和目录上注册回调函数,当文件或目录改变、或者会话过期时,回调函数会通知客户程序。
Bigtable 使用 Chubby 完成以下的几个任务:
- 确保在任何给定的时间内最多只有一个活动的 Master 副本;
- 存储 BigTable 数据的自引导指令的位置(参考 5.1 节);
- 查找 Tablet 服务器,以及在 Tablet 服务器失效时进行善后(5.2 节);
- 存储 BigTable 的模式信息(每张表的列族信息);
- 以及存储访问控制列表。
如果Chubby长时间无法访问,BigTable就会失效。
介绍
Bigtable包括了主要的三个组件:
- 链接到客户程序中的库
- 一个Master服务器
- 多个Tablet服务器
根据系统工作负载的变化,BigTable动态的向集群中添加(或删除)Tablet服务器。
Master服务器
Master服务器主要负责以下工作:
- 为 Tablet 服务器分配 Tablets
- 检测新加入的或者过期失效的 Table 服务器
- 对 Tablet 服务器进行负载均衡
- 对保存在 GFS 上的文件进行垃圾收集
- 处理对模式的相关修改操作,例如建立表和列族
Tablet服务器
每个 Tablet 服务器都管理一个 Tablet 的集合(通常每个服务器有大约数十个至上千个 Tablet)。每个 Tablet服务器负责处理它所加载的 Tablet 的读写操作,以及在 Tablets 过大时,对其进行分割。
和很多 Single-Master 类型的分布式存储系统【17.21】类似,客户端读取的数据都不经过 Master 服务器:客户程序直接和 Tablet 服务器通信进行读写操作。由于 BigTable 的客户程序不必通过 Master 服务器来获取 Tablet 的位置信息,因此,大多数客户程序甚至完全不需要和 Master 服务器通信。在实际应用中,Master服务器的负载是很轻的。 一个 BigTable 集群存储了很多表,每个表包含了一个 Tablet 的集合,而每个 Tablet 包含了某个范围内的行的所有相关数据。初始状态下,一个表只有一个 Tablet。随着表中数据的增长,它被自动分割成多个Tablet,缺省情况下,每个 Tablet 的尺寸大约是 100MB 到 200MB。
Tablet的位置
我们使用一个三层的、类似B+树[10]的结构存储 Tablet 的位置信息。
- 第一层是一个存储在 Chubby 中的文件,包含 Root Tablet 的位置信息。
- Root Tablet 包含一个特殊的 METADATA 表里所有的 Tablet 的位置信息。
- METADATA 表的每个 Tablet 包含了一个用户 Tablet 的集合。
- Root Tablet 是 METADATA 表的第一个 Tablet,对它的处理比较特殊 ,Root Tablet 永远不会被分割 ,这就保证了 Tablet 的位置信息存储结构不会超过三层。
- METADATA 表中每个 Tablet 的位置信息都存放在一个行关键字下面,而这个行关键字是由 Tablet所在的表的标识符和 Tablet 的最后一行编码而成的。
- METADATA 的每一行都存储了大约 1KB 的内存数据。
- 在一个大小适中的、容量限制为 128MB 的 METADATA Tablet 中,采用这种三层结构的存储模式,可以标识 2^34 个 Tablet 的地址(如果每个 Tablet 存储 128MB 数据,那么一共可以存储 2^61 字节数据)。
客户程序使用的库会缓存 Tablet 的位置信息。如果客户程序没有缓存某个 Tablet 的地址信息,或者发现它缓存的地址信息不正确,客户程序就在树状的存储结构中递归的查询 Tablet 位置信息;如果客户端缓存是的,那么寻址算法需要通过三次网络来回通信寻址,这其中包括了一次 Chubby 读操作;如果客户端缓存的地址信息过期了,那么寻址算法可能需要最多6次网络来回通信才能更新数据,因为只有在缓存中没有查到数据的时候才能发现数据过期。
尽管 Tablet 的地址信息是存放在内存里的,对它的操作不必访问 GFS 文件系统,但是,通常我们会通过预取 Tablet 地址来进一步的减少访问的开销:每次需要从 METADATA 表中读取一个 Tablet 的元数据的时候,它都会多读取几个 Tablet 的元数据。
在 METADATA 表中还存储了次级信息,包括每个 Tablet 的事件日志(例如,什么时候一个服务器开始为该 Tablet 提供服务)。这些信息有助于排查错误和性能分析。
Tablet分配
- 在任何一个时刻,一个Tablet只能分配给一个Tablet服务器。
Master服务器记录了:
- 当前有哪些活跃的Tablet服务器
- 哪些 Tablet 分配给了哪些 Tablet 服务器
- 哪些 Tablet 还没有被分配
当一个 Tablet 还没有被分配、并且刚好有一个 Tablet 服务器有足够的空闲空间装载该 Tablet 时,Master 服务器会给这个 Tablet 服务器发送一个装载请求,把 Tablet 分配给这个服务器。
- BigTable 使用 Chubby 跟踪记录 Tablet 服务器的状态。
- 当一个 Tablet 服务器启动,它在 Chubby 的一个指定目录下建立一个有唯一性名字的文件,获取该文件的独占锁
- Master 服务器实时监控着这个目录(服务器目录),因此 Master 服务器能够知道有新的 Tablet 服务器加入了
- 如果 Tablet 服务器丢失了 Chubby 上的独占锁,比如由于网络断开导致 Tablet 服务器和 Chubby 的会话丢失,它就停止对 Tablet 提供服务
- 只要文件还存在,Tablet 服务器就会试图重新获得对该文件的独占锁;如果文件不存在了,那么Tablet 服务器就不能再提供服务了,它会自行退出
- 当 Tablet 服务器终止时(比如,集群的管理系统将运行该 Tablet 服务器的主机从集群中移除),它会尝试释放它持有的文件锁,这样一来,Master 服务器就能尽快把Tablet 分配到其它的 Tablet 服务器
- Master 服务器负责检查一个 Tablet 服务器是否已经不再为它的 Tablet 提供服务了,并且要尽快重新分配 它加载的 Tablet。
- Master 服务器通过轮询 Tablet 服务器文件锁的状态来检测何时 Tablet 服务器不再为 Tablet提供服务。
- 如果一个 Tablet 服务器报告它丢失了文件锁,或者 Master 服务器最近几次尝试和它通信都没有得到响应,Master 服务器就会尝试获取该 Tablet 服务器文件的独占锁;如果 Master 服务器成功获取了独占锁,那么就说明 Chubby 是正常运行的,而 Tablet 服务器要么是宕机了、要么是不能和 Chubby 通信了,因此,Master 服务器就删除该 Tablet 服务器在 Chubby 上的服务器文件以确保它不再给 Tablet 提供服务。一旦 Tablet 服务器在 Chubby 上的服务器文件被删除了,Master 服务器就把之前分配给它的所有的 Tablet 放入未分配的 Tablet集合中。
- 为了确保 Bigtable 集群在 Master 服务器和 Chubby 之间网络出现故障的时候仍然可以使用,Master服务器在它的 Chubby 会话过期后主动退出。但是不管怎样,如同我们前面所描述的,Master 服务器的故障不会改变现有 Tablet 在 Tablet 服务器上的分配状态。
当集群管理系统启动了一个 Master 服务器之后,Master 服务器首先要了解当前 Tablet 的分配状态,之后才能够修改分配状态。Master 服务器在启动的时候执行以下步骤:
- Master 服务器从 Chubby 获取一个唯一的 Master 锁,用来阻止创建其它的 Master 服务器实例;
- Master 服务器扫描 Chubby 的服务器文件锁存储目录,获取当前正在运行的服务器列表;
- Master 服务器和所有的正在运行的 Tablet 表服务器通信,获取每个 Tablet 服务器上 Tablet 的分配信息;
- Master 服务器扫描 METADATA 表获取所有的 Tablet 的集合。
在扫描的过程中,当 Master 服务器发现了一个还没有分配的 Tablet,Master 服务器就将这个 Tablet 加入未分配的 Tablet 集合等待合适的时机分配。
Tablet服务
- Tablet 的持久化状态信息保存在 GFS 上
- 更新操作提交到 REDO 日志中
MEMTABLE 和 SSTABLE
Tablet 的持久化状态信息保存在 GFS 上。更新操作提交到 REDO 日志中。
在这些更新操作中,最近提交的那些存放在一个排序的缓存中,我们称这个缓存为 memtable;较早的更新存放在一系列SSTable 中。
为了恢复一个 Tablet,Tablet 服务器首先从 METADATA 表中读取它的元数据。
Tablet 的元数据包含了组成这个 Tablet 的SSTable 的列表,以及一系列的 Redo Point,这些 Redo Point 指向可能含有该 Tablet数据的已提交的日志记录。Tablet 服务器把 SSTable 的索引读进内存,之后通过重复 Redo Point 之后提交的更新来重建 memtable。
当对 Tablet 服务器进行写操作时,Tablet 服务器首先要检查这个操作格式是否正确、操作发起者是否有执行这个操作的权限。一个有效的读操作在一个由一系列 SSTable 和 memtable 合并的视图里执行。由于 SSTable 和 memtable 是按字典排序的数据结构,因此可以高效生成合并视图。
另外,当进行 Tablet 的合并和分割时,正在进行的读写操作能够继续进行。
空间收缩
- 随着写操作的执行,memtable 的大小不断增加。当 memtable 的尺寸到达一个门限值的时候,这个 memtable就会被冻结,然后创建一个新的 memtable
- 被冻结住 memtable 会被转换成 SSTable,然后写入 GFS
目的
shrink Tablet 服务器使用的内存,以及在服务器灾难恢复过程中,减少必须从提交日志里读取的数据量。
流程
- 每一次 Minor Compaction 都会创建一个新的 SSTable。
- 如果 Minor Compaction 过程不停滞的持续进行下去,读操作可能需要合并来自多个 SSTable 的更新;否则,我们通过定期在后台执行 Merging Compaction 过程合并文件,限制这类文件的数量。
- Merging Compaction 过程读取一些 SSTable 和 memtable 的内容,合并成一个新的 SSTable。只要Merging Compaction 过程完成了,输入的这些 SSTable 和 memtable 就可以删除了。
- 合并所有的 SSTable 并生成一个新的 SSTable 的 Merging Compaction 过程叫作 Major Compaction。由非 Major Compaction 产生的 SSTable 可能含有特殊的删除条目,这些删除条目能够隐藏在旧的、但是依然有效的SSTable中已经删除的数据。
- Major Compaction过程生成的SSTable不包含已经删除的信息或数据。
- Bigtable循环扫描它所有的 Tablet,并且定期对它们执行 Major Compaction。Major Compaction 机制允许 Bigtable 回收已经删除的数据占有的资源,并且确保 BigTable 能及时清除已经删除的数据。
优化
局部性群组
特性
-
多个列族组合成一个局部性群组
-
Tablet中每个局部性群组都会生成一个单独的SSTable
-
通常将不会一起访问的列族分割成不同的局部性群组可以提高读取效率
例如,在 Webtable表中,网页的元数据(比如语言和 Checksum)可以在一个局部性群组中,网页的内容可以在另外一个群组:当一个应用程序要读取网页的元数据的时候,它没有必要去读取所有的页面内容。
-
可以以局部性群组为单位设定一些有用的调试参数
比如,可以把一个局部性群组设定为全部存储在内存中。Tablet 服务器依照惰性加载的策略将设定为放入内存的局部性群组的 SSTable 装载进内存。加载完成之后,访问属于该局部性群组的列族的时候就不必读取硬盘了。这个特性对于需要频繁访问的小块数据特别有用:在 Bigtable 内部,我们利用这个特性提高 METADATA 表中具有位置相关性的列族的访问速度。
压缩
两遍压缩
第一遍采用 Bentley and McIlroy’s 方式,这种方式在一个很大的扫描窗口里对常见的长字符串进行压缩。
第二遍是采用快速压缩算法,即在一个 16KB 的小扫描窗口中寻找重复数据。
通过缓存提高读操作性能
二级缓存
- 目的:为例提高读操作的性能
- 流程:
- 扫描缓存是第一级缓存,主要缓存 Tablet服务器通过 SSTable 接口获取的 Key-Value 对(对于经常要重复读取相同数据的应用程序来说,扫描缓存非常有效)
- Block 缓存是二级缓存,缓存的是从 GFS 读取的 SSTable 的 Block(对于经常要读取刚刚读过的数据附近的数据的应用程序来说,Block 缓存更有用)
Bloom过滤器
一个读操作必须读取构成 Tablet 状态的所有 SSTable 的数据。如果这些 SSTable 不在内存中,那么就需要多次访问硬盘。我们通过允许客户程序对特定局部性群组的 SSTable 指定 Bloom 过滤器来__减少硬盘访问的次数__。
Commit日志的实现
如果我们把对每个 Tablet 的操作的 Commit 日志都存在一个单独的文件的话,那么就会产生大量的文件,并且这些文件会并行的写入 GFS。根据 GFS 服务器底层文件系统实现的方案,要把这些文件写入不同的磁盘日志文件时,会有大量的磁盘 Seek 操作。另外,由于批量提交25中操作的数目一般比较少,因此,对每个Tablet 设置单独的日志文件也会给批量提交本应具有的优化效果带来很大的负面影响。
为了避免这些问题,我们__设置每个 Tablet 服务器一个 Commit 日志文件,把修改操作的日志以追加方式写入同一个日志文件__,因此__一个实际的日志文件中混合了对多个 Tablet 修改的日志记录__。
使用单个日志显著提高了普通操作的性能,但是将恢复的工作复杂化了。
为了避免多次读取日志文件:
- 把日志按照关键字(table,row name,log sequence number)排序;
- 排序之后,对同一个 Tablet 的修改操作的日志记录就连续存放在了一起。
因此,我们只要一次磁盘 Seek 操作, 之后顺序读取就可以。
Tablet恢复提速
- Master 服务器将一个 Tablet 从一个 Tablet 服务器移到另外一个 Tablet 服务器时,源 Tablet 服务器会对这个 Tablet 做一次 Minor Compaction;
- 这个 Compaction 操作减少了 Tablet 服务器的日志文件中没有归并的记录,从而减少了恢复的时间。
- Compaction 完成之后,该服务器就停止为该 Tablet 提供服务;
- 在卸载 Tablet 之前,源 Tablet 服务器还会再做一次(通常会很快)Minor Compaction,以消除前面在一次压缩过程中又产生的未归并的记录;
- 第二次 Minor Compaction 完成以后,Tablet 就可以被装载到新的 Tablet 服务器上了,并且不需要从日志中进行恢复。
利用不变性
- 在使用 Bigtable 时,除了 SSTable 缓存之外的其它部分产生的 SSTable 都是不变的,我们可以利用这一点对系统进行简化:例如,当从 SSTable 读取数据的时候,我们不必对文件系统访问操作进行同步,这样一来,就可以非常高效的实现对行的并行操作;
- memtable 是唯一一个能被读和写操作同时访问的可变数据结构;
- 对内存表采用 COW(Copy-on-write)机制,这样就允许读写操作并行执行;
- 可以把永久删除被标记为“删除”的数据的问题,转换成对废弃的SSTable 进行垃圾收集的问题;
- 每个 Tablet 的 SSTable 都在 METADATA 表中注册了Master 服务器采用“标记-删除”的垃圾回收方式删除 SSTable 集合中废弃的 SSTable;
- METADATA 表保存了 Root SSTable的集合;
- SSTable 的不变性使得分割 Tablet 的操作非常快捷。我们不必为每个分割出来的 Tablet 建立新的SSTable 集合,而是共享原来的 Tablet 的 SSTable 集合。
译者
作者/编著者:
阎伟
博客: http://andyblog.sinaapp.com
微博:http://weibo.com/2152410864