[20240825] Apache Arrow

Table of Contents

아파치 애로우에 대해서 알아보고자 한다.

XTDB에서 이것을 사용해서 성능에 개선을 도모했다고 하지만 소스코드를 보다보니 좀 더 내부구현을 이해하려면 Apache Arrow 를 이해해봐야 하겠다는 생각을 했다.

그래서 Apache Arrow 공식문서의 튜토리얼을 따라해보기로 했다. 하면서 개념들도 이해해보자.

1 Quick Start Guide

Arrow 는 몇가지 빌딩 블록을 제공한다.

  • Data type : 값 타입
  • ValueVector : 입력된 값의 시퀀스
  • field : 표 형식 데이터의 열 유형
  • VectorSchemaRoot : 표 형식 데이터.

또한 Arrow 는 스토리지에서 데이터를 로드, 퍼시스트(저장) 하기위한 리더, 라이터를 제공한다.

1.1 ValueVector 만들기

ValueVector는 같은 유형의 값 시퀀스를 말함. 컬럼포맷의 배열(array) 라고도 함.

예: [1, null, 2] 를 나타내는 32비트 정수의 벡터를 만든다.

import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.IntVector;

try(
    BufferAllocator allocator = new RootAllocator();
    IntVector intVector = new IntVector("fixed-size-primitive-layout", allocator);
){
    intVector.allocateNew(3);
    intVector.set(0,1);
    intVector.setNull(1);
    intVector.set(2,2);
    intVector.setValueCount(3);
    System.out.println("Vector created in memory: " + intVector);
}
---
Vector created in memory: [1, null, 2]

예: ["1", "2", "3"]을 나타내는 UTF-8로 인코딩된 문자열 벡터를 만든다:

import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.VarCharVector;

try(
    BufferAllocator allocator = new RootAllocator();
    VarCharVector varCharVector = new VarCharVector("variable-size-primitive-layout", allocator);
){
    varCharVector.allocateNew(3);
    varCharVector.set(0, "one".getBytes());
    varCharVector.set(1, "two".getBytes());
    varCharVector.set(2, "three".getBytes());
    varCharVector.setValueCount(3);
    System.out.println("Vector created in memory: " + varCharVector);
}
---
Vector created in memory: [one, two, three]

1.2 필드 만들기

필드는 표 형식 데이터의 특정 열을 나타냄.

필드는 이름, 데이터 유형, 열이 null 값을 가질 수 있는지 여부를 나타내는 플래그, 선택적 키-값 메타데이터로 구성됩니다.

예: 문자열 유형의 'document'라는 이름의 필드를 만든다:

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 java.util.HashMap;
import java.util.Map;

Map<String, String> metadata = new HashMap<>();
metadata.put("A", "Id card");
metadata.put("B", "Passport");
metadata.put("C", "Visa");
Field document = new Field("document",
	new FieldType(true, new ArrowType.Utf8(), /*dictionary*/ null, metadata),
	/*children*/ null);
System.out.println("Field created: " + document + ", Metadata: " + document.getMetadata());
---
Field created: document: Utf8, Metadata: {A=Id card, B=Passport, C=Visa}

1.3 Schema 만들기

스키마는 일련의 필드와 몇가지 선택적 메타데이터를 함께 보관한다.

예: int32 컬럼 "A" 와 UTF8로 인코딩된 문자열 컬럼 "B" 로 구성된 데이터 집합을 설명하는 스키마

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;
import java.util.HashMap;
import java.util.Map;
import static java.util.Arrays.asList;

Map<String, String> metadata = new HashMap<>();
metadata.put("K1", "V1");
metadata.put("K2", "V2");
Field a = new Field("A", FieldType.nullable(new ArrowType.Int(32, true)), /*children*/ null);
Field b = new Field("B", FieldType.nullable(new ArrowType.Utf8()), /*children*/ null);
Schema schema = new Schema(asList(a, b), metadata);
System.out.println("Schema created: " + schema);
---
Schema created: Schema<A: Int(32, true), B: Utf8>(metadata: {K1=V1, K2=V2})

1.4 VectorSchemaRoot 만들기

ValueVector 와 스키마를 결합하여 표 형식의 데이터를 표현함.

