我們在 F5 使用 Apache Arrow 的旅程 (第 1 部分)
已發布 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 的優勢已獲得廣泛認可和讚賞(請參閱這篇 文章,深入了解 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 專案,致力於改進協定,以顯著提高其在高遙測資料量下的效率。我們與來自 Lightstep 的 Joshua MacDonald 合作,將這些最佳化整合到 實驗性 OTel 收集器 中,目前正在與 OTel 技術委員會討論最終的程式碼 捐贈。
這個專案分為兩個階段。第一階段即將完成,旨在提高協定的壓縮率。第二階段計劃在未來進行,重點是透過在所有層級整合 Apache Arrow 來提高端對端效能,從而消除在新舊協定之間進行轉換的需求。到目前為止,結果令人鼓舞,我們的基準測試顯示壓縮率提高了 x1.5 到 x5,具體取決於資料類型(指標、日誌、追蹤)、分佈和壓縮演算法。對於第二階段,我們的估計表明,資料處理加速可能在 x2 到 x12 之間,同樣取決於資料的性質和分佈。如需更多資訊,我們鼓勵您查看 規格 和 參考實作。
Arrow 依賴於架構來定義其處理和傳輸的資料批次的結構。後續章節將討論可用於最佳化這些架構建立的各種技術。
如何利用 Arrow 來最佳化網路傳輸成本
Apache Arrow 是一個複雜的專案,其生態系統發展迅速,有時可能會讓新手感到不知所措。幸運的是,Arrow 社群發布了三篇入門文章 1、2 和 3,我們建議那些有興趣探索這項技術的人閱讀。
本文主要著重於將資料從 XYZ 格式轉換為高效的 Arrow 表示法,以優化壓縮率和資料處理。這種轉換方法有很多種,我們將檢視這些方法如何影響轉換過程中的壓縮率、CPU 使用率和記憶體消耗以及其他因素。
您初始模型的複雜性顯著影響您需要做出的 Arrow 對應選擇。首先,務必確定您想要針對特定環境最佳化的屬性。壓縮率、轉換速度、記憶體消耗、最終模型的速度和易用性、相容性和可擴展性都是可能影響您最終對應決策的因素。從那裡,您必須探索多個替代架構。
為每個欄位選擇 Arrow 類型和資料編碼將影響您架構的效能。有多種方法可以表示階層式資料或高度動態的資料模型,並且需要協調傳輸層的配置來評估多個選項。還應仔細考慮此傳輸層。Arrow 支援壓縮機制和字典增量,這些機制和增量可能預設未啟用。
經過多次反覆運算此過程後,您應該獲得一個最佳化的架構,該架構符合您最初設定的目標。務必使用真實資料比較不同方法的效能,因為每個欄位中資料的分佈可能會影響您是否使用字典編碼。我們現在將在本文的其餘部分更詳細地檢視這些選擇。
Arrow 資料類型選擇
選擇 Arrow 資料類型的原則與定義資料庫資料模型時使用的原則非常相似。Arrow 支援廣泛的資料類型。其中一些類型受到所有實作的支援,而另一些類型僅適用於 Arrow 社群支援最強的語言(請參閱此 頁面 以取得不同實作的比較矩陣)。對於基本類型,通常最好選擇提供最簡潔表示法且最接近初始欄位語意的類型。例如,雖然可以使用 int64 表示時間戳記,但使用原生 Arrow Timestamp 類型更有優勢。這種選擇並不是因為二進位表示法更有效率,而是因為在您的管線中處理和操作它會更容易。查詢引擎(例如 DataFusion)為這種型別的欄提供專用的時間戳記處理函數。對於基本類型(例如日期、時間、持續時間和間隔),也可以做出相同的選擇。但是,如果您的專案需要最大的相容性,則在某些情況下,優先選擇具有通用支援的類型而不是在記憶體佔用方面最優的類型可能至關重要。
選擇 Arrow 資料類型時,務必考慮壓縮前後的資料大小。兩種不同類型的壓縮後大小可能相同,但記憶體中的實際大小可能大兩倍、四倍甚至八倍(例如,uint8 與 uint64)。這種差異會影響您處理大量資料批次的能力,也會顯著影響在記憶體中處理這些資料的速度(例如,快取最佳化、SIMD 指令效率)。
也可以使用 擴充類型 機制來擴充這些類型,該機制建立在目前支援的基本類型之一之上,同時新增特定語意。這種擴充機制可以簡化在您自己的專案中使用此資料的方式,同時對於將此資料解譯為基本基本類型的中繼系統保持透明。
基本類型的編碼方式存在一些差異,我們接下來將探討這些差異。
資料編碼
最佳化 Arrow 架構的另一個關鍵方面是分析資料的基數。只能有有限數量值的欄位通常可以使用字典編碼更有效率地表示。
欄位的最大基數決定了字典的資料類型特性。例如,對於表示 HTTP 交易狀態碼的欄位,最好使用索引類型為「uint8」且值類型為「uint16」的字典(表示法:「Dictionary<uint8, uint16>」)。這樣消耗的記憶體更少,因為主陣列的類型將為「[]uint8」。即使可能值的範圍大於 255,只要不同值的數量不超過 255,表示法仍然有效率。同樣,使用類型為「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 的轉換 未直接支援,並且需要轉換步驟(請參閱 反正規化與扁平化表示法,將稀疏聯合分解為可為 Null 的結構和類型 ID 欄)。同樣,DataFusion 版本 20 中 尚不支援 列表和映射(部分支援巢狀結構)。
以下範例是一個 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 聯合類型針對不同的情況進行了最佳化。密集聯合類型的記憶體表示法相對簡潔,但不支援向量化操作,這使其在處理階段效率較低。相反,稀疏聯合支援向量化操作,但會帶來與聯合變體數量成正比的記憶體開銷。密集和稀疏聯合具有非常相似的壓縮率,有時稀疏聯合略有優勢。此外,通常應避免使用具有大量變體的稀疏聯合,因為它們可能會導致過多的記憶體消耗。如需有關聯合記憶體表示法的更多詳細資訊,您可以查閱此 頁面。
在某些情況下,使用多個架構(即每個子類型一個架構)來表示繼承關係可能更符合語言習慣,從而避免使用聯合類型。但是,將此方法應用於上述模型可能不是最佳的,因為繼承關係之前的資料(即 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 支援位元圖有效性的概念,以識別各種資料類型的 Null 值(每個條目 1 位元),包括那些沒有唯一 Null 表示法(例如,基本類型)的資料類型。位元圖有效性的使用使查詢部分更容易,並且 DataFusion 等查詢引擎知道如何有效率地使用它。具有大量 Null 值的欄通常可以非常有效率地壓縮,因為底層陣列通常以 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 生態系統正在快速發展,因此查詢引擎中對列表、映射和聯合資料類型的支援可能會迅速改進。如果核心函數足以滿足您的應用程式或更適合您的應用程式,則通常可以使用這些巢狀類型。
適應性/動態表示法
某些資料模型可能更難轉換為 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 介面卡 的參考實作中使用。
資料傳輸
與 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 架構,以根據正在表示的資料來最佳化記憶體使用量、壓縮率和處理速度。這種方法非常獨特,值得單獨撰寫一篇文章。