$ 【译】Kudu论文

$ 3 架构

$ 3.1 集群角色

kudu依赖一个负责元数据的单一的master服务器,和任意数量负责数据的tablet服务器。

$ 3.2 分区

与大多数分布式数据库系统一样,Kudu 中的表是水平分区的。Kudu 和 BigTable 一样,将这些水平分区称为tablet。任何行都可以根据其主键的值精确映射到一个tablet,从而确保随机访问操作(例如插入或更新)仅影响单个tablet。对于吞吐量很重要的大型表,我们建议每台机器 10-100 片。每个tablet可以达到数十 GB。

与仅提供基于键范围的分区的 BigTable 不同,与几乎总是使用基于哈希的分区部署的 Cassandra 不同,Kudu 支持灵活的分区方案。创建表时,用户为该表指定分区模式。分区模式充当从主键元组映射到二进制分区键的函数。每个tablet都涵盖了这些分区键的连续范围。因此,客户端在执行读取或写入时,可以轻松确定哪个tablet保存给定的key并路由相应请求。

分区模式由零个或多个哈希分区规则和可选的范围分区规则组成:

  • 散列分区规则由主键列的子集和多个bucket组成。例如,正如我们的 SQL 方言中所表达的,DISTRIBUTE BY HASH(hostname, ts) INTO 16 BUCKETS。这些规则首先连接指定列的值,然后以bucket数为模计算结果字符串的哈希码,从而将元组转换为二进制键。这个bucket 序号在分区键中被编码为一个 32 位大端整数。
  • 范围分区规则由主键列的有序子集组成。此规则通过使用保序编码连接指定列的值,将元组映射为二进制字符串。

通过使用这些分区规则,用户可以根据他们的特定工作负载轻松地在查询并行性和查询并发性之间进行权衡。例如,考虑一个时间序列应用程序,它按(host, metric, time, value)存储行,其中插入几乎总是以单调递增的时间值完成。选择按时间戳进行哈希分区可以在所有服务器上最佳地分散插入负载;但是,在短时间内对特定主机上的特定metric的查询必须扫描所有tablet,从而限制了并发性。用户可能会选择按时间戳进行范围分区,同时为metric和host添加单独的哈希分区规则,这将提供写入并行性和读取并发性的良好折衷。

$ 3.3 副本

Kudu 使用 Raft共识算法来复制其tablet。特别是,Kudu 使用 Raft 来商定每个tablet的逻辑操作日志(例如插入/更新/删除)。当客户端希望执行写入时,它首先定位leader副本并向该副本发送写入 RPC。如果客户端的信息过时并且副本不再是leader,它将拒绝请求,导致客户端无效并刷新其元数据缓存并将请求重新发送给新leader。如果副本实际上仍然充当leader,它会使用本地锁管理器将操作与其他并发操作序列化,选择一个 MVCC 时间戳,并通过 Raft 向其follower提议操作。如果大多数副本接受写入并将其记录到自己的本地预写日志,写入被认为是持久复制的,因此可以在所有副本上提交。请注意,没有限制leader必须在提交操作之前将操作写入其本地日志:即使leader的磁盘性能不佳,这也提供了良好的延迟平滑属性。

在少数副本失败的情况下,leader可以继续提出和提交操作tablet的复制日志。如果leader本身失败,Raft 算法会迅速选出一个新的leader。默认情况下,Kudu 使用 500 毫秒的心跳间隔和 1500 毫秒的选举超时;因此,在leader失败后,通常会在几秒钟内选出新的leader。

Kudu 对 Raft 算法进行了一些小的改进。特别是:

  1. 我们在leader选举失败后采用指数退避算法。我们发现,由于我们通常将 Raft 的持久元数据提交给繁忙的硬盘驱动器,因此需要这样的扩展来确保繁忙集群上的选举收敛。
  2. 当一个新的leader联系一个日志与自己不同的follower时,Raft 建议一次回退一个操作,直到发现他们分歧的点。相反,Kudu 立即跳回到最后一个已知的 committedIndex,它保证始终出现在任何有分歧的follower上。这以可能通过网络发送冗余操作为代价,最大限度地减少了潜在的往返次数。我们发现这很容易实现,它确保在单次往返后中止分歧的操作。

