我们在 F5 使用 Apache Arrow 的旅程(第一部分)


已发布 2023 年 4 月 11 日
作者 Laurent Quérel

Apache Arrow 是一种广泛应用于大数据、分析和机器学习应用中的技术。在本文中,我们将分享 F5 在 Arrow 方面的经验,特别是它在遥测中的应用,以及我们在优化 OpenTelemetry 协议以显着降低带宽成本方面遇到的挑战。我们取得的有希望的结果促使我们分享我们的见解。本文特别关注将相对复杂的数据结构从各种格式转换为有效的 Arrow 表示,从而优化压缩比、传输和数据处理。我们还将探讨不同映射和标准化策略之间的权衡,以及使用 Arrow 和 Arrow Flight 进行流式和批量通信的细微差别。到目前为止,我们的基准测试显示了有希望的结果,压缩率提高了 1.5 倍到 5 倍,具体取决于数据类型(指标、日志、跟踪)、分布和压缩算法。为解决这些挑战而提出的方法也可能适用于其他 Arrow 领域。本文是分为两部分的系列文章的第一篇。

什么是 Apache Arrow

Apache Arrow 是一个开源项目,提供了一种标准化的、与语言无关的内存格式,用于表示结构化和半结构化数据。这实现了系统之间的数据共享和零拷贝数据访问,消除了在不同 CPU 架构和编程语言之间交换数据集时进行序列化和反序列化的需要。此外,Arrow 库具有一套广泛的高性能、并行和矢量化内核函数,专为高效处理大量列式数据而设计。这些特性使得 Arrow 成为大数据处理、数据传输、分析和机器学习应用中极具吸引力的技术。越来越多的 产品和开源项目 在其核心采用了 Apache Arrow 或提供 Arrow 支持,反映了其优势得到了广泛认可和赞赏(有关 Arrow 生态系统和采用的深入概述,请参阅这篇 文章)。超过 11,000 名 GitHub 用户支持该项目,超过 840 名贡献者使该项目取得了不可否认的成功。

人们经常问起 Arrow 和 Apache Parquet 或其他列式文件格式之间的区别。Arrow 专为内存处理而设计和优化,而 Parquet 则针对基于磁盘的存储量身定制。实际上,这些技术是互补的,它们之间存在桥梁以简化互操作性。在这两种情况下,数据都以列的形式表示,以优化访问、数据局部性和可压缩性。但是,权衡略有不同。Arrow 优先考虑数据处理速度而不是最佳数据编码。与 Parquet 等格式不同,Arrow 通常不原生支持不从 SIMD 指令集中受益的复杂编码。以 Parquet 格式存储数据,以 Arrow 格式处理和传输数据已成为大数据社区中流行的模型。

Memory representations: row vs columnar data.
图 1:内存表示:行与列数据。

图 1 说明了面向行和面向列的方法之间内存表示的差异。面向列的方法将来自同一列的数据分组在连续的内存区域中,这有助于并行处理 (SIMD) 并提高压缩性能。

为什么我们对 Apache Arrow 感兴趣

F5,我们已采用 OpenTelemetry (OTel) 作为我们所有产品(例如 BIGIP 和 NGINX)的遥测标准。由于各种原因,从性能评估到取证,这些产品可能会生成大量的指标和日志。这些系统产生的数据通常集中在专用系统中进行处理。传输和处理这些数据占与遥测管道相关的成本的很大一部分。在这种情况下,我们对 Apache Arrow 产生了兴趣。我们没有重新发明另一种遥测解决方案,而是决定投资 OpenTelemetry 项目,致力于改进该协议,以显着提高其在高遥测数据量下的效率。我们与来自 LightstepJoshua MacDonald 合作,将这些优化集成到 实验性 OTel 收集器 中,并且目前正在与 OTel 技术委员会讨论以最终确定代码 捐赠

Performance improvement in the OpenTelemetry Arrow experimental project.
图 2:OpenTelemetry Arrow 实验项目中的性能改进。