예: 이름(strings) 와 나이(32-bit signed integers) 를 만든다.

import org.apache.arrow.memory.BufferAllocator;
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.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;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static java.util.Arrays.asList;

Field age = new Field("age",
	FieldType.nullable(new ArrowType.Int(32, true)),
	/*children*/null
);
Field name = new Field("name",
	FieldType.nullable(new ArrowType.Utf8()),
	/*children*/null
);
Schema schema = new Schema(asList(age, name), /*metadata*/ null);
try(
    BufferAllocator allocator = new RootAllocator();
    VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator);
    IntVector ageVector = (IntVector) root.getVector("age");
    VarCharVector nameVector = (VarCharVector) root.getVector("name");
){
    ageVector.allocateNew(3);
    ageVector.set(0, 10);
    ageVector.set(1, 20);
    ageVector.set(2, 30);
    nameVector.allocateNew(3);
    nameVector.set(0, "Dave".getBytes(StandardCharsets.UTF_8));
    nameVector.set(1, "Peter".getBytes(StandardCharsets.UTF_8));
    nameVector.set(2, "Mary".getBytes(StandardCharsets.UTF_8));
    root.setRowCount(3);
    System.out.println("VectorSchemaRoot created: \n" + root.contentToTSVString());
}
---
VectorSchemaRoot created:
age      name
10      Dave
20      Peter
30      Mary

1.5 Interprocess Communication (IPC) - 프로세스 간 통신

Arrow 데이터는 디스크에 쓰거나 디스크에서 읽을 수 있으며, 애플리케이션 요구 사항에 따라 이 두 가지 모두 스트리밍 또는 랜덤 엑세스 방식으로 수행할 수 있따.

예: 이전 예제의 데이터 집합을 Arrow IPC 파일에 쓰기(random-access).

import org.apache.arrow.memory.BufferAllocator;
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.ipc.ArrowFileWriter;
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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static java.util.Arrays.asList;

Field age = new Field("age",
	FieldType.nullable(new ArrowType.Int(32, true)),
	/*children*/ null);
Field name = new Field("name",
	FieldType.nullable(new ArrowType.Utf8()),
	/*children*/ null);
Schema schema = new Schema(asList(age, name));
try(
    BufferAllocator allocator = new RootAllocator();
    VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator);
    IntVector ageVector = (IntVector) root.getVector("age");
    VarCharVector nameVector = (VarCharVector) root.getVector("name");
){
    ageVector.allocateNew(3);
    ageVector.set(0, 10);
    ageVector.set(1, 20);
    ageVector.set(2, 30);
    nameVector.allocateNew(3);
    nameVector.set(0, "Dave".getBytes(StandardCharsets.UTF_8));
    nameVector.set(1, "Peter".getBytes(StandardCharsets.UTF_8));
    nameVector.set(2, "Mary".getBytes(StandardCharsets.UTF_8));
    root.setRowCount(3);
    File file = new File("random_access_file.arrow");
    try (
	FileOutputStream fileOutputStream = new FileOutputStream(file);

	ArrowFileWriter writer = new ArrowFileWriter(root, /*provider*/ null, fileOutputStream.getChannel());
    ) {
	writer.start();
	writer.writeBatch();
	writer.end();
	System.out.println("Record batches written: " + writer.getRecordBlocks().size()
		+ ". Number of rows written: " + root.getRowCount());
    } catch (IOException e) {
	e.printStackTrace();
    }
}
---
Record batches written: 1. Number of rows written: 3

읽기 예: 이전 예제의 데이터 집합을 Arrow IPC 파일(random-access)에서 읽기.

import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.ipc.ArrowFileReader;
import org.apache.arrow.vector.ipc.message.ArrowBlock;
import org.apache.arrow.vector.VectorSchemaRoot;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

try(
    BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
    FileInputStream fileInputStream = new FileInputStream(new File("random_access_file.arrow"));
    ArrowFileReader reader = new ArrowFileReader(fileInputStream.getChannel(), allocator);
){
    System.out.println("Record batches in file: " + reader.getRecordBlocks().size());
    for (ArrowBlock arrowBlock : reader.getRecordBlocks()) {
	reader.loadRecordBatch(arrowBlock);
	VectorSchemaRoot root = reader.getVectorSchemaRoot();
	System.out.println("VectorSchemaRoot read: \n" + root.contentToTSVString());
    }
} catch (IOException e) {
    e.printStackTrace();
}
---
Record batches in file: 1
VectorSchemaRoot read:
age      name
10       Dave
20       Peter
30       Mary

