Hotdry.
ai-systems

Exa-d:如何在S3中存储整个网络的数据架构设计

深入分析Exa AI的exa-d框架如何设计大规模网络数据存储架构于S3,涵盖数据分片、索引策略、成本优化与一致性保证的工程实践。

构建现代搜索引擎需要摄取整个网络的数据,并确保在实时变化中保持可查询性。网络数据具有几个使其极具挑战性的特性:每个页面产生数十种输出(提取的文本、元数据、嵌入向量等)、异构内容类型(HTML、PDF、JavaScript 渲染应用、多媒体)、变化频率从小时级到永不更新,以及数百亿页面、PB 级原始内容的庞大规模。

Exa AI 的 exa-d 框架正是为应对这些挑战而设计的内部数据处理框架,其核心目标是在 S3 上高效存储和管理整个网络的数据。本文将深入分析 exa-d 的架构设计,特别关注其在 S3 上的存储策略、数据分片机制、索引优化以及成本控制方案。

网络规模数据存储的核心挑战

在深入 exa-d 架构之前,我们需要理解网络数据存储的特殊性。与传统的企业数据不同,网络数据具有以下关键特征:

  1. 衍生数据爆炸:每个网页不仅包含原始 HTML,还会产生提取文本、元数据、多种嵌入向量(不同模型版本)、结构化提取结果等数十种衍生数据。这导致数据表面积呈指数级增长。

  2. 更新模式异构:新闻文章可能每小时更新,学术论文可能永不更新,而社交媒体内容则处于持续变化中。这种混合的更新频率要求存储系统同时支持大规模全量重建和小规模精确更新。

  3. 计算密集型处理:从原始 HTML 到语义嵌入向量的转换涉及复杂的 NLP 流水线,包括分词、模型推理等 GPU 密集型操作。这些操作的成本和延迟直接影响索引的新鲜度。

  4. 存储成本敏感性:当数据规模达到 PB 级别时,即使微小的存储效率提升也能带来显著的成本节约。S3 的存储类选择、数据生命周期管理成为关键经济因素。

exa-d 的架构设计理念

exa-d 的设计围绕三个核心优化目标展开:声明式依赖管理、精确更新能力和高效并行执行。

声明式依赖图:从命令式到声明式的范式转变

传统的数据处理流水线通常是命令式的 —— 工程师编写脚本指定每个步骤的执行顺序。当团队规模扩大、衍生特征增多时,这种模式会导致代码重复、依赖关系难以追踪、变更影响评估困难等问题。

exa-d 采用了声明式依赖图的模式。工程师通过定义列(Column)及其依赖关系来描述数据处理逻辑,而不是指定执行步骤。例如:

documents = Column(name="documents", type=str)
tokenized = Column(name="documents_tokenized", type=torch.Tensor).derive()._from(documents).impl(Tokenizer)
embeddings = Column(name="embeddings").derive()._from(tokenized, type=torch.Tensor).impl(EmbeddingModel)

这种声明式设计带来了几个关键优势:

  • 自动执行排序:系统根据依赖图自动确定计算顺序,无需手动维护流水线脚本。
  • 类型安全保证:列定义作为强类型契约,在编译时捕获类型错误,避免运行时故障。
  • 依赖关系显式化:每个衍生列的输入输出关系清晰可见,便于理解数据流转和影响分析。

精确更新:避免写放大的关键设计

网络数据的动态性要求存储系统支持多种更新模式:从单个页面的小规模更新到整个索引的全量重建。传统的数据框架在处理部分更新时往往需要重写整个数据集,导致严重的写放大问题。

exa-d 通过引入片段(Fragment) 作为更新粒度单位来解决这个问题。每个片段是一组行的物理文件集合,系统可以针对单个片段中的特定列进行更新,而不影响其他列或其他片段。

这种设计的关键洞察是:在网络规模的数据集中,大多数更新只影响数据的一小部分。通过将更新粒度细化到片段级别,exa-d 能够将写放大系数控制在接近 1 的水平,显著降低了存储成本和计算开销。

S3 存储层的具体实现:Lance 格式与片段管理

