规模化简化

Simplicity at Scale


Frank Wessels 是 Sneller 的创始人,之前在 MinIO 工作。

简介

在 MinIO 工作期间,我学到的最重要的教训或许是关于简单性的。如果你想要实现(真正的)可扩展性,简单性至关重要。这贯穿 MinIO 的架构和软件设计,从前到后,并推动了我在下文中将要描述的 Sneller 的重要决策。

首先,一个关键的决定是避免使用任何(外部)存储系统或数据库来存储元数据。虽然这在最初看来很有吸引力,可以“快速入门”,但它会增加大量的复杂性,这在 TB 或 PB 级规模及更高规模下会变得更加具有挑战性。想象一下,你的软件正在正常运行,但另一个依赖的系统却成为了瓶颈和/或关键依赖。无论你喜欢与否,你最终都会成为这个系统的专家(而且可能比你自己的系统和软件更了解它)。

其次,坚持“单二进制”方法极大地简化了设置和安装(实际上几乎完全避免了安装)。只需下载一个可执行文件,你就可以开始使用,并且同一个可执行文件可以在从边缘设备(甚至 Raspberry Pi)到提供 100 GB/秒网络吞吐量的最大服务器安装中运行。此外,没有参数需要调整,同一个二进制文件可以在 MinIO 允许的不同操作模式下使用(文件系统模式与擦除编码模式)。

第三,对于 MinIO 多服务器模式,我们决定构建自己的分布式锁机制。虽然市面上有各种共识协议机制,我们对所有这些机制都进行了评估,但最终我们决定,这些机制都不适合 MinIO 架构,并且对于我们正在寻找的“简单”机制来说过于复杂。此外,使用任何一个系统都会违反我们的单二进制方法,并极大地使安装过程复杂化,并可能成为维护负担。

