JVM上高性能數據格式庫包Apache Arrow入門和架構詳解(Gkatziouras)

Apache Arrow是是各種大數據工具(包括BigQuery)使用的一種流行格式,它是平面和分層數據的存儲格式。它是一種加快應用程序內存密集型。

數據處理和數據科學領域中的常用庫: Apache Arrow 。諸如Apache Parquet,Apache Spark,pandas之類的開放源代碼項目以及許多商業或封閉源代碼服務都使用Arrow。它提供以下功能:

  • 內存計算
  • 標準化的柱狀存儲格式
  • 一個IPC和RPC框架,分別用於進程和節點之間的數據交換

讓我們看一看在Arrow出現之前事物是如何工作的:

我們可以看到,為瞭使Spark從Parquet文件中讀取數據,我們需要以Parquet格式讀取和反序列化數據。這要求我們通過將數據加載到內存中來制作數據的完整副本。首先,我們將數據讀入內存緩沖區,然後使用Parquet的轉換方法將數據(例如字符串或數字)轉換為我們的編程語言的表示形式。這是必需的,因為Parquet表示的數字與Python編程語言表示的數字不同。

由於許多原因,這對於性能來說是一個很大的問題:

  • 我們正在復制數據並在其上運行轉換步驟。數據的格式不同,我們需要對所有數據進行讀取和轉換,然後再對數據進行任何計算。
  • 我們正在加載的數據必須放入內存中。您隻有8GB的RAM,數據是10GB嗎?你真倒黴!

現在,讓我們看一下Apache Arrow如何改進這一點:

Arrow無需復制和轉換數據,而是瞭解如何直接讀取和操作數據。為此,Arrow社區定義瞭一種新的文件格式以及直接對序列化數據起作用的操作。可以直接從磁盤讀取此數據格式,而無需將其加載到內存中並轉換/反序列化數據。當然,部分數據仍將被加載到RAM中,但您的數據不必放入內存中。Arrow使用其文件的內存映射功能,僅在必要和可能的情況下將盡可能多的數據加載到內存中。

Apache Arrow支持以下語言:

  • C++
  • C#
  • Go
  • Java
  • JavaScript
  • Rust
  • Python (through the C++ library)
  • Ruby (through the C++ library)
  • R (through the C++ library)
  • MATLAB (through the C++ library).

Arrow特點

Arrow首先是提供用於內存計算的列式數據結構的庫,可以將任何數據解壓縮並解碼為Arrow柱狀數據結構,以便隨後可以對解碼後的數據進行內存內分析。Arrow列格式具有一些不錯的屬性:隨機訪問為O(1),每個值單元格在內存中的前一個和後一個相鄰,因此進行迭代非常有效。

Apache Arrow定義瞭一種二進制“序列化”協議,用於安排Arrow列數組的集合(稱為“記錄批處理”),該數組可用於消息傳遞和進程間通信。您可以將協議放在任何地方,包括磁盤上,以後可以對其進行內存映射或讀入內存並發送到其他地方。

Arrow協議的設計目的是使您可以“映射”一個Arrow數據塊而不進行任何反序列化,因此對磁盤上的Arrow協議數據執行分析可以使用內存映射並有效地支付零成本。該協議用於很多事情,例如Spark SQL和Python之間的流數據,用於針對Spark SQL數據塊運行pandas函數,這些被稱為“ pandas udfs”。

Arrow是為內存而設計的(但是您可以將其放在磁盤上,然後再進行內存映射)。它們旨在相互兼容,並在應用程序中一起使用,而其競爭對手Apache Parquet文件是為磁盤存儲而設計的。

優點:Apache Arrow為平面和分層數據定義瞭一種獨立於語言的列式存儲格式,該格式組織為在CPU和GPU等現代硬件上進行高效的分析操作而組織。Arrow存儲器格式還支持零拷貝讀取,以實現閃電般的數據訪問,而無需序列化開銷。

Java的Apache Arrow

導入庫:

