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优先考虑数据处理速度而不是最佳数据编码。不支持SIMD指令集的复杂编码通常不被Arrow原生支持,而像Parquet这样的格式则支持。在内存中以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社区发布了三篇入门文章 123,我们推荐给有兴趣探索这项技术的读者。

本文主要关注将数据从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 vs. 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:简单与复杂数据模型。

自然表示

表示简单分层数据模型最直接和直观的方法是使用Arrow的列表、映射和联合数据类型。然而,需要注意的是,其中一些数据类型在整个Arrow生态系统中并非完全支持。例如,将联合转换为Parquet 不支持直接转换,需要进行转换步骤(请参阅 反规范化和展平表示 以将稀疏联合分解为可为空的结构和类型ID列)。同样,DataFusion 20版 尚不支持 列表和映射(嵌套结构部分支持)。

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

以下示例是一个Go程序片段,展示了一个使用这些不同数据类型表示上述模型的Arrow模式。

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类型的查询引擎仍然能够处理这些数据。有趣的是,压缩后,这种数据表示方式可能不一定比之前的方法更大。这是因为当数据存在冗余时,列式表示可以很好地压缩。

如果您的管道的某些组件不支持联合类型,也可以通过合并联合变体来消除它们(嵌套结构‘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模式,例如下面的Protobuf示例。在此示例中,将属性集合添加到每个数据点。这些属性是通过递归定义定义的,大多数语言和格式(如Protobuf)都支持(见下文的‘AnyValue’定义)。不幸的是,Arrow(像大多数经典数据库模式一样)不支持模式内的这种递归定义。

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模式定义,您可以采用一种自适应的迭代方法,该方法根据正在转换的数据自动构建Arrow模式。通过这种方法,字段会根据其基数自动进行字典编码,未使用的字段会被消除,并且递归结构会以特定方式表示。另一种解决方案是使用多模式方法,其中属性在单独的Arrow记录中显示,并用自引用关系表示继承关系。这些策略将在未来的文章中更深入地介绍。对于渴望了解更多的人来说,第一种方法在 OTel Arrow Adapter 的参考实现中得到了应用。

数据传输

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

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

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需要定义一个静态模式,这有时会使表示此类数据变得复杂或不理想,尤其是在初始模式包含递归定义时。本文讨论了几种解决此问题的方法。下一篇文章将专门介绍一种混合策略,该策略涉及动态调整Arrow模式,以根据正在表示的数据来优化内存使用、压缩率和处理速度。这种方法非常独特,值得单独介绍。