exa-d 选择 S3 作为底层存储,并采用 Lance 格式组织数据。这一选择基于几个关键考虑:

Lance 格式的优势

Lance 是一种为大规模机器学习数据集设计的列式存储格式,具有以下特性:

  1. 部分 Schema 支持:不同片段可以有不同的列集合,缺失的衍生列被视为 "待计算" 状态。这种灵活性对于渐进式数据填充至关重要。

  2. 片段级原子操作:Lance 使用全局 manifest 文件记录数据集的完整状态,更新 manifest 是 S3 上的原子操作。这为分布式环境下的并发更新提供了简单而强大的协调机制。

  3. 高效的列操作:Lance 支持在片段级别添加、删除或替换单个列,而无需重写整个片段。这是实现精确更新的基础能力。

S3 上的存储组织策略

exa-d 在 S3 上的数据组织遵循以下模式:

s3://bucket/dataset-name/
├── manifest.json
├── fragments/
│   ├── 0/
│   │   ├── documents.lance
│   │   ├── tokenized.lance
│   │   └── embeddings_v1.lance
│   ├── 1/
│   │   ├── documents.lance
│   │   └── tokenized.lance
│   └── 2/
│       ├── documents.lance
│       ├── tokenized.lance
│       └── embeddings_v2.lance

这种组织方式具有几个重要特性:

  • 清单驱动:全局 manifest 文件是数据集的唯一真实来源,记录了所有片段及其包含的列。
  • 列分离存储:每个列存储在独立的文件中,便于独立更新和版本管理。
  • 路径可预测:片段和列的存储路径遵循固定模式,便于程序化访问和管理。

数据分片与索引策略

分片策略的设计考虑

exa-d 的分片策略需要平衡多个因素:

  1. 更新粒度:分片大小应足够小,以便大多数更新只影响少数分片,但又不能太小以避免管理开销过大。

  2. 并行效率:分片应均匀分布计算负载,充分利用分布式计算资源。

  3. 存储效率:每个分片应有足够的数据量以摊销 S3 操作的固定成本。

基于 Exa AI 的实践经验,他们发现将数据划分为数百万到数千万行级别的分片能够在这些因素间取得良好平衡。每个分片通常对应几十到几百 MB 的存储空间,这样的规模既便于并行处理,又不会产生过度的管理开销。

索引策略:列式存储与部分物化

exa-d 采用列式存储策略,这与传统的关系型数据库行式存储形成鲜明对比。列式存储特别适合网络数据处理的场景:

  1. 选择性读取:大多数查询只需要访问少数几列(如嵌入向量和元数据),列式存储避免了读取不必要的数据。

  2. 高效压缩:同一列中的数据通常具有较高的局部性,便于应用高效的压缩算法。

  3. 独立更新:不同列的更新频率和模式差异很大,列式存储支持独立的生命周期管理。

更重要的是,exa-d 支持部分物化策略。不是所有衍生列都需要立即计算和存储,系统可以根据查询模式和资源可用性动态决定哪些列需要物化,哪些可以按需计算。

成本优化考虑

在 PB 级数据规模下,成本优化不再是可选项,而是系统设计的核心要求。exa-d 通过多个层面的优化来控制 S3 存储成本:

存储类分层策略

虽然 exa-d 的官方文档未详细讨论存储类选择,但基于 S3 的最佳实践,我们可以推断出合理的分层策略:

  1. 热数据层:频繁访问的当前索引数据使用 S3 Standard 或 S3 Express One Zone(对于延迟敏感的操作)。

  2. 温数据层:历史版本、备份数据使用 S3 Standard-IA 或 S3 Intelligent-Tiering。

  3. 冷数据层:归档数据、训练用的历史快照使用 S3 Glacier Instant Retrieval 或 S3 Glacier Deep Archive。

关键洞察是:不同衍生列具有不同的访问模式。嵌入向量可能被频繁查询(热),而原始 HTML 可能只在重新处理时访问(温),历史版本可能几乎从不访问(冷)。exa-d 的列分离存储天然支持这种细粒度的存储类分配。

生命周期自动化