2 Memory Management

memory 모듈은 Arrow 가 메모리를 할당하고 할당해제하는데 사용하는 모든 기능이 포함되어 있다.

이 문서는 두 부분은 나뉜다.

  1. Memory Basics - 개괄적인 소개를 제공한다.
  2. Arrow Memory In-Depth - 자세한 내용을 설명함.

2.1 Memory Basics

java 메모리 주요 개념을 설명할 것

  • ArrowBuf
  • BufferAllocator
  • Reference counting

또한 Arrow에서 메모리 작업을 위한 몇 가지 지침을 제공하고 메모리 문제가 발생했을 때 디버깅하는 방법을 설명.

Arrow의 메모리 관리는 열 형식의 요구 사항과 오프 힙 메모리 사용을 중심으로 구축되었습니다. Arrow Java에는 자체적인 독립 구현이 있습니다.

이 프레임워크는 자바 코드에서 사용하는 C++에서 할당된 메모리와 함께 사용할 수 있을 만큼 유연하지만, C++ 구현을 래핑하지는 않습니다.

Arrow는 핵심 인터페이스와 인터페이스의 구현이라는 여러 모듈을 제공합니다. 사용자는 핵심 인터페이스와 구현 중 정확히 한 가지를 필요로 합니다.

  • memory-core: Arrow 라이브러리 및 애플리케이션에서 사용하는 인터페이스를 제공합니다.
  • memory-netty: Netty 라이브러리를 기반으로 하는 메모리 인터페이스 구현.
  • memory-unsafe: sun.misc.Unsafe 라이브러리를 기반으로 하는 메모리 인터페이스 구현.

2.1.1 ArrowBuf

ArrowBuf는 직접 메모리의 연속된 단일 영역을 나타냅니다. 주소와 길이로 구성되며, 내용물 작업을 위한 저수준 인터페이스를 제공하며, ByteBuffer와 유사합니다.

(Direct)ByteBuffer와 달리, 나중에 설명하는 것처럼 참조 카운팅이 내장되어 있습니다.

[직접메모리 관련 링크]​

  1. Why Arrow Uses Direct Memory
    • 직접 메모리/직접 버퍼를 사용할 때 JVM은 버퍼 내용을 중간 버퍼로 복사하지 않도록 시도하여 I/O 작업을 최적화할 수 있습니다. 이를 통해 Arrow의 IPC 속도를 높일 수 있습니다.
    • Arrow는 항상 직접 메모리를 사용하기 때문에 JNI 모듈은 데이터를 복사하는 대신 네이티브 메모리 주소를 직접 래핑할 수 있습니다. 이를 C 데이터 인터페이스와 같은 모듈에서 사용합니다.
    • 반대로, JNI 경계의 C++ 측에서는 데이터를 복사하지 않고도 ArrowBuf의 메모리에 직접 액세스할 수 있습니다.

2.1.2 BufferAllocator

BufferAllocator는 주로 버퍼(ArrowBuf 인스턴스)의 어카운팅에 사용되는 arena or nursery 이다. 이름에서 알 수 있듯이 자신과 관련된 새 버퍼를 할당할 수 있지만 다른 곳에서 할당된 버퍼에 대한 계정(accounting)도 처리할 수 있습니다. 예를 들어, C++에서 할당되고 C-Data 인터페이스를 사용하여 Java와 공유되는 메모리에 대한 Java 측의 계정을 처리합니다. (다른 곳에서 할당된 버퍼에 대한 accounting도 처리할 수 있다는 예시)

arena allocation 은 메모리 관리 기법의 한 종류이다. 여러 작은 메모리 할당을 하나의 큰 메모리 블록에서 수행합니다. 또한 개별 객체의 해제 대신 전체 아레나를 한 번에 해제할 수 있습니다. 빠른 할당과 일괄 해제가 장점입니다.

관련 문서 좀 더 알아보기 [오라클 arena 문서]​