该项目分为两个阶段。第一阶段即将完成,旨在提高协议的压缩率。计划在未来进行的第二阶段,重点是通过在所有级别合并 Apache Arrow 来提高端到端性能,从而消除在新旧协议之间进行转换的需要。到目前为止,结果令人鼓舞,我们的基准测试显示压缩率提高了 1.5 倍到 5 倍,具体取决于数据类型(指标、日志、跟踪)、分布和压缩算法。对于第二阶段,我们的估计表明,数据处理加速可能在 2 倍到 12 倍之间,同样取决于数据的性质和分布。有关更多信息,我们建议您查看 规范参考实现

Arrow 依赖于模式来定义其处理和传输的数据批次的结构。后续章节将讨论可用于优化这些模式创建的各种技术。

如何利用 Arrow 优化网络传输成本

Apache Arrow 是一个复杂的项目,拥有快速发展的生态系统,对于新手来说有时会感到不知所措。幸运的是,Arrow 社区发布了三篇介绍性文章 1, 2, 和 3,我们建议那些有兴趣探索这项技术的人阅读。

本文主要关注将数据从 XYZ 格式转换为有效的 Arrow 表示,从而优化压缩比和数据处理。这种转换有多种方法,我们将研究这些方法如何影响转换过程中的压缩比、CPU 使用率和内存消耗等因素。

Fig 3: Optimization process for the definition of an Arrow schema.
图 3:Arrow 模式定义的优化过程。

您的初始模型的复杂性会显着影响您需要做出的 Arrow 映射选择。首先,务必确定您要为特定上下文优化的属性。压缩率、转换速度、内存消耗、最终模型的速度和易用性、兼容性和可扩展性都是可能影响您的最终映射决策的因素。从那里,您必须探索多种替代方案。

为每个单独的字段选择 Arrow 类型和数据编码将影响您的模式的性能。有多种方法可以表示分层数据或高度动态的数据模型,并且需要与传输层的配置协调评估多个选项。还应仔细考虑此传输层。Arrow 支持压缩机制和字典增量,这些机制和增量可能默认不处于活动状态。

经过此过程的多次迭代后,您应该获得一个满足您最初设定的目标的优化模式。使用真实数据比较不同方法的性能至关重要,因为每个单独字段中数据的分布可能会影响您是否使用字典编码。我们现在将在本文的其余部分中更详细地研究这些选择。

Arrow 数据类型选择

选择 Arrow 数据类型的原则与定义数据库数据模型时使用的原则非常相似。Arrow 支持各种数据类型。其中一些类型受所有实现的支持,而另一些类型仅适用于具有最强 Arrow 社区支持的语言(有关不同实现的比较矩阵,请参阅此 页面)。对于基本类型,通常最好选择提供最简洁的表示形式并且最接近您的初始字段语义的类型。例如,虽然可以使用 int64 表示时间戳,但使用原生 Arrow Timestamp 类型更有优势。这种选择不是因为更有效的二进制表示形式,而是因为它更容易在您的管道中进行处理和操作。查询引擎(例如 DataFusion)为此类类型的列提供专用的时间戳处理函数。对于基本类型(例如日期、时间、持续时间和间隔),也可以做出相同的选择。但是,如果您的项目需要最大的兼容性,那么在某些情况下,必须优先选择具有通用支持的类型,而不是在内存占用方面最佳的类型。

Fig 4: Data types supported by Apache Arrow.
图 4:Apache Arrow 支持的数据类型。

选择 Arrow 数据类型时,务必考虑压缩前后的数据大小。两种不同类型的数据压缩后的尺寸可能相同,但实际在内存中的大小可能相差两倍、四倍甚至八倍(例如,uint8 与 uint64)。这种差异会影响您处理大型数据批次的能力,也会显著影响在内存中处理这些数据的速度(例如,缓存优化、SIMD 指令效率)。