Kudu 不会复制tablet的磁盘存储,而只会复制其操作日志。tablet的每个副本上的物理存储完全解耦。这产生了几个优点:

  • 当一个副本正在进行物理层后台操作时,例如刷新或压缩(参见第 4 节),其他节点不太可能同时在同一个tablet上运行。因为 Raft 可能在大多数副本确认后提交,这减少了此类物理层操作对客户端写入尾部延迟的影响。将来,我们预计会实施诸如 [16] 中描述的推测性读取请求之类的技术,以进一步降低并发读/写工作负载中读取的尾部延迟。
  • 在开发过程中,我们在 Kudu tablet的物理存储层中发现了一些罕见的竞争条件。因为存储层是跨副本解耦的,所以这些竞争条件都不会导致不可恢复的数据丢失:在所有情况下,我们都能够检测到一个副本已损坏(或静默地偏离大多数副本)并修复它。

$ 3.3.1 配置变更

Kudu 按照 [24] 中提出的一对一算法实现 Raft 配置更改。在这种方法中,Raft 配置中的投票者数量可能在每次配置更改时最多更改一个。为了将 3 个副本配置增长到 5 个副本,必须提出并提交两个单独的配置更改(3→4、4→5)。

Kudu 通过称为remote bootstrap的过程实现添加新服务器。在我们的设计中,为了添加新副本,我们首先将其添加为 Raft 配置中的新成员,甚至在通知目标服务器将向其复制新副本之前。提交此配置更改后,当前 Raft leader副本会触发 StartRemoteBootstrap RPC,这会导致目标服务器从当前leader那里拉取tablet数据和日志的快照。传输完成后,新服务器会按照与服务器重启后相同的过程打开tablet。当tablet打开tablet数据并重放任何必要的预写日志时,它已经完全复制了开始传输时leader的状态,并可以作为一个全功能的副本来响应Raft RPC。

在我们当前的实现中,新服务器会立即添加为 VOTER 副本。这样做的缺点是,从 3 台服务器配置移动到 4 台服务器配置后,四台服务器中的三台必须确认每个操作。由于新服务器正在复制中,因此无法确认操作。如果另一台服务器在快照传输过程中崩溃,则tablet将无法写入,直到remote bootstrap完成。

为了解决这个问题,我们计划实现一个 PRE VOTER 副本状态。在这种状态下,leader 将发送 Raft 更新并在目标副本上触发remote bootstrap,但在计算配置大多数的大小时不会将其视为投票者。在检测到 PRE VOTER 副本已完全赶上当前日志时,leader将自动提出并提交另一个配置更改,以将新副本转换为完整 VOTER。

当从一个 tablet 中删除副本时,我们遵循类似的方法:当前的 Raft leader提出一个操作,将配置更改为不包含要驱逐的节点的配置。如果这被提交,那么剩余的节点将不再向被驱逐的节点发送消息,尽管被驱逐的节点不会知道它已被删除。当提交配置更改时,其余节点将配置更改报告给 Master,Master 负责清理孤立的副本(参见第 3.4.2 节)。

$ 3.4 Kudu Master

Kudu Master进程负责一些关键的职责:

  • 充当catalog manager,跟踪存在哪些表和tablet,以及它们的schema、所需的复制级别和其他元数据。当创建/修改/删除表时,Master 会在tablets中协调这些操作,确保最终生效。
  • 充当集群协调器,跟踪集群中哪些服务器处于活动状态,并在服务器出现故障后协调数据的重新分配。
  • 充当tablet目录,跟踪哪些tablet server托管每个tablet的副本。

