虽然我并不完全羡慕我们现在的实习生(因为,你知道,整个在家工作的事情),但我承认我发现自己在回忆我自己是实习生的时候。我仍然惊讶于工程团队让我接近他们所做的任何事情。
当我四年前第一次实习的时候,我们已经宣布了一个公正的代码黄色,以集中精力稳定CRDB。我加入了新成立的分布式查询执行团队,但现在它的重点在其他地方,这对我来说意味着可以自由地充实分布式哈希和合并连接,少数聚合基元(想想SUM,COUNT, DISTINCT等),以及一些排序算法。
这足以让我回来参加第二次实习。这一次我把我亲爱的朋友Bilal也带来了,他也去实习了两次。我甚至设法把我的兄弟(一个严格意义上的差劲的工程师)偷偷带进来,成为两次实习生。
所有这些都是想说,我认为在这里的实习可以是相当棒的。CRDB是一个很酷的工作系统,而且我们仍然处在这样一个阶段:我们很乐意让初级工程师承担在其他地方只能由更高级的人承担的工作。这对我来说是真的,我想说的是,这也适用于我们最近的这批人。
这些年来,我们已经接待了好几个实习生(并聘用了很多人担任全职工作),他们都在从事值得发表长篇博文的项目。然而,今天我们将突出最近一批项目中的两个项目,并对其余的项目进行简单的处理。
基于阅读的压实启发式方法
Aaditya Sondhi在我们的存储团队实习,从事Pebble的工作,这是一个基于日志结构的合并树(简称LSM)的存储引擎。Aaditya致力于为Pebble引入基于读的压缩,但在深入了解这意味着什么之前,我们首先需要了解什么是读放大和压缩。
LSM中的压实和读扩增
在LSM中,键和值以排序字符串的形式存储在称为SST(排序字符串表)的不可变的blob中。SSTs在多个层次(L1,L2,......)上堆叠,在一个层次内不重叠,当搜索一个与多个SSTs重叠的键时(必须跨越多个层次),在较高层次发现的键被认为是权威的。这就给我们带来了读取放大率:每次逻辑操作所做的物理工作量(读取的字节数、磁盘搜索次数、解压的块数等)。当从两级LSM中读取一个关键的k ,如果在第一级中没有找到它,我们可能不得不在两级中翻阅。
这又给我们带来了压缩的问题。当数据流入更高层次的SST时,LSM通过将它们压缩到(更少但更大的)较低层次的SST中来保持一个 "健康 "的结构。在一个层面上(抱歉),这让LSM回收存储(范围删除墓碑和较新的修订掩盖了旧的值),但也有助于约束维持固定工作负载所需的读取IOPS。像所有的事情一样,这与保持合理的{写,空间}放大的需要是相抵触的,压缩率直接影响到这一点。
图1.一个SST压缩;L1的SST与两个L2的SST重叠并被压缩到其中。
(题外话:关于存储引擎在资源利用率方面的特点,而不是无条件的 "吞吐量 "或 "延迟",有一些东西是值得一提的。像$/tpmC这样的系统范围的衡量标准也是同样的例子。这些感觉相对来说更容易推理,对容量规划更有用,而且容易验证)。
基于读数的LSM压缩并不是一个新的想法。它最初是在google/leveldb中实现的,后来又在facebook/rocksdb中放弃了。至于Go的重新实现(golang/leveldb,顺便说一下,我们从那里分叉了Pebble),它还没有移植这个启发式方法。使用一个专门的存储引擎的部分动机是为了让我们能够准确地拉动这样的线程。
我们假设,通过为常读的关键范围安排压缩,我们可以降低后续读取的放大率,从而降低资源利用率,提高读取性能。在实施过程中,我们借鉴了google/leveldb中的想法。对于每一个返回用户键的定位操作(如Next,Prev,Seek, 等等),我们对键的范围进行采样(由可调整的旋钮来调节)。采样过程检查了LSM中各层的SST是否重叠。如果发现一个常读的SST与较低层次的SST重叠,它的分数就会更高,以优先压缩。
$ benchstat baseline-1024.txt read-compac.txt
name old ops/sec new ops/sec delta
ycsb/C/values=1024 605k ± 8% 1415k ± 5% +133.93% (p=0.008 n=5+5)
name old r-amp new r-amp delta
ycsb/C/values=1024 4.28 ± 1% 1.24 ± 0% -71.00% (p=0.016 n=5+4)
name old w-amp new w-amp delta
ycsb/C/values=1024 0.00 0.00 ~ (all equal)
$ benchstat baseline-64.txt read-compac.txt
name old ops/sec new ops/sec delta
ycsb/B/values=64 981k ±11% 1178k ± 2% +20.14% (p=0.016 n=5+4)
name old r-amp new r-amp delta
ycsb/B/values=64 4.18 ± 0% 3.53 ± 1% -15.61% (p=0.008 n=5+5)
name old w-amp new w-amp delta
ycsb/B/values=64 4.29 ± 1% 14.86 ± 3% +246.80% (p=0.008 n=5+5)
图2.基准测试显示了基于读的压缩对吞吐量、读放大和写放大的影响。
正如预期的那样,我们发现基于读的压缩导致了对重读工作负载的明显改善。我们的基准运行YCSB-C(100%读取),使用1KB写入,读取放大率减少了71%,吞吐量增加了133%。在YCSB-B(95%读取)中,使用小值读/写(64字节),我们将读取放大率降低了15%,导致吞吐量增加了20%。这些基准直接针对Pebble,在参数调整方面还有一些工作要做(在这个过程中,我们必然要牺牲一些写放大),但结果是令人鼓舞的。
查询拒绝列表(和我们的RFC过程)
Angela Wen在我们的SQL体验团队实习,该团队拥有SQL客户端和数据库的前沿阵地。在实习期间,Angela致力于引入一种机制,以阻止某些类别的查询在数据库中运行。这是因为我们的云计算SRE在运行大型的CRDB装置,并且希望在紧急情况下能够拒绝查询(死亡)(想想 "断路器")的能力。
安吉拉的经历反映了给予实习生的那种广泛的回旋余地,我认为我们比其他地方做得更好。一个通用的查询拒绝列表是一个非常开放的问题,你可以为它设计许多角色,而且需要刻意地努力来建立共识。我们用来组织这些对话的过程是RFC,而我们最后也在这里写了一个。
RFC和随后的讨论澄清了谁是目标用户,"必须有的"/"最好有的",对各种类型的可否认查询进行了分类,最重要的是,概述了否认本身的实际机制。尽管我对RFC有种种不满,但我发现实际写RFC的过程很有教育意义。它可以促进对一个组件的设计的真正代理权,并且作为一个教学工具工作得很好(同时我认为有公共的设计文件来与同样喜欢查询拒绝列表的朋友分享也是很酷的)。
我们最终放弃了我们最初的建议,即实施基于文件安装的重组词的denylists(这里的争论是围绕着可用性、部署等),而支持集群设置的形式。
SET CLUSTER SETTING feature.changefeed.enabled = FALSE;
SET CLUSTER SETTING feature.schema_change.enabled = TRUE;
配置的改变通过流言蜚语的方式在整个集群中传播。各个节点监听这些更新,并使用deltas来保持内存块缓存(对不起)的最新状态。在查询执行过程中,这将被检查,以确定它是否是一个允许的操作。
如前所述,在这个过程中,我们放弃了很多备选的设计,并因此得到了更好的结果。我们重新调整了我们的范围,使之专注于某些类别的查询,而不是更细化地匹配特定的查询。这是在观察到之前的事件中,绝大多数有问题的查询都被很好地理解,并且可以在结构上进行分组/门禁批发。也就是说,我们将我们的工作模块化,以便根据需要简单地引入新的类别。
可观察性、设计代币、数据丢失修复,等等。
本学期我们还接待了其他几个实习生,他们的个人贡献有很多值得一提。我们的计划通常是让人们在一个或两个 "主要 "项目上工作,并以"启动 "项目为基础。在此,我们将简要介绍一下这些项目的情况。
图3.一个全表扫描的查询执行计划,然后是AVG 。
Cathy Wang在我们的SQL执行团队实习,致力于改善运行中查询的可观察性。我们有一些现有的基础设施来显示各种执行统计数据。Cathy在此基础上加入了网络延迟的细节(对于调试在地理分布的集群中运行的查询非常有用),对我们的追踪进行了结构化,以分解系统中各层所花费的时间,并在我们的追踪中加入了内存利用率,以准确地反映出在执行过程中的任何一点上有多少内存被使用。最后这一点值得详细说明。Go的垃圾收集器并没有给我们提供对分配的细粒度控制,为此我们不得不设计自己的内存核算/监控基础设施,以密切跟踪查询生命周期内的使用情况。通过公开这些内部统计数据,我们希望开发人员能够更好地了解各个查询的内存占用情况,并对其进行相应调整。
设计令牌
Pooja Maniar在云计算方面实习,特别是在我们的控制台团队。她所做的项目之一是巩固和规范我们的"设计标记"。把这些看作是对视觉属性的抽象,取代硬编码调色板的变量,字体,按下按钮的盒状阴影,等等。这里的动机是为了限制开发者必须做出的设计决定的数量,无论是在特定的代码、UI组件之间的选择,等等。我们希望创建并将指南提升到一个集中的、共享的repo中,然后将其整合到我们的几个控制台页面中(可通过数据库本身和我们的云服务访问)。当时我们也正在进行品牌更新,Pooja的大统一帮助确保了整个品牌的一致性。
定额恢复
Sam Huang在KV团队实习(他们让我指导这位同学),我们所做的项目之一是在CRDB中引入一个法定人数恢复机制。因为CRDB是建立在筏式复制的密钥范围之上的,当一个集群永久地失去了给定的密钥集的法定人数(想想持续的节点/磁盘故障),它就无法从中恢复。这必然会造成数据损失,但我们仍然希望有能力将这些密钥用纸覆盖,并提供手动修复的工具。萨姆致力于引入一个带外机制来 "重置 "给定密钥范围的法定人数,并且在某种程度上,我们能够利用现有的Raft机器来这样做。这来自于这样的观察:如果我们构建一个合成快照(使用现有副本的数据作为种子,如果有的话),并将其配置为指定一组新的参与者,我们基本上可以欺骗底层复制子系统来恢复这个密钥范围的法定人数。我们的合成快照增加了相关的计数器以 "追随 "现有的数据,这也从系统中清除了旧的复制。
Jayant Shrivastava在我们的SQL Schemas团队实习,他在这里度过了他的时间,使我们的schemas基础设施更加坚固。CRDB使用了一些先进的测试策略来确保正确性和稳定性,包括使用模糊器、变态和混乱测试、Jepsen等等。最近在这个领域观察到了一些潜在的脆弱性,Jayant充实了一个同等的测试线束,但重点是模式变化。我们构建了一个工作负载生成器来执行随机生成的DDL语句,在单个事务的范围内执行。这些语句在飞行中生成和删除表,对具有随机类型的列做同样的事情,并与针对这些表/列的语句同时执行。我们通过断言系统的不变量而不是具体的输出(比如 "从某一列读取的事务应该在随后的读取中总是能找到它"),在这里利用了变形方法。综合起来,我们能够覆盖大量可能的交错空间,并在这个过程中发现了几个 关键的 错误。
进口兼容性
莫妮卡-徐从她有抱负的音乐事业中暂时抽身出来,在我们的批量IO团队实习。她的团队主要负责让数据尽可能快地进出CRDB(特别是导入/导出和备份/恢复)。莫妮卡在这个领域做出了一些贡献,包括对转储文件进行进度跟踪,支持干式 导入,以及提高pg_dump 的兼容性。后者有一些问题需要解决,因为CRDB只支持Postgres语法的一个子集,这在处理pg_dump 文件时可能会有问题。Monica帮助解决的一系列问题是,在处理潜在的破坏性导入指令时,"合理的行为 "是什么。考虑到DROP TABLE [IF EXISTS] ,或CREATE VIEW ,这特别棘手,因为它存储了它所构建的查询的结果,这些结果在导入过程中会发生变化。莫妮卡在形成这些判断时与我们的产品团队合作(我们现在只是通过指导性的信息传递给用户),并帮助大大 减轻了从现有安装中迁移出来的开发人员的上机体验。
分别的想法
如果你还在这里,并且感兴趣,请查看我们的职业页面。不要被这些数据库术语所迷惑,我们中的大多数人在进来时都不知道这些术语。