还可以使用扩展类型机制来扩展这些类型,该机制建立在当前支持的原始类型之一之上,同时添加特定的语义。此扩展机制可以简化在您自己的项目中使用此数据,同时对将此数据解释为基本原始类型的中间系统保持透明。

原始类型的编码有一些变体,我们将在下面探讨。

数据编码

优化 Arrow 模式的另一个关键方面是分析数据的基数。值数量有限的字段通常可以使用字典编码更有效地表示。

字段的最大基数决定了字典的数据类型特征。例如,对于表示 HTTP 事务状态代码的字段,最好使用索引类型为“uint8”和值类型为“uint16”的字典(表示法:“Dictionary<uint8, uint16>”)。这样消耗的内存更少,因为主数组的类型将为“[]uint8”。即使可能值的范围大于 255,只要不同值的数量不超过 255,表示仍然有效。类似地,“user-agent”的表示使用类型为“Dictionary<uint16, string>”的字典会更有效(见图 5)。在这种情况下,主数组的类型将为“uint16”,从而可以在内存中和传输过程中实现紧凑的表示,但代价是在反向转换期间进行间接寻址。

Fig 5: Dictionary encoding.
图 5:字典编码。

字典编码在 Apache Arrow 中具有高度的灵活性,允许为任何 Arrow 原始类型创建编码。索引的大小也可以根据上下文进行配置。

通常,建议在以下情况下使用字典:

  • 枚举的表示
  • 文本或二进制字段的表示,这些字段具有很高的值冗余概率。
  • 基数已知低于 2^16 或 2^32 的字段的表示。

有时,字段的基数不是先验已知的。例如,将数据流从面向行的格式转换为一系列面向列编码的批次(例如,OpenTelemetry 收集器)的代理可能无法提前预测字段是否具有固定数量的不同值。有两种方法是可能的:1) 一种保守的方法,使用最大的数据类型(例如,“int64”、“string”等,而不是字典),2) 一种自适应的方法,该方法根据观察到的字段基数动态修改模式。在第二种方法中,如果没有基数信息,您可以乐观地从使用“Dictionary<uint8, original-field-type>”字典开始,然后在转换期间检测潜在的字典溢出,并在发生溢出时将模式更改为“Dictionary<uint16, original-field-type>”。这种字典溢出的自动管理技术将在未来的文章中详细介绍。

Apache Arrow 最近的进展包括行程长度编码的实现,该技术有效地表示具有重复值序列的数据。这种编码方法特别有利于处理包含长串相同值的数据集,因为它提供了更紧凑和优化的表示。

总而言之,字典编码不仅占用更少的内存和传输空间,而且还显著提高了压缩率和数据处理速度。但是,这种类型的表示在提取初始值时需要间接寻址(尽管即使在某些数据处理操作期间,这并非总是必需的)。此外,重要的是管理字典索引溢出,尤其是在编码字段没有明确定义的基数时。

分层数据

基本的分层数据结构可以很好地转换为 Arrow。但是,正如我们将看到的,在更一般的情况下需要处理一些复杂性(见图 6)。虽然 Arrow 模式确实支持嵌套结构、映射和联合,但 Arrow 生态系统中的某些组件并不完全支持它们,这使得这些 Arrow 数据类型不适用于某些场景。此外,与大多数语言和格式(如 Protobuf)不同,Arrow 不支持递归定义的模式的概念。Arrow 模式在其定义中是静态的,并且其嵌套元素的深度必须预先知道。有多种策略可以解决此限制,我们将在以下各节中探讨这些策略。

Fig 6: simple vs complex data model.
图 6:简单 vs 复杂数据模型。

自然表示

表示简单分层数据模型最直接和直观的方法是使用 Arrow 的列表、映射和联合数据类型。但是,重要的是要注意,某些数据类型在整个 Arrow 生态系统中未得到完全支持。例如,联合到 Parquet 的转换不直接支持,并且需要转换步骤(参见非规范化 & 平铺表示,将稀疏联合分解为可为空的结构和类型 ID 列)。类似地,列表和映射在 DataFusion 20 版中尚未支持(嵌套结构部分支持)。