$ 3.4.1 Catalog Manager

Kudu Master 内部维护了一个单tablet 的表,用户无法直接访问该表。master在内部将catalog信息写入此tablet,同时始终在内存中保留catalog的完整写入缓存。

catalog表维护了系统中每张表的少量状态。表结构的当前版本,表的状态(creating, running, deleting等),tablet集合。master通过首先将表记录写入catalog表来为创建表的请求提供服务,以指示 CREATING 状态。异步地,它选择tablet server来托管tablet副本,创建master侧的tablet元数据,并发送异步请求以在tablet server上创建副本。如果大多数副本创建失败或超时,则可以安全地删除该tablet,并使用一组新副本创建一个新tablet。如果 Master 在此操作中失败,表记录表明回滚是必要的,并且master可以从它停止的地方恢复。类似的方法用于其他操作,例如schema变更和删除,其中 Master 确保在将新状态写入自己的存储之前将更改传播到相关的tablet server。在所有情况下,从 Master 到 tablet server的消息都被设计为幂等的,这样在崩溃和重启时,可以安全地重新发送。

由于catalog表本身是持久化在 Kudu tablet 中的,Master 支持使用 Raft 将其持久状态复制到备 master 进程。目前,备份主节点仅充当 Raft follower,不为客户端请求提供服务。在通过 Raft 算法成为leader后,备master扫描其目录表,加载其内存缓存,并按照与master重启相同的过程开始充当活跃master。

$ 3.4.2 集群协调

Kudu 集群中的每个tablet server都静态配置了 Kudu 主服务器的主机名列表。启动时,tablet server向master注册并继续发送tablet报告,指示它们托管的tablet集合。第一个此类tablet报告包含所有tablet的信息。所有未来的tablet报告都是增量的,只包含新创建、删除或修改(例如处理schema变更或 Raft 配置更改)的tablet报告。

Kudu 的一个关键设计点是,虽然 Master 是catalog信息的真实来源,但它只是动态集群状态的观察者。tablet server本身总是对tablet副本的位置、当前的 Raft 配置、tablet的当前schema版本等具有权威性。由于tablet副本通过 Raft 同意所有状态更改,因此每个此类更改都可以映射到特定的 Raft提交的操作索引。这允许 Master 确保所有的 tablet 状态更新都是幂等的并且对传输延迟有弹性:Master 只需比较一个 tablet 状态更新的 Raft 操作索引,如果索引不比 Master 的当前世界观更新,则丢弃它。

这种设计选择让tablet服务器自己承担了很大的责任。例如,Kudu 不是从 Master 检测 tablet server 崩溃,而是将该责任委托给在崩溃机器上具有副本的任何 tablet 的 Raft LEADER 副本。Leader 会跟踪它上次与每个 follower 成功通信的时间,如果它在很长一段时间内都没有成功通信,它就会宣布 follower 死亡并提出 Raft 配置更改以将 follower 从 Raft 配置中驱逐。当此配置更改成功提交后,其余的 tablet servers 将向 Master 发出 tablet 报告,告知其Leader做出的决定。

为了重新获得 Tablet 所需的复制计数,Master 会根据集群的全局视图选择一个 Tablet 服务器来托管新副本。选择服务器后,Master 会建议对 tablet 的当前leader副本进行配置更改。但是,Master 本身无权更改tablet的配置——它必须等待leader副本提出并提交配置更改操作,此时通过tablet报告通知 Master 配置更改成功。如果 Master 的建议失败(例如因为消息丢失),它将顽固地定期重试直到成功。因为这些操作被标记为降级配置的唯一索引,所以它们是完全幂等且无冲突的,即使可能因为fail-over之后master很快发出了几个冲突的建议。

