シングルノードのCitusをDockerでシャーディングしてみる

Azure

この記事は PostgreSQL Advent Calendar 2021 7 日目のエントリです。

Citusって何?

CitusはPostgreSQLをシャーディングしてスケールアウト出来るようにするPostgreSQLのExtensionです。例えば以下の”customer_data”のようなテーブルがあるとして、

user_idaccountaddress
1hashimotosotaro青森県新島村麹町32丁目11番8号 東和町シティ729
2manabu89福井県鴨川市蟇沼23丁目6番20号
3hiroshisato茨城県八王子市横林13丁目15番3号 コーポ芝大門474
4inouemai長野県練馬区鳥越33丁目22番8号
customer_data

“user_id”をキーとして2つの分散テーブルに分けます。

user_idaccountaddress
1hashimotosotaro青森県新島村麹町32丁目11番8号 東和町シティ729
3hiroshisato茨城県八王子市横林13丁目15番3号 コーポ芝大門474
customer_data_1001
user_idaccountaddress
2manabu89福井県鴨川市蟇沼23丁目6番20号
4inouemai長野県練馬区鳥越33丁目22番8号
customer_data_1002

この分散テーブルを2つ以上のWorker Nodeに格納し、「何をシャーディングのキーにするか」「どのシャーディングキーを持つレコードをいずれのWorker Nodeに格納するか」をCoordinator Nodeが「調停」するようにすることで、PostgreSQLをスケールアウトします。

psqlやアプリケーションなどのPostgreSQLのクライアントはCoordinator Nodeに接続し、通常のPostgreSQLと同じようにクエリを実行することになります。シャーディングの管理はCoordinatorにお任せです。

なお、OSSで開発されているCitusと関連するいくつかの機能拡張がインストールされている点を除けば、Coordinator / Workerは通常のPostgreSQLと全く同じなので、

  1. Linuxをインストールする
  2. PostgreSQLをインストールする
  3. Citusと関連機能拡張をインストールする
  4. 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