CitusDB + PG-StromでScale-up+outする。

PostgreSQL Advent Calendar 2019の14日目です。

PG-Stromの開発をやってると、しばしば聞かれるのが

『マルチノードの並列処理って対応してるんですか?』

という質問。

まぁ、『対応しておりませんし、対応する予定もございません』という回答になるんですが、別にこれはウチのやる気の問題ではなく、PG-StromはPostgreSQLの拡張モジュールとして設計されているため、並列分散処理に関しては他のメカニズムに任せてしまえばよい、というだけの話である。

そこで、今回は同じくPostgreSQLの拡張モジュールとして実装されているスケールアウト機能の Citus と、PG-Stromを組み合わせてちゃんと動作するんですよという事を検証してみる事にする。

Citusとは?

PostgreSQLにデータ分散と並列処理機構を付加する拡張モジュールで、PostgreSQLの拡張モジュールとして実装されている。*1
この人はコーディネータ+多数のワーカーノードという構成になっており、指定したテーブルを複数のワーカーノードに分割して保存するシャーディング機能を持っている。したがって、テーブルサイズが肥大化しても、一台のワーカーノードで処理すべきデータ量を抑える事ができる。
また、巨大テーブルをスキャンする集計クエリを受け取ると、コーディネータはそれを各ワーカーノードで分割実行できるよう切り分け、ワーカーノードは自らの担当範囲だけを集計した上で、コーディネータが最終結果を生成してユーザに返すというMap-Reduceチックな並列処理を行う事ができる。

一部機能を限定はしているものの、Citusの拡張モジュールはオープンソースとして公開されており、お手軽に試すことができる。
開発元の CitusData社 は2011年創業のスタートアップで、2019年には Microsoft に買収される(パチパチ)。現在は Azure上で HyperScale というブランドで展開されている。

検証構成

CitusをPG-Stromを組み合わせて使うケースで最も素直な構成は、各ワーカーノードが GPU + NVME を持ち、そこでPG-Stromが動作するという構成である。
Citusのコーディネータがクエリを受け取った後、それを各ワーカーノードが実行可能な形に書き換えて処理をキックするが、これはワーカーノードにとっては通常のSQL処理と変わらないので、そこでGPUを用いる方が合理的であれば(つまり、オプティマイザがそう判断すれば)GPUを使用するし、NVME-SSDからのデータ読出しであればSSD-to-GPU Direct SQLを使用する事もできる。

少し機材の都合で、自宅のパソコンのPCIEカードを以下のように組み替えて、2台の独立したサーバに相当する構成を作ってみた。
この場合、NVME0~3⇒GPU0と、NVME4~7⇒GPU1へのデータ転送は全く他方に影響を与えないため、I/O負荷は完全に独立して処理されると考えてよい。

まず、これらディスク領域の準備からはじめる。

[kaigai@kujira ~]$ ls -l /dev/nvme*n1p1
brw-rw----. 1 root disk 259,  1 Dec  6 14:47 /dev/nvme0n1p1
brw-rw----. 1 root disk 259,  3 Dec  6 14:47 /dev/nvme1n1p1
brw-rw----. 1 root disk 259, 14 Dec  6 14:47 /dev/nvme2n1p1
brw-rw----. 1 root disk 259,  5 Dec  6 14:47 /dev/nvme3n1p1
brw-rw----. 1 root disk 259, 13 Dec  6 14:47 /dev/nvme4n1p1
brw-rw----. 1 root disk 259, 15 Dec  6 14:47 /dev/nvme5n1p1
brw-rw----. 1 root disk 259, 12 Dec  6 14:47 /dev/nvme6n1p1
brw-rw----. 1 root disk 259, 10 Dec  6 14:47 /dev/nvme7n1p1

ご覧のように、8台のNVME-SSDが見えており、これらを4台ずつ束ねてストライピング構成を作る。
ここで使用しているNVME-SSDIntelDC P4510 (U.2; 1.0TB)で、SeqReadのカタログスペックは 2850MB/s。つまり、4台束ねれば理論上 11.4GB/s 程度までは出る事になる。*2

以下のようにmdadmコマンドを用いて md-raid0 区画を作成する。
最後のは、/etc/mdadm.confファイルを作成して再起動時に区画が復元されるようにするための設定。