主节点对tablet的额外副本的响应类似。如果 Master 收到一个 tablet 报告,表明一个副本已经从一个 tablet 配置中删除,它会顽固地向被删除的节点发送 DeleteTablet RPC,直到 RPC 成功。为了确保即使在 master 崩溃的情况下也能最终清理,Master 也会发送这样的 RPC 来响应 tablet 报告,该报告识别出一个 tablet 服务器正在托管一个不在最新提交的 Raft 配置中的副本。

$ 3.4.3 tablet目录

为了在没有中间网络跳的情况下有效地执行读写操作,客户端向 Master 查询tablet的位置信息。客户端很“厚”,并维护一个本地元数据缓存,其中包括他们之前访问过的每个tablet的最新信息,包括tablet的分区键范围及其 Raft 配置。在任何时候,客户端的缓存都可能是陈旧的;如果客户端尝试向不再是tabletleader的服务器发送写入,服务器将拒绝该请求。客户端然后联系 Master 以了解新的leader。在客户端收到与其假定的leader通信的网络错误的情况下,它遵循相同的策略,假设tablet可能已经选举了一个新的eader。

将来,如果客户端联系非leader副本,我们计划在错误响应上捎带当前的 Raft 配置。这将防止在leader选举后与主节点进行额外的往返,因为通常follower将拥有最新信息。

由于 Master 在内存中维护所有 tablet 分区范围信息,它可以扩展到每秒大量请求,并以非常低的延迟响应。在运行具有数千个tablet的基准工作负载的 270 节点集群中,我们测量了tablet位置查找 RPC 的第 99.99 个百分点延迟为 3.2 毫秒,第 95 个百分点为 374 微秒,第 75 个百分点为 91 微秒。因此,我们预计在当前目标集群大小下,tablet 目录查找将成为可扩展性瓶颈。如果它们确实成为瓶颈,我们注意到提供陈旧的位置信息总是安全的,因此 Master 的这部分可以在任意数量的机器上进行简单的分区和复制。

$ 4 tablet存储

$ 4.1 概述

Kudu 中tablet存储的实现解决了几个目标:

  1. 快速列扫描,为了提供与同类最佳的不可变数据格式(例如 Parquet 和 ORCFile[7])相媲美的分析性能,高效编码的列存数据文件对于大多数scan来说至关重要。
  2. 低延迟随机更新,为了提供对任意行更新或读取的快速访问,我们需要 O(lgn) 的随机访问查找复杂度。
  3. 性能的一致性,根据我们支持其他数据存储系统的经验,我们发现用户愿意牺牲峰值性能以实现可预测性。

为了同时提供这些特性,Kudu 没有重用任何预先存在的存储引擎,而是选择实现新的混合列式存储架构。

$ 4.2 RowSets

Kudu 中的 Tablets 本身被细分为更小的单元,称为 RowSets。一些 RowSet 仅存在于内存中,称为 MemRowSet,而另一些存在于磁盘和内存的组合中,称为 DiskRowSet。任何给定的活动(未删除)行都存在于一个 RowSet 中;因此,行集形成不相交的行集。但是需要注意的是,不同RowSets的主键区间可能会相交。

在任何时候,tablet都有一个 MemRowSet 存储所有最近插入的行。由于这些存储完全在内存中,后台线程会定期将 MemRowSet 刷新到磁盘。这些刷新的调度在第 4.11 节中有更详细的描述。

当一个 MemRowSet 被选择刷新时,一个新的、空的 MemRowSet 被换入以替换它。之前的 MemRowSet 写入磁盘,成为一个或多个 DiskRowSet。这个刷新过程是完全并发的:在刷新过程中,读者可以继续访问旧的 MemRowSet,刷新 MemRowSet 中行的更新和删除被仔细跟踪,并在刷新过程完成后滚动到磁盘数据中。

$ 4.3 MemRowSet 实现