Fig 7: initial data model.
图 7:初始数据模型。

以下示例是使用这些不同数据类型来表示上述模型的 Arrow 模式的 Go 程序代码段。

import "github.com/apache/arrow/go/v11/arrow"


const (
  GaugeMetricCode arrow.UnionTypeCode = 0
  SumMetricCode   arrow.UnionTypeCode = 1
)


var (
  // uint8Dictionary represent a Dictionary<Uint8, String>
  uint8Dictionary = &arrow.DictionaryType{
     IndexType: arrow.PrimitiveTypes.Uint8,
     ValueType: arrow.BinaryTypes.String,
  }
  // uint16Dictionary represent a Dictionary<Uint16, String>
  uint16Dictionary = &arrow.DictionaryType{
     IndexType: arrow.PrimitiveTypes.Uint16,
     ValueType: arrow.BinaryTypes.String,
  }


  Schema = arrow.NewSchema([]arrow.Field{
     {Name: "resource_metrics", Type: arrow.ListOf(arrow.StructOf([]arrow.Field{
        {Name: "scope", Type: arrow.StructOf([]arrow.Field{
           // Name and Version are declared as dictionaries (Dictionary<Uint16, String>)).
           {Name: "name", Type: uint16Dictionary},
           {Name: "version", Type: uint16Dictionary},
        }...)},
        {Name: "metrics", Type: arrow.ListOf(arrow.StructOf([]arrow.Field{
           {Name: "name", Type: uint16Dictionary},
           {Name: "unit", Type: uint8Dictionary},
           {Name: "timestamp", Type: arrow.TIMESTAMP},
           {Name: "metric_type", Type: arrow.UINT8},
           {Name: "data_point", Type: arrow.ListOf(arrow.StructOf([]arrow.Field{
              {Name: "metric", Type: arrow.DenseUnionOf(
                 []arrow.Field{
                    {Name: "gauge", Type: arrow.StructOf([]arrow.Field{
                       {Name: "data_point", Type: arrow.FLOAT64},
                    }...)},
                    {Name: "sum", Type: arrow.StructOf([]arrow.Field{
                       {Name: "data_point", Type: arrow.FLOAT64},
                       {Name: "is_monotonic", Type: arrow.BOOL},
                    }...)},
                 },
                 []arrow.UnionTypeCode{GaugeMetricCode, SumMetricCode},
              )},
           }...))},
        }...))},
     }...))},
  }, nil)
)

在这种模式中,我们使用联合类型来表示继承关系。有两种类型的 Arrow 联合针对不同的情况进行了优化。密集联合类型具有相对简洁的内存表示形式,但不支持可向量化操作,这使其在处理阶段效率较低。相反,稀疏联合支持向量化操作,但会带来与联合中的变体数量直接成正比的内存开销。密集和稀疏联合具有非常相似的压缩率,有时稀疏联合略有优势。此外,通常应避免使用具有大量变体的稀疏联合,因为它们可能导致过多的内存消耗。有关联合内存表示的更多详细信息,您可以查阅此页面

在某些情况下,使用多个模式(即,每个子类型一个模式)来表示继承关系可能更符合习惯,从而避免使用联合类型。但是,将此方法应用于上述模型可能不是最佳选择,因为继承关系之前的数据(即,ResourceMetricsScopeMetrics)可能会被多次复制。如果 ResourceMetricsMetricsDataPoint 之间的关系为 0..1(零到一)关系,那么多模式方法可能是最简单和最符合习惯的解决方案。

非规范化 & 平铺表示

如果您的遥测管道不支持 List 类型,您可以非规范化您的数据模型。此过程通常在数据库世界中使用,以删除两个表之间的连接以进行优化。在 Arrow 世界中,非规范化用于通过复制一些数据来消除 List 类型。转换后,先前的 Arrow 模式变为。

Schema = arrow.NewSchema([]arrow.Field{
  {Name: "resource_metrics", Type: arrow.StructOf([]arrow.Field{
     {Name: "scope", Type: arrow.StructOf([]arrow.Field{
        // Name and Version are declared as dictionaries (Dictionary<Uint16, String>)).
        {Name: "name", Type: uint16Dictionary},
        {Name: "version", Type: uint16Dictionary},
     }...)},
     {Name: "metrics", Type: arrow.StructOf([]arrow.Field{
        {Name: "name", Type: uint16Dictionary},
        {Name: "unit", Type: uint8Dictionary},
        {Name: "timestamp", Type: arrow.TIMESTAMP},
        {Name: "metric_type", Type: arrow.UINT8},
        {Name: "data_point", Type: arrow.StructOf([]arrow.Field{
           {Name: "metric", Type: arrow.DenseUnionOf(
              []arrow.Field{
                 {Name: "gauge", Type: arrow.StructOf([]arrow.Field{
                    {Name: "value", Type: arrow.FLOAT64},
                 }...)},
                 {Name: "sum", Type: arrow.StructOf([]arrow.Field{
                    {Name: "value", Type: arrow.FLOAT64},
                    {Name: "is_monotonic", Type: arrow.BOOL},
                 }...)},
              },
              []arrow.UnionTypeCode{GaugeMetricCode, SumMetricCode},
           )},
        }...)},
     }...)},
  }...)},
}, nil)

List 类型在所有级别都被消除。通过复制每个数据点值下方级别的数据来保留模型的初始语义。内存表示通常比前一个大得多,但是不支持 List 类型的查询引擎仍然能够处理此数据。有趣的是,一旦压缩,这种表示数据的方式可能不一定比以前的方法大。这是因为当数据中存在冗余时,列式表示形式可以很好地压缩。

如果您的管道的某些组件不支持联合类型,也可以通过合并联合变体来消除它们(嵌套结构“metric”将被删除,见下文)。

Schema = arrow.NewSchema([]arrow.Field{
  {Name: "resource_metrics", Type: arrow.StructOf([]arrow.Field{
     {Name: "scope", Type: arrow.StructOf([]arrow.Field{
        // Name and Version are declared as dictionaries (Dictionary<Uint16, String>)).
        {Name: "name", Type: uint16Dictionary},
        {Name: "version", Type: uint16Dictionary},
     }...)},
     {Name: "metrics", Type: arrow.StructOf([]arrow.Field{
        {Name: "name", Type: uint16Dictionary},
        {Name: "unit", Type: uint8Dictionary},
        {Name: "timestamp", Type: arrow.TIMESTAMP},
        {Name: "metric_type", Type: arrow.UINT8},
        {Name: "data_point", Type: arrow.StructOf([]arrow.Field{
           {Name: "value", Type: arrow.FLOAT64},
           {Name: "is_monotonic", Type: arrow.BOOL},
        }...)},
     }...)},
  }...)},
}, nil)

最终模式已演变为一系列嵌套结构,其中联合变体的字段合并为一个结构。这种方法的权衡类似于稀疏联合 - 变体越多,内存占用越高。Arrow 支持位图有效性的概念,以识别各种数据类型的空值(每个条目 1 位),包括那些没有唯一空表示形式的数据类型(例如,原始类型)。位图有效性的使用使查询部分更容易,并且 DataFusion 等查询引擎知道如何有效地使用它。具有大量空值的列通常可以有效地压缩,因为底层数组通常用 0 初始化。压缩后,这些大量的 0 序列会导致高压缩效率,尽管在稀疏联合的情况下,压缩前存在内存开销。因此,必须根据您的具体情况选择适当的权衡。

在某些嵌套结构不受支持的极端情况下,可以使用平铺方法来解决此问题。

