この記事は PostgreSQL Advent Calendar 2021 7 日目のエントリです。
Citusって何?
CitusはPostgreSQLをシャーディングしてスケールアウト出来るようにするPostgreSQLのExtensionです。例えば以下の”customer_data”のようなテーブルがあるとして、
user_id | account | address |
1 | hashimotosotaro | 青森県新島村麹町32丁目11番8号 東和町シティ729 |
2 | manabu89 | 福井県鴨川市蟇沼23丁目6番20号 |
3 | hiroshisato | 茨城県八王子市横林13丁目15番3号 コーポ芝大門474 |
4 | inouemai | 長野県練馬区鳥越33丁目22番8号 |
… | … | … |
“user_id”をキーとして2つの分散テーブルに分けます。
user_id | account | address |
1 | hashimotosotaro | 青森県新島村麹町32丁目11番8号 東和町シティ729 |
3 | hiroshisato | 茨城県八王子市横林13丁目15番3号 コーポ芝大門474 |
… | … | … |
user_id | account | address |
2 | manabu89 | 福井県鴨川市蟇沼23丁目6番20号 |
4 | inouemai | 長野県練馬区鳥越33丁目22番8号 |
… | … | … |
この分散テーブルを2つ以上のWorker Nodeに格納し、「何をシャーディングのキーにするか」「どのシャーディングキーを持つレコードをいずれのWorker Nodeに格納するか」をCoordinator Nodeが「調停」するようにすることで、PostgreSQLをスケールアウトします。
psqlやアプリケーションなどのPostgreSQLのクライアントはCoordinator Nodeに接続し、通常のPostgreSQLと同じようにクエリを実行することになります。シャーディングの管理はCoordinatorにお任せです。
なお、OSSで開発されているCitusと関連するいくつかの機能拡張がインストールされている点を除けば、Coordinator / Workerは通常のPostgreSQLと全く同じなので、
- Linuxをインストールする
- PostgreSQLをインストールする
- Citusと関連機能拡張をインストールする
- CoordinatorにWorkerを登録する
という手順をCoordinator / Workerの台数分だけ繰り返せば自分でも構築可能です。ま、面倒なので、本番やお仕事ならManagedサービスであるAzure Database for PostgreSQL Hyperscale (Citus)を使って下さいw
シングルノードのHyperscale (Citus)
実は現在のAzure Database for PostgreSQL Hyperscale (Citus) ではシングルノードのHyperscaleがデフォルトになっています。
Azure PortalからPostgreSQLを探すとデプロイオプションが4つあります。ここからHyperscale (Citus) を選びます。(Flexible Serverは2021年11月30日(日本時間:12月1日)にAzure東日本・西日本でもGA(一般提供)されました。)
次の画面で[サーバーグループの構成]をクリックします。
すると、[Basic] Tierが選択されています。このBasic Tierというのは、Coordinator / Worker Nodeを1つのVMに押し込めるもので、vCPUは2〜8、メモリは32GB、ストレージは512GiBまでスケールアップ出来ますが、あくまで開発・テスト用です。というのも、Citusのメリットはスケールアウト、つまりvCPUを線形に増やしつつ、ストレージ(Premium SSD)の数も増やすことで並列に入出力を行うことにあるからです。
が。ミニマム構成でも約2.2万円/月のコストがかかるのはイヤ、もーっと手軽にCitusを試したい!分かります。やり方説明しますw
みんな大好きDocker
まずDockerが動く環境を用意します。筆者の手元はmacOS (Intel版Mac mini 2018) ですが、他の環境でも同じように動くはずです。
Coordinator Nodeを起動
Docker環境が準備できたら、まずはCoordinator Nodeを動かしてみましょう。ターミナル.appを起動し、
% export PGPASSWORD='mypassword'
% docker run -d --name citus_coordinator -p 5432:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
とします。最初のexportで$PGPASSWORD
を設定しておくとlibpgが利用してくれるので、後でクライアントとして用いるpsqlコマンドでパスワードを入力する必要がなくなります。Dockerイメージをpullして起動すれば、docker psコマンドで確認できるはずです。
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d63ef017c7d4 citusdata/citus "docker-entrypoint.s…" 10 seconds ago Up 8 seconds (healthy) 0.0.0.0:5432->5432/tcp citus_coordinator
psqlコマンドがインストールされていなければ、以下のようにインストールしても良いですし、
# macOS
% brew install postgresql
# CentOS/RHEL
$ sudo dnf install postgresql
# Ubuntu
$ sudo apt-get install postgresql-client
# OS共通
% psql -h localhost -p 5432 -U postgres
docker execでコンテナ内のpsqlコマンドを利用しても良いです。
% docker exec -it citus_coordinator psql -U postgres
接続できたら、機能拡張を確認してみましょう。citus extensionがインストールされていることが分かります。
postgres=# \dx
List of installed extensions
Name | Version | Schema | Description
---------+---------+------------+------------------------------
citus | 10.2-4 | pg_catalog | Citus distributed database
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
(2 rows)
postgres=# \q
Worker Nodeを起動
Coordinator Nodeが動いたので、次にWorker Nodeを2つ起動します。
% docker run -d --name citus_worker_1 -p 5501:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
% docker run -d --name citus_worker_2 -p 5502:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
起動しているか確認しましょう。
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4e7f488a5ba3 citusdata/citus "docker-entrypoint.s…" 22 seconds ago Up 22 seconds (healthy) 0.0.0.0:5502->5432/tcp citus_worker_2
4d6b5e3b37b5 citusdata/citus "docker-entrypoint.s…" 28 seconds ago Up 28 seconds (healthy) 0.0.0.0:5501->5432/tcp citus_worker_1
d63ef017c7d4 citusdata/citus "docker-entrypoint.s…" 22 minutes ago Up 22 minutes (healthy) 0.0.0.0:5432->5432/tcp citus_coordinator
これで以下の構成のCitusクラスタを作成する準備が整いました。
Citusクラスタとして設定する
Coordinator Nodeにpsqlで接続し、以下の設定をします。ここで、"192.168.1.23"
は筆者の手元のMac miniのIPアドレスです。
postgres=# SELECT citus_set_coordinator_host('192.168.1.23', 5432);
citus_set_coordinator_host
----------------------------
(1 row)
postgres=# SELECT citus_add_node('192.168.1.23', 5501);
citus_add_node
----------------
2
(1 row)
postgres=# SELECT citus_add_node('192.168.1.23', 5502);
citus_add_node
----------------
3
(1 row)
拍子抜けするぐらい簡単ですね。
テストデータを投入する
データを作成
適当にPythonか何かで作りましょう。
#!/usr/bin/env python3
from faker import Faker
import csv
import random
fake = Faker('ja_JP')
with open('./dummy_data.csv', 'w') as f:
res = [[i, fake.user_name(), fake.name(), random.choice(['M', 'F', 'N']), fake.address(), fake.email(), fake.date_of_birth()] for i in range(1, 10001)]
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
writer.writerows(res)
面倒な人は以下から作成済みデータをダウンロードしてもOKです。
分散テーブルを作成する
作成したデータを流し込むテーブルを作成します。2つ目のコマンド・create_dstributed_table()
がCitus固有のUDFです。1つ目の引数で分散するテーブルを、2つ目の引数でシャードキーを指定します。
postgres=# CREATE TABLE users (id BIGSERIAL, username TEXT, name TEXT, sex VARCHAR(1), address TEXT, email TEXT, birthdate DATE);
CREATE TABLE
postgres=# SELECT create_distributed_table('users', 'id');
create_distributed_table
--------------------------
(1 row)
postgres=# \q
CSVからデータを流し込む
ここではCSVファイルを読み込む必要があるので、docker execではなくpsqlコマンドから実行します。
% psql -h localhost -p 5432 -U postgres -c '\COPY users FROM dummy_data.csv WITH CSV;'
COPY 10000
データを確認する
Coordinator Nodeを確認する
Coordinator Nodeにpsqlで接続して確認します。
% psql -h localhost -p 5432 -U postgres
psql (14.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=# SELECT COUNT(*) FROM users;
count
-------
10000
(1 row)
postgres=# SELECT * FROM users ORDER BY id LIMIT 10;
id | username | name | sex | address | email | birthdate
----+--------------+-------------+-----+--------------------------------------------------------+-----------------------------+------------
1 | katoshota | 林 里佳 | M | 和歌山県横浜市旭区太田ヶ谷30丁目6番6号 下吉羽パレス973 | yoko01@example.net | 1935-04-13
2 | gnakajima | 山本 知実 | N | 滋賀県香取市前弥六22丁目2番6号 | hanako22@example.com | 1944-12-31
3 | akemiinoue | 鈴木 直人 | M | 長崎県東久留米市戸塚町2丁目22番15号 小入アーバン428 | matsudaakemi@example.net | 1952-12-12
4 | wsaito | 佐藤 直樹 | F | 山口県印旛郡栄町芝大門20丁目22番20号 ハイツ勝どき569 | jkimura@example.net | 1915-07-09
5 | kyosukeinoue | 山本 健一 | F | 高知県横浜市中区南郷屋12丁目3番7号 パレス百村421 | miturusato@example.net | 1980-07-17
6 | watanabetaro | 山崎 明美 | N | 富山県日野市所野8丁目23番12号 パレス西関宿121 | takuma01@example.com | 1971-09-04
7 | manabuikeda | 佐藤 英樹 | F | 千葉県江戸川区卯の里25丁目14番4号 南赤田シャルム674 | bnakamura@example.net | 1959-04-04
8 | minoru05 | 小林 結衣 | M | 群馬県白井市平河町42丁目26番14号 パレス戸山295 | kyosuke23@example.org | 1944-10-11
9 | taichiito | 斎藤 聡太郎 | N | 高知県西多摩郡奥多摩町押上32丁目1番17号 シティ藤金981 | harukanishimura@example.com | 1974-01-02
10 | yumikoabe | 石井 聡太郎 | N | 三重県世田谷区日光28丁目25番20号 クレスト花川戸053 | sito@example.org | 1999-03-29
(10 rows)
postgres=# \q
確かに入ってますね。
Worker Nodeを確認する
Citusクラスタがどのようにデータを格納しているのかを確認するには、Worker Nodeのデータを覗いてみるのが手っ取り早いです。Worker Node 1に接続します。
% psql -h localhost -p 5501 -U postgres
psql (14.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=# select count(*) from users;
ERROR: relation "users" does not exist
LINE 1: select count(*) from users;
"users"
というテーブルは存在しない、という怒られが発生しましたね。テーブル一覧から探して見てみましょう。
postgres=# \dt;
List of relations
Schema | Name | Type | Owner
--------+--------------+-------+----------
public | users_102040 | table | postgres
public | users_102042 | table | postgres
public | users_102044 | table | postgres
public | users_102046 | table | postgres
public | users_102048 | table | postgres
public | users_102050 | table | postgres
public | users_102052 | table | postgres
public | users_102054 | table | postgres
public | users_102056 | table | postgres
public | users_102058 | table | postgres
public | users_102060 | table | postgres
public | users_102062 | table | postgres
public | users_102064 | table | postgres
public | users_102066 | table | postgres
public | users_102068 | table | postgres
public | users_102070 | table | postgres
(16 rows)
postgres=# SELECT * FROM users_102040 ORDER BY id LIMIT 10;
id | username | name | sex | address | email | birthdate
-----+--------------+-----------+-----+-----------------------------------------------------------+----------------------------+------------
8 | minoru05 | 小林 結衣 | M | 群馬県白井市平河町42丁目26番14号 パレス戸山295 | kyosuke23@example.org | 1944-10-11
20 | kumiko09 | 吉田 和也 | M | 和歌山県香取郡神崎町脚折町14丁目11番6号 | kazuya97@example.net | 1933-07-26
60 | atsushi97 | 青木 篤司 | N | 大分県鴨川市方京27丁目7番15号 クレスト美原町880 | fhasegawa@example.org | 1984-02-05
132 | yui32 | 伊藤 拓真 | M | 岐阜県江戸川区外神田42丁目4番5号 パーク浅草653 | momoko57@example.com | 1990-05-17
138 | gotoryosuke | 岡本 康弘 | F | 山梨県八王子市東神田11丁目11番16号 コート卯の里362 | kyosukesuzuki@example.net | 2021-04-29
139 | yoshidaosamu | 渡辺 花子 | F | 長崎県立川市竜泉10丁目2番5号 コーポ下宇和田388 | yoichi78@example.org | 1927-07-25
146 | qfujii | 吉田 知実 | N | 三重県目黒区明石町4丁目22番16号 三ノ輪コーポ277 | itonaoki@example.org | 1934-11-20
280 | maayakato | 阿部 健一 | M | 沖縄県西多摩郡奥多摩町横林26丁目4番17号 シャルム南郷屋004 | yamashitaasuka@example.net | 1942-05-13
298 | suzukishota | 高橋 直子 | N | 長野県いすみ市一ツ橋30丁目16番4号 平須賀アーバン446 | rika25@example.org | 2021-10-24
301 | maikobayashi | 木村 里佳 | M | 埼玉県江戸川区雷門29丁目27番10号 台場コート340 | mikakosasaki@example.net | 1987-02-24
(10 rows)
良かった、データが格納されてないWorker Nodeなんて無かったんだ。。。
どのようにシャードされているのか
デフォルトではCitusはシャードを32に分割するため(citus.shard_countパラメータ)、デプロイしたWorker Nodeが2つなので16シャード/Nodeで格納されていることが分かります。このことはCoordinator Nodeでももちろん確認できます。
% psql -h localhost -p 5432 -U postgres
psql (14.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=# SELECT * FROM pg_dist_shard;
logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue
--------------+---------+--------------+---------------+---------------
users | 102040 | t | -2147483648 | -2013265921
users | 102041 | t | -2013265920 | -1879048193
users | 102042 | t | -1879048192 | -1744830465
users | 102043 | t | -1744830464 | -1610612737
users | 102044 | t | -1610612736 | -1476395009
users | 102045 | t | -1476395008 | -1342177281
users | 102046 | t | -1342177280 | -1207959553
users | 102047 | t | -1207959552 | -1073741825
users | 102048 | t | -1073741824 | -939524097
users | 102049 | t | -939524096 | -805306369
users | 102050 | t | -805306368 | -671088641
users | 102051 | t | -671088640 | -536870913
users | 102052 | t | -536870912 | -402653185
users | 102053 | t | -402653184 | -268435457
users | 102054 | t | -268435456 | -134217729
users | 102055 | t | -134217728 | -1
users | 102056 | t | 0 | 134217727
users | 102057 | t | 134217728 | 268435455
users | 102058 | t | 268435456 | 402653183
users | 102059 | t | 402653184 | 536870911
users | 102060 | t | 536870912 | 671088639
users | 102061 | t | 671088640 | 805306367
users | 102062 | t | 805306368 | 939524095
users | 102063 | t | 939524096 | 1073741823
users | 102064 | t | 1073741824 | 1207959551
users | 102065 | t | 1207959552 | 1342177279
users | 102066 | t | 1342177280 | 1476395007
users | 102067 | t | 1476395008 | 1610612735
users | 102068 | t | 1610612736 | 1744830463
users | 102069 | t | 1744830464 | 1879048191
users | 102070 | t | 1879048192 | 2013265919
users | 102071 | t | 2013265920 | 2147483647
(32 rows)
このシャードがどのように配置されるかは、シャードの"groupid"
で分かります。
postgres=# SELECT * FROM pg_dist_node;
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
--------+---------+--------------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
1 | 0 | 192.168.1.23 | 5432 | default | t | t | primary | default | t | f
2 | 1 | 192.168.1.23 | 5501 | default | f | t | primary | default | f | t
3 | 2 | 192.168.1.23 | 5502 | default | f | t | primary | default | f | t
(3 rows)
postgres=# SELECT * FROM pg_dist_placement;
placementid | shardid | shardstate | shardlength | groupid
-------------+---------+------------+-------------+---------
33 | 102040 | 1 | 0 | 1
34 | 102041 | 1 | 0 | 2
35 | 102042 | 1 | 0 | 1
36 | 102043 | 1 | 0 | 2
37 | 102044 | 1 | 0 | 1
38 | 102045 | 1 | 0 | 2
39 | 102046 | 1 | 0 | 1
40 | 102047 | 1 | 0 | 2
41 | 102048 | 1 | 0 | 1
42 | 102049 | 1 | 0 | 2
43 | 102050 | 1 | 0 | 1
44 | 102051 | 1 | 0 | 2
45 | 102052 | 1 | 0 | 1
46 | 102053 | 1 | 0 | 2
47 | 102054 | 1 | 0 | 1
48 | 102055 | 1 | 0 | 2
49 | 102056 | 1 | 0 | 1
50 | 102057 | 1 | 0 | 2
51 | 102058 | 1 | 0 | 1
52 | 102059 | 1 | 0 | 2
53 | 102060 | 1 | 0 | 1
54 | 102061 | 1 | 0 | 2
55 | 102062 | 1 | 0 | 1
56 | 102063 | 1 | 0 | 2
57 | 102064 | 1 | 0 | 1
58 | 102065 | 1 | 0 | 2
59 | 102066 | 1 | 0 | 1
60 | 102067 | 1 | 0 | 2
61 | 102068 | 1 | 0 | 1
62 | 102069 | 1 | 0 | 2
63 | 102070 | 1 | 0 | 1
64 | 102071 | 1 | 0 | 2
(32 rows)
Citusが完全にPostgreSQLだけで構成されていることが分かりますね。つまりメタデータもテーブルとして格納されている、わけです。
まとめ
ペタバイトクラスのデータをPostgreSQLに格納することも出来るCitusですが、とりあえず数十TBぐらいからどうでしょう?
とはいえ、Citusがどういうものなのかを掴むのにDockerでお手軽に構築して遊んでみるのが良いと思います。本記事が一助になりますように。
追記
スクリプトにしておくと数秒で立ち上がって便利だった。for macOS。
#!/bin/bash
ipaddr=$(ipconfig getifaddr en0)
docker rm -f citus_coordinator
docker rm -f citus_worker_1
docker rm -f citus_worker_2
export PGPASSWORD='mypassword'
docker run -d --name citus_coordinator -p 5432:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
docker run -d --name citus_worker_1 -p 5501:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
docker run -d --name citus_worker_2 -p 5502:5432 -e POSTGRES_PASSWORD=$PGPASSWORD citusdata/citus
# wait for the coordinator running up
until [ "`docker inspect -f {{.State.Health.Status}} citus_coordinator`" == "healthy" ]; do
sleep 0.1
done
docker exec -it citus_coordinator psql -U postgres -c "SELECT citus_set_coordinator_host('${ipaddr}', 5432);"
docker exec -it citus_coordinator psql -U postgres -c "SELECT citus_add_node('${ipaddr}', 5501);"
docker exec -it citus_coordinator psql -U postgres -c "SELECT citus_add_node('${ipaddr}', 5502);"
docker exec -it citus_coordinator psql -U postgres