MemRowSets 是由内存中并发 B 树实现的,带有乐观锁,大致基于 MassTree[22] 的设计,有以下变化:

  1. 我们不支持从树中移除元素。相反,我们使用 MVCC 记录来表示删除。MemRowSets 最终会刷新到其他存储,因此我们可以将这些记录的删除推迟到系统的其他部分。

  2. 同样,我们不支持树中记录的任意就地更新。相反,我们只允许不改变值大小的修改:这允许原子性的比较并交换操作将变更附加到每个记录的链表。

  3. 我们用next指针将叶节点链接在一起,如 B+-tree[13]。这提高了我们的顺序扫描性能,这是一项关键操作。

  4. 我们没有实现完整的“trie树”,而只是实现一棵树,因为与原始应用程序相比,我们不太关心极高的随机访问吞吐量。

为了优化随机访问的扫描性能,我们使用稍大的内部节点和叶节点,每个节点的大小为 4 个缓存行(256 字节)。

与 Kudu 中的大多数数据不同,MemRowSets 以行布局存储行。这仍然提供可接受的性能,因为数据始终在内存中。尽管选择了行存储,但为了最大限度地提高吞吐量,我们利用 SSE2 内存预取指令在我们的scanner之前预取一个叶节点,并使用 LLVM [5] 进行 JIT 编译记录projection操作。这些优化相对于简单的实现提供了显着的性能提升。

为了形成插入 B 树的键,我们使用 3.2 节中描述的保序编码对每一行的主键进行编码。这允许仅使用 memcmp 操作进行比较的高效树遍历,并且 MemRowSet 的排序特性允许对主键范围或单个键查找进行有效扫描。

$ 4.4 DiskRowSet 实现

当 MemRowSets flush到磁盘时,它们变成了 DiskRowSets。在刷新 MemRowSet 时,我们在每 32 MB IO 后滚动 DiskRowSet。这确保了没有太大的 DiskRowSet ,从而允许有效的增量压缩,如后面第 4.10 节所述。因为 MemRowSet 是有序的,所以被flush的 DiskRowSet 本身也将是有序的,并且每个滚动的segment将有不相交的主键区间。

DiskRowSet 由两个主要组件组成:基本数据和增量存储。基本数据是 DiskRowSet 中行的按列组织的表示。每列都单独写入磁盘的单个连续数据块中。列本身被细分为小页面以允许粒度随机读取,并且嵌入的 B 树索引允许根据每个page在rowset中的序数偏移量高效查找。列page使用各种编码进行编码,例如字典编码、bitshuffle[23] 或前端编码,并且可以选择使用通用二进制压缩方案(例如 LZ4、gzip 或 bzip2)进行压缩。这些编码和压缩选项可以由用户在每列的基础上明确指定,例如指定一个大的不经常访问的文本列应该被gzip压缩,而通常存储小整数的列应该是bit压缩的。Kudu 支持的几种page格式与 Parquet 支持的page格式相同,我们的实现与 Impala 的 Parquet 库共享许多代码。

除了为表中每个用户指定的列flush之外,我们还编写了一个主键索引列,它存储了每行的编码主键。我们还flush了一个分块的布隆过滤器 [10],它可用于根据一行编码的主键来测试它是否可能存在。

由于列式编码很难就地更新,因此基础数据中的列一旦flush就被认为是不可变的。相反,更新和删除通过称为delta stores的结构进行跟踪。Delta stores要么是内存中的 DeltaMemStores,要么是磁盘上的 DeltaFiles。DeltaMemStore 是一个并发 B 树,它共享上述实现。DeltaFile 是一个二进制类型的列块。在这两种情况下,增量存储都维护从(行偏移量、时间戳)元组到 RowChangeList 记录的映射。行偏移量只是 RowSet 中一行的序数索引——例如,具有最低主键的行偏移量为 0。时间戳是最初写入操作时分配的 MVCC 时间戳。RowChangeList 是行更改的二进制编码list,例如SET column id 3 = ‘foo’DELETE