Schema = arrow.NewSchema([]arrow.Field{
  {Name: "scope_name", Type: uint16Dictionary},
  {Name: "scope_version", Type: uint16Dictionary},
  {Name: "metrics_name", Type: uint16Dictionary},
  {Name: "metrics_unit", Type: uint8Dictionary},
  {Name: "metrics_timestamp", Type: arrow.TIMESTAMP},
  {Name: "metrics_metric_type", Type: arrow.UINT8},
  {Name: "metrics_data_point_value", Type: arrow.FLOAT64},
  {Name: "metrics_data_point_is_monotonic", Type: arrow.BOOL},
}, nil)

通过连接父结构的名称来重命名终端字段(叶子),以提供适当的作用域。Arrow 生态系统的所有组件都支持这种类型的结构。如果兼容性是您的系统的关键标准,此方法可能会很有用。但是,它与其他替代非规范化模型具有相同的缺点。

Arrow 生态系统正在迅速发展,因此查询引擎中对 List、Map 和 Union 数据类型的支持可能会快速改进。如果内核函数对您的应用程序来说足够或更可取,通常可以使用这些嵌套类型。

自适应/动态表示

某些数据模型可能更难转换为 Arrow schema,例如以下 Protobuf 示例。 在此示例中,一系列属性被添加到每个数据点。 这些属性使用递归定义进行定义,大多数语言和格式(如 Protobuf)都支持这种定义(请参阅下面的“AnyValue”定义)。 遗憾的是,Arrow(像大多数经典数据库 schema 一样)不支持 schema 中的这种递归定义。

syntax = "proto3";


message Metric {
 message DataPoint {
   repeated Attribute attributes = 1;
   oneof value {
     int64 int_value = 2;
     double double_value = 3;
   }
 }


 enum MetricType {
   UNSPECIFIED = 0;
   GAUGE = 1;
   SUM = 2;
 }


 message Gauge {
   DataPoint data_point = 1;
 }


 message Sum {
   DataPoint data_point = 1;
   bool is_monotonic = 2;
 }


 string name = 1;
 int64 timestamp = 2;
 string unit = 3;
 MetricType type = 4;
 oneof metric {
   Gauge gauge = 5;
   Sum sum = 6;
 }
}


message Attribute {
 string name = 1;
 AnyValue value = 2;
}


// Recursive definition of AnyValue. AnyValue can be a primitive value, a list
// of AnyValues, or a list of key-value pairs where the key is a string and
// the value is an AnyValue.
message AnyValue {
 message ArrayValue {
   repeated AnyValue values = 1;
 }
 message KeyValueList {
   message KeyValue {
     string key = 1;
     AnyValue value = 2;
   }
   repeated KeyValue values = 1;
 }


 oneof value {
   int64 int_value = 1;
   double double_value = 2;
   string string_value = 3;
   ArrayValue list_value = 4;
   KeyValueList kvlist_value = 5;
 }
}

如果属性的定义是非递归的,则可以直接将其转换为 Arrow Map 类型。

为了解决此类问题并进一步优化 Arrow schema 定义,您可以采用一种自适应的迭代方法,根据正在转换的数据自动构建 Arrow schema。 通过这种方法,字段会根据其基数自动进行字典编码,未使用的字段会被消除,递归结构会以特定的方式表示。 另一种解决方案是使用多 schema 方法,其中属性在单独的 Arrow Record 中描述,继承关系由自引用关系表示。 这些策略将在以后的文章中更深入地介绍。 对于那些渴望了解更多信息的人,第一种方法已在 OTel Arrow Adapter 的参考实现中使用。

数据传输

