我们在 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 格式中处理和传输数据已成为大数据社区中普遍存在的模型。
图 1 说明了面向行和面向列的方法在内存表示方面的差异。面向列的方法将来自同一列的数据分组到连续的内存区域中,这有利于并行处理 (SIMD) 并提高压缩性能。
为什么我们对 Apache Arrow 感兴趣
在 F5,我们已采用 OpenTelemetry (OTel) 作为我们所有产品(如 BIGIP 和 NGINX)中所有遥测的标准。这些产品可能会出于各种原因生成大量的指标和日志,从性能评估到取证目的。这些系统产生的数据通常集中在专用系统中进行处理。传输和处理这些数据占遥测管道相关成本的很大一部分。在这种情况下,我们开始对 Apache Arrow 感兴趣。我们没有重新发明另一种遥测解决方案,而是决定投资 OpenTelemetry 项目,致力于改进协议,以显著提高其在高遥测数据量下的效率。我们与 Joshua MacDonald(来自 Lightstep)合作,将这些优化集成到 实验性 OTel 收集器中,目前正在与 OTel 技术委员会讨论以最终确定代码捐赠。
该项目已分为两个阶段。第一阶段即将完成,旨在提高协议的压缩率。第二阶段计划在未来进行,重点是通过在所有级别整合 Apache Arrow 来提高端到端性能,从而消除旧协议和新协议之间转换的需要。到目前为止,结果令人鼓舞,我们的基准测试显示,根据数据类型(指标、日志、跟踪)、分布和压缩算法的不同,压缩率提高了 1.5 倍到 5 倍。对于第二阶段,我们的估计表明,数据处理加速可能在 2 倍到 12 倍之间,同样取决于数据的性质和分布。有关更多信息,我们鼓励您查看规范和参考实现。
Arrow 依赖于模式来定义其处理和传输的数据批次的结构。后续章节将讨论可用于优化这些模式创建的各种技术。
如何利用 Arrow 来优化网络传输成本
Apache Arrow 是一个复杂的项目,其生态系统正在快速发展,这有时会让新手感到不知所措。幸运的是,Arrow 社区发布了三篇入门文章1、2和3,我们建议那些有兴趣探索这项技术的人阅读。
本文主要关注将数据从 XYZ 格式转换为高效的 Arrow 表示,从而优化压缩率和数据处理。这种转换有很多种方法,我们将研究这些方法如何影响转换过程中的压缩率、CPU 使用率和内存消耗等因素。
初始模型的复杂性会显著影响您需要做出的 Arrow 映射选择。首先,必须确定您希望针对特定上下文进行优化的属性。压缩率、转换速度、内存消耗、最终模型的速度和易用性、兼容性和可扩展性都是可能影响您的最终映射决策的因素。从那里开始,您必须探索多种替代模式。
为每个单独的字段选择 Arrow 类型和数据编码将影响您的模式的性能。有多种方法可以表示分层数据或高度动态的数据模型,需要与传输层的配置协调评估多种选项。还应仔细考虑此传输层。Arrow 支持压缩机制和字典增量,这些机制和增量可能不会默认处于活动状态。
经过几次迭代后,您应该得到一个满足您最初设定的目标的优化模式。至关重要的是使用真实数据来比较不同方法的性能,因为每个单独字段中的数据分布可能会影响您是否使用字典编码。我们现在将在本文的剩余部分中更详细地研究这些选择。
Arrow 数据类型选择
选择 Arrow 数据类型的原则与为数据库定义数据模型时使用的原则非常相似。Arrow 支持各种数据类型。某些类型在所有实现中都受支持,而其他类型仅适用于具有最强大的 Arrow 社区支持的语言(有关不同实现的比较矩阵,请参见此页面)。对于基本类型,通常最好选择提供最简洁表示且最接近初始字段语义的类型。例如,虽然可以使用 int64 表示时间戳,但使用本机 Arrow 时间戳类型更有利。这种选择不是因为二进制表示更有效,而是因为它在您的管道中更容易处理和操作。诸如 DataFusion 之类的查询引擎为这种类型的列提供了专门的时间戳处理函数。对于基本类型(如日期、时间、持续时间和间隔),可以选择相同的类型。但是,如果您的项目需要最大兼容性,在某些情况下,可能必须优先选择具有通用支持的类型,而不是内存占用方面最理想的类型。
在选择 Arrow 数据类型时,务必考虑压缩前后的数据大小。两种不同类型的数据在压缩后可能大小相同,但实际内存中的大小可能相差两倍、四倍甚至八倍(例如,uint8 与 uint64)。这种差异会影响您处理大型数据批次的能力,并且还会显著影响内存中处理这些数据的速度(例如,缓存优化、SIMD 指令效率)。
还可以使用扩展类型机制来扩展这些类型,该机制基于当前支持的原始类型之一,同时添加特定的语义。这种扩展机制可以简化您在自己项目中使用此数据的过程,同时对于将此数据解释为基本原始类型的中间系统保持透明。
原始类型的编码存在一些差异,我们接下来将进行探讨。
数据编码
优化 Arrow 模式的另一个关键方面是分析数据的基数。通常,仅具有有限数量值的字段使用字典编码可以更有效地表示。
字段的最大基数决定了字典的数据类型特征。例如,对于表示 HTTP 事务状态码的字段,最好使用索引类型为 “uint8”、值类型为 “uint16” 的字典(表示法:“Dictionary<uint8, uint16>”)。这样可以消耗更少的内存,因为主数组的类型将为 “[]uint8”。即使可能值的范围大于 255,只要不同值的数量不超过 255,表示仍然高效。类似地,“user-agent” 的表示使用类型为 “Dictionary<uint16, string>” 的字典会更高效(见图 5)。在这种情况下,主数组的类型将为 “uint16”,从而在内存中和传输过程中实现紧凑的表示,代价是在反向转换过程中需要间接寻址。
字典编码在 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 模式在其定义中是静态的,并且其嵌套元素的深度必须预先知道。有多种方法可以解决此限制,我们将在以下章节中探讨这些方法。
自然表示
表示简单分层数据模型最直接和直观的方法是使用 Arrow 的列表、映射和联合数据类型。但是,需要注意的是,这些数据类型中的某些类型在整个 Arrow 生态系统中并未完全支持。例如,将联合转换为 Parquet 不直接支持,并且需要一个转换步骤(请参阅去规范化和扁平化表示,将稀疏联合分解为可为空的结构和类型 ID 列)。类似地,列表和映射在 DataFusion 版本 20 中尚未支持(嵌套结构部分支持)。
以下示例是使用这些不同数据类型表示上述模型的 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 联合针对不同的情况进行了优化。密集联合类型具有相对简洁的内存表示,但不支持矢量化操作,从而在处理阶段效率较低。相反,稀疏联合支持矢量化操作,但会带来内存开销,该开销与联合中的变体数量直接成正比。密集和稀疏联合的压缩率非常相似,有时稀疏联合略有优势。此外,通常应避免使用具有大量变体的稀疏联合,因为它们可能导致过多的内存消耗。有关联合的内存表示的更多详细信息,您可以参考此页面。
在某些情况下,使用多个模式(即每个子类型一个模式)来表示继承关系可能更惯用,从而避免使用联合类型。但是,将此方法应用于上述模型可能不是最佳的,因为继承关系之前的数据(即 ResourceMetrics
、Scope
和 Metrics
)可能会被重复多次。如果 ResourceMetrics
、Metrics
和 DataPoint
之间的关系是 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 流,它在系统之间传输 Arrow 数据方面起着至关重要的作用。可以采用多种方法来传递这些 Arrow IPC 流。最简单的方法是使用 Arrow Flight,它将 Arrow IPC 流封装在基于 gRPC 的协议中。但是,也可以为特定上下文使用您自己的实现。无论您选择哪种解决方案,都必须了解底层协议必须是有状态的,才能充分利用 Arrow IPC 流方法。为了实现最佳的压缩率,仅发送一次模式和字典至关重要,以便摊销成本并最大限度地减少批次之间的数据冗余。这需要支持面向流通信的传输,例如 gRPC。
对于大型批次,可以使用无状态协议,因为与使用字典编码和列式表示法实现的压缩增益相比,模式的开销将可以忽略不计。但是,必须为每个批次传递字典,这使得这种方法通常不如面向流的方法有效。
Arrow IPC 流还支持“增量字典”的概念,这可以进一步优化批次传输。当批次将数据添加到现有字典(在发送方端)时,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 模式,以根据正在表示的数据优化内存使用、压缩率和处理速度。这种方法非常独特,值得单独撰写一篇文章。