在为 DiskRowSet 中的数据更新提供服务时,我们首先查询主键索引列。通过使用其嵌入的 B 树索引,我们可以有效地查找到包含目标行的page。使用page级元数据,我们可以确定该page内第一个单元格的行偏移量。通过在page内搜索(例如通过内存中的二分搜索),我们可以计算目标行在整个 DiskRowSet 中的偏移量。确定此偏移量后,我们将新的增量记录插入行集的 DeltaMemStore。

$ 4.5 Delta flush

因为 DeltaMemStore 是内存存储,所以它的容量有限。调度 MemRowSets flush的相同后台进程也调度 DeltaMemStores flush。刷新 DeltaMemStore 时,会换入一个新的空存储,同时将现有存储写入磁盘并成为 DeltaFile。DeltaFile 是一个简单的二进制列,其中包含以前在内存中的数据的不可变副本。(同一个列对应多个deltaFile?)

$ 4.6 插入路径

如前所述,每个tablet都有一个单独的 MemRowSet,用于保存最近插入的数据;然而,仅仅将所有insert直接写入当前 MemRowSet 是不够的,因为 Kudu 强制执行主键唯一性约束。换句话说,与许多 NoSQL 存储不同,Kudu 将 INSERT 与 UPSERT 区分开来。

为了强制执行唯一性约束,Kudu 必须在插入新行之前查询所有现有的 DiskRowSet。因为每个tablet可能有成百上千个 DiskRowSet,所以要查询的 DiskRowSet 数量和在 DiskRowSet 中进行高效查找,有效地完成这项工作很重要。

在 INSERT 操作中,为了挑选一组 DiskRowSets 来查询,每个 DiskRowSet 存储了现有key的 Bloom filter。因为新key永远不会插入到现有的 DiskRowSet 中,所以这个布隆过滤器是静态数据。我们将布隆过滤器分成 4KB 的page,每个page对应一个小范围的key,并使用不可变的 B 树结构索引这些page。这些page及其索引缓存在服务器范围的 LRU 页面缓存中,确保大多数 Bloom filter访问不需要物理磁盘搜索。

此外,对于每个 DiskRowSet,我们存储最小和最大主键,并使用这些键边界在区间树中索引 DiskRowSet。这进一步剔除了查询任何给定的key查找DiskRowSets 的集合。第 4.10 节中描述的后台压缩进程重新组织了 DiskRowSet,以提高基于区间树的剔除的有效性。

对于任何无法剔除的 DiskRowSet,我们必须退化为在编码主键列中查找要插入的键。这是通过该列中嵌入的 B 树索引完成的,这确保了在最坏情况下对数量级的磁盘搜索次数。同样,这种数据访问是通过page cache执行的,确保对于热点区域的key,不需要物理磁盘寻址。

$ 4.7 读取路径

与 X100[11] 之类的系统类似,Kudu 的读取路径总是分批运行,以分摊函数调用成本并为循环展开和 SIMD 指令提供更好的机会。Kudu 的内存批处理格式由一个顶级结构组成,该结构包含指向正在读取的每一列的较小的block的指针。因此,批处理本身在内存中是列式的,这避免了从列式磁盘存储复制到批处理时的任何偏移计算成本。

当从 DiskRowSet 读取数据时,Kudu 首先确定是否可以使用扫描的范围谓词来剔除此 DiskRowSet 中的行范围。例如,如果扫描设置了主键下限,我们在主键列内执行查找以确定下限行偏移量;我们对任何key上限都做同样的事情。这将key范围谓词转换为行偏移范围谓词,因为它不需要昂贵的字符串比较,所以更容易满足。

接下来,Kudu 一次扫描一列。首先,它将目标列寻找到正确的行偏移量(0,如果没有提供谓词,或者起始行,如果它之前确定了下限)。接下来,它使用特定页面编码的解码器将源列中的单元格复制到我们的行批处理中。最后,它会根据当前扫描的 MVCC 快照查询增量存储,以查看是否有任何后续更新已将单元替换为较新版本,并根据需要将这些更改应用于内存中的batch。因为增量是基于数字行偏移而不是主键存储的,所以这个增量应用过程非常高效:它不需要任何每行分支或昂贵的字符串比较。