nursery 는 주로 세대별 가비지 컬렉션에서 사용되는 용어. 새로 생성된 객체들이 처음 할당되는 메모리 영역을 가리킴(young generation) 대부분의 객체가 짧은 수명을 가진다는 가정 하에 설계 [오라클 메모리 관리 문서]​

아래 코드에서는 할당을 수행합니다:

import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;

try(BufferAllocator bufferAllocator = new RootAllocator(8 * 1024)){
    ArrowBuf arrowBuf = bufferAllocator.buffer(4 * 1024);
    System.out.println(arrowBuf);
    arrowBuf.close();
}
---
ArrowBuf[2], address:140363641651200, length:4096

BufferAllocator 인터페이스의 구현체는 RootAllocator 이다. 애플리케이션은 일반적으로 프로그램을 시작할 때 하나의 RootAllocator를 생성하고 BufferAllocator 인터페이스를 통해 이를 사용해야 한다.

Allocators 는 AutoCloseable 을 구현해서 애플리케이션이 완료된 후에는 반드시 닫아야한다. 닫기를 해야 미사용 메모리가 모두 해제되었는지 확인한다.

Arrow는 메모리 할당을 위한 트리기반모델을 제공한다. RootAllocator가 처음 생성된 다음, newChildAllocator를 통해 자식으로 더 많은 allocator 가 생성된다. RootAllocator 또는 child allocator 를 생성할 때 메모리 제한이 제공되며, 메모리르 할당할 때 제한을 확인한다.

또한 child allocator 에서 메모리를 할당할 때, 해당 할당은 모두 parent allocator에 반영된다.

따라서 RootAllocator는 프로그램 전체 메모리 제한을 효과적으로 설정하고 모든 메모리 할당에 대한 master bookkeeper 역할을 한다.

child allocator는 반드시 필요한 것은 아니지만 코드를 더 잘 정리하는데 도움이 될 수 있다. 예를들어 코드의 특정 섹션에 더 낮은 메모리 제한을 설정할 수 있다. 해당 섹션이 완료되면 child allocator를 close 할 수 있으며, 이 시점에서 해당 섹션에서 메모리가 누출되지 않았는지 확인한다. child allocator에 이름을 지정할 수도 있으므로 디버깅 중에 ArrowBuf가 어디에서 왔는지 쉽게 알 수 있다.

2.1.3 Reference counting

직접 메모리는 할당 및 할당 해제에 많은 비용이 들기 때문에 할당자는 직접 버퍼를 공유할 수 있다. 공유 버퍼를 결정론적으로 관리하기 위해 가비지 컬렉터 대신 수동 참조 카운팅을 사용. 즉, 각 버퍼에는 버퍼에 대한 참조 수를 추적하는 카운터가 있으며, 사용자는 버퍼가 사용됨에 따라 카운터를 적절히 증가/감소시킬 책임이 있다.

Arrow에서 각 ArrowBuf에는 참조 수를 추적하는 참조 관리자(ReferenceManager)가 연관되어 있다. ArrowBuf.getReferenceManager()로 이를 검색할 수 있습니다. 참조 개수를 줄이려면 ReferenceManager.release를, 늘리려면 ReferenceManager.retain을 사용하여 업데이트.

물론 이 작업은 지루하고 오류가 발생하기 쉬우므로 버퍼로 직접 작업하는 대신 일반적으로 ValueVector와 같은 상위 수준의 API를 사용합니다. 이러한 클래스는 일반적으로 Closeable/AutoCloseable을 구현하며 닫히면 자동으로 참조 수를 줄입니다.

allocatgor도 AutoCloseable 구현이 필요함. 이 경우 얼로케이터를 닫으면 얼로케이터에서 얻은 모든 버퍼가 닫혔는지 확인합니다. 그렇지 않은 경우 close() 메서드는 예외를 발생시켜 닫히지 않은 버퍼에서 메모리 누수를 추적하는 데 도움이 됩니다.

참조 카운팅은 신중하게 처리해야 합니다. 독립적인 코드 섹션에서 할당된 모든 버퍼를 완전히 정리하려면 새 자식 할당자(new child allocator)를 사용하세요.

2.1.4 개발 가이드