最后,但并非最不重要的一点是,严格遵守“纯”Golang 方法意味着 MinIO 可以保持其开发过程敏捷和灵活(直到今天,你仍然可以在几秒钟内 `git clone` minio 并构建它)。但为了做到这一点,我们必须避免任何依赖于 `cgo` 之类的工具来包含高性能算法。鉴于擦除编码和位腐败检测是任何对象存储系统的关键算法,我们在 Golang 本机汇编中为各种架构(如英特尔、ARM 和 PowerPC)优化了这些算法,并付出了相当大的努力(如果你想了解更多信息,请参见 [此处](https://blog.min-io.cn/intel_vs_gravitron/))。

Sneller 的指导原则

Sneller 是一个专为 JSON 设计的高性能 SQL 引擎。使用 Sneller,你可以在存储在 S3 上的深度嵌套的半结构化 JSON 的大型 TB 级数据集上运行交互式 SQL。目标用例是事件数据管道,例如安全、可观察性、运维、用户事件以及传感器/物联网。我们将简要介绍 Sneller 及其方法,然后向你展示如何将其应用于 MinIO。

Sneller 的性能来自于其 250 多个核心原语中对 SIMD 的广泛使用,特别是 AVX-512 汇编。主引擎能够在每个核心上并行处理多个通道,从而实现非常高的处理吞吐量。

下面,我将描述 Sneller 架构的指导原则

不可使用本地磁盘

从一开始,我们就决定不使用任何形式的本地磁盘存储,如 NVMe 或 SSD。如你所知,根据过去的经验,我们非常清楚在以正确的方式使用时,对象存储的速度有多快。我们的云仅仅是一个核心、内存和快速网络的集合,仅此而已。

你应拥有自己的数据(或:我们不应拥有你的数据)

鉴于我们完全依赖对象存储来存储所有数据和状态,我们决定所有数据都应在我们的客户控制之下。也就是说,我们不会将潜在的 PB 级数据导入或摄取到“我们的”云中,而是会在我们客户的存储桶中就地处理数据。这对我们的客户来说最大的优势在于他们可以完全控制自己的数据,这正是应该如此。当然,这也意味着我们无法通过有意地使数据导出变得困难且昂贵来“控制”我们的客户(规模越大,这一点就越重要)。

你无需进行规划

S3 API 最好的方面之一是 _没有_“免费”调用。不仅 S3 或一般的对象存储具有令人难以置信的耐用性和弹性(如果你在自己的复制中构建了这些特性,就很难实现),而且你不必再担心“空间不足”这一事实,这让人感觉非常轻松。

在实践中,在构建架构时,很难准确地预测使用模式以及这将如何影响数据的生成(=存储)以及数据的处理。将所有状态保留在对象存储中真正地将存储与计算分离,并允许你根据实际使用情况而不是某个固定的或预定义的比率(这可能是也可能不是你需要的,而且往往不是你需要的)来完全独立地扩展两者。

你无需使用模式

现代数据堆栈已在 REST API 上收敛到 JSON,而 JSON 已成为网络的实际“通用语言”。越来越多的 API 允许自定义有效负载,这些有效负载在不同记录之间会发生变化,甚至会(意外地)更改相同字段或列名称的“类型”。这会导致“映射爆炸”或“模式漂移”之类的现象,这些现象在大规模情况下(以及当你检测到它时,你可能已经丢失了大量数据)非常难于检测。

从根本上说,使用 JSON,真正解决此问题的唯一方法是 100% 的模式在读取时定义。也就是说,能够在查询数据时处理多种类型。这就是为什么 Sneller 支持 `WHERE foo.bar = 1 OR foo.bar = "one" OR foo.bar = TRUE` 形式的查询,并在遍历数据时根据字段的类型选择正确的比较。

你无需建立索引

传统上,数据库/查询系统依赖于各种索引来加快查询速度。虽然这些方法确实有效,但在规模化时,它们开始出现一些问题。首先,它通常会导致存储数据的某种程度上的放大(当然,这在对象存储上不是什么大问题),但更重要的是,它会导致 _哪些_ 字段要建立索引的问题?

特别是对于高基数数据集(例如,GitHub 档案数据有数百个字段,其中大多数是嵌套的),"全部"答案会变得极其昂贵,并会导致极其缓慢的摄取速度。因此,你很可能被迫在表的设计阶段做出选择,这可能不是你的表用户想要使用或发现有趣的字段。

因此,除了非常稀疏的基于时间戳的索引(每个 100 MB 的数据只有一个最小值和最大值),Sneller 不会建立任何索引,而是依赖于其 AVX-512 SIMD 原语的强大功能和速度来执行查询。

Sneller 是 *更少的*

秉承“少即是多”的流行说法,Sneller 架构(遵循我在 MinIO 学到的许多知识)可以由以下特性和优点来描述

无服务器:停止考虑为满足你的需求而全天候运行的单个服务器。开始考虑基于你查询的数据量进行始终在线和按需付费的模型。让 Sneller 基于所有租户的总查询需求(以及后台)来处理动态向上或向下扩展。

无状态:通过完全依赖对象存储来存储任何持久性数据,由于任何查询服务器错误或异常,数据被损坏或丢失的可能性将变得微乎其微。如果查询节点发生崩溃,只需启动另一个节点并让它从 S3 重新加载其部分数据,然后继续不间断地运行。

无模式:无需预先定义模式,Sneller 会无缝地处理任何模式漂移。你不再需要 ETL 或 ELT 仅仅为了将 JSON 转换为另一种格式。

无分支:Sneller 的性能来自于其 _无分支_ 的 SIMD 代码,该代码允许在单个 CPU 核心上并行处理多个通道。

无索引:通过避免建立索引,简化并加快摄取/同步过程,并以经济高效的方式实现大规模扩展。

MinIO 上的 Sneller

如果你想自己尝试一下,启动一个“Sneller on MinIO”堆栈非常容易,这将为你提供处理 JSON 的所有工具。实际上,只需使用 `curl`,你就可以将 SQL 查询提交到 REST API,如下所示

```

$ curl -G -H "Authorization: Bearer $SNELLER_TOKEN" --data-urlencode "database=gha" \

--data-urlencode 'json' --data-urlencode 'query=SELECT type, COUNT(*) FROM gharchive GROUP BY type ORDER BY COUNT(*) DESC' \

'https://:9180/executeQuery'

{"type": "PushEvent", "count": 1303922}

{"type": "CreateEvent", "count": 261401}

...

{"type": "GollumEvent", "count": 4035}

{"type": "MemberEvent", "count": 2644}

```

使用Docker,你可以在自己的笔记本电脑上执行此操作,或者如果你对开发或测试环境更感兴趣,你可以尝试使用Kubernetes 集成。

开源

我们已经开源了 Sneller 技术的核心 (包括所有 AVX-512 代码).

如果你想了解更多信息,请前往GitHub 上的 Sneller 存储库(如果你喜欢你看到的东西,欢迎点赞 😉)。

结论

对象存储在管理云中最大数量的数据方面具有非凡的强大功能,并且已成为事实上的标准。

将其与高性能的无服务器查询引擎相结合,可以实现存储与计算的真正分离,并为用户提供最大程度的灵活性,以完全独立地扩展每个部分到其自身的最佳水平。