Bigtable 构建在属于 Google 基础设施的其他几个模块之上。Bigtable 使用 Google 分布式文件系统 GFS 来存储日志和数据文件。一个 Bigtable 集群通常与多种其他分布式应用运行在同一批计算机上,Bigtable 进程与这些分布式应用的进程共享同样的计算机。Bigtable 依赖于集群管理系统进行任务安排、机器资源管理、机器故障处理以及机器状态监控。
用 Google SSTable 文件格式来保存 Bigtable 的数据。SSTable 是一种持久的、排序的、不变的键值映射文件,其中的键值是任意的字节串。对 SSTable 的操作包括根据键查找对应值,遍历制定范围的键值等。每个 SSTable 包含 64K 大小的 block 序列,并用 block 索引来定位 block,索引保存在 SSTable 文件的末尾。在 SSTable 文件被打开的时候 block 被装入内存。一次查询只需一次磁盘读写:首先在内存里通过对索引二分搜索定位合适的 block 然后从磁盘读取 block。另外,SSTable 还能完全映射到内存,这样不用读取磁盘就可以完成查询。
Bigtable 依赖于名为 Chubby 的高可用、分布式、持久化锁服务。一个 Chubby 服务包含 5 个活跃复本,其中一个选为 master 并响应查询。只有在多数复本在运行并且他们可用互相通讯的情况下,Chubby服务才可用。Chubby 采用 paxos 算法复本之间在面对故障情况下的一致性。Chubby 提供一个由目录和小文件组成的名字空间。每个目录或者文件可用用作锁,对文件的读写时原子的。Chubby 客户程序库提供关于 Chubby 文件的一致的缓冲。每个 Chubby 客户与服务器维持一个 session。Client 如果不能在一定时间内更新其租期,将导致 session 将过期,从而其拥有的锁和句柄。Chubby 客户端还能注册回调函数,以便在文件目录变更或者 session 过期时得到通知。Bigtable 使用 Chubby 来完成许多任务:确保任何时候最多只有一个活跃的 master;存储 Bigtable 数据的加载地址;发现 tablet 服务器以及宣告 bablet 服务器的终止;保存 Bigtable 的模式信息(每个表的列族信息);存储访问控制列表;如果 Chubby 服务长时间不可访问,那么 Bigtable 也不能访问。
Bigtable 的实现包括 3 个主要组件:一个程序库(连接进每个客户端),一个 master 服务器,多个 tablet 服务器。Tablet 服务器可以动态添加和删除以适应负载的变化。Master 服务器负责将 tablet 分配给 tablet 服务器,探测 tablet 服务器的添加和过期,平衡 tablet 服务器之间的负载,以及在 GFS 上收集垃圾文件。另外,他还处理模式的变更,如表和列族的创建。每个 tablet 服务器管理一些 tablet,负载它所加载的 tablet 的读写请求,并在 table 增长到一定规模时进行拆分。客户端数据并不经过 master:客户端直接与 tablet 服务器交互完成读写,因为 bigtable 的客户端不依赖 master 来定位 tablet 位置,多数客户端从不与 master 通讯,所以 master 负载很小。一个 bigtable 集群存储很多表,每个表是 tablet 的集合,每个 tablet 包含一定范围内的所有数据。初始,每个表只包含一个 tablet,当表增长时,它自动分裂成多个 tablet,每个 tablet 大约 100-200M 大小。
我们采用三层的结构(类似B+树)来存储 tablet 位置信息。第一层是存储在 chubby 中的一个文件,它包含 root tablet 的位置信息。而 root tablet 包含了一个叫做 METADATA 的表的所有 tablet 的位置信息。每个 METADATA tablet 包含了一些用户 tablet 的信息。Root tablet 其实是 METADATA 表的第一个 bablet 但是它被特殊对待:它从不分裂,以保证层次结构不超过 3 层。
METADATA 表存储 tablet 的位置,所用的行键是由表标识和结束行的编码。每个METADATA 行可以存储大约 1KB 数据。如果 METADAT 表大小为 128M 那么三层的结构可以存储 2^34 个 tablet。客户端程序库缓存 tablet 的地址。如果客户端不知道 tablet 的位置,或者它发现缓存的位置信息不对,则它递归地在层次结构中查找 tablet 位置。如果缓存为空,那么定位算法需要三个网络请求,包括一个 chubby 请求。如果客户端缓存过期,那么可能需要 6 次请求。尽管 tablet 的位置存储在内存,所以不需要 GFS 访问,我们可以通过预取 tablet 位置进一步减少通常情况下的访问成本:每次读取 METADATA 表是读取多个 tablet 的位置信息。我们也在 METADATA 表中存储 secondary 信息:包括关于每个 tablet 的许多事件(比如某个服务器服务它的时间)。这些信息对于调试和性能分析有用。
Master 负责探测 tablet 服务器停止响应的时刻,并负责尽快重新分配该 tablet 服务器所服务的 tablet 。为了探测 tablet 服务器的问题,master 定期地向每个 tablet 服务器询问其锁的状况。如果 tablet 服务器报告锁丢失,或者 master 联系不上 tablet 服务器,master 尝试获取该服务器的独占锁。如果服务器可以获得独占锁,说明 chubby 正常,而 tablet 服务器死机或者不能访问 chubby ,所以 master 删除该锁文件,这样对应 tablet 服务器再也不能服务相关 tablet ,master 就可以将这些 tablet 纳入未分配 tablet 集合。如果 master 不能访问 chubby 则自动终止,master 的故障不改变 tablets 的分配情况。
当 master 启动的时候,他需要首先发现目前 tablet 分配情况。它执行以下步骤:(1) 从 chubby 获取一个唯一的 master 锁,从而避免多个 master 并发;(2) 扫描 chubby 中的服务器目录找到所有 tablet 服务器;(3) 询问每个 tablet 服务器获知 tablet 映射情况;(4) 扫描 METADATA 表构建未映射 tablet 集合;但是 METADATA 表的 tablet 必须先分配 master 才能扫描 METADATA 表。所以,在 (4) 之前必须先将 root tablet 分配给某个服务器,如果它还未分配的话。因为 root tablet 包括了 METADATA 表的所有tablet 信息,在扫描 root tablet 之后 master 就知道所有 METADATA tablet 了。
所有现存 tablet 的集合只有在新建删除表,或者 tablet 归并或分裂时才变化。Master 知道这些变化,因为其中前三个都是由 master 发起的。Tablet 的分裂是由 tablet 服务器发起的,所有特别处理:tablet 通过在 METADATA 表中登记新的 tablet 来提交分裂结果,当提交成功后它通知 master ,如果通知丢失,master 要求 tablet 服务器加载已经分裂的 tablet 时,可以探测到新的 tablet,tablet 服务器将通知 master 关于分裂的情况,因为它在 METADATA 表中找到的 tablet 只包括 master 要求加载的部分内容。
关于 tablet 的持久状态保持在 GFS 之中。更新提交到 commit 日志以保持重做记录。在这些更新中,最近的更新存储在内存的一个叫 memtable 的排序缓冲区中,比较早的更新存储在一系列 SSTable 文件中。如果要恢复一个 tablet ,服务器从 METADATA 表读它的元数据,包括 组成这个 tablet 的SSTable 和一些重做点,重做点指向包含该 tablet 的 commit 日志。Tablet 服务器从 SSTable 中读取这些指标,利用 commit log 重构 memtable,使 memtable 中包含重做点之后的所有更新。当写请求到达时,服务器检查格式正确性及读权限。权限检查通过读 chubby 上的文件来确定允许访问的用户集合。合法的修改请求记录到 commit 日志,多次小修改可以一次提交。写提交之后,其内容插入到 memtable 中。读请求到达时,检查格式和权限,并在由 SSTable 和 memtable 合并的视图上查询。由于他们都是排序的,效率较高。在 tablet 合并分裂时读写可以同时进行。
随着写操作执行,memtable 的大小逐渐增长,增长超过一定阀值时,老 memtable 冻结并生成一个新 memtable。冻结的 memtable 转换成 SSTable 并写入 GFS 中。这种次级压缩有两个目的:减少内存的使用,减少故障恢复是需要重做的 Log 记录。次级压缩过程中读写能同时进行。
每个次级压缩产生一个 SSTable 文件,如果不加限制,读操作需要从大量 SSTable 中归并结果。通过在后台周期性地执行归并压缩可以避免这个问题。归并压缩将多个 SSTable 和 memtable 压缩成一个 SSTable。老的 SSTable 和 memtable 在归并压缩完成之后即可丢弃。
这个归并压缩过程叫做 major 压缩。次级压缩产生的 SSTable 中包含一些标志已删除的条目,major 压缩过程将丢弃这写删除条目。
优化措施
Locality Group : 指定将一些列族组织成 Locality Group,这样在每个tablet 中,这些列族的数据都保存于单独的 SSTable 中,将列族分离有利于提高读效率。对不同的 Locality Group 可以设置一些参数,如 in-memory 以提高性能。
使用压缩:对每个 Locality Group 可以指定压缩方式。
使用缓存:两级缓存,SSTable 和 Tablet Server 之间的键值对缓存,以及 GFS SSTable 块的缓存。
使用 Bloom Filter:为 Locality Group 指定使用过滤器。