在线存储满足低延迟服务需求,而离线特征存储则为模型训练、复杂特征工程和历史分析提供了重要基础。其设计必须优先考虑高效且经济地处理海量数据,这些数据通常涵盖数月甚至数年的历史特征值。可扩展性不仅仅指存储容量;它还包含生成特征和组装训练数据集的计算效率。可扩展离线存储的核心要求结构良好的离线存储满足了多项基本需求:数据量处理: 必须能够存储可能达到TB或PB级的历史特征数据,涵盖了长时间段内的大量实体和特征。高效批量处理: 需要与分布式处理框架(例如Apache Spark, Flink, Dask)集成,用于大规模特征计算和获取。时间点正确性: 必须有助于创建训练数据集,这些数据集准确反映特定历史事件时间已知的特征值,从而防止数据泄露。分析访问: 应允许数据科学家和分析师查看历史特征分布、执行特征分析并调试潜在问题。成本效益: 与大型历史数据集相关的存储和计算成本必须是可管理的。常见架构模式离线存储的架构通常建立在组织生态系统中现有的大规模数据存储方案之上。数据湖集成这可以说是构建可扩展离线特征存储的最普遍模式。它使用云对象存储(例如Amazon S3, Google Cloud Storage, Azure Data Lake Storage)作为主要存储层。优势:可扩展性与持久性: 利用云对象存储几乎无限的可扩展性和高持久性。成本效益: 对象存储提供了低存储成本,特别是在使用适当的存储类别和生命周期策略时。解耦: 将存储与计算分离,允许灵活使用不同的处理引擎。生态系统集成: 与Spark, Flink, Presto/Trino以及其他数据处理工具的原生集成。实施细节:文件格式: 数据通常以优化的列式格式存储,例如Apache Parquet。Parquet提供了高效的压缩和编码方案,并支持谓词下推,显著加快了仅需部分列的查询速度。表格式: 现代表格式,如Delta Lake、Apache Iceberg或Apache Hudi,正越来越多地应用于对象存储之上。这些格式提供了ACID事务、模式演变管理和时间旅行能力,简化了数据管理,并实现了可靠的时间点查询。分区: 数据在对象存储内部进行策略性分区(例如,按日期、实体类型、特征组),通过裁剪不相关的数据分区来优化查询性能。digraph DataLakeOfflineStore { rankdir=LR; node [shape=cylinder, style=filled, fillcolor="#ced4da"]; edge [arrowhead=vee]; subgraph cluster_sources { label = "数据源"; style=filled; fillcolor="#e9ecef"; node [shape=box, style=rounded, fillcolor="#ffffff"]; Source1 [label="流数据"]; Source2 [label="批量数据 (数据库, 日志)"]; } subgraph cluster_compute { label = "批量特征计算"; style=filled; fillcolor="#e9ecef"; node [shape=box, style=filled, fillcolor="#a5d8ff"]; Spark [label="Apache Spark / Flink"]; } subgraph cluster_storage { label = "离线存储 (数据湖)"; style=filled; fillcolor="#e9ecef"; node [shape=folder, style=filled, fillcolor="#ffec99"]; ObjectStore [label="对象存储 (S3/GCS/ADLS)\n[Parquet / Delta / Iceberg]"]; Partitioning [label="分区文件夹\n(例如, /feature_group/dt=YYYY-MM-DD/)"]; ObjectStore -> Partitioning [style=invis]; // 布局提示 } subgraph cluster_consumers { label = "消费者"; style=filled; fillcolor="#e9ecef"; node [shape=box, style=filled]; Training [label="模型训练", fillcolor="#b2f2bb"]; Analytics [label="数据分析", fillcolor="#b2f2bb"]; OnlineIngestion [label="摄取到在线存储", fillcolor="#bac8ff"]; } Source1 -> Spark; Source2 -> Spark; Spark -> ObjectStore [label="写入特征"]; ObjectStore -> Spark [label="读取用于训练/分析"]; Spark -> Training; Spark -> Analytics; Spark -> OnlineIngestion; }基于数据湖的离线特征存储的数据流,显示了数据源、批量计算、存储和消费者。数据仓库集成另一种选择是,离线存储可以使用云数据仓库(例如Google BigQuery, Snowflake, Amazon Redshift, Azure Synapse Analytics)实现。优势:SQL接口: 熟悉SQL接口,用于数据操作和查询。托管服务: 与从头管理数据湖堆栈相比,降低了运维开销。优化查询引擎: 用于分析工作负载的高性能查询执行。缺点:成本: 可能比对象存储更昂贵,特别是对于非常大的数据集或高计算使用量。灵活性: 对于某些类型的复杂、非SQL的转换(这些转换通常使用Spark/Flink完成)可能灵活性较低。潜在耦合: 存储与特定数据仓库计算引擎之间的耦合更紧密。混合方法组织通常采用混合策略。原始数据可能进入数据湖,使用Spark进行转换,然后精炼后的特征可能会同时加载到数据湖(以获得最大灵活性和归档)和数据仓库(以便于SQL访问和BI集成)。离线存储中的数据建模和组织无论底层存储系统如何,数据的组织方式对于可扩展性和可用性都很重要。以实体为中心与特征组表: 特征通常根据主要实体(例如,customer_daily_features)组织成表,或者根据计算它们的逻辑进行分组(例如,user_ad_interaction_features)。时间戳: 每个特征记录都必须至少有两个重要的时间戳: " 1. 事件时间戳: 与特征描述或派生自的事件相关联的时间(例如,交易时间、点击时间)。" 2. 计算时间戳: 特征值计算完成并存储可用的时间。分区: 有效的分区对于性能非常重要。常见策略包括按日期分区(例如,dt=YYYY-MM-DD),以及可能的二级分区键,如实体类型或区域。需要仔细规划,避免创建过多的小分区(损害文件列表性能)或过大的分区(降低查询并行性)。倾斜的分区也可能造成瓶颈。文件/表格式再议: 使用列式格式(Parquet)是标准做法。采用表格式(Delta Lake, Iceberg, Hudi)显著简化了更新、删除和时间旅行查询的管理,这些对于时间点正确性非常基础。这些格式维护事务日志和文件版本的元数据,使得能够进行“显示表在时间戳T时的状态”这样的查询。实现可扩展性离线存储中的可扩展性意味着要处理数据量、特征复杂度和查询负载的增长。计算扩展: 利用分布式处理引擎的弹性。像Spark这样的框架可以根据特征计算或训练数据生成任务的工作负载需求,动态扩展工作节点。存储扩展: 云对象存储提供了固有的可扩展性。数据仓库也提供了扩展存储和计算的机制,尽管通常具有不同的成本模型。查询优化: 采用谓词下推(在存储层过滤数据)、投影下推(仅读取必要列)等技术,以及在数据湖之上可能使用查询加速引擎(例如Presto, Trino)。成本优化: 在对象存储中使用分层存储(例如,将旧数据移动到不常用访问层),实施数据生命周期策略以过期旧数据,优化计算任务以高效使用资源,并为处理集群选择适当的实例类型。确保时间点正确性离线存储的一个主要功能是为模型训练提供历史准确的特征集。使用在与训练标签关联的事件时间戳之后计算的特征,会导致数据泄露,并使得模型在评估中表现不切实际地好,但在生产中表现不佳。离线存储设计通过以下方式方便时间点连接:存储时间戳: 可靠地存储每个特征值的事件时间戳和计算时间戳。时间旅行查询: 使用表格式(Delta, Iceberg),这些格式允许查询特征表在特定时间戳AS OF时的状态。连接逻辑: 实现逻辑(通常在Spark或查询引擎中),将所需训练事件列表(每个事件都有一个event_timestamp)与特征表连接,确保对于每个事件,只选择event_timestamp(或计算时间戳,具体取决于确切要求)小于或等于训练事件event_timestamp的特征值。设计可扩展的离线存储需要仔细考虑底层存储技术、数据组织、文件格式、分区策略以及与大规模计算引擎的集成。它是构建可靠模型训练和特征分析的重要组成部分。