|
| 1 | +--- |
| 2 | +title: "Spilling" |
| 3 | +excerpt: "spilling in velox" |
| 4 | +categories: |
| 5 | + - data |
| 6 | +tags: |
| 7 | + - data |
| 8 | +last_modified_at: 2024-12-14T08:00:00-08:00 |
| 9 | +--- |
| 10 | + |
| 11 | +[https://facebookincubator.github.io/velox/develop/spilling.html](https://facebookincubator.github.io/velox/develop/spilling.html) |
| 12 | +를 정리한 글. |
| 13 | + |
| 14 | +# Background |
| 15 | +- 스필은 제한된 메모리 상에서도 쿼리가 성공하게 한다. |
| 16 | +- 예를 들어 해시 집계 연산자는 중간 집계 상태를 해시 테이블에 저장한다. 모든 입력을 처리한 이후에 결과를 출력한다. 높은 카티널리티 워크로드에서는 해시 테이블이 메모리 제한을 초과한다. |
| 17 | + - 스필은 연산자의 상태 일부를 디스크에 쓴다. 연산자가 모든 입력을 받은 후 디스크에 스필한 상태와 메모리의 상태를 읽어서 병합 후 결과를 출력한다. |
| 18 | +- 스필은 두 단계로 이루어진다. 스필과 복구 |
| 19 | + - 스필 : 연산자가 입력을 처리할 때 수행된다. 연산자 상태의 어떤 부분을 스필할지 결정하고 디스크에 어떻게 저장할지 결정한다. |
| 20 | + - 복구 : 연산자가 모든 입력을 처리한 이후 수행된다. 디스크에서 스필한 상태와 메모리의 상태를 읽어서 병합 후 결과를 출력한다. |
| 21 | +- 다른 연산자는 각기 다른 스필 알고리즘을 사용한다. 이 문서는 해시 집계, Order By, 해시 조인 연산자에 대해 다룬다. |
| 22 | + |
| 23 | +# Spilling Framework |
| 24 | + |
| 25 | + |
| 26 | +- Velox의 스필 프레임웍은 공통 스필 함수, 데이터 컬렉션, 파티션, 정렬, (역)직렬화, 저장소 읽기/쓰기 등을 제공한다. |
| 27 | +- 각 스필 가능한 연산자는 이 함수를 이용해 자신의 스필 알고리즘을 구현한다. |
| 28 | + |
| 29 | +# Spill Objects |
| 30 | +스필 프레임웍은 다음 주요 소프트웨어 객체로 구성된다. |
| 31 | + |
| 32 | +## Spiller |
| 33 | +- 스필러 객체는 연산자를 위한 스필 함수를 제공하고 연산자가 디스크에 스필 상태를 관리하도록 돕는다. |
| 34 | +- 각 연산자마다 하나의 스필러 객체가 생성된다. |
| 35 | +- 스필러 객체는 row 컨테이너 객체를 생성자에서 받는다. |
| 36 | + - hash probe 연산자 제외. 그것은 입력을 버퍼하지 않고 스필 row를 즉시 디스크에 쓴다. |
| 37 | +- row 컨테이너는 행 기반의 인메모리 데이터 저장소다. 메모리가 부족할 때 디스크에 스필할 수 있는 상태를 저장한다. |
| 38 | + - 예를 들어, 해시 집계 연산자의 로우 컨테이너는 각 그룹(고유 키 값의 조합)별 하나의 행으로 중간 집계 상태를 저장한다. |
| 39 | +- 연산자 스필이 필요할 때 스필러는 로우 컨테이너의 모든 행을 스캔하고 각 로우의 파티션 번호를 계산하고, 스필할 파티션을 결정하고, 파티션을 파일의 리스트로 스필한다. |
| 40 | +- 스필러는 디스크에 데이터를 쓰기 전에 정렬할 수 있다. |
| 41 | + - 정렬이 허용된다면 스필러는 각 정렬된 수행에 대해 별도의 파일을 생성한다. |
| 42 | + - 복구 단계에서 스필러는 스필한 데이터를 읽어서 인메모리 상태로 복구한다. 정렬이 허용된다면 스필러는 병합 정렬 reader를 생성하여 정렬된 데이터를 읽는다. |
| 43 | + - 이러한 기능은 해시 집계나 `ORDER BY` 연산자에 사용된다. |
| 44 | + |
| 45 | +스필러는 아래 주요 함수를 구현한다. |
| 46 | +- **Spill data partition** |
| 47 | + - 스필 시, 디스크에 최소한의 데이터를 쓰고, 나머지는 인메모리에 유지하고 싶을 것이다. IO를 최소화하고, 복구도 빠르게 하기 위함이다. |
| 48 | + - 스필러는 로우 컨테이너의 로우들을 파티션으로 나누고 그중 일부만 스필한다. |
| 49 | + - 각 파티션은 디스크에 별개의 파일 집합으로 저장된다. |
| 50 | + - 해시 집계 연산자는 그룹핑 키를 사용하여 데이터를 파티션으로 나눈다. 동일 그룹의 모든 로우는 동일 파티션으로 함께 스필, 복구된다. |
| 51 | +- **Select partitions to spill** |
| 52 | + - 스필러는 스필 시 가장 많은 데이터를 가진 파티션을 선택한다. |
| 53 | + - 스필 가능한 데이터는 로우 컨테이너 내 로우가 점유한 메모리 바이트를 측정한 것이다. |
| 54 | + - 정렬 스필을 사용하는 연산자는 데이터량이 적은 파티션이 이전에 스필되었어도 스필을 피해야 한다. |
| 55 | + - 정렬 스필은 각 정렬 수행에 대해 새 파일을 생성하고 파일에 대해 데이터 추가를 허용하지 않기 때문이다. |
| 56 | + - 비정렬 스필은 파일에 데이터를 추가할 수 있기 때문에 이런 제약이 없다. |
| 57 | +- **Sort data while spilling** |
| 58 | + - 정렬 스필을 사용하는 연산자는 스필 파일과 인메모리 데이터를 정렬-병합 알고리즘을 사용해 조합한다. |
| 59 | + - 인메모리 데이터 또한 정렬 수행에 따라 정렬되어 있다. |
| 60 | + - 스필러는 파티션 컬럼과 연산자가 정의한 비교 플래그의 집합으로 로우를 정렬한다. |
| 61 | + - 예를 들어, `Order By` 연산자는 쿼리 플랜 노드가 명시한 것과 동일한 정렬 순서가 보장되어야 한다. |
| 62 | +- **Spill data IO** |
| 63 | + - 스필러는 저장 시스템과의 상호작용을 관리한다. |
| 64 | + - SpillFileList와 SpilFile 객체는 스필 파일의 생성, 쓰기, 읽기, 삭제 등 생명주기를 관리한다. |
| 65 | + - 스필 쓰기는 전용 입출력 수행기로 위임한다. 각 스필 파티션 쓰기는 스레드 수행 단위다. |
| 66 | + - 스필 읽기는 드라이버 수행기로 수행된다. |
| 67 | + - 읽기 쓰기 모두 동기 입출력이다. |
| 68 | + |
| 69 | +## Spill APIs |
| 70 | +- **spill with targets** |
| 71 | + - 연산자는 스필을 목적으로 하는 로우와 바이트 수를 명시한다. |
| 72 | + - 스필러는 목적에 맞는 스필 대상 파티션 수를 선택한다. |
| 73 | + - 스필은 내부적으로 수행되고 완료되면 반환한다. |
| 74 | + - 스필 처리는 연산자에 의해 통제되지 않는다. |
| 75 | + - 다만 어떤 파티션이 스필되고 얼마나 많은 데이터가 스필되었는지 통계 API를 통해 확인할 수 있다. |
| 76 | + |
| 77 | +```c++ |
| 78 | +void Spiller::spill(uint64_t targetRows, uint64_t targetBytes); |
| 79 | +SpillPartitionNumSet Spiller::spilledPartitionSet() const; |
| 80 | +Stats Spiller::stats() const; |
| 81 | +``` |
| 82 | +
|
| 83 | +- **spill partitions** |
| 84 | + - 연산자는 스필할 파티션을 명시한다. 스필러는 해당 파티션의 모든 로우를 디스크로 스필한다. |
| 85 | + - 스필 처리는 연산자에 의해 통제된다. |
| 86 | + - 해시 빌드 연산자가 사용한다. |
| 87 | + - 스필이 시작되면 연산자 중 하나가 모든 연산자를 수행하도록 선택된다.(그룹 스필) |
| 88 | + - 모든 연산자들에서 스필 가능한 통계를 수집하여 스필할 파티션의 수를 선택한다. |
| 89 | +
|
| 90 | +```c++ |
| 91 | +void Spiller::spill(const SpillPartitionNumSet& partitions); |
| 92 | +void Spiller::fillSpillRuns(std::vector<SpillableStats>& statsList); |
| 93 | +``` |
| 94 | + |
| 95 | +- **spill vector** |
| 96 | + - 연산자는 로우 벡터를 특정 파티션으로 스필한다. |
| 97 | + - 스필러는 직접 로우 벡터를 현재 열려있는 스필 파티션 파일에 추가한다. |
| 98 | + - 스필 처리는 연산자에 의해 통제된다. |
| 99 | + - 해시 조인 연산자가 사용한다. |
| 100 | + - 해시 빌드와 해시 프로브 연산자는 연관 파티션이 스필되었다면 입력 로우를 디스크에 스필한다. |
| 101 | + - 해시 빌드 연산자는 파티션이 스필되었다면 그 파티션의 모든 입력 로우는 스필되어야 한다. |
| 102 | + - 조인할 파티션의 일부 로우로만 해시 테이블을 빌드할 수 없기 때문. |
| 103 | + - 해시 프로브 연산자는 그 자체로는 스필 불가능하다. 다만 해시 빌드에서 연관된 파티션이 스필된 경우 입력 행을 스필해야 합니다. |
| 104 | + |
| 105 | +```c++ |
| 106 | +voild Spiller::spill(uint32_t partition, const RowVectorPtr& spillVector); |
| 107 | +``` |
| 108 | +
|
| 109 | +## Restore APIs |
| 110 | +
|
| 111 | +- **sorted spill restore** |
| 112 | + - order by 와 해시 집계 연산자에서 사용 |
| 113 | + - 1. `Spiller::finishSpill()`을 호출하여 스필의 완료를 마킹한다. |
| 114 | + - 2. 스필러는 스필되지 않은 파티션에서 로우를 수집하여 연산자에 반환. |
| 115 | + - 3. 연산자는 스필되지 않은 파티션을 처리, 결과를 방출하고 로우 컨테이너의 공간을 해제. |
| 116 | + - 4. 스필된 파티션을 하나씩 적재한다. |
| 117 | + - 5. 각 스필 파티션은 `SpillPartition::createOrderedReader()`를 호출하여 정렬된 reader를 생성하고 복구한다. |
| 118 | +
|
| 119 | +```c++ |
| 120 | +void Spiller::finishSpill(SpillPartitionSet& partitionSet); |
| 121 | +SpillPartition::createOrderedReader(); |
| 122 | +``` |
| 123 | + |
| 124 | +- **unsorted spill restore** |
| 125 | + - 해시 빌드, 해시 프로브 연산자에서 사용 |
| 126 | + - 1. `Spiller::finishSpill()` 호출하여 스필 완료 마킹 |
| 127 | + - 2. 스필러는 스필된 파티션에서 메타데이터를 수집하여 연산자에 반환. |
| 128 | + - 3. 연산자는 스필되지 않은 파티션을 처리, 결과를 방출하고 로우 컨테이너의 공간을 해제. |
| 129 | + - 4. 스필된 파티션을 하나씩 적재한다. |
| 130 | + - 5. 각 스필 파티션은 `SpillPartition::createReader()`를 호출하여 비정렬 reader를 생성하고 복구한다. |
| 131 | + |
| 132 | +```c++ |
| 133 | +void Spiller::finishSpill(SpillPartitionSet& partitionSet); |
| 134 | + |
| 135 | +SpillPartition::createReader(); |
| 136 | +``` |
| 137 | +
|
| 138 | +## SpillFileList and SpillFile |
0 commit comments