<dependency>
    <groupId>org.apache.arrow</groupId>
    <artifactId>arrow-memory-netty</artifactId>
    <version>${arrow.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.arrow</groupId>
    <artifactId>arrow-vector</artifactId>
    <version>${arrow.version}</version>
</dependency>

在開始之前,必須瞭解對於Arrow的讀/寫操作,使用瞭字節緩沖區。諸如讀取和寫入之類的操作是字節的連續交換。為瞭提高效率,Arrow附帶瞭一個緩沖區分配器,該緩沖區分配器可以具有一定的大小,也可以具有自動擴展功能。支持分配管理的庫是arrow-memory-netty和arrow-memory-unsafe。我們這裡使用netty。

用Arrow存儲數據需要一個模式,模式可以通過編程定義:

package com.gkatzioura.arrow;

import java.io.IOException;

import java.util.List;

import org.apache.arrow.vector.types.pojo.ArrowType;

import org.apache.arrow.vector.types.pojo.Field;

import org.apache.arrow.vector.types.pojo.FieldType;

import org.apache.arrow.vector.types.pojo.Schema;

public class SchemaFactory {

public static Schema DEFAULT_SCHEMA = createDefault();

public static Schema createDefault() {

var strField = new Field("col1", FieldType.nullable(new ArrowType.Utf8()), null);

var intField = new Field("col2", FieldType.nullable(new ArrowType.Int(32, true)), null);

return new Schema(List.of(strField, intField));

}

public static Schema schemaWithChildren() {

var amount = new Field("amount", FieldType.nullable(new ArrowType.Decimal(19,4,128)), null);

var currency = new Field("currency",FieldType.nullable(new ArrowType.Utf8()), null);

var itemField = new Field("item", FieldType.nullable(new ArrowType.Utf8()), List.of(amount,currency));

return new Schema(List.of(itemField));

}

public static Schema fromJson(String jsonString) {

try {

return Schema.fromJSON(jsonString);

} catch (IOException e) {

throw new ArrowExampleException(e);

}

}

}

他們也有一個可解析的json表示形式:

{
  "fields" : [ {
    "name" : "col1",
    "nullable" : true,
    "type" : {
      "name" : "utf8"
    },
    "children" : [ ]
  }, {
    "name" : "col2",
    "nullable" : true,
    "type" : {
      "name" : "int",
      "bitWidth" : 32,
      "isSigned" : true
    },
    "children" : [ ]
  } ]
}

另外,就像Avro一樣,您可以在字段上設計復雜的架構和嵌入式值:

public static Schema schemaWithChildren() {
    var amount = new Field("amount", FieldType.nullable(new ArrowType.Decimal(19,4,128)), null);
    var currency = new Field("currency",FieldType.nullable(new ArrowType.Utf8()), null);
    var itemField = new Field("item", FieldType.nullable(new ArrowType.Utf8()), List.of(amount,currency));
 
    return new Schema(List.of(itemField));
}

基於上面的的Schema,我們將為我們的類創建一個DTO:

package com.gkatzioura.arrow;
 
import lombok.Builder;
import lombok.Data;
 
@Data
@Builder
public class DefaultArrowEntry {
 
    private String col1;
    private Integer col2;
 
}

我們的目標是將這些Java對象轉換為Arrow字節流。

1. 使用分配器創建 DirectByteBuffer

這些緩沖區是 堆外的 。您確實需要釋放所使用的內存,但是對於庫用戶而言,這是通過在分配器上執行 close() 操作來完成的。在我們的例子中,我們的類將實現 Closeable 接口,該接口將執行分配器關閉操作。

通過使用流api,數據將被流傳輸到使用Arrow格式提交的OutPutStream:

package com.gkatzioura.arrow;
 
import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.List;
 
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.IntVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.dictionary.DictionaryProvider;
import org.apache.arrow.vector.ipc.ArrowStreamWriter;
import org.apache.arrow.vector.util.Text;
 
import static com.gkatzioura.arrow.SchemaFactory.DEFAULT_SCHEMA;
 
public class DefaultEntriesWriter implements Closeable {
 
    private final RootAllocator rootAllocator;
    private final VectorSchemaRoot vectorSchemaRoot;//向量分配器創建:
 
    public DefaultEntriesWriter() {
        rootAllocator = new RootAllocator();
        vectorSchemaRoot = VectorSchemaRoot.create(DEFAULT_SCHEMA, rootAllocator);
    }
 
    public void write(List<DefaultArrowEntry> defaultArrowEntries, int batchSize, WritableByteChannel out) {
        if (batchSize <= 0) {
            batchSize = defaultArrowEntries.size();
        }
 
        DictionaryProvider.MapDictionaryProvider dictProvider = new DictionaryProvider.MapDictionaryProvider();
        try(ArrowStreamWriter writer = new ArrowStreamWriter(vectorSchemaRoot, dictProvider, out)) {
            writer.start();
 
            VarCharVector childVector1 = (VarCharVector) vectorSchemaRoot.getVector(0);
            IntVector childVector2 = (IntVector) vectorSchemaRoot.getVector(1);
            childVector1.reset();
            childVector2.reset();
 
            boolean exactBatches = defaultArrowEntries.size()%batchSize == 0;
            int batchCounter = 0;
 
            for(int i=0; i < defaultArrowEntries.size(); i++) {
                childVector1.setSafe(batchCounter, new Text(defaultArrowEntries.get(i).getCol1()));
                childVector2.setSafe(batchCounter, defaultArrowEntries.get(i).getCol2());
 
                batchCounter++;
 
                if(batchCounter == batchSize) {
                    vectorSchemaRoot.setRowCount(batchSize);
                    writer.writeBatch();
                    batchCounter = 0;
                }
            }
 
            if(!exactBatches) {
                vectorSchemaRoot.setRowCount(batchCounter);
                writer.writeBatch();
            }
 
            writer.end();
        } catch (IOException e) {
            throw new ArrowExampleException(e);
        }
    }
 
    @Override
    public void close() throws IOException {
        vectorSchemaRoot.close();
        rootAllocator.close();
    }
 
}

為瞭在Arrow上顯示批處理的支持,已在函數中實現瞭簡單的批處理算法。對於我們的示例,隻需考慮將數據分批寫入。

讓我們深入瞭解上面代碼功能:

向量分配器創建:

public DefaultEntriesToBytesConverter() {
    rootAllocator = new RootAllocator();
    vectorSchemaRoot = VectorSchemaRoot.create(DEFAULT_SCHEMA, rootAllocator);
}

然後在寫入流時,實現並啟動瞭Arrow流編寫器

ArrowStreamWriter writer = new ArrowStreamWriter(vectorSchemaRoot, dictProvider, Channels.newChannel(out));
writer.start();

我們將數據填充向量,然後還重置它們,但讓預分配的緩沖區 存在 :

VarCharVector childVector1 = (VarCharVector) vectorSchemaRoot.getVector(0);
IntVector childVector2 = (IntVector) vectorSchemaRoot.getVector(1);
childVector1.reset();
childVector2.reset();

寫入數據時,我們使用 setSafe 操作。如果需要分配更多的緩沖區,應采用這種方式。對於此示例,此操作在每次寫入時都完成,但是在考慮瞭所需的操作和緩沖區大小後可以避免:

childVector1.setSafe(i, new Text(defaultArrowEntries.get(i).getCol1()));
childVector2.setSafe(i, defaultArrowEntries.get(i).getCol2());

然後,將批處理寫入流中:

vectorSchemaRoot.setRowCount(batchSize);
writer.writeBatch();

最後但並非最不重要的一點是,我們關閉瞭writer:

@Override
public void close() throws IOException {
    vectorSchemaRoot.close();
    rootAllocator.close();
}

以上就是JVM上高性能數據格式庫包Apache Arrow入門和架構詳解(Gkatziouras)的詳細內容,更多關於Apache Arrow入門的資料請關註WalkonNet其它相關文章!

推薦閱讀: