技術戦略部 SREグループの古越です。
MoTで開発しているサービスの多くはAurora PostgreSQLを利用しています。一部サービスにはデータが蓄積し、肥大化していくテーブルの管理が課題になっています。今回は開発者からの要望で新規サービスの幾つかにパーティショニングを導入する事になりました。SREグループでパーティショニング導入までに検証したことを検討編と実践編という2つの記事で紹介できればと思います。
検討編では事前検証として行った事のまとめとして、導入検討の段階で注意するべきポイントを紹介いたします。実践編では導入するために行った具体的な施策について紹介してきます。
PostgreSQLに限らずDB一般に言えることですが、ログや履歴が含まれるテーブルはサービス成熟とともに徐々にデータが蓄積していきます。蓄積しすぎて肥大化すると簡単なクエリの応答が遅くなったり、削除が難しくなったり、INDEX付与の時間や容量が課題になるなど様々な難点が出てきます。
具体例を交えて言うと、タクシーアプリGOの決済履歴を格納するテーブルが肥大化してしまい、管理が難しいという状況になっていました。
具体例: 決済履歴テーブルの特徴
このテーブルの管理のため特定時刻のレコードを検索したり、古いデータを削除するという作業が必要でした。しかし肥大化したことでクエリ応答に時間がかかりDB負荷も高まってしまうという事になり、気軽に実行できなくなっていました。
テーブルを実際に運用していたのは決済基盤の開発グループでしたが、SREグループに依頼があり「このような課題の解決策として、新しいサービスでパーティショニングを導入できないか?」 という相談を受けました。そこでSREグループで開発用の足回り整備やAurora PostgreSQLへ導入するまでの事前検証などを行う事になりました。
私自身そこまでPostgreSQLについて詳しく無い所から開始したため、調べた事と行った事の総括という所も含めて紹介できればと思います。
話を進める前に、パーティショニングの基本的なところをおさらいとして触れていきます。
PostgreSQLでは大きなテーブル管理の課題解決策としてテーブルパーティショニングが標準機能として用意されています。パーティショニングについての概要はこれから触れていきますが、初めて知る人には富士通さんの記事がお勧めです。
テーブルパーティショニングは
の2種類のテーブルを作ることで実現されます。
作った後は親テーブルにSQLクエリを投げると子テーブルにルーティングされる動きとイメージすればだいたい合っています。
パーティショニング 利用イメージ
データの実体が扱いやすい単位で子テーブルに自動格納されるため、管理上の恩恵が幾つか有ります。例えば履歴を格納するテーブルを1ヶ月単位でパーティション分割したとすると以下のメリットが得られます。
運用中のリスク軽減やコスト削減という所に役立ちます。
パーティションと一口に言っても分割方式によって種類があります。データの特徴に合わせて適切なパーティションを選択する必要があるので、種類と特徴については理解しておくと良いと思います。
PostgreSQL15時点で3種類の分割方法が用意されています
今回は履歴を格納するテーブルが課題になっていたため、レンジパーティショニングを前提として導入検証などを行っています。
パーティショニングを構成する親子テーブルはそれぞれ以下のようなDDLで定義できます。
取引履歴を格納するようなテーブルをサンプルとして見ていきましょう。
/* Create Parent Table */
CREATE TABLE payment_transactions
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
uuid uuid NOT NULL,
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
-- 他カラム省略
PRIMARY KEY (id, created_at),
UNIQUE (uuid, created_at)
) PARTITION BY RANGE (created_at) WITHOUT OIDS;
/* Create Partition */
CREATE TABLE payment_transactions_p2022_11 PARTITION OF payment_transactions
FOR VALUES FROM ('2022-11-01'::timestamp) TO ('2022-12-01'::timestamp);
CREATE TABLE payment_transactions_p2022_12 PARTITION OF payment_transactions
FOR VALUES FROM ('2022-12-01'::timestamp) TO ('2023-01-01'::timestamp);
/* Create Default Partition */
CREATE TABLE payment_transactions_default PARTITION OF payment_transactions
DEFAULT;
親テーブルの特徴
子テーブル(パーティション)の特徴
ドキュメントにも記載がありますが、通常テーブルを後でパーティションテーブルに変更したり、逆にパーティションテーブルを通常テーブルに戻すということは出来ません。パーティションキーを途中変更したい場合もテーブルを改める必要があります。
SQLクエリ上でも PARTITION BY でパーティションを設定出来るのは CREATE TABLE だけに制限されており、ALTER TABLE で後から変更することは出来ません。後から変更可能なのはパーティションのアタッチ、デタッチという程度です。
パーティションを後から変更するのは難しい作業が必要になるため、入念に設計する必要が有ります。簡単な解説としては以上です。
事前検証していく中で見つかった要注意点について解説していきます。
基本的にデータを格納する前に十分なパーティションを追加しておくのが好ましいです。時系列のRANGEパーティションを設定する場合は、時間経過とともにパーティションの追加削除といったメンテナンス作業を継続的に行う必要があります。
というような運用をしていく必要があります。運用段階ではパーティション追加削除の自動化や省力化が課題になります。
パーティションの追加削除を自動化する方法として pg_partmanという拡張機能を用いる方法が有り、今回採用する事にしています。具体的には次回のブログで解説いたします。
例にも記載しましたが、PostgreSQL 11以後に追加された機能でdefaultパーティションという取りこぼしを防ぐ一時的なテーブルを作る事ができます。defaultという名前になっていますが、RANGEパーティションの場合はテンポラリ領域に近い一時的な置き場所とイメージしたほうが良いです。
注意すべき挙動として、「defaultパーティションに格納したレコードと一致する条件で新しいパーティションを作ろうとするとエラーになる」という動きが有ります。
参考 https://www.enterprisedb.com/blog/default-partition-adopting-odds
具体的に例を上げると、サンプルに書いたパーティションテーブルを放置したとすると以下のようなシナリオが考えられます。
直接的な害は少ないですが、リカバリとしてdefaultパーティションから別のパーティションを作って再配置する作業が必要になります。再配置に必要なデータ量が多いとLockが長めに掛かったりDB負荷が高まったりするため運用上好ましくありません。
このような状態を放っておくとdefaultパーティションしか利用してない状態になり、分離することに意味のあるパーティショニングのメリットが失われます。
取りこぼしを防ぐためにdefaultパーティションを作っておくのが安全ではあるのですが、可能な限りdefaultパーティションにデータが蓄積しないように運用設計する必要があります。
パーティションテーブルにカラムを追加する場合は親テーブルにカラムを追加すれば子テーブルに継承されるため問題はありません。しかし、INDEX付与はLockのかかり方に注意が必要です。
通常PostgreSQLでINDEX付与する際はテーブルに対してShareLockがかかります。ShareLockはINSERT/UPDATE/DELETEなどの書き込みクエリと競合するLockであるため、INDEX付与完了までは書き込みクエリがLock待ちで滞留します。
このLock待ちを回避するために CREAET INDEXに CONCURRENTLY というオプションが用意されています。オプションを付けるとLockのレベルが落ちますので、書き込みを滞留させることなくINDEX付与することが可能になります。
しかし、PostgreSQL15時点ではパーティション化したテーブルにCONCURRENTLYオプション有りでINDEX付与することが出来ません。
実際に親テーブルに対して実行すると以下のようなエラーが出力されます。
> CREATE INDEX CONCURRENTLY payment_transactions_id_idx ON payment_transactions(id);
ERROR: cannot create index on partitioned table "payment_transactions" concurrently
このような制約があるため、親テーブルに対してはそのままCREATE INDEXを実行する必要が有ります。しかし、そのまま実行するとINDEX付与中は親子テーブルの全てにShareLockがかかるという挙動になってしまいます。
CREATE INDEX CONCURRENTLYの未サポート部分については公式ドキュメント上に回避策が提示されており、2段階のINDEX付与作業をすることでShareLockの時間を短縮することが出来ます。
Lock時間短縮手順
クエリの実例としては以下のようになります。
-- 1. add index child
CREATE INDEX CONCURRENTLY payment_transactions_p2022_11_id_idx ON payment_transactions_p2022_11(id);
CREATE INDEX CONCURRENTLY payment_transactions_p2022_12_id_idx ON payment_transactions_p2022_12(id);
CREATE INDEX CONCURRENTLY payment_transactions_default_id_idx ON payment_transactions_default(id);
-- 2. add index parent
CREATE INDEX payment_transactions_id_idx ON payment_transactions(id);
はじめに実行する子テーブルに対するINDEX付与は少し時間がかかりますが、CONCURRENTLYオプションが有効なためLock影響はほぼ有りません。
2つめの親テーブルにINDEX付与する箇所は、子テーブルに対するINDEX付与が終わっていれば一瞬で完了します。
INDEX付与検証
導入前に上記作業の検証を行っています。
この状態で2段階のINDEX付与を行いましたが、結果は親子テーブルともにShareLockを観測することなく終わるというものでした。
ShareLockは発生していたかもしれませんが、0.5秒未満の軽微なものに抑えられていたようです。
本番環境で行うINDEX付与には上記のような2ステップの手順で行うように運用設計をするのが良いかと思います。INDEX周りの情報も公式ドキュメントに記載されていますので、本番影響を気にする場合は一読することをお勧めします。
参考: https://www.postgresql.org/docs/current/sql-createindex.html
パーティション化したテーブルでは実体が子テーブルに格納されます。何もせず論理レプリケーションと組み合わせると、子テーブルに対する書き込みイベントが延々と発行されます。
MoTの幾つかのサービスでは Debezium connector for PostgreSQLとGCP PubSubを使い、CDC方式で分析基盤へデータを転送しています。デフォルトのまま連携させるとDebeziumから伝わるCDCイベントは子テーブルに対する書き込みイベントになってしまいます。オリジナルDBと同じような見え方にするためには分析基盤側でテーブルを集約させるなどの別の仕組みが必要になることが予見されました。
回避策を探したところ、以下のようにpublicationに publish_via_partition_root というオプションを付ける事が有効ということが分かりました。
CREATE PUBLICATION pub FOR ALL TABLES WITH (publish_via_partition_root = true);
このオプションを入れるとパーティションの子テーブルではなく親テーブルに対するイベントとして論理レプリケーションのイベントが発行されます。
上記のように作成したpublicationをDebeziumが参照するよう設定したところ分析基盤側の不要な作業を回避することが出来ました。
今回はDebeziumに関する部分でしか検証していませんが、一般的な論理レプリケーションにも活きる重要なポイントになるかと思います。
参考:
ここまで読んだ方には言うまでも無いかも知れませんが、すべてのテーブルに導入するのはお勧めしません。
例えば
というようなテーブルはパーティショニングによるメリットがほぼ有りません。
複雑になるデメリットが勝るので、避けたほうが良いでしょう。
感覚値になりますが、数年で1億レコード規模のテーブルに成長する見込みであればパーティショニングを検討すると良いかと思います。
これから導入を検討する方向けに、検討フェーズで確認するべきポイントをまとめます。
後からの変更が辛いポイントが多いため、十分に事前検証することをお勧めします。
導入時点のAurora PostgreSQL最新安定版は14でしたので本記事は14を前提としていますが、パーティショニングに関してはPostgreSQL 15に大きな変更はありません。15でも同様と考えて良さそうです。
基礎知識と注意点を諸々述べて来ましたが、導入する前には以下を重点的に検証していました。
この内容については次のブログで紹介しようと思います。
次回: Aurora PostgreSQLでテーブルパーティショニングを導入した話 【実践編】
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!