通过 S3 生命周期策略,系统可以自动将数据迁移到成本更低的存储类。例如:

  • 30 天后将原始 HTML 从 Standard 迁移到 Standard-IA
  • 90 天后将旧版本嵌入向量迁移到 Glacier Instant Retrieval
  • 365 天后将历史快照迁移到 Glacier Deep Archive

减少不必要的存储操作

exa-d 的精确更新机制本身就减少了不必要的存储操作。通过避免全量重写,系统显著降低了 PUT 操作的数量,这直接转化为成本节约,因为 S3 的 PUT 请求是收费的。

此外,通过智能的缓存策略和增量计算,系统可以减少 GET 操作的数量。例如,如果某个片段的嵌入向量已经是最新版本,查询时可以直接使用,无需重新计算或从 S3 读取中间结果。

一致性保证机制

在分布式环境中维护数据一致性是极具挑战性的问题。exa-d 通过 Lance 的 manifest 机制提供了简单而强大的一致性保证。

原子提交协议

Lance 的全局 manifest 文件作为数据集的唯一真实来源。任何对数据集的修改都必须通过更新 manifest 来完成,这个更新是 S3 上的原子操作。具体流程如下:

  1. 读取当前状态:进程读取当前的 manifest 文件,了解数据集的完整状态。
  2. 计算变更:基于当前状态计算需要添加、删除或修改的片段。
  3. 准备新数据:将变更写入新的片段文件。
  4. 原子提交:尝试更新 manifest 文件。如果在此期间其他进程已经修改了 manifest,则当前操作失败,需要重新基于最新状态计算变更。

这种 "读取 - 修改 - 提交" 模式虽然简单,但在实践中非常有效。它避免了复杂的分布式锁机制,同时保证了最终一致性。

冲突解决与重试

当多个进程同时尝试更新同一数据集时,冲突不可避免。exa-d 采用乐观并发控制策略:

  1. 冲突检测:通过 manifest 版本检测冲突。
  2. 自动重试:检测到冲突后,进程自动重新基于最新状态计算变更。
  3. 幂等操作:所有数据操作都设计为幂等的,允许安全重试。

对于最常见的片段修补操作,重新计算变更通常很简单,因为大多数更新只影响数据的一小部分。

工程实践建议

基于 exa-d 的设计理念,我们可以提炼出一些通用的工程实践建议,适用于任何需要在 S3 上存储大规模网络数据的场景:

1. 采用声明式数据流水线

避免编写命令式的数据处理脚本,转而定义数据之间的依赖关系。这不仅提高了代码的可维护性,还使系统能够自动优化执行计划。

2. 设计细粒度的更新机制

不要假设所有更新都需要全量重写。通过合理的数据分片和列分离,支持精确到片段和列级别的更新。这能显著降低写放大和计算成本。

3. 利用 S3 的存储类分层

根据数据的访问模式选择合适的存储类。热数据用高性能存储,冷数据用低成本归档存储。定期审查和调整存储策略。

4. 实施自动化的生命周期管理

不要依赖手动操作管理数据生命周期。使用 S3 生命周期策略自动迁移数据到合适的存储类,删除过期数据。

5. 监控关键成本指标

建立完善的监控体系,跟踪存储成本、请求成本、数据传输成本等关键指标。设置警报,及时发现异常成本增长。

6. 设计可扩展的架构

从第一天就考虑可扩展性。使用列式存储、支持部分物化、设计无状态的处理节点,确保系统能够平滑扩展到 PB 级数据规模。

总结

exa-d 框架展示了如何在 S3 上构建大规模网络数据存储系统的现代方法。通过声明式依赖图、精确更新机制、列式存储和智能的成本优化,它解决了网络数据存储的核心挑战。

虽然 exa-d 是 Exa AI 的内部框架,但其设计理念具有广泛的适用性。任何需要处理大规模、动态、异构数据的系统都可以从这些模式中受益。关键是要理解数据的特性,设计与之匹配的存储策略,并充分利用云存储服务的特性。

随着网络数据的持续增长和 AI 应用的普及,高效的数据存储和处理架构将变得越来越重要。exa-d 提供了一个有价值的参考框架,展示了如何将现代数据工程原则应用于实际的大规模系统构建中。

资料来源

查看归档