일반적으로 애플리케이션은:

  • API에서 RootAllocator 대신 BufferAllocator 인터페이스를 사용하세요.
  • 프로그램을 시작할 때 하나의 RootAllocator를 생성하고 필요할 때 명시적으로 전달하세요.
  • (자식 할당자든 루트 할당자든) 할당자를 사용한 후 수동으로 또는 가급적이면 try-with-resources 문을 통해 close() 할당자를 호출합니다.

2.1.5 Debugging Memory Leaks/Allocation

DEBUG 모드에서는 얼로케이터와 지원 클래스가 추가 디버그 추적 정보를 기록하여 메모리 누수 및 문제를 더 잘 추적할 수 있습니다. 디버그 모드를 활성화하려면 시작할 때 다음 시스템 속성을 VM에 전달합니다 -Darrow.memory.debug.allocator=true.

디버그가 활성화되면 할당에 대한 로그가 보관됩니다. 이러한 로그를 볼 수 있도록 SLF4J를 구성합니다(예: Logback/Apache Log4j를 통해).

다음 예제를 통해 할당자 추적에 어떤 도움이 되는지 살펴보세요:

import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;

try (BufferAllocator bufferAllocator = new RootAllocator(8 * 1024)) {
    ArrowBuf arrowBuf = bufferAllocator.buffer(4 * 1024);
    System.out.println(arrowBuf);
}

디버그 모드를 활성화하지 않은 상태에서 할당자를 닫으면 다음과 같은 메시지가 표시됩니다:

11:56:48.944 [main] INFO  o.apache.arrow.memory.BaseAllocator - Debug mode disabled.
ArrowBuf[2], address:140508391276544, length:4096
16:28:08.847 [main] ERROR o.apache.arrow.memory.BaseAllocator - Memory was leaked by query. Memory leaked: (4096)
Allocator(ROOT) 0/4096/4096/8192 (res/actual/peak/limit)

디버그 모드를 활성화하면 자세한 내용을 확인할 수 있습니다:

11:56:48.944 [main] INFO  o.apache.arrow.memory.BaseAllocator - Debug mode enabled.
ArrowBuf[2], address:140437894463488, length:4096
Exception in thread "main" java.lang.IllegalStateException: Allocator[ROOT] closed with outstanding buffers allocated (1).
Allocator(ROOT) 0/4096/4096/8192 (res/actual/peak/limit)
  child allocators: 0
  ledgers: 1
    ledger[1] allocator: ROOT), isOwning: , size: , references: 1, life: 261438177096661..0, allocatorManager: [, life: ] holds 1 buffers.
	ArrowBuf[2], address:140437894463488, length:4096
  reservations: 0

또한 디버그 모드에서는 ArrowBuf.print()를 사용하여 디버그 문자열을 얻을 수 있습니다. 여기에는 버퍼가 할당된 시기/위치와 같은 스택 추적과 함께 버퍼의 할당 작업에 대한 정보가 포함됩니다.

등등등 기능이 있지만 넘어가도록 하자.

2.2 Arrow Memory In-Depth

2.2.1 Design Principles

Arrow의 메모리 모델은 다음과 같은 기본 개념을 기반으로 합니다:

  • 메모리는 일정 한도까지 할당할 수 있습니다. 이 한도는 실제 한도(OS/JVM)일 수도 있고 로컬로 부과된 한도일 수도 있습니다.
  • 할당은 계정과 실제 할당이라는 두 단계로 진행됩니다. 어느 시점에서든 할당이 실패할 수 있습니다.
  • 할당 실패는 복구할 수 있어야 합니다. 모든 경우에 얼로케이터 인프라는 메모리 할당 실패(OS 또는 내부 제한 기반)를 OutOfMemoryException으로 노출해야 합니다.
  • 모든 얼로케이터는 생성 시 메모리를 예약할 수 있습니다. 이 메모리는 이 얼로케이터가 항상 해당 양의 메모리를 할당할 수 있도록 보유되어야 합니다.
  • 특정 애플리케이션 컴포넌트는 로컬 메모리 사용량을 파악하고 메모리 누수를 더 잘 디버그하기 위해 로컬 얼로케이터를 사용하도록 작동해야 합니다.
  • 동일한 물리적 메모리는 여러 할당자가 공유할 수 있으며 할당자는 이를 위한 계정 패러다임을 제공해야 합니다.

