EPYC単機のELTメモリ割り当て
768GBのEPYCサーバーで、512GBをELTステップごとに再利用する設計です。データ基盤の開発に向け、In-Memory、Arrow IPC、Parquetと実行制限を整理しました。
設計した実行ルール
Dagster、Trino、DataFusion、PostgreSQL を使う ELT 基盤のメモリ割り当てを設計しました。前提は AMD EPYC 9175F 16c/32t、768GB DDR5-6400、RTX 6000 Pro Max-Q、NVMe 3.84TB です。このうち 512GB を ELT 専用プールとし、ステップごとに再利用する方針です。
Airbyte -> Iceberg / Trino -> 可視化 をローカル・オフライン中心で動かすデータ基盤を想定しています。48h Prototype や データ基盤整備 の提供形につなぐ構想の、実行ルールを整理したものです。
メモリのピークを分ける
768GB あっても、Trino の heavy query、DataFusion の変換、PostgreSQL のロード、Dagster の並列 worker が重なると、メモリのピークが衝突します。swap や OOM に加え、途中で失敗した処理の再開も考える必要があります。
メモリの使い方を次のように決めました。
512GBをステップごとに再利用します。軽い処理はIn-Memory、重い処理はArrow IPCまたはParquetに退避します。
この方針で、処理速度と失敗時の再開方法を一緒に設計します。
基本リソース前提
設計の前提です。
- サーバー:
AMD EPYC 9175F 16c/32t (5GHz) + 768GB DDR5-6400 + RTX 6000 Pro Max-Q + NVMe 3.84TB - ELT プール:
512GBをワークフロー専用に確保 - 帯域: メモリ帯域
~600GB/s、NVMe I/O 数GB/s、ネットワーク10GbE
512GB は、実行中の ELT ステップに割り当てる資源です。取り込み、可視化、MLOps のサービスが増えても、ワークロードごとに上限を管理します。
メモリ運用ポリシー
原則
メモリの原則は次の3点です。
512GBプールをステップごとに循環利用する- 同一プロセスなら
In-Memory直渡しを優先する - 別プロセスやコンテナに分ける場合は、合算で
512GB上限に収める
重い分析処理は1本ずつ実行し、終了後に解放して次のステップに渡す方針です。Dagster の op を増やす際も、この制限を守ります。
切替基準
中間データのサイズで方式を切り替えます。
| 中間サイズ | 推奨方式 | 備考 |
|---|---|---|
<= 40-50% (200-250GB) | In-Memory(同一プロセス + InMemoryIOManager) | 帯域 600GB/s を直接使えるので最速 |
50-80% | Arrow IPC ストリーム | NVMe 退避でもオーバーヘッドを抑えやすい |
>80% または複数 op 同時 | Parquet 一時出力 | 失敗時再開や安全性を優先 |
この基準に沿って、メモリ内で渡すか Parquet に保存するかを決めます。
各コンポーネントの最適化
DataFusion (Rust)
DataFusion では巨大な中間データの collect を避けます。
MemoryPool上限を op 単位でS/M/Lプロファイルとして設定DiskManagerは NVMe の/opt/tmpを指定し、必要なら強制スピル可能にするStreaming APIのexecute_stream()を優先する- パーティション数は実コア数である
16cに合わせる
ストリームで処理できる部分を集めずに流し、メモリのピークを抑えます。
Trino
Trino は設定と実行本数の両方を制限します。
JVM: -Xmx192g
max-spill-per-node=512GB
query.max-memory-per-node=128GB
query.max-total-memory-per-node=160GB
コンテナは 256GB とし、NVMe に spill します。heavy query は Dagster の concurrency 制御で同時 1 本に制限します。
PostgreSQL
PostgreSQL は永続化と大量ロードを担当します。
shared_buffers=8-16GB
work_mem=128MB
maintenance_work_mem=2-4GB
wal_compression=lz4
checkpoint_timeout=20-30min
max_wal_size=12-16GB
大量ロードには、次の方式を使う方針です。
UNLOGGED TABLE -> COPY -> インデックス作成 -> LOGGEDCTAS (CREATE TABLE AS SELECT)
必要に応じて取り込み中だけ synchronous_commit=off を使います。常時無効にはしません。
Dagster
Dagster の executor を処理の重さで切り替えます。
- 軽量:
in_process_executor + InMemoryIOManager - 重量:
multiprocess_executor + opごとに Podman コンテナ (--memory/--cpuset-cpus)
タグで並列数を制限します。
- heavy 系 (
Trino/DataFusion大 op):max_concurrent=1 - 軽い op(メタ処理、
dbt run、ログ): 並列可
異なる job でも同じ実行方針を使います。
プロセス / コンテナ設計
In-Memory(最速)
同一ジョブを in-process executor で動かし、pyarrow.Table や RecordBatch を shallow copy で渡す方式です。中間サイズ <=200GB 程度、ELT プールの 50% 以下を条件にしています。
この条件を超える場合は、後続 op を起動するときのメモリ増加も考慮します。
opごとのコンテナ分離
各 op を podman run --memory=... --cpuset-cpus=... で分離します。終了時にプロセスのメモリが解放され、次のステップが 512GB のプールを再利用できます。
Podman を使ったローカル環境の再現にも、この方式を使う方針です。
子プロセス起動前のメモリ解放
Python の親が大きいバッファを持ったまま Rust 子プロセスを起動すると、合計メモリが増えます。起動前に次を行います。
- 子起動前に親のバッファを
del - スコープを抜ける
gc.collect()を呼ぶ- Python は
forkよりspawnを推奨する
fork の Copy-on-Write が破れると、実メモリの使用量が増えるためです。
I/O 最適化
中間データの形式と置き場を次のように分けます。
Arrow IPC: 速度を優先する退避先。ゼロコピーに近い受け渡しParquet: 安全性とリトライ性重視。圧縮はzstd(1-3)、行グループは128-256MBNVMe: スピルディレクトリを/opt/tmpに集約/dev/shm: Arrow Plasma / IPC なら--shm-size 2-4GB- クリーンアップ: ジョブ終了時に一時ファイルを削除し、失敗時もハンドルする
Arrow IPC は速度、Parquet は保存とリトライを重視する場合に使います。
運転ルール
運用ルールは次の5点です。
- 重いクエリは
1本ずつ実行します - op ごとに終了させて解放し、
512GBを循環利用する - 中間サイズに応じて
In-Mem -> Arrow IPC -> Parquetを切り替える - 障害許容性が必要な処理は
Parquetを選ぶ - 速度最優先の処理は
In-Memoryで shallow copy / ポインタ渡しを使う
複数の ELT job を開発する際も、同じ基準で実行方式を選びます。
期待する効果
この設計で期待する効果です。
- 速度:
In-Memoryでメモリ帯域600GB/sを利用する - 安定性:
512GBの再利用、cgroup 制御、spill でメモリ不足を抑える - 柔軟性: 中間サイズで
Arrow IPC/Parquetを切り替える - 実行管理:
Dagsterの executor で方式を変える
これらは設計上の狙いです。単機での同時実行を制限し、メモリのピークを管理します。
まとめ
512GB を再利用し、軽い処理は In-Memory、重い処理は Arrow / Parquet に退避する方針です。コンテナを終了させて op のメモリを解放します。
設定への反映予定
次は podman-compose.yml のメモリ / CPU プロファイルと Dagster executor の設定に反映する予定です。Airbyte OSS、Great Expectations、OpenMetadata、MLflow を含む周辺サービスも、同じ実行ルールに沿って構成する計画です。