[root@kujira kaigai]# mdadm -C /dev/md0 -c 128 -l 0 -n 4 /dev/nvme0n1p1 /dev/nvme1n1p1 /dev/nvme2n1p1 /dev/nvme3n1p1
mdadm: Defaulting to version 1.2 metadata
mdadm: array /dev/md0 started.

[root@kujira kaigai]# mdadm -C /dev/md1 -c 128 -l 0 -n 4 /dev/nvme4n1p1 /dev/nvme5n1p1 /dev/nvme6n1p1 /dev/nvme7n1p1
mdadm: Defaulting to version 1.2 metadata
mdadm: array /dev/md1 started.

[root@kujira kaigai]# mdadm --detail --scan > /etc/mdadm.conf

この辺は流れ作業だが、md-raid0区画上にパーティションを切り、Ext4で初期化する。

# fdisk /dev/md0
# fdisk /dev/md1

# mkfs.ext4 -LNVME0 /dev/md0p1
# mkfs.ext4 -LNVME1 /dev/md1p1

# cat /etc/fstab
        :
    (snip)
        :
LABEL=NVME0             /nvme/0                 ext4    nofail          0 0
LABEL=NVME1             /nvme/1                 ext4    nofail          0 0

ここで設定した /nvme/0 ボリューム上にCitus+PG-Stromのワーカー1号を、/nvme/1ボリューム上にワーカー2号を配置する。

PostgreSQL (Citus + PG-Strom) の構築

まず、各ワーカーノードの構築を行う。物理的にI/Oは切り離されているとはいえ、同じマシンなので、ポート番号を微妙にずらすことにする。
ワーカー1号を 5433 ポートで、ワーカー2号を 5434 ポートで動かすことにする。

initdbでデータベースクラスタを作成する。

# mkdir /nvme/0/pgdata
# chown postgres:postgres -R /nvme/0/pgdata
# su - postgres -c 'initdb -D /nvme/0/pgdata'

次に、ワーカー側のpostgreql.confを修正する。とはいってもshared_preload_librariesなど基本的なものだけ。

port = 5433
shared_buffers = 12GB
work_mem = 10GB
max_worker_processes = 100
shared_preload_libraries = 'citus,pg_strom'

これで、ワーカー側のPostgreSQLを起動する事ができる。

# su - postgres -c 'pg_ctl start -D /nvme/0/pgdata'

ログを見ると、2台のTesla P40にそれぞれ近傍のNVME-SSDが4台ずつ認識されている事がわかる。
(見やすさのため、タイムスタンプをカット)

LOG:  number of prepared transactions has not been configured, overriding
LOG:  PG-Strom version 2.3 built for PostgreSQL 11
LOG:  PG-Strom: GPU1 Tesla P40 (3840 CUDA cores; 1531MHz, L2 3072kB), RAM 22.38GB (384bits, 3.45GHz), CC 6.1
LOG:  PG-Strom: GPU2 Tesla P40 (3840 CUDA cores; 1531MHz, L2 3072kB), RAM 22.38GB (384bits, 3.45GHz), CC 6.1
LOG:    -  PCIe[0000:17]
LOG:        -  PCIe(0000:17:00.0)
LOG:            -  PCIe(0000:18:00.0)
LOG:                -  PCIe(0000:19:08.0)
LOG:                    -  PCIe(0000:1a:00.0)
LOG:                        -  PCIe(0000:1b:00.0)
LOG:                            -  PCIe(0000:1c:00.0) nvme0 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:1b:01.0)
LOG:                            -  PCIe(0000:1d:00.0) nvme1 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:1b:02.0)
LOG:                            -  PCIe(0000:1e:00.0) nvme2 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:1b:03.0)
LOG:                            -  PCIe(0000:1f:00.0) nvme3 (INTEL SSDPE2KX010T8)
LOG:                -  PCIe(0000:19:10.0)
LOG:                    -  PCIe(0000:20:00.0) GPU1 (Tesla P40)
LOG:    -  PCIe[0000:ae]
LOG:        -  PCIe(0000:ae:00.0)
LOG:            -  PCIe(0000:af:00.0)
LOG:                -  PCIe(0000:b0:08.0)
LOG:                    -  PCIe(0000:b1:00.0) GPU2 (Tesla P40)
LOG:                -  PCIe(0000:b0:10.0)
LOG:                    -  PCIe(0000:b2:00.0)
LOG:                        -  PCIe(0000:b3:00.0)
LOG:                            -  PCIe(0000:b4:00.0) nvme4 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:b3:01.0)
LOG:                            -  PCIe(0000:b5:00.0) nvme5 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:b3:02.0)
LOG:                            -  PCIe(0000:b6:00.0) nvme6 (INTEL SSDPE2KX010T8)
LOG:                        -  PCIe(0000:b3:03.0)
LOG:                            -  PCIe(0000:b7:00.0) nvme7 (INTEL SSDPE2KX010T8)
LOG:  GPU<->SSD Distance Matrix
LOG:             GPU0     GPU1
LOG:      nvme1  (   5)     -1
LOG:      nvme0  (   5)     -1
LOG:      nvme7     -1   (   5)
LOG:      nvme4     -1   (   5)
LOG:      nvme5     -1   (   5)
LOG:      nvme3  (   5)     -1
LOG:      nvme6     -1   (   5)
LOG:      nvme2  (   5)     -1
LOG:  HeteroDB License: { "version" : 2, "serial_nr" : "HDB-TRIAL", "issued_at" : "6-Dec-2019", "expired_at" : "1-Jan-2030", "gpus" : [ { "uuid" : "GPU-b44c118b-1058-16cb-1cbb-5dbe0fe6181a", "pci_id" : "0000:20:00.0" } , { "uuid" : "GPU-a137b1df-53c9-197f-2801-f2dccaf9d42f", "pci_id" : "0000:b1:00.0" } ] }
LOG:  listening on IPv6 address "::1", port 5434
LOG:  listening on IPv4 address "127.0.0.1", port 5434
LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5434"
LOG:  listening on Unix socket "/tmp/.s.PGSQL.5434"
LOG:  redirecting log output to logging collector process
HINT:  Future log output will appear in directory "log".