2.2.2 Reserving Memory

Arrow는 메모리를 예약하는 두 가지 방법을 제공합니다:

BufferAllocator account reservations: (RootAllocator가 아닌) 새 얼로케이터가 초기화되면 평생 동안 로컬에 보관할 메모리를 따로 설정할 수 있습니다. 이는 할당자가 닫힐 때까지 부모 할당자에게 다시 반환되지 않는 메모리입니다.

BufferAllocator.newReservation()을 통한 AllocationReservation: 특정 하위 시스템이 특정 요청을 지원하기 위해 향후 메모리를 사용할 수 있도록 단기 사전 할당 전략을 허용합니다.

2.2.3 Reference Counting Details

일반적으로 사용되는 참조 관리자 구현은 BufferLedger의 인스턴스입니다. (다시말해 BufferAllocator와 ArrowBuf집합 사이를 바인딩하는 참조 관리자) BufferLedger는 참조 매니저(ReferenceManager)로, AllocationManager, BufferAllocator 및 하나 이상의 개별 ArrowBuf 간의 관계를 유지합니다.

단일 BufferLedger/BufferAllocator 조합과 관련된 모든 ArrowBuf(직접 또는 슬라이스)는 동일한 참조 수를 공유하며 모두 유효하거나 모두 무효가 됩니다. 계산을 단순화하기 위해 해당 메모리는 해당 메모리와 연결된 BufferAllocator 중 하나에서 사용하는 것으로 취급합니다. 해당 할당자가 해당 메모리에 대한 소유권을 해제하면 메모리 소유권은 동일한 할당 관리자에 속한 다른 버퍼레저로 이동합니다.

2.2.4 Allocation Details

Arrow Java에는 여러 가지 얼로케이터 유형이 있습니다:

  • BufferAllocator - 공용 인터페이스 애플리케이션 사용자가 활용해야 합니다.
  • BaseAllocator - 메모리 할당의 기본 구현으로, Arrow 얼로케이터 구현의 핵심을 포함합니다.
  • RootAllocator - 루트 얼로케이터. 일반적으로 JVM에 대해 하나만 생성됩니다. 자식 얼로케이터의 부모/조상 역할을 합니다.
  • ChildAllocator - 루트 얼로케이터에서 파생되는 자식 얼로케이터입니다.

많은 BufferAllocator가 동시에 동일한 물리적 메모리를 참조할 수 있습니다. 이러한 상황에서 루트의 관점에서 모든 메모리가 정확하게 계산되도록 하고, 모든 버퍼할당자가 해당 메모리 사용을 중단하면 메모리가 올바르게 해제되도록 하는 것은 할당 관리자의 책임입니다.

계산을 단순화하기 위해 해당 메모리는 해당 메모리와 연결된 BufferAllocator 중 하나에서 사용하는 것으로 간주합니다. 해당 할당자가 해당 메모리에 대한 클레임(claim)을 해제하면 메모리 소유권은 동일한 할당 관리자에 속한 다른 버퍼레저로 이동합니다. 실제로 메모리 소유권 이전을 발생시키는 것은 ArrowBuf.release()이므로, (할당자 제한을 위반하더라도) 항상 소유권 이전을 진행한다는 점에 유의하세요. 특정 얼로케이터를 소유한 애플리케이션은 얼로케이터가 메모리 제한을 초과했는지(BufferAllocator.isOverLimit()) 자주 확인하고, 초과한 경우 상황을 개선하기 위해 적극적으로 메모리를 해제하려고 시도해야 할 책임이 있습니다.

2.2.5 Object Hierarchy

Arrow의 메모리 관리 체계에 대한 객체 계층구조를 살펴볼 수 있는 두 가지 주요 방법이 있습니다.