在对projection中的每一行执行此过程后,它会返回批处理结果,这些结果可能会被复制到 RPC 响应中并发送回客户端。tablet服务器在服务器端为每个scanner维护有状态的迭代器,这样连续的请求不需要重新搜索,而是可以从每个列文件中的断点继续。

$ 4.8 延迟物化

如果已经为scanner指定了谓词,我们将为列数据执行延迟物化。特别是,我们更倾向于在读取任何其他列之前读取具有相关范围谓词的列。在读取每个这样的列之后,我们评估相关的谓词。当谓词过滤此批次中的所有行的情况下,我们不读取其他列。这在应用选择性谓词时提供了显着的速度提升,因为永远不会从磁盘读取其他选定列的大部分数据。

$ 4.9 Delta Compaction

由于增量不以列式存储,tablet的扫描速度将随着越来越多应用于基础数据的增量而降低。因此,Kudu 的后台maintenance manager会定期扫描 DiskRowSets 以查找累积大量增量(由基本数据行计数和增量计数之间的比率确定)的情况,并调度增量compaction操作,将这些增量合并回基础数据列。

特别是,增量压缩操作识别了大多数增量仅应用于列的子集的常见情况:例如,SQL 批处理操作通常只更新宽表中的一列。在这种情况下,增量压缩只会重写该单个列,避免其他未修改列上的 IO。

$ 4.10 RowSet Compaction

除了压缩增量到基础数据之外,Kudu 还会在称为 RowSet Compaction的过程中定期将不同的 DiskRowSet 压缩在一起。此过程基于键合并两个或多个 DiskRowSet ,从而产生排序的输出行流。输出被写回新的 DiskRowSet,每 32 MB 再次滚动一次,以确保系统中的 DiskRowSet 不会太大。

RowSet Compaction有两个目标:

  1. 我们借此机会删除已删除的行。
  2. 此过程减少了在key范围内重叠的 DiskRowSet 的数量。通过减少 RowSet 重叠的数量,我们减少了预期包含tablet中随机选择的键的 RowSet 的数量。这个值作为布隆过滤器查找次数的上限,因此磁盘查找,预计为tablet内的写操作提供服务?。

$ 4.11 调度维护

如上所述,Kudu 有几种不同的后台维护操作,它执行这些操作以减少内存使用并提高磁盘布局的性能。这些操作由在 tablet server 进程中运行的维护线程池执行。为了实现一致性能的设计目标,这些线程一直运行,而不是由特定事件或条件触发。完成一个维护操作后,调度程序进程会评估磁盘存储的状态,并根据一组旨在平衡内存使用、预写日志保留和未来性能的启发式方法来选择要执行的下一个操作。读和写操作。

为了选择要compact的 DiskRowSet,maintenance调度器解决了一个优化问题:给定 IO 预算(通常为 128 MB),选择一组 DiskRowSet,这样压缩它们将减少预期的查找次数,如上所述。这个优化结果是一系列众所周知的整数背包问题的实例,并且能够在几毫秒内有效地解决。

因为维护线程总是运行小的工作单元,所以操作可以对工作负载行为的变化做出快速反应。例如,当插入工作量增加时,调度程序会迅速做出反应并将内存中的存储flush到磁盘。当插入工作量减少时,服务器会在后台执行compaction以提高未来写入的性能。这提供了性能的平滑过渡,使开发人员和操作人员能够更轻松地执行容量规划并估计其工作负载的延迟情况。

$ 参考

kudu.pdf (opens new window)

Kudu论文解读: Fast Analytics on Fast Data (上) (opens new window)

Kudu论文解读: Fast Analytics on Fast Data (下) (opens new window)

更新时间: 8/13/2021, 2:28:52 AM