与 Protobuf 不同,通常参与交换的双方事先不知道 Arrow schema。 在能够以 Arrow 格式交换数据之前,发送方必须首先将 schema 以及数据中使用的字典的内容传达给接收方。 只有在此初始化阶段完成后,发送方才能以 Arrow 格式传输批次数据。 此过程称为 Arrow IPC Stream,在系统之间传输 Arrow 数据方面起着至关重要的作用。 可以采用多种方法来传达这些 Arrow IPC Stream。 最简单的方法是使用 Arrow Flight,它将 Arrow IPC 流封装在基于 gRPC 的协议中。 但是,也可以为您自己的特定上下文使用自己的实现。 无论您选择哪种解决方案,理解底层协议必须是有状态的,才能充分利用 Arrow IPC stream 方法至关重要。 为了获得最佳压缩率,必须只发送一次 schema 和字典,以便分摊成本并最大限度地减少批次之间的数据冗余。 这就需要支持面向流通信的传输,例如 gRPC。

对于大型批次,可以使用无状态协议,因为与使用字典编码和列式表示实现的压缩增益相比,schema 的开销可以忽略不计。 但是,必须为每个批次传达字典,这使得这种方法通常不如面向流的方法有效。

Arrow IPC Stream 还支持“增量字典”的概念,这可以进一步优化批次传输。 当批次向现有字典(在发送方)添加数据时,Arrow IPC 允许发送增量字典,然后发送引用它的批次。 在接收方,此增量用于更新现有字典,从而无需在发生更改时重新传输整个字典。 只有使用有状态协议才能实现此优化。

要充分利用 Apache Arrow 的面向列的格式,必须考虑排序和压缩。 如果您的数据模型很简单(即,扁平的),并且有一个或多个列表示数据的自然顺序(例如,时间戳),则可能有利于对数据进行排序以优化最终压缩比。 在实施此优化之前,建议对真实数据执行测试,因为好处可能会有所不同。 在任何情况下,在发送批次时使用压缩算法都是有利的。 Arrow IPC 通常支持 ZSTD 压缩算法,该算法在速度和压缩效率之间取得了极好的平衡,尤其是在面向列的数据方面。

最后,某些实现(例如,Arrow Go)默认情况下未配置为支持增量字典和压缩算法。 因此,必须确保您的代码采用这些选项,以最大限度地提高数据传输效率。

实验

如果您的初始数据很复杂,建议进行您自己的实验,以根据您的数据和目标优化 Arrow 表示形式(例如,优化压缩比或增强 Arrow 格式数据的查询能力)。 在我们的例子中,我们为 Apache Arrow 开发了一个覆盖层,使我们能够轻松地进行这些实验,而无需处理 Arrow API 的内在复杂性。 但是,与直接使用 Arrow API 相比,这会以较慢的转换阶段为代价。 虽然此库目前未公开,但如果存在足够的兴趣,它可能会变为可用。

我们还采用了一种“黑盒优化”方法,该方法自动找到满足我们旨在优化的目标的最佳组合(有关此方法的描述,请参阅“使用 Google Vertex AI Vizier 优化您的应用程序”)。

结论和后续步骤

从本质上讲,Apache Arrow 背后的关键概念是它消除了序列化和反序列化的需要,从而实现了零拷贝数据共享。 Arrow 通过定义一种与语言无关的内存格式来实现这一点,该格式在各种实现中保持一致。 因此,原始内存字节可以直接通过网络传输,而无需任何序列化或反序列化,从而显着提高了数据处理效率。

将数据模型转换为 Apache Arrow 需要进行适配和优化工作,正如我们已开始在本文中描述的那样。 必须考虑许多参数,建议执行一系列实验来验证在此过程中做出的各种选择。

使用 Arrow 处理高度动态的数据可能具有挑战性。 Arrow 需要定义静态 schema,这有时会使表示此类数据变得复杂或欠佳,尤其是在初始 schema 包含递归定义时。 本文讨论了几种解决此问题的方法。 下一篇文章将专门介绍一种混合策略,该策略涉及即时调整 Arrow schema,以根据正在表示的数据优化内存使用、压缩比和处理速度。 这种方法非常独特,值得单独写一篇文章。