在过去的这个秋天,我有机会在Cockroach实验室相对较新的Bulk I/O团队实习。我们团队的工作重点是改善CockroachDB的批量数据操作。我的工作包括在导入或备份数据时创造更好的体验,而无需使用外部存储服务,如亚马逊的S3或Azure的Blob服务。CockroachDB支持与节点本地存储系统中的文件进行交互,通过一个名为nodelocal 的功能。然而,由于其实现方式,这造成了混乱和误用,我所负责的项目涉及到完善用户体验,围绕nodelocal 。
CockroachDB如何处理外部存储
为了理解我的项目,首先要获得一些关于CockroachDB如何与外部存储系统互动的背景知识。大量的数据操作,如IMPORT 、BACKUP 、或RESTORE ,经常需要与外部存储系统进行交互。
让我们以IMPORT 为例。如果你想把数据导入你的集群,我们提供了几个选项供你选择。第一个也是推荐的选项是使用云提供商的存储系统。你的导入语句看起来会是这样的。
IMPORT INTO my_table (id, data)
CSV DATA (s3://my-bucket/path/file.csv’);
我们提供的第二个选项是一个类似的机制,从一个HTTP服务器上读取文件,你可以在本地提供服务,离开你的电脑。
IMPORT INTO my_table (id, data)
CSV DATA (http://mylocalserver/file.csv’);
第三种选择,是使用我们所说的nodelocal :在节点的本地文件系统的目录下读写文件。该声明看起来如下。
IMPORT INTO my_table (id, data)
CSV DATA (‘nodelocal:///path/file.csv’);
乍一看,这似乎是最简单的方法,因为它不依赖于访问云提供商的存储系统或单独的文件服务器,而用户经常想用它来把他们的数据导入集群中。这个想法是,你可以把你的导入文件放到一个节点的本地文件系统中,然后导入语句就会去导入它。然而,在实践中,在我对CockroachDB 20.1进行修改之前,它并不总是那么简单。
之前关于 "nodelocal "存储的问题
关于nodelocal ,有几个微妙的地方值得注意,其中很多都是CockroachDB用户的困惑来源。首先,nodelocal URL,nodelocal://path/file.csv,指定了文件的路径,但没有指定文件所在的节点。CockroachDB被设计成一个分布式的多节点系统,IMPORT ,通过使用许多节点来IMPORT 数据,从而利用了这一点。在旧版本的CockroachDB中,如果用户将导入文件放在节点1上,但节点2最终被指定运行部分导入进度,就会失败,因为节点2在其本地文件系统中查找时,无法找到该文件。
这种行为很难向用户澄清,而且在使用nodelocal 时经常会引起混乱,特别是当它在单节点集群上测试或开发时 "刚刚好",但在进入多节点生产部署时却会出现混乱。
一个类似的问题影响了BACKUPs到nodelocal ,它也会成功,但不能被RESTORE'd。在BACKUP ,每个节点将自己的那部分数据写入备份文件,当使用nodelocal ,备份文件会进入每个节点自己的本地文件系统。
然而,这些备份是不可恢复的,因为备份完全分散在集群中各节点的本地文件系统中,成了碎片。这也让用户感到困惑,因为他们无法恢复成功的备份。

让 "nodelocal "在集群的任何地方工作
上述两种情况的主要问题是,节点1上的nodelocal 与节点2上的nodelocal (除非它恰好被配置为指向nfs或其他)。为了改善上述情况下的用户体验,我们计划了一个两部分项目。项目的第一部分也是最大的部分是创建一个节点间的文件交换服务,它可以在节点间分享大文件。我们现在将支持指定一个nodelocal URL,如下所示。
IMPORT INTO my_table (id, data)
CSV DATA (‘nodelocal://2/path/file.csv’);
其中2是节点的节点ID,该节点的文件系统包含预定的导入文件。为了支持这一点,需要读取该文件的节点将从存储该文件的节点中获取该文件,然后导入该文件。
这个服务,我们命名为BlobService,也支持写文件,因此可以在运行下面的时候做可恢复的备份。
BACKUP db.table TO `nodelocal://1/path/backup`
AS OF SYSTEM TIME ‘-10s’;
RESTORE db.table FROM `nodelocal://1/path/backup`;
在上面的例子中,我们在每个节点上并行写入完整的备份,每个节点使用BlobService直接写入节点1上的目标路径。然后我们可以用导入的方式恢复备份,RESTORE 中的每个节点直接从节点1的文件系统中获取。
为了将BlobService集成到IMPORT 和BACKUP 等功能中围绕外部存储交互的现有基础设施中,我们需要实现ExternalStorage接口,我们支持的每个外部存储系统都会实现该接口,包括GCP、AWS、Azure、HTTP,当然还有nodelocal 。这意味着我们的BlobService需要被ExternalStorage的nodelocal 实现所使用,该接口如下。
type ExternalStorage interface {
ReadFile(ctx context.Context, basename string) (io.ReadCloser, error)
WriteFile(ctx context.Context, basename string, content io.ReadSeeker) error
ListFiles(ctx context.Context) ([]string, error)
Delete(ctx context.Context, basename string) error
Size(ctx context.Context, basename string) (int64, error)
}
我们决定,可以在集群中的每个节点上创建一个运行于gRPC的BlobService。然后我们创建了一个BlobClient,它能够拨通任何其他节点的BlobService并读取文件。BlobClient支持与ExternalStorage 完全相同的接口,这使得它可以轻松集成到我们现有的逻辑中。这些变化,加上添加节点ID的URL变化,固定了nodelocal 的行为,更接近用户的期望。
最后一公里:将文件上传到nodelocal
我的项目的第二部分涉及使上传文件到nodelocal 更加容易。所有上述改进都大大改善了使用nodelocal 的体验,然而,当我们观察围绕IMPORT 使用案例的用户旅程时,大多数用户通常从他们的笔记本电脑或工作站上的文件开始,有时仍然会被卡住,只是把这些文件放到他们的节点可以IMPORT 的地方。在某些情况下,用户可能通过负载均衡器与他们的集群有一个SQL连接,但没有直接的文件系统访问权来写到nodelocal IO目录。或者即使他们有,也要弄清楚它在哪里,并把文件放在正确的地方,这是一个经常性的混乱的根源。我们希望通过允许用户通过他们在SQL shell中已经拥有的相同的SQL连接无缝上传文件到CockroachDB来缓解这一问题。
为了解决这个问题,我们利用了COPY 语句,它使用一个特殊的协议将大量数据传输到数据库中。我们对COPY 语句的处理方式做了一些细微的改变,将发送的数据写入用户指定的位置的文件中,而不是像通常那样将其作为行插入到表中。然后,在我们的Cockroach CLI中,这被包装成一条命令,这样,用户现在可以运行以下命令。
cockroach nodelocal upload \
/path/to/localfile.csv /destination/path/file.csv
增加这一功能后,想要在本地机器上IMPORT 文件的用户可以使用一个全新的工作流程,不需要依赖任何外部存储服务或文件服务器。他们现在可以使用Cockroach CLI和现有的SQL连接字符串,从他们的笔记本电脑上直接上传他们的导入文件,然后使用nodelocal URIIMPORT 该文件。希望这能让新用户更快地进入CockroachDB,并改善开发者使用nodelocal 的体验。
在Cockroach Labs的批量I/O团队工作
希望你喜欢阅读我的项目。在过去的4个月里,在Cockroach Labs的Bulk I/O团队工作真的很有意义。我的项目涉及到跨团队合作,因为很多工作跨越了Cockroach基础设施的所有层面。也就是说,从维护pgwire协议,特别是其COPY 子协议的SQL团队,到维护和调整我们许多高吞吐量流式gRPC服务的KV和复制层。这是一个很好的团队,让我对数据库系统的不同层次有了一个大致的了解,以及如何在分布式系统的世界里工作。非常感谢我的队友David和Lucy监督我的项目并指导我完成实习。感谢Cockroach实验室的每一个人,让我度过了令人惊奇的4个月!