この様に、それぞれ/nvme/0/nvme/1上にワーカーのPostgreSQLインスタンスを起動した。
最後に、これらワーカー側に直接接続してCREATE EXTENSIONを実行する。

$ psql -U postgres -p 5433
psql (11.5)
Type "help" for help.

postgres=# create extension pg_strom;
CREATE EXTENSION
postgres=# create extension citus;
CREATE EXTENSION

次にコーディネータの構築であるが、この人は自分でデータを持つわけでもなく、大量のスキャンを行うわけでもないので、どこか適当な磁気ディスク上の区画でも割り当てておけばよい。
デフォルトの/var/lib/pgdataにDBを構築し、Citusモジュールだけをロードするように構成した。

以上でコーディネータ、ワーカーの初期設定は完了である。

テーブルの作成とデータの投入

テスト用に、いつもの SSBM (Star Schema Benchmark) のテーブルを構築し、このうち最もサイズの大きなlineorderテーブルを各ワーカーに分散配置する事にする。
scale factorは401で、DBサイズにすると 353GB 程度。数字に深い意味はないが、これまでこのサイズで性能計測をしていた事が多かったので。

まず、通常通りCREATE TABLE文を投入する。例えばlineorderテーブルであれば以下のような、いたって何の変哲もないCREATE TABLEである。

CREATE TABLE lineorder (
    lo_orderkey numeric,
    lo_linenumber integer,
    lo_custkey numeric,
    lo_partkey integer,
    lo_suppkey numeric,
    lo_orderdate integer,
    lo_orderpriority character(15),
    lo_shippriority character(1),
    lo_quantity numeric,
    lo_extendedprice numeric,
    lo_ordertotalprice numeric,
    lo_discount numeric,
    lo_revenue numeric,
    lo_supplycost numeric,
    lo_tax numeric,
    lo_commit_date character(8),
    lo_shipmode character(10)
);

少し Citus のオリジナル要素はこの次。lineorderを分散配置する場合は、create_distributed_table関数を用いて、システムに分散配置すべきテーブルと分散キーに用いるカラムを教えてやる。

postgres=# SELECT create_distributed_table('lineorder', 'lo_orderkey');
 create_distributed_table
--------------------------

(1 row)

テーブルを分散配置せず、各ワーカーに複製をコピーするというやり方もある。この場合はcreate_reference_table関数を使用する。

postgres=# SELECT create_reference_table('date1');
 create_reference_table
------------------------

(1 row)

このように設定する事でdistributed tableとreference tableのJOINをワーカー側で実行する事が可能となり、コーディネータの負荷を最大限に下げる事ができる。