첫 번째는 아래와 같은 메모리 기반 관점입니다:

  1. Memory Perspective
    + AllocationManager
    |
    |-- UnsignedDirectLittleEndian (One per AllocationManager)
    |
    |-+ BufferLedger 1 ==> Allocator A (owning)
    | ` - ArrowBuf 1
    |-+ BufferLedger 2 ==> Allocator B (non-owning)
    | ` - ArrowBuf 2
    |-+ BufferLedger 3 ==> Allocator C (non-owning)
      | - ArrowBuf 3
      | - ArrowBuf 4
      ` - ArrowBuf 5
    

    이 그림에서 메모리 조각은 얼로케이터 관리자가 소유하고 있습니다. 얼로케이터 매니저는 어떤 얼로케이터를 사용하든 해당 메모리 조각을 책임집니다. 얼로케이터 매니저는 원시 메모리 조각과 (UnsignedDirectLittleEndian에 대한 참조를 통해) 관계를 가질 뿐만 아니라 관계가 있는 각 BufferAllocator에 대한 참조도 갖게 됩니다.

  2. Allocator Perspective
    + RootAllocator
    |-+ ChildAllocator 1
    | | - ChildAllocator 1.1
    | ` ...
    |
    |-+ ChildAllocator 2
    |-+ ChildAllocator 3
    | |
    | |-+ BufferLedger 1 ==> AllocationManager 1 (owning) ==> UDLE
    | | `- ArrowBuf 1
    | `-+ BufferLedger 2 ==> AllocationManager 2 (non-owning)==> UDLE
    |   `- ArrowBuf 2
    |
    |-+ BufferLedger 3 ==> AllocationManager 1 (non-owning)==> UDLE
    | ` - ArrowBuf 3
    |-+ BufferLedger 4 ==> AllocationManager 2 (owning) ==> UDLE
      | - ArrowBuf 4
      | - ArrowBuf 5
      ` - ArrowBuf 6
    

    이 그림에서 루트얼로케이터는 세 개의 차일드얼로케이터를 소유하고 있습니다. 첫 번째 차일드얼로케이터(차일드얼로케이터 1)는 후속 차일드얼로케이터를 소유하고 있습니다.

    ChildAllocator에는 두 개의 BufferLedgers/AllocationManager 참조가 있습니다. 공교롭게도 이러한 각 할당 관리자는 루트 할당자와도 연관되어 있습니다. 이 경우, 이러한 할당 관리자 중 하나는 자식 할당 관리자 3(할당 관리자 1)이 소유하고 다른 할당 관리자(할당 관리자 2)는 루트 할당자에 의해 소유/관리됩니다. 이 시나리오에서 ArrowBuf 1은 기본 메모리를 ArrowBuf 3으로 공유하지만 해당 메모리의 하위 집합(예: 슬라이싱을 통해)은 다를 수 있습니다. 또한 ArrowBuf 2와 ArrowBuf 4, 5, 6도 동일한 기본 메모리를 공유한다는 점에 유의하세요. 또한 ArrowBuf 4, 5, 6은 모두 동일한 참조 수와 운명을 공유한다는 점에 유의하세요.

3 ValueVector

ValueVector 인터페이스(C++ 구현 및 사양에서는 Array라고 함)는 동일한 유형의 값 시퀀스를 개별 열에 저장하는 데 사용되는 추상화입니다. 내부적으로 이러한 값은 하나 또는 여러 개의 버퍼로 표현되며, 그 수와 의미는 벡터의 데이터 유형에 따라 달라집니다.

사양에 설명된 각 기본 데이터 유형과 중첩된 유형에 대한 ValueVector의 구체적인 서브클래스가 있습니다. 사양에 설명된 유형 이름과 이름 지정에 몇 가지 차이가 있습니다: 직관적이지 않은 이름을 가진 테이블(BigInt = 64비트 정수 등).

읽기나 쓰기를 시도하기 전에 벡터를 할당하는 것이 중요하며, ValueVector는 생성 > 할당 > 변경 > 값 수 설정 > 액세스 > 지우기(또는 프로세스를 다시 시작하려면 할당)의 작업 순서를 보장하기 위해 “노력해야” 합니다. 다음 섹션에서 구체적인 예제를 통해 각 연산을 시연해 보겠습니다.

3.1 Vector Life Cycle

위에서 설명한 것처럼 각 벡터는 수명 주기에서 여러 단계를 거치며 각 단계는 벡터 작업에 의해 트리거됩니다. 특히 다음과 같은 벡터 연산이 있습니다.

  1. Vector creation

    다음은 IntVector 를 생성한다.

    RootAllocator allocator = new RootAllocator(Long.MAX_VALUE);
    ...
    IntVector vector = new IntVector("int vector", allocator);
    

    이렇게 벡터는 생성되었지만 메모리는 할당되지 않았다.

  2. Vector allocation

    벡터에 메모리를 할당하는 방법은 두 가지가 있다. 1) 최대 벡터 용량을 아는 경우 allocationNew(10) 와 같이 최대값을 지정한다. 2) 그렇지 않으면 allocationNew() 로 메모리를 할당한다.

  3. Vector mutation

    이제 원하는 값으로 벡터를 채울 수 있습니다.

    모든 벡터의 경우 벡터 작성기(vector writer)를 통해 벡터 값을 채울 수 있습니다(다음 섹션에서 예시를 보여드리겠습니다).

    프리미티브(primitive) 타입의 경우 set 메서드를 사용하여 벡터를 수정(mutation)할 수도 있습니다.

    set 메서드에는 두 가지 클래스가 있습니다.

    1. 벡터의 용량이 충분하다고 확신할 수 있는 경우, set(index, value) 메서드를 호출할 수 있습니다.
    2. 벡터 용량이 확실하지 않은 경우 용량이 충분하지 않은 경우 자동으로 벡터 재할당을 처리하는 setSafe(index, value) 메서드를 호출해야 합니다.

      실행 중인 예제에서는 벡터의 용량이 충분하다는 것을 알고 있으므로, 벡터의 용량이 충분하지 않은 경우:

      vector.set(/*index*/5, /*value*/25);
      
  4. Set value count

    이 단계에서는 setValueCount(int) 메서드를 호출하여 벡터의 값 개수를 설정: vector.setValueCount(10);

    이 단계가 끝나면 벡터는 불변 상태가 된다. (벡터를 재할당해서 사용하지 않는 한; Unless we reuse the vector by allocating it again. This will be discussed shortly.)

  5. vector access

    두 가지 방법이 있다. 1) get methods and 2) vector reader.

    벡터 리더는 모든 유형의 벡터에 사용할 수 있는 반면, get 메서드는 primitive 벡터에만 사용할 수 있습니다.

    get메서드를 이용한 간단한 접근:

    int value = vector.get(5); // value == 25
    
  6. vector clear

    vector.close() 로 메모리를 릴리즈(release) 할 수 있다.

4 Interface BufferAllocator

https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/BufferAllocator.html

바이트 버퍼 할당을 처리하는 래퍼 클래스. 사용자가 지정된 메서드만 사용하도록 합니다.

BufferAllocator newChildAllocator(String name,
 AllocationListener listener,
 long initReservation,
 long maxAllocation)  

Create a new child allocator.

  • Parameters:
    • name - the name of the allocator.
    • listener - allocation listener for the newly created child
    • initReservation - the initial space reservation (obtained from this allocator)
    • maxAllocation - maximum amount of space the new allocator can allocate
  • Returns: the new allocator, or null if it can't be created

사용되는 코드 : https://github.com/xtdb/xtdb/blob/527b17d11e2d9e5715ec3d0d47b73350d659f534/core/src/main/clojure/xtdb/buffer_pool.clj#L68

default ArrowBuf wrapForeignAllocation(ForeignAllocation allocation)

실험적: 이 버퍼 할당기 외부에서 생성된 할당을 래핑합니다. (EXPERIMENTAL: Wrap an allocation created outside this BufferAllocator.)

이 기능은 네이티브 코드(native code)의 할당(allocation)을 Java 할당 버퍼와 동일한 메모리 관리 프레임워크에 통합하여 사용자에게 일관된 API를 제공하는 데 유용합니다. 생성된 버퍼는 이 Allocator에 의해 추적되며 Java 할당 버퍼(Java-allocated buffers)처럼 전송할 수 있습니다.

사용되는 코드 링크 : https://github.com/xtdb/xtdb/blob/527b17d11e2d9e5715ec3d0d47b73350d659f534/core/src/main/clojure/xtdb/util.clj

Date: 2024-08-25 Sun 00:00

Author: Younghwan Nam

Created: 2024-09-25 Wed 07:42

Emacs 27.2 (Org mode 9.4.4)

Validate