Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CustomScan APIを使い倒して俺様DBエンジンを実装する

CustomScan APIを使い倒して俺様DBエンジンを実装する

2022/11/11(ポッキーの日) Japan PostgreSQL Conferenceでの発表資料です。

Avatar for KaiGai Kohei

KaiGai Kohei

November 11, 2022
Tweet

More Decks by KaiGai Kohei

Other Decks in Technology

Transcript

  1. CustomScan APIとは? postgres=# explain select sum(lo_revenue), d_year, p_brand1 from flineorder,

    date1, part where lo_orderdate = d_datekey and lo_partkey = p_partkey and p_category = 'MFGR#12’ group by d_year, p_brand1; QUERY PLAN ----------------------------------------------------------------------------------------------------- HashAggregate (cost=3656703.22..3656773.22 rows=7000 width=22) Group Key: date1.d_year, part.p_brand1 -> Custom Scan (GpuPreAgg) (cost=3656580.72..3656650.72 rows=7000 width=22) Reduction: GroupBy (Global+Local [nrooms: 1974]) Group keys: d_year, p_brand1 Combined GpuJoin: enabled -> Custom Scan (GpuJoin) on flineorder (cost=16028.65..3638534.69 rows=28873638 width=18) Outer Scan: flineorder (cost=0.00..10277198.49 rows=720040849 width=12) Depth 1: GpuHashJoin(nrows 720040849...28873638) HashSize: 1135.95KB HashKeys: flineorder.lo_partkey JoinQuals: (flineorder.lo_partkey = part.p_partkey) Depth 2: GpuHashJoin(nrows 28873638...28873638) HashSize: 442.11KB HashKeys: flineorder.lo_orderdate JoinQuals: (flineorder.lo_orderdate = date1.d_datekey) referenced: lo_partkey, lo_orderdate, lo_revenue files0: /opt/pgdata14/f_lineorder.arrow (read: 8.05GB, size: 81.81GB) -> Seq Scan on part (cost=0.00..5426.00 rows=8020 width=14) Filter: (p_category = 'MFGR#12'::bpchar) -> Seq Scan on date1 (cost=0.00..72.56 rows=2556 width=8) (21 rows) 見覚えのない 実行計画があるなぁ…。 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 2
  2. CustomScan APIとは? ▌概要  エグゼキュータの一部を拡張モジュールによる 実装で置き換えたいときに利用するAPI  拡張モジュールの開発者が利用するもので、 SQL経由でユーザが利用するものではない。 

    PostgreSQL v9.5 (2016) で最初のバージョンが 提供されるようになった。 ▌利点  独自にSQL実行エンジンを強化したい場合でも、 PostgreSQLからのフォークを行わずに済む。  自分が興味のある部分“だけ”を置き換える事が できるので、開発規模を抑える事ができる。 IndexScan on table-Y CustomScan (MyScan) on table-X CustomScan (MyJoin) Agg (Group By) NestLoop SeqScan on table-Z PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 3
  3. CustomScan APIとFDW(Foreign Data Wrapper)の違い ▌FDW: Foreign Data Wrapper  外部のデータをあたかもPostgreSQLのテーブルで

    あるかのように読み書きするためのAPI  何を(What?)スキャンするか。 ▌CustomScan API  PostgreSQLのテーブルを異なる方法でスキャン するためのAPI ✓ 実装上、キャッシュを参照するなどの手法もアリ  どのように(How?)スキャンするか Foreign Table Foreign Table Foreign Table Foreign Table CSV mysql_fdw oracle_fdw file_fdw arrow_fdw MySQL データベース Oracle データベース CSV ファイル Arrow ファイル PostgreSQL テーブル SeqScan IndexScan CacheScan FpgaScan DpuScan GpuScan columnar cache PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 4
  4. HeteroDB社について ▌会社概要  商号 ヘテロDB株式会社  設立 2017年7月4日  資本金

    999万円  所在地 品川区北品川5-5-15 大崎ブライトコア4F  事業内容 高速データベース製品の開発・販売 GPU&DB領域の技術コンサルティング  受賞など ✓ IPA 未踏アドバンスト事業 第一期生(2017) ✓ GPU Technology Conference Japan 2017、Inception Award受賞 ✓ 東京都ベンチャー技術大賞 技術奨励賞(2022) ▌代表者プロフィール  海外 浩平|KaiGai Kohei OSS開発者コミュニティにおいて、 PostgreSQLやLinux kernelの開発に 15年以上従事。主にセキュリティ・ FDWなどの技術領域に貢献する。 本日のテーマ CustomScan API や、 GPUを用いたSQL高速化モジュール・PG-Stromの開発者。 筑波大学情報学類卒(2001)、同大学院経営・政策科学研究 科修了(2003)、日本電気株式会社勤務(2003-2017)の後、 HeteroDB社を創業。  受賞など ✓ IPA未踏ソフト「天才プログラマ―」認定(2006) ✓ OSS推進フォーラム OSS貢献者賞(2014) ✓ 日本情報処理学会 喜安記念業績賞(2014) ヘテロジニアスコンピューティング技術をデータベース領域に適用し、 誰もが使いやすく、シンプルで高速なデータ解析基盤を提供する。 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 5
  5. CustomScan APIの利用例 – HeteroDB PG-Strom postgres=# explain select sum(lo_revenue), d_year,

    p_brand1 from flineorder, date1, part where lo_orderdate = d_datekey and lo_partkey = p_partkey and p_category = 'MFGR#12’ group by d_year, p_brand1; QUERY PLAN ----------------------------------------------------------------------------------------------------- HashAggregate (cost=2631106.55..2631176.55 rows=7000 width=22) Group Key: date1.d_year, part.p_brand1 -> Custom Scan (GpuPreAgg) (cost=2630984.05..2631054.05 rows=7000 width=22) Reduction: GroupBy (Global+Local [nrooms: 1974]) Group keys: d_year, p_brand1 Combined GpuJoin: enabled GPU Preference: GPU0 (NVIDIA A100-PCIE-40GB) -> Custom Scan (GpuJoin) on flineorder (cost=14190.72..2612938.03 rows=28873638 width=18) Outer Scan: flineorder (cost=0.00..10277198.49 rows=720040849 width=12) Depth 1: GpuHashJoin(nrows 720040849...28873638) HashSize: 1135.95KB HashKeys: flineorder.lo_partkey JoinQuals: (flineorder.lo_partkey = part.p_partkey) Depth 2: GpuHashJoin(nrows 28873638...28873638) HashSize: 442.11KB HashKeys: flineorder.lo_orderdate JoinQuals: (flineorder.lo_orderdate = date1.d_datekey) GPU Preference: GPU0 (NVIDIA A100-PCIE-40GB) GPUDirect SQL: enabled referenced: lo_partkey, lo_orderdate, lo_revenue files0: /opt/pgdata14/f_lineorder.arrow (read: 8.05GB, size: 81.81GB) -> Seq Scan on part (cost=0.00..5426.00 rows=8020 width=14) Filter: (p_category = 'MFGR#12'::bpchar) -> Seq Scan on date1 (cost=0.00..72.56 rows=2556 width=8) (24 rows) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 8
  6. 最小構成での PostgreSQL 拡張モジュールのビルド $ ls Makefile ctidscan.h ctidscan.c ctidscan.sql ctidscan.control

    $ cat Makefile MODULES = ctidscan EXTENSION = ctidscan DATA = ctidscan.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) $ cat ctidscan.control # ctidscan extension comment = 'example module for custom-scan api’ default_version = '1.0' module_pathname = '$libdir/ctidscan' relocatable = true ✓ PostgreSQL向け拡張モジュールは、メジャー バージョン跨ぎのバイナリ互換性の保証なし。 ✓ ビルド時の ./configure 次第で変わるパラメータ 例:BLCKSZの定義やAssertの有無、構造体のレイアウトも ✓ ターゲットとなるPostgreSQLに同梱の pg_config を 利用してビルドオプションの同一性を担保する。 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 12
  7. 拡張モジュールが制御を獲得するタイミング #include “postgres.h” : : void _PG_init(void) { DefineCustomBoolVariable("enable_ctidscan", "Enables

    use of ctidscan”, NULL, &enable_ctidscan, true, PGC_USERSET, GUC_NOT_IN_SAMPLE, NULL, NULL, NULL); : /* registration of the hook to add alternative path */ set_rel_pathlist_next = set_rel_pathlist_hook; set_rel_pathlist_hook = SetCtidScanPath; } ✓ PostgreSQLが拡張モジュールをロードすると、 まず _PG_init() 関数を呼び出す。 ✓ 拡張モジュールをロードするタイミングは… ✓ その拡張モジュールで実装されているSQL関数の呼び出し。 ✓ (shared|local|session)_preload_libraries パラメー タによる指定。 ✓ SQLのLOADコマンド ➔ CustomScan APIを使用する拡張モジュールの場合は、 *_preload_libraries パラメータを使うのが自然。 ✓ 拡張モジュールで制御を獲得するには? ✓ “C”で実装されたSQL関数の実行 ➔ 演算子やSQL関数、インデックスの評価やFDWなど、 大半のケースではこちら。 ✓ フック、コールバックで登録されたタイミング ➔ CustomScan APIでは、一連の処理の中でSQL関数を必要と する場面がないため、_PG_init() でフックやコールバックの 登録を済ませておく。 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 13
  8. 比較的シンプルな CustomScan のサンプル PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 14

    ▌https://github.com/kaigai/ctidscan  検索条件に ctid システム列(*)に対する大小比較を含む 場合、明らかにマッチしないブロックを読み飛ばす。  PostgreSQL v14で対応した TidRangeScan と同等だが、 CustomScan APIの提案時には 意味のある拡張モジュール だった。 ✓ (*) ctidシステム列は、タプルの存在するブロック番号と アイテム番号を示す。
  9. PostgreSQLがクエリを処理する流れ SQL Parser Query Optimizer Query Executor Buffer Manager SQLクエリ

    パース木 クエリ実行計画 クエリ 実行結果  SQL構文を分解し、取り扱いやすい 内部データ形式(パース木)に変換  文法エラーの検出  統計情報を元にコスト値を算出  最も合理的と予想される実行計画を作成する。  クエリ実行計画に基づいて、 ScanやJoin、Sortなどの処理を実行する。  PostgreSQL内部のインフラを使用 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 16 Transaction Control Resource Management IPC, Shared Memory クエリの実行をサポートする 多種多様な内部インフラが 用意されている。
  10. PostgreSQLはどのように実行計画を作るか t0 JOIN t1の候補パス Scan t0 Scan t1 Join t0,t1

    統計情報) nrows: 120万行 width: 80 インデックス:なし 候補パス HashJoin cost=4000 候補パス MergeJoin cost=12000 候補パス NestLoop cost=99999 候補パス Parallel Hash Join cost=3000 候補パス GpuJoin cost=2500 WINNER! PostgreSQLビルトインの実行パス 拡張モジュールによる提案 (PostgreSQL v9.5以降) (PostgreSQL v9.6以降) GpuJoin t0,t1 統計情報) nrows: 4000行 width: 120 インデックス:id列 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 17
  11. PostgreSQLがクエリを処理する流れ PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 18 ▌リレーション(Scan, Join,

    ...)に紐づいた 候補パス(Path構造体)を作成する。 ✓ Pathには実行コストや、実行結果の出力順 (ソートされているか否か)などを保持する。 ✓ ビルトインの実行計画の他に CustomPath を作成 する事ができれば、ここに突っ込む。 ▌オプティマイザは最もコストの安い候補パスを 選択し、Path ⇒ Plan 構造体に変換する。 ✓ Pathと異なり、Plan構造体は copyObject などの 内部APIを用いて操作できる。 ▌式表現の書き換え ✓ 場合によっては、オプティマイザにヒントを与える 必要がある(custom_scan_tlist) SQL Parser Query Optimizer Query Executor SQLクエリ パース木 クエリ実行計画 クエリ 実行結果
  12. set_rel_pathlist_hook による実行計画の登録 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 19 /*

    Hook for plugins to get control in set_rel_pathlist() */ typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); extern PGDLLIMPORT set_rel_pathlist_hook_type set_rel_pathlist_hook; static void SetCtidScanPath(PlannerInfo *root, RelOptInfo *baserel, Index rtindex, RangeTblEntry *rte) { /* call the secondary custom-scan provider */ if (set_rel_pathlist_next) set_rel_pathlist_next(root, baserel, rtindex, rte); : /* * 1. リレーション種別や検索条件をチェック * 2. CustomPathを作成し、CustomPathMethods を登録 * 3. add_path()を使ってパスを登録 */ ✓ set_rel_pathlist_hook はいつ呼ばれるのか? ➔ 特定のテーブルに対して、オプティマイザが スキャンする方法を検討するタイミング。 ✓ ビルトインのスキャン方法 • SeqScan • IndexScan • BitmapHeapScan • TidScan • ForeignScan ➔ ここに割り込んで、『俺々CustomScan』の方が より効率的にこのテーブルをスキャンできると いう事を教えてやる。
  13. CustomPath構造体 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 20 typedef struct

    Path { NodeTag type; NodeTag pathtype; RelOptInfo *parent; PathTarget *pathtarget; ParamPathInfo *param_info; : Cardinality rows; Cost startup_cost; Cost total_cost; List *pathkeys; } Path; typedef struct CustomPath { Path path; uint32 flags; List *custom_paths; List *custom_private; const struct CustomPathMethods *methods; } CustomPath; ▌CustomPath構造体  『俺ならこれぐらいの処理コストで実行できる』を PostgreSQLのオプティマイザに教えてやるための データ構造。  Path中の rows, startup_cost, total_cost で 処理コストを表現する。  methods には、次工程で Path --> Plan への変換を 行うコールバック関数などを設定する。  flagsには当該パスの特徴を指定することができる。 ✓ CUSTOMPATH_SUPPORT_BACKWARD_SCAN ✓ CUSTOMPATH_SUPPORT_MARK_RESTORE ✓ CUSTOMPATH_SUPPORT_PROJECTION
  14. RelOptInfoから条件句を抜き出す PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 21 static void

    SetCtidScanPath(PlannerInfo *root, RelOptInfo *baserel, Index rtindex, RangeTblEntry *rte) { : /* walk on the restrict info */ foreach (lc, baserel->baserestrictinfo) { RestrictInfo *rinfo = lfirst(lc); if (IS_SUPPORTED_EXPR(rino->clause)) dev_quals = lappend(dev_quals, rinfo); } if (dev_quals == NIL) return; : cpath = makeNode(CustomPath); cpath->custom_private = ctid_quals; cpath->methods = &ctidscan_path_methods; : add_path(baserel, (Path *)cpath); } ▌RelOptInfo  FROM行に列挙されたリレーションを表現する構造体  このリレーションをスキャンする Path の一覧  基本的な統計情報 ✓ ブロック数、レコード数、ALL_VISIBLEの割合  検索に利用できるインデックスの情報  リレーションをスキャンする条件句  被参照列のビットマップ  パーティションの子テーブル一覧
  15. 選ばれた Path から Plan を生成する(1/2) PostgreSQL Conference Japan 2022 ~CustomScan

    APIを使い倒して俺様DBエンジンを実装する~ 22 typedef struct CustomPathMethods { : Plan *(*PlanCustomPath)(PlannerInfo *root, RelOptInfo *rel, CustomPath *best_path, List *tlist, List *clauses, List *custom_plans); : } typedef struct CustomScan { Scan scan; uint32 flags; List *custom_plans; /* list of Plan nodes */ List *custom_exprs; /* expressions nodes */ List *custom_private; /* private data */ List *custom_scan_tlist; /* optional tlist */ Bitmapset *custom_relids; /* RTIs by this can */ const struct CustomScanMethods *methods; } CustomScan;  PathからPlanを生成する ✓ 先に登録した CustomPath のコストが最も小さく、 オプティマイザに選択された場合、method に登録 した PlanCustomPath 関数がコールされる。 ✓ CustomPathの内容をベースに、CustomPlanを作成する。  オプティマイザから渡される情報 ✓ tlist ターゲットリスト ✓ clauses 検索条件句 ✓ custom_plans 既に Plan に変換された下位ノード (custom_pathsをセットした場合)  CustomScanに含めるもの ✓ 疑似ターゲットリスト(custom_scan_tlist) ✓ プライベートデータ(expressionとそれ以外) ✓ CustomScan --> CustomScanState に変換するための関数 コールバック
  16. custom_scan_tlist の使い方(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 23 postgres=#

    explain verbose select count(*), sum(lo_revenue), lo_orderdate % 10000 from lineorder where lo_orderdate between 19950101 and 19950331 group by 3; QUERY PLAN ----------------------------------------------------------------------------------------------------- HashAggregate (cost=1155141.38..1155177.47 rows=2406 width=44) Output: count(*), sum(lo_revenue), ((lo_orderdate % 10000)) Group Key: ((lineorder.lo_orderdate % 10000)) -> Custom Scan (GpuScan) on public.lineorder (cost=11355.56..1140071.00 rows=2009384 width=10) Output: ((lo_orderdate % 10000)), lo_revenue GPU Projection: (lineorder.lo_orderdate % 10000), lineorder.lo_revenue GPU Filter: ((lineorder.lo_orderdate >= 19950101) AND (lineorder.lo_orderdate <= 19950331)) GPU Preference: GPU0 (NVIDIA A100-PCIE-40GB) Kernel Source: /var/lib/pgdata/pgsql_tmp/pgsql_tmp_strom_3737600.12.gpu (9 rows)
  17. custom_scan_tlist の使い方(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 24 tuple

    tuple SeqScan Projection CustomScan Projection tuple CustomScan Projection’ 次工程(JOIN、GROUP BYなど) tuple” tuple tuple tuple” tuple” tuple” テーブル定義通りの データ構造を持つタプル EXPLAIN VERBOSEコマンドで Output: に表示された列から 構成されるタプル GPU Device Projection CPU GPU SSD Smart-SSD
  18. 2種類のプライベート領域 custom_exprs と custom_privates PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~

    25 ▌後工程での式表現(expression)書き換え  式表現がある場合や、複数の入力から入ってきた データの取扱いを簡便にするため、実行計画を作成 した後に setrefs.c で式表現の書き換えを行う。  例えば、「tbl_x の a列」「tbl_y.b % 1000の計算結果」と いう形で表現されていた式表現を「INNER側の0番目」 「OUTER側の0番目」といった形に書き換える。 ▌CustomScanのプライベート領域に式表現を 含める場合  setrefs.c による書き換えを行ってほしい場合 ➔ custom_exprs に保存する  setrefs.c による書き換えを行わない場合 ➔ custom_privates に保存する SeqScan CustomScan JOIN tbl_x.a = tbl_y.b % 1000 on tbl_x on tbl_y output: tbl_y.b % 1000, tbl_y.c output: tbl_x.a, tbl_y.c SeqScan CustomScan JOIN INNER[0] = OUTER[0] on tbl_x on tbl_y output: tbl_y.b % 1000, tbl_y.c output: INNER[0], OUTER[1] setrefs.cによる書き換え
  19. パラレルスキャン用の CustomPath PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 26 ▌パラレルスキャンに必要な事

    ✓ 条件句に出現する関数・演算子が、ワーカーノードでも 動作するものとして宣言されている。 (immutable or stable かつ parallel_safe) ✓ テーブル上のスキャンすべきブロックを、他のワーカー と協調して特定する事ができる。  重複して同じブロックを読み出さない  共有メモリ(後述のDSM)を利用して、ワーカーノードと 読出し位置を調整する。 ▌CustomPathに必要な事 ✓ parallel_safe = true ✓ parallel_aware = true ✓ parallel_workers > 0 ✓ add_partial_path() を用いて登録する。 PG backend PG worker-1 PG worker-2
  20. PostgreSQLがクエリを処理する流れ PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 28 ▌実行計画(Plan)を元に、 実行時情報(PlanState)を生成する。

    ▌上位のノードから呼び出されたら、 有効な1行、またはNULLを返す。 ▌実行終了時にリソースを開放する。 ▌パラレルクエリの場合、共有メモリ(DSM: Dynamic Shared Memory)の初期化とアタッチを行う。 SQL Parser Query Optimizer Query Executor SQLクエリ パース木 クエリ実行計画 クエリ 実行結果
  21. 実行時情報の生成(1/3) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 29 typedef struct

    CustomScan { Scan scan; uint32 flags; List *custom_plans; /* list of Plan nodes */ List *custom_exprs; /* expressions nodes */ List *custom_private; /* private data */ List *custom_scan_tlist; /* optional tlist */ Bitmapset *custom_relids; /* RTIs by this can */ const struct CustomScanMethods *methods; } CustomScan; typedef struct CustomScanMethods { const char *CustomName; Node *(*CreateCustomScanState)(CustomScan *cscan); } CustomScanMethods;  CreateCustomScanState ➔ CustomScanに紐づく唯一のコールバック  役割:CustomScanを元に、CustomScanStateまたは 構造体の先頭にCustomScanStateを含む実行時情報 を割り当てる。 MyOwnScanState それ以外に独自に 必要な実行時情報 CustomScanState カスタムスキャンに 必要な実行時情報 ScanState テーブルスキャンに 必要な実行時情報 PlanState 全ての実行ノードに 共通のフィールド ⚫ Estate(共通の実行時情報)の参照 ⚫ 入力行 ➔ 出力行の変換(Projection) ⚫ 出力行の型定義 ⚫ INNER/OUTERノードへの参照 ⚫ オープンしている Relation ハンドラ ⚫ 現在のスキャン位置情報 ⚫ 実行時に呼び出されるコールバック 関数(methods) ⚫ 下位ノードのリスト(あれば) ⚫ その他、実行時に必要な情報 (GPUのデバイス管理情報、 ワーカースレッドのID、など)
  22. 実行時情報の生成(2/3) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 30 typedef struct

    CustomExecMethods { const char *CustomName; /* Required executor methods */ void (*BeginCustomScan) (CustomScanState *node, EState *estate, int eflags); TupleTableSlot * (*ExecCustomScan)(CustomScanState *node); void (*EndCustomScan) (CustomScanState *node); void (*ReScanCustomScan) (CustomScanState *node); : : void (*ExplainCustomScan) (CustomScanState *node, List *ancestors, ExplainState *es); } CustomExecMethods; ▌BeginCustomScan  出力行/入力行の型定義情報を初期化  入力行 ➔ 出力行への変換(Projection)を初期化  その他の実行時情報を初期化 ▌ExecCustomScan  初回実行時に  実行結果から一行を返却する。 もしくは終端に達した場合はNULL ▌EndCustomScan  リソースを開放する。 ▌ReScanCustomScan  もう一度スキャンをやり直す。 ▌ExplainCustomScan  EXPLAINで追加的な情報を出力する場合、 ここで追加する。
  23. 実行時情報の生成(3/3) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 31 void ExecInitCustomScan(...)

    { if (scanrelid > 0) { scan_rel = ExecOpenScanRelation(...); css->ss.ss_currentRelation = scan_rel; } : if (cscan->custom_scan_tlist != NIL || scan_rel == NULL) { scan_tupdesc = ExecTypeFromTL(cscan->custom_scan_tlist); ExecInitScanTupleSlot(estate, &css->ss, scan_tupdesc, &TTSOpsVirtual); tlistvarno = INDEX_VAR; } else { ExecInitScanTupleSlot(estate, &css->ss, RelationGetDescr(scan_rel), &TTSOpsVirtual); tlistvarno = scanrelid; } : /* Init result slot, type and projection */ ExecInitResultTupleSlotTL(&css->ss.ps, &TTSOpsVirtual); ExecAssignScanProjectionInfoWithVarno(&css->ss, tlistvarno); : css->methods->BeginCustomScan(css, estate, eflags); } tuple CustomScan Projection’ tuple” GPU Device Projection tuple CustomScan Projection tuple 次工程(JOIN、GROUP BYなど) tuple” tuple” custom_scan_tlist なしの 初期化パターン custom_scan_tlist ありの 初期化パターン ※ 結局、自前で初期化を やり直す方が多いが…。
  24. TupleTableSlot構造体について PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 32 typedef struct

    TupleTableSlot { NodeTag type; uint16 tts_flags; /* Boolean states */ AttrNumber tts_nvalid; /* # of valid values */ const TupleTableSlotOps *const tts_ops; TupleDesc tts_tupleDescriptor; /* 列定義情報 */ Datum *tts_values; /* 列ごとの値 */ bool *tts_isnull; /* NULL値フラグ */ MemoryContext tts_mcxt; ItemPointerData tts_tid; /* stored tuple's tid */ Oid tts_tableOid; /* table oid of tuple */ } TupleTableSlot; typedef struct HeapTupleTableSlot { TupleTableSlot base; HeapTuple tuple; /* physical tuple */ uint32 off; /* state for deforming */ HeapTupleData tupdata; /* optional workspace */ } HeapTupleTableSlot; ▌TupleTableSlot構造体の役割  列定義情報(TupleDesc)と、そのデータ形式に沿った 一行分のデータを保持する。 ➔ 各実行ノード間でデータの受け渡しに用いる。  PostgreSQL v12 のプラガブルストレージ機構のマージと 共に、多様なデータ構造を定義できるようになった。 ▌TTSOpsVirtualの場合  有効長tts_nvalid の配列tts_values/tts_isnullに 列単位で展開された値が入る。  最も汎用的な形式である一方、物理テーブルをスキャン した場合、タプルを展開しないといけない。 ▌物理タプルをそのまま使う場合は?  HeapTupleTableSlot か、さらに Buffer へのポインタを 付加した BufferHeapTupleTableSlot を利用すれば、 TupleTableSlot に物理タプルを格納できる。  table_slot_callbacks(relation) を使えば、 テーブルの TupleTableSlotOps を取得できる。 (v12以降、特定の物理タプル形式を前提にできない)
  25. エグゼキュータの構造 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 33 BeginCustomScan ExecutorStart

    initPlan 上位ノードの初期化 ExecInitCustomScan ExecCustomScan ExecutorRun ExecutePlan 上位ノードの実行 ExecCustomScan EndCustomScan ExecutorEnd ExecEndPlan 上位ノードの終了処理 ExecEndCustomScan 拡張モジュールで 実装すべき箇所 どのような実装手段でも構わないので、 ✓ 指定されたテーブルを読み出す ✓ 条件句に合致したタプルだけを返す ✓ 直上のノードが「入力値」として 期待するデータ型のタプルを返す。 ことを守っていればよい。 GPU 列キャッシュ SIMD命令 量子 コンピュータ Smart-SSD
  26. パラレルクエリの対応(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 34 postgres=# explain

    verbose select count(*), lo_orderdate % 10000 from lineorder where lo_orderdate between 19950101 and 19950331 group by 2; QUERY PLAN -------------------------------------------------------------------------------------------- Finalize HashAggregate (cost=726605.39..726635.47 rows=2406 width=12) Output: count(*), ((lo_orderdate % 10000)) Group Key: ((lineorder.lo_orderdate % 10000)) -> Gather (cost=726070.06..726581.33 rows=4812 width=12) Output: ((lo_orderdate % 10000)), (PARTIAL count(*)) Workers Planned: 2 -> Partial HashAggregate (cost=725070.06..725100.13 rows=2406 width=12) Output: ((lo_orderdate % 10000)), PARTIAL count(*) Group Key: ((lineorder.lo_orderdate % 10000)) -> Parallel Custom Scan (GpuScan) on public.lineorder (cost=29652.44..720883.84 rows=837243 width=4) Output: ((lo_orderdate % 10000)) GPU Projection: (lineorder.lo_orderdate % 10000) GPU Filter: ((lineorder.lo_orderdate >= 19950101) AND (lineorder.lo_orderdate <= 19950331)) GPU Preference: GPU0 (NVIDIA A100-PCIE-40GB) Kernel Source: /var/lib/pgdata/pgsql_tmp/pgsql_tmp_strom_3870445.12.gpu Kernel Binary: /var/lib/pgdata/pgsql_tmp/pgsql_tmp_strom_3870445.13.ptx (16 rows) Gatherがワーカーを立ち上げ、 ワーカーから返された結果も含め、 上位ノードへ結果を返す。
  27. パラレルクエリの対応(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 35 typedef struct

    CustomExecMethods { const char *CustomName; : /* optional: needed if parallel execution */ Size (*EstimateDSMCustomScan)(CustomScanState *node, ParallelContext *pcxt); void (*InitializeDSMCustomScan)(CustomScanState *node, ParallelContext *pcxt, void *coordinate); void (*ReInitializeDSMCustomScan)(CustomScanState *node, ParallelContext *pcxt, void *coordinate); void (*InitializeWorkerCustomScan)(CustomScanState *node, shm_toc *toc, void *coordinate); void (*ShutdownCustomScan) (CustomScanState *node); : } CustomExecMethods; Gatherノードの動き ① 下位ノードの共有メモリ(DSM)必要量を計算 ✓ EstimateDSMCustomScanが呼ばれる ② IPC用の共有メモリを確保し、各ノードごとに初期化 ✓ InitializeDSMCustomScanが呼ばれる ③ Partial Aggregate以下を実行するワーカプロセスを起動 ④ 各ワーカで初期化済みの共有メモリをアタッチする。 ✓ InitializeWorkerCustomScan が呼ばれる ⑤ バックエンド・各ワーカーで終端までスキャンを行う ⑥ 各ワーカープロセスの終了 ⑦ 共有メモリを開放する前に、 シャットダウン処理を実行 ✓ ShutdownCustomScan が呼ばれる。 ⑧ エグゼキュータの終了処理 ✓ EndCustomScanが呼ばれる Gather Partial Aggregate Hash Join CustomScan SeqScan Partial Aggregate Hash Join CustomScan SeqScan Partial Aggregate Hash Join CustomScan SeqScan 共有メモリ (DSM)
  28. JOINとGROUP BY(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 37 HashJoin

    Projection CustomScan Projection tuple tuple tuple tuple tuple” tuple” 次工程(他のJOIN、GROUP BYなど) どのような実装手段でも構わないので、 ✓ 要求された下位ノードからタプルを読み出す。 ✓ 結合条件に従ってJOIN処理を行い、直上のノードが 「入力値」として期待するデータ型のタプルを返す。 ✓ 集約式に従ってGROUP BY処理を行い、直上のノードが 「入力値」として期待するデータ型のタプルを返す。 ことを守っていればよい。
  29. JOINとGROUP BY(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 38 /*

    Hook for plugins to get control in add_paths_to_joinrel() */ typedef void (*set_join_pathlist_hook_type) (PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel, RelOptInfo *innerrel, JoinType jointype, JoinPathExtraData *extra); extern PGDLLIMPORT set_join_pathlist_hook_type set_join_pathlist_hook; /* Hook for plugins to get control when grouping_planner() plans upper rels */ typedef void (*create_upper_paths_hook_type) (PlannerInfo *root, UpperRelationKind stage, RelOptInfo *input_rel, RelOptInfo *output_rel, void *extra); extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook; ▌JOINに対する Path の追加  set_join_pathlist_hook を利用する。  joinrelは、outerrelとinnerrelを結合した表を 意味する RelOptInfo で、ここに CustomPath を追加す る。 ▌GROUP BYに対する Path の追加  SCAN/JOINが一通り終わった後、より上位の実行計画を 積み上げるという意味で UPPER-PATH と呼んでいる。  create_upper_paths_hookを利用する。  UpperRelationKindは以下の8種類 ✓ UPPERREL_SETOP ✓ UPPERREL_PARTIAL_GROUP_AGG ✓ UPPERREL_GROUP_AGG ✓ UPPERREL_WINDOW ✓ UPPERREL_PARTIAL_DISTINCT ✓ UPPERREL_DISTINCT ✓ UPPERREL_ORDERED ✓ UPPERREL_FINAL
  30. 下位ノードを持つ CustomPath の作成 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 39

    typedef struct CustomPath { Path path; uint32 flags; /* mask of CUSTOMPATH_* flags */ List *custom_paths; /* list of child Path */ List *custom_private; const struct CustomPathMethods *methods; } CustomPath; typedef struct CustomPathMethods { const char *CustomName; /* Convert Path to a Plan */ Plan *(*PlanCustomPath) (PlannerInfo *root, RelOptInfo *rel, CustomPath *best_path, List *tlist, List *clauses, List *custom_plans); : } typedef struct CustomScan { Scan scan; uint32 flags; List *custom_plans; /* list of Plan nodes */ List *custom_exprs; List *custom_private; List *custom_scan_tlist; Bitmapset *custom_relids; CustomScanMethods *methods; } CustomScan; void BeginCustomScan(CustomScanState *node, ...) { CustomScan *cscan = (CustomScan *)node->ss.ps->plan; foreach (lc, cscan->custom_plans) { Plan *child_plan = lfirst(lc); child_ps = ExecInitNode(child_plan, ...); :
  31. FDWにおける非同期API(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 41 子テーブル① (Foreign

    Table) 子テーブル② (Foreign Table) 子テーブル③ (Foreign Table) 子テーブル④ (Foreign Table) リモートサーバー Gather Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ Foreign Scan on ④ パーティションテーブル nworkers=2 backend worker-1 worker-2
  32. FDWにおける非同期API(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 42 子テーブル① (Foreign

    Table) 子テーブル② (Foreign Table) 子テーブル③ (Foreign Table) 子テーブル④ (Foreign Table) リモートサーバー Gather Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ Foreign Scan on ④ パーティションテーブル nworkers=2 worker-1
  33. FDWにおける非同期API(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 43 /* Support

    functions for asynchronous execution */ bool IsForeignPathAsyncCapable(ForeignPath *path); ForeignPathが非同期実行をサポートしているかどうかを オプティマイザに通知する void ForeignAsyncRequest(AsyncRequest *areq); リモートサーバから非同期に受け取ったタプルを一つ、 呼び出し元へ返す、またはareq->callback_pending=trueを セットして結果待ちである事を呼び出し元に知らせる、あるい はスロットにNULLをセットして終端に達した事を知らせる。 void ForeignAsyncConfigureWait(AsyncRequest *areq); 結果待ちである場合、WaitEventSetWait()の対象となる ファイルディスクリプタを追加する。 (読み出し可能となった時点でプロセスが起床する) void ForeignAsyncNotify(AsyncRequest *areq); 新たにリモートから処理結果を受け取ったら、そのタプルを 一つ、呼び出し元へ返す。 ① ForeignAsyncRequest(初回)を通じて、リモートサーバに 実行開始のリクエスト。 Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ SeqScan on ④
  34. FDWにおける非同期API(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 44 /* Support

    functions for asynchronous execution */ bool IsForeignPathAsyncCapable(ForeignPath *path); ForeignPathが非同期実行をサポートしているかどうかを オプティマイザに通知する void ForeignAsyncRequest(AsyncRequest *areq); リモートサーバから非同期に受け取ったタプルを一つ、 呼び出し元へ返す、またはareq->callback_pending=trueを セットして結果待ちである事を呼び出し元に知らせる、あるい はスロットにNULLをセットして終端に達した事を知らせる。 void ForeignAsyncConfigureWait(AsyncRequest *areq); 結果待ちである場合、WaitEventSetWait()の対象となる ファイルディスクリプタを追加する。 (読み出し可能となった時点でプロセスが起床する) void ForeignAsyncNotify(AsyncRequest *areq); 新たにリモートから処理結果を受け取ったら、そのタプルを 一つ、呼び出し元へ返す。 ① ForeignAsyncRequest(初回)を通じて、リモートサーバに 実行開始のリクエスト。 ② ForeignAsyncConfigureWaitにより同期イベントをセット Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ SeqScan on ④
  35. FDWにおける非同期API(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 45 /* Support

    functions for asynchronous execution */ bool IsForeignPathAsyncCapable(ForeignPath *path); ForeignPathが非同期実行をサポートしているかどうかを オプティマイザに通知する void ForeignAsyncRequest(AsyncRequest *areq); リモートサーバから非同期に受け取ったタプルを一つ、 呼び出し元へ返す、またはareq->callback_pending=trueを セットして結果待ちである事を呼び出し元に知らせる、あるい はスロットにNULLをセットして終端に達した事を知らせる。 void ForeignAsyncConfigureWait(AsyncRequest *areq); 結果待ちである場合、WaitEventSetWait()の対象となる ファイルディスクリプタを追加する。 (読み出し可能となった時点でプロセスが起床する) void ForeignAsyncNotify(AsyncRequest *areq); 新たにリモートから処理結果を受け取ったら、そのタプルを 一つ、呼び出し元へ返す。 ① ForeignAsyncRequest(初回)を通じて、リモートサーバに 実行開始のリクエスト。 ② ForeignAsyncConfigureWaitにより同期イベントをセット ③ リモート実行中に、同期実行できるものを実行 Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ SeqScan on ④
  36. FDWにおける非同期API(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 46 /* Support

    functions for asynchronous execution */ bool IsForeignPathAsyncCapable(ForeignPath *path); ForeignPathが非同期実行をサポートしているかどうかを オプティマイザに通知する void ForeignAsyncRequest(AsyncRequest *areq); リモートサーバから非同期に受け取ったタプルを一つ、 呼び出し元へ返す、またはareq->callback_pending=trueを セットして結果待ちである事を呼び出し元に知らせる、あるい はスロットにNULLをセットして終端に達した事を知らせる。 void ForeignAsyncConfigureWait(AsyncRequest *areq); 結果待ちである場合、WaitEventSetWait()の対象となる ファイルディスクリプタを追加する。 (読み出し可能となった時点でプロセスが起床する) void ForeignAsyncNotify(AsyncRequest *areq); 新たにリモートから処理結果を受け取ったら、そのタプルを 一つ、呼び出し元へ返す。 ① ForeignAsyncRequest(初回)を通じて、リモートサーバに 実行開始のリクエスト。 ② ForeignAsyncConfigureWaitにより同期イベントをセット ③ リモート実行中に、同期実行できるものを実行 ④ 結果が返ってきていれば、ForeignAsyncNotifyを通じ て結果を取得。一行を返す。 Append Foreign Scan on ① Foreign Scan on ② Foreign Scan on ③ SeqScan on ④
  37. CustomScan APIにおける非同期処理APIの追加(1/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 47 ▌ForeignScan

    の非同期APIとの違いは?  ほとんどない。  CustomScanが非同期APIをサポートするかどうか、 CUSTOMPATH_SUPPORT_ASYNC_EXECUTIONフラグを用いてオプ (専用のコールバックは設けていない) ▌追加するコールバックは  void CustomScanAsyncRequest(AsyncRequest *areq);  void CustomScanAsyncConfigureWait(AsyncRequest *areq);  void CustomScanAsyncNotify(AsyncRequest *areq); ➔ 使い方は FDW のケースと全く同一
  38. CustomScan APIにおける非同期処理APIの追加(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 48 Computational

    Storage SSDドライブ上にFPGAやARMプロセッサを 搭載し、ストレージドライブ上でユーザの 開発したソフトウェアを実行する事ができる。 Flashストレージの業界では最もホットな話題の一つ。 仮に2U/24ドライブのサーバにフル実装したら、 24ノードのワーカーが走っているのと同じ。☺ 子テーブル① (Heap Table) 子テーブル② (Heap Table) 子テーブル③ (Heap Table) 子テーブル④ (Heap Table) パーティションテーブル shared buffer Filesystem NVME Block System 参照 SELECT + Index 書き込み INSERT/UPDATE/DELETE
  39. CustomScan APIにおける非同期処理APIの追加(2/2) PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 49 Computational

    Storage SSDドライブ上にFPGAやARMプロセッサを 搭載し、ストレージドライブ上でユーザの 開発したソフトウェアを実行する事ができる。 Flashストレージの業界では最もホットな話題の一つ。 仮に2U/24ドライブのサーバにフル実装したら、 24ノードのワーカーが走っているのと同じ。☺ 子テーブル① (Heap Table) 子テーブル② (Heap Table) 子テーブル③ (Heap Table) 子テーブル④ (Heap Table) パーティションテーブル shared buffer Filesystem NVME Block System 検索・集計 SELECT + Scan
  40. まとめ  CustomScan APIとは ✓ FDWが「何を」テーブルとして見せるのかに対して、テーブルを「どのように」処理するかを プラガブルにするための機構。 ✓ PostgreSQL v9.5でサポート。PostgreSQLを母体とするソリューションで割と使われている。

     オプティマイザ ✓ set_rel_pathlist_hook やset_join_pathlist_hookを用いて、独自の CustomPath を追加する。 ✓ 上位ノードに結果を返す際の列定義情報(custom_scan_tlist)に注意する。  エグゼキュータ ✓ BeginCustomScanで初期化、ExecCustomScanで実行、EndCustomScanで終了 ✓ 上位ノードに結果を返す際の列定義情報(TupleDesc)に注意。必ず一致していないとダメ。 ✓ 本来の SCAN や JOIN の処理と同じ結果を返しさえすれば、どのような方法で実装してもよい。 ✓ パラレルクエリの場合は、共有メモリ(DSM)を介して他のワーカーと協調する。  今後の機能強化 ✓ ForeignScan同様の非同期処理用のAPIを提案中 ✓ Smart-SSDやSmart-NICでSQLを処理する際に、非同期処理を記述できるようになる。 PostgreSQL Conference Japan 2022 ~CustomScan APIを使い倒して俺様DBエンジンを実装する~ 50