そして、最後にコーディネータ側からデータを投入する。

postgres=# \copy lineorder from program './dbgen-ssbm -X -Tl -s 401' delimiter '|'
SSBM (Star Schema Benchmark) Population Generator (Version 1.0.0)
Copyright Transaction Processing Performance Council 1994 - 2000
COPY 2406009932

コーディネータ側から見ても実際のデータサイズは分からないが、、、

postgres=# \d+
                        List of relations
 Schema |   Name    | Type  |  Owner   |    Size    | Description
--------+-----------+-------+----------+------------+-------------
 public | customer  | table | postgres | 8192 bytes |
 public | date1     | table | postgres | 8192 bytes |
 public | lineorder | table | postgres | 8192 bytes |
 public | part      | table | postgres | 8192 bytes |
 public | supplier  | table | postgres | 8192 bytes |
(5 rows)

ワーカー側に接続してみると、確かに分散テーブルが細切れになって保存されている様子が分かる。
(そして reference table は単純にコピーされている)

[kaigai@kujira ~]$ psql -U postgres postgres -p 5433
psql (11.5)
Type "help" for help.

postgres=# \d+
                          List of relations
 Schema |       Name       | Type  |  Owner   |  Size   | Description
--------+------------------+-------+----------+---------+-------------
 public | customer_102072  | table | postgres | 1627 MB |
 public | date1_102075     | table | postgres | 416 kB  |
 public | lineorder_102040 | table | postgres | 11 GB   |
 public | lineorder_102042 | table | postgres | 11 GB   |
 public | lineorder_102044 | table | postgres | 11 GB   |
 public | lineorder_102046 | table | postgres | 11 GB   |
 public | lineorder_102048 | table | postgres | 11 GB   |
 public | lineorder_102050 | table | postgres | 11 GB   |
 public | lineorder_102052 | table | postgres | 11 GB   |
 public | lineorder_102054 | table | postgres | 11 GB   |
 public | lineorder_102056 | table | postgres | 11 GB   |
 public | lineorder_102058 | table | postgres | 11 GB   |
 public | lineorder_102060 | table | postgres | 11 GB   |
 public | lineorder_102062 | table | postgres | 11 GB   |
 public | lineorder_102064 | table | postgres | 11 GB   |
 public | lineorder_102066 | table | postgres | 11 GB   |
 public | lineorder_102068 | table | postgres | 11 GB   |
 public | lineorder_102070 | table | postgres | 11 GB   |
 public | part_102073      | table | postgres | 206 MB  |
 public | supplier_102074  | table | postgres | 528 MB  |
(20 rows)

vacuum analyzeを走らせる

最後に、SSD-to-GPU Direct SQLに必要な visibility map を強制的に作成するために、vacuum analyzeを実行する。まさかコーディネータ側の空っぽテーブルをvacuumするだけじゃないよね?と思ったが、さすがに杞憂だったというか、ワーカー側できちんと分散処理してくれていた。

postgres=# vacuum analyze lineorder ;

を、実行中のtopコマンドの出力

top - 16:14:08 up  4:03,  5 users,  load average: 5.79, 1.35, 1.04
Tasks: 418 total,  24 running, 394 sleeping,   0 stopped,   0 zombie
%Cpu(s): 15.2 us, 33.0 sy,  0.0 ni,  7.9 id, 43.8 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 19651427+total,   525488 free,  5696528 used, 19029225+buff/cache
KiB Swap:  8388604 total,  8380412 free,     8192 used. 18758867+avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
   150 root      20   0       0      0      0 R  59.3  0.0   0:52.53 kswapd0
 17275 postgres  20   0   12.9g  48344  44076 D  56.3  0.0   0:05.91 postgres
   151 root      20   0       0      0      0 R  53.7  0.0   0:27.33 kswapd1
   321 root      20   0       0      0      0 S  51.0  0.0   0:33.39 kworker/u274:5
 17311 root      20   0       0      0      0 S  48.7  0.0   0:01.91 kworker/u274:0
 17286 postgres  20   0   12.9g  48016  43760 D  44.7  0.0   0:04.55 postgres
 17295 postgres  20   0   12.9g  47956  43704 R  42.3  0.0   0:04.33 postgres
 17298 postgres  20   0   12.9g  47932  43680 R  42.0  0.0   0:04.65 postgres
  6657 postgres  20   0   12.9g 339180 324212 R  41.3  0.2   6:54.01 postgres
 17281 postgres  20   0   12.9g  48340  44076 R  41.3  0.0   0:05.74 postgres
 17301 postgres  20   0   12.9g  48180  43928 R  41.3  0.0   0:05.26 postgres
 17278 postgres  20   0   12.9g  47804  43540 R  40.7  0.0   0:04.32 postgres
 17277 postgres  20   0   12.9g  48180  43916 R  40.3  0.0   0:05.22 postgres
 17287 postgres  20   0   12.9g  47928  43660 R  38.7  0.0   0:04.15 postgres
 17279 postgres  20   0   12.9g  48000  43732 R  38.3  0.0   0:04.10 postgres
 17284 postgres  20   0   12.9g  48056  43796 R  36.3  0.0   0:04.96 postgres
 17280 postgres  20   0   12.9g  48196  43928 R  34.0  0.0   0:04.84 postgres
  6658 postgres  20   0   12.9g 330544 322924 D  31.3  0.2   6:29.77 postgres
 17292 postgres  20   0   12.9g  47804  43552 R  31.3  0.0   0:04.45 postgres
 17294 postgres  20   0   12.9g  48068  43812 D  27.7  0.0   0:04.30 postgres
 17283 postgres  20   0   12.9g  47832  43560 R  26.0  0.0   0:04.14 postgres
 17289 postgres  20   0   12.9g  48060  43800 R  25.7  0.0   0:05.18 postgres
 17291 postgres  20   0   12.9g  47804  43552 R  25.7  0.0   0:04.01 postgres
 17302 postgres  20   0   12.9g  47940  43688 R  24.0  0.0   0:05.04 postgres
 17299 postgres  20   0   12.9g  48000  43744 R  21.7  0.0   0:04.01 postgres
 17304 postgres  20   0   12.9g  47792  43540 R  21.3  0.0   0:04.53 postgres
 17282 postgres  20   0   12.9g  47828  43564 R  19.3  0.0   0:03.52 postgres
 17290 postgres  20   0   12.9g  48080  43816 R  17.3  0.0   0:04.83 postgres
 17285 postgres  20   0   12.9g  47956  43688 D  15.0  0.0   0:04.18 postgres
 17288 postgres  20   0   12.9g  47980  43724 D  15.0  0.0   0:04.75 postgres
 17303 postgres  20   0   12.9g  48084  43828 D  14.0  0.0   0:04.89 postgres
 17276 postgres  20   0   12.9g  47968  43692 D  13.0  0.0   0:04.28 postgres
 17300 postgres  20   0   12.9g  47800  43548 R  12.3  0.0   0:03.76 postgres

集計クエリを実行してみる(Take.1)

早速、Star Schema Benchmarkの集計クエリを実行してみる事にする。
先ずは実行計画。

postgres=# explain
select sum(lo_extendedprice*lo_discount) as revenue
from lineorder,date1
where lo_orderdate = d_datekey
and d_year = 1993
and lo_discount between 1 and 3
and lo_quantity < 25;
                                                                          QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=0.00..0.00 rows=0 width=0)
   ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
         Task Count: 32
         Tasks Shown: One of 32
         ->  Task
               Node: host=localhost port=5433 dbname=postgres
               ->  Aggregate  (cost=958010.91..958010.92 rows=1 width=32)
                     ->  Gather  (cost=957962.46..958006.83 rows=408 width=8)
                           Workers Planned: 2
                           ->  Parallel Custom Scan (GpuPreAgg)  (cost=956962.46..956966.03 rows=204 width=8)
                                 Reduction: NoGroup
                                 Combined GpuJoin: enabled
                                 GPU Preference: GPU1 (Tesla P40)
                                 ->  Parallel Custom Scan (GpuJoin) on lineorder_102040 lineorder  (cost=30257.08..962321.28 rows=590200 width=10)
                                       Outer Scan: lineorder_102040 lineorder  (cost=30259.93..963572.01 rows=4133020 width=14)
                                       Outer Scan Filter: ((lo_discount >= '1'::numeric) AND (lo_discount <= '3'::numeric) AND (lo_quantity < '25'::numeric))
                                       Depth 1: GpuHashJoin  (hash-size: 66.06KB, nrows 4133020...1416481)
                                                HashKeys: lineorder.lo_orderdate
                                                JoinQuals: (lineorder.lo_orderdate = date1.d_datekey)
                                       GPU Preference: GPU1 (Tesla P40)
                                       ->  Seq Scan on date1_102075 date1  (cost=0.00..78.95 rows=365 width=4)
                                             Filter: (d_year = 1993)
(22 rows)

これを見ると、確かに Citus の独自プランであるCustom Scan (Citus Adaptive)と、PG-Stromの独自プランであるCustom Scan (GpuPreAgg)Custom Scan (GpuJoin)が混在している。

ではさっそく実行してみると、、、、Out of managed memoryエラーで止まってしまった。要はGPUリソースの食いすぎである。

GPUを使用するプログラムは、GPUバイス上に「CUDAコンテキスト」と呼ばれる実行時情報を保持する必要がある。これが最低でも150MB程度の大きさがあり、さらに、PG-Stromで処理すべきデータを保持すると、プロセスあたり平均して600MB~800MB程度のメモリを消費する事になる。
今回は、Citusがlineorderテーブルを32個のセグメントに分割し、ワーカーノード1個あたり16個のセグメントを持っている事になる。加えて、ワーカーノードで実行される集計クエリはPostgreSQLのパラレルクエリ機能により、3並列で実行((Workers Planned: 2と表示されているため、自分自身+バックグラウンドワーカー×2))されるため、GPUあたり48プロセスが全力で回るという事になる。
さすがにこれは辛みがあるので、そもそも16セグメントに分割されている分、ワーカー側の並列実行を止める事にする。

また、この実行計画には NVMe-Strom: enabledというメッセージが表示されておらず、せっかくのNVME-SSDなのに、SSD-to-GPU Direct SQLモードを利用できない。
これは、lineorderが細かく分割された結果、テーブル1個あたりのサイズが、SSD-to-GPU Direct SQLを使用する閾値よりも小さな値になっているからである。

そこで、上記2つの問題を解決するため、ワーカー側で以下の設定を追加した。

max_parallel_workers_per_gather = 0
pg_strom.nvme_strom_threshold = 1GB

集計クエリを実行してみる(Take.2)

さて、再度集計クエリを実行してみる事にする。

まずは実行計画を確認。

postgres=# explain
select sum(lo_extendedprice*lo_discount) as revenue
from lineorder,date1
where lo_orderdate = d_datekey
and d_year = 1993
and lo_discount between 1 and 3
and lo_quantity < 25;
                                                                       QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=0.00..0.00 rows=0 width=0)
   ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
         Task Count: 32
         Tasks Shown: One of 32
         ->  Task
               Node: host=localhost port=5433 dbname=postgres
               ->  Aggregate  (cost=1007098.32..1007098.33 rows=1 width=32)
                     ->  Custom Scan (GpuPreAgg)  (cost=1007092.71..1007096.28 rows=204 width=8)
                           Reduction: NoGroup
                           Combined GpuJoin: enabled
                           GPU Preference: GPU1 (Tesla P40)
                           ->  Custom Scan (GpuJoin) on lineorder_102040 lineorder  (cost=9114.62..1019939.70 rows=1416481 width=10)
                                 Outer Scan: lineorder_102040 lineorder  (cost=9426.71..1098945.56 rows=9919249 width=14)
                                 Outer Scan Filter: ((lo_discount >= '1'::numeric) AND (lo_discount <= '3'::numeric) AND (lo_quantity < '25'::numeric))
                                 Depth 1: GpuHashJoin  (hash-size: 66.06KB, nrows 9919249...1416481)
                                          HashKeys: lineorder.lo_orderdate
                                          JoinQuals: (lineorder.lo_orderdate = date1.d_datekey)
                                 GPU Preference: GPU1 (Tesla P40)
                                 NVMe-Strom: enabled
                                 ->  Seq Scan on date1_102075 date1  (cost=0.00..78.95 rows=365 width=4)
                                       Filter: (d_year = 1993)
(21 rows)

ワーカー側でのパラレルクエリが消えており、また、今度はNVMe-Strom: enabledの表示が出た。

今度はEXPLAIN ANALYZEで実際にクエリを実行してみる。
実行時間は24.67sで、合計 353GB のテーブルをスキャンした結果としては中々のものである。

postgres=# explain analyze
select sum(lo_extendedprice*lo_discount) as revenue
from lineorder,date1
where lo_orderdate = d_datekey
and d_year = 1993
and lo_discount between 1 and 3
and lo_quantity < 25;
                                                                                  QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=0.00..0.00 rows=0 width=0) (actual time=24666.217..24666.218 rows=1 loops=1)
   ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0) (actual time=24666.174..24666.181 rows=32 loops=1)
         Task Count: 32
         Tasks Shown: One of 32
         ->  Task
               Node: host=localhost port=5433 dbname=postgres
               ->  Aggregate  (cost=1007098.32..1007098.33 rows=1 width=32) (actual time=3569.988..3569.989 rows=1 loops=1)
                     ->  Custom Scan (GpuPreAgg)  (cost=1007092.71..1007096.28 rows=204 width=8) (actual time=3569.953..3569.960 rows=1 loops=1)
                           Reduction: NoGroup
                           Combined GpuJoin: enabled
                           GPU Preference: GPU1 (Tesla P40)
                           ->  Custom Scan (GpuJoin) on lineorder_102040 lineorder  (cost=9114.62..1019939.70 rows=1416481 width=10) (never executed)
                                 Outer Scan: lineorder_102040 lineorder  (cost=9426.71..1098945.56 rows=9919249 width=14) (actual time=261.049..628.315 rows=75214815 loops=1)
                                 Outer Scan Filter: ((lo_discount >= '1'::numeric) AND (lo_discount <= '3'::numeric) AND (lo_quantity < '25'::numeric))
                                 Rows Removed by Outer Scan Filter: 65374058
                                 Depth 1: GpuHashJoin  (hash-size: 66.06KB actual-size: 23.29KB, plan nrows: 9919249...1416481, actual nrows: 9840757...1490844)
                                          HashKeys: lineorder.lo_orderdate
                                          JoinQuals: (lineorder.lo_orderdate = date1.d_datekey)
                                 GPU Preference: GPU1 (Tesla P40)
                                 NVMe-Strom: load=1437612
                                 ->  Seq Scan on date1_102075 date1  (cost=0.00..78.95 rows=365 width=4) (actual time=0.075..0.348 rows=365 loops=1)
                                       Filter: (d_year = 1993)
                                       Rows Removed by Filter: 2191
                   Planning Time: 0.485 ms
                   Execution Time: 3759.495 ms
 Planning Time: 2.875 ms
 Execution Time: 24666.270 ms
(27 rows)

以下は、集計クエリ実行中の iostat の出力。これはいつも通り、I/Oの帯域をほぼ限界まで使い切っている事が分かる。

Device:            tps    MB_read/s    MB_wrtn/s    MB_read    MB_wrtn
nvme0n1       21134.00      2641.50         0.01       5283          0
nvme1n1       21214.00      2641.22         0.00       5282          0
nvme2n1       21128.50      2640.96         0.00       5281          0
nvme6n1       20678.00      2584.75         0.00       5169          0
nvme5n1       20761.50      2584.89         0.01       5169          0
nvme4n1       20683.00      2585.31         0.00       5170          0
nvme7n1       20753.00      2585.35         0.00       5170          0
nvme3n1       21194.50      2639.98         0.00       5279          0
md0           84611.50     10556.17         0.01      21112          0
md1           82911.00     10344.69         0.01      20689          0

結論

PostgreSQL向けスケールアウト拡張であるCitusと、スケールアップ拡張であるPG-Stromを組み合わせて実行できることを確認した。
こういった形で、PostgreSQL向けに設計された周辺ソフトウェアと必要に応じて組み合わせる事ができるのが、他のGPU-DBにはないPG-Stromの強みであろう。

ただし、分散テーブルの分割度合いと、PG-Stromの得意とするデータサイズを見極めて、セグメント数や並列ワーカー数を設定する必要がある。
現実問題として、PG-Stromがシングルノードで処理し切れないレベルの大規模DBという事になると、それなりの体制を組んで事前検証を行うはずなので問題にはならないハズだが。CitusはCitusで、PG-StromはPG-Stromで、それぞれ単独で使ったケースを想定してのデフォルト値設定なので、これは致し方ないところ。

*1:PG-Stromと同じくCustomScan APIを利用している

*2:実際にはこのPCIeホストカードでは10.5GB/s程度が上限っぽいが…。ただし、別の製品では11.5GB/sを記録した事はある。