DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

pg_repack運用の勘所

はじめに

DIGGLEエンジニアのaki0344です。
DIGGLEではPostgreSQLを利用しています。
PostgreSQLは自動バキュームによって適時統計情報が更新されますが、パラメータやユーザの使い方などの要因により、こちらの意図したタイミングでバキュームが実行されない可能性があります。
そこで、自動バキュームが実行されない場合でも一定の性能を保つために、毎日ユーザの利用が少ない夜間にpg_repackを実行することで統計情報の更新を行っています。

reorg.github.io

今回はpg_repackを使用する中で、DIGGLEを利用していただいている企業が増えてきたことで発生してきた問題と、それに対してどのように対処してきたかを書いてみたいと思います。
この記事が、pg_repackを利用中の方、あるいはこれから利用しようと考えている方の一助となれば幸いです。

発生した問題点と解決策

前提として、実装当初は以下のようなコマンドを実行していました。

pg_repack  -j 2 -D

コネクション数はPostgreSQLのコア数に合わせて指定し、60秒経過してもロックが取得できない場合はテーブルの更新をスキップします。
また、すべてのスキーマに対して実行するためスキーマ名は指定していません。

【問題その1】毎回同じスキーマで失敗する

ある日、pg_repackの実行が途中で失敗する事象が発生しました。
統計情報が更新されないのが1日であれば性能にはそれほど影響はないため、翌日に成功すれば問題はありません。
そのため、最初に失敗した際は特に対応はせず、様子を見ることにしました。
ところが、翌日も途中で失敗となりました。
ログを確認すると前日と同じ個所で失敗しているようです。
ログには以下のようなエラーが出力されていました。

ERROR: query failed: ERROR:  type "pk_123456789" already exists

データベースを確認した結果、pg_repackというデータベースに、エラーに出力されている情報が残っていることがわかりました。

sample_database => \dt
              List of relations
 Schema |      Name       | Type  |  Owner   
--------+-----------------+-------+----------
 repack | log_123456789   | table | postgres

sample_database => \d log_123456789   
                                       Table "repack.log_123456789"
 Column |             Type             | Collation | Nullable |                  Default                  
--------+------------------------------+-----------+----------+-------------------------------------------
 id     | bigint                       |           | not null | nextval('log_123456789_id_seq'::regclass)
 pk     | pk_123456789                 |           |          | 
 row    | schema_sample.tables |           |          | 
Indexes:
    "log_123456789_pkey" PRIMARY KEY, btree (id)

本来は統計情報更新後に削除されるはずの一時データが、何らかの契機で残ってしまったようです。

【解決策】pg_repackのデータベースを毎回作り直す

公式ページトラブルシューティングに解決策が記載されていました。

FATALエラーが発生した場合、手動でクリーンアップを行う必要があります。
クリーンアップするには、pg_repackをデータベースから一度削除し、再度登録するだけです。
PostgreSQL 9.1以降では、DROP EXTENSION pg_repack CASCADEをエラーが起きたデータベースで実行し、続いてCREATE EXTENSION pg_repackを実行します。

障害が発生するたびにコマンドを実行するのは手間がかかるため、毎回pg_repackを実行する前にDROPおよびCREATEコマンドを実行するようにしてこの問題を回避しました。

【問題その2】処理が終わらない

DIGGLEではApartmentを利用してマルチテナントを実現しています。

diggle.engineer

PostgreSQLでApartmentを利用する場合、テナント毎にスキーマが作成されます。
一方、pg_repackはスキーマ/テーブル毎に処理を行うため、DIGGLEを利用する企業の増加に比例して、pg_repackの処理時間も増加していくことになります。
また、pg_repackはAmazon ECSのコンテナ上で他の処理とリソースを共有して実行していますが、キャッシュによる空き容量のひっ迫や万が一のメモリリークに備えて、コンテナを定期的に再起動させています。
DIGGLEの利用者が増えることは大変喜ばしいことですが、それによりコンテナ再起動の時刻までにpg_repackが終わらないという問題が発生しました。
pg_repackコマンドでスキーマを指定しない場合、統計情報の更新はスキーマ名の昇順で実行されます。
したがって、毎回途中で処理が止まってしまうと昇順で後半となるスキーマの更新される機会が失われることになります。

【解決策】同時実行処理数を増やした

公式ページのオプション-jに以下の説明があります。

PostgreSQLサーバのCPUコア数およびディスクI/Oに余裕がある場合には、このオプションを利用することでpg_repackの処理を高速化するための有力な手段になりえます。

もともと実装当初から同時実行数は指定していましたが、値の更新は行われていませんでした。
一方、PostgreSQLはユーザの増加に伴いスケールアップしてコア数を増やしていたため、同時実行数はコア数よりも小さい値となっていました。
そこで、同時実行数をコア数と同じ値に変更したところ、全体の処理時間が短縮されるという効果を得られました。
ただし、当然ですが同時実行数を増やすとPostgreSQLに対する負荷も増加します。
他の処理に影響を与えないよう、この対応を行う場合は普段のpg_repack実行時のPostgreSQLのCPU使用率などを確認しながら、段階的に行うことをお勧めします。

【問題その3】まだ処理が終わらない

同時実行数を増やすことでコンテナ再起動の時刻までに処理できるスキーマも増加しましたが、全スキーマに対する処理完了までには至りませんでした。

【解決策】開始時間の前倒し

統計情報の更新が実施されない時間が長くなると、性能が劣化する恐れがあります。
すでに数日間更新が行われていないスキーマが発生してたため、早急に対処する必要がありました。
そこで、応急的な対応としてpg_repackを開始する時刻を早めることにしました。
その結果、コンテナ再起動の時刻までにすべてのスキーマに対して統計情報の更新が実行されるようになりました。

【問題その4】不定期に失敗するようになった

pg_repackの開始時刻を変更してから、処理がすべて成功する日と一部で失敗する日が発生するようになりました。
失敗の発生が不規則で、1日だけ失敗する時や、数日間に渡って失敗が続く日がありました。
調査したところ、PostgreSQL側に原因となるログがありました。

ERROR:  schema "schema_sample" does not exist

DIGGLEでは解約や不要となったデモ環境を削除する際、一旦スキーマを論理削除して一定期間経過後に物理削除しています。
物理削除は毎日自動で行われますが、pg_repackの開始時刻を変更したことにより、スキーマの削除される時刻がpg_repackの開始後となっていました。
一方、pg_repackはコマンド実行時に対象となるスキーマをすべて取得して順に統計情報の更新を行っていくようです。
そのため、すでに削除されたスキーマに対して統計情報の更新をしようとして失敗となっていました。
また、上記エラーが発生するとDBとのセッションが切れてしまうようで、失敗となったスキーマ以降の全スキーマに対する処理が失敗となっていました。

【解決策】スキーマ削除はpg_repack開始前に終わらせる

毎日実行されるスキーマの物理削除処理を、pg_repack開始時刻よりも前に実行するようスケジュールを変更しました。
これにより、pg_repackの実行中にスキーマが変化することは無くなり、エラーは発生しなくなりました。

【問題その5】リトライ時に最初からやり直しになる

pg_repackの失敗は性能の劣化につながるため、何らかの要因により失敗した場合に備えてリトライを行うように設定していました。
pg_repack自体は処理が途中で失敗しても、どこまで実行したかという情報は保持していません。
また、各テーブルに対して統計情報の更新が必要か(統計情報が古くなっているか)の判断は行わず、指定された全スキーマに対して処理を実行します。
そのため、リトライ処理でも最初から処理をやり直すことになり、長時間動き続けることになります。
DIGGLEではアクティブユーザの少ない夜間にpg_repackを実行していましたが、リトライが発生するとユーザが利用を始める時間にもpg_repackが動き続け、PostgreSQLのリソースを一部占有し続けるようになりました。

【解決策】どこまで実施したか保持して、リトライ時は続きから実施する

pg_repackの機能では賄えないため、呼び出し元で対処を行いました。
初回実行時、メモリ上にスキーマ一覧を保持し、pg_repackコマンドで各スキーマを指定して実行します。

pg_repack  -j 4 -D -c schema_sample

実行したスキーマはメモリ上から削除します。
リトライ時はメモリに残っているスキーマに対してのみpg_repackを実行します。
これにより、リトライが実行された場合でもユーザが本格的に利用を始める時間帯よりも前にすべてのスキーマに対して統計情報の更新が実行されるようになりました。

今後の課題

現状のテナントに対する統計情報の更新は想定時間内に完了するようになりましたが、DIGGLEはさらなる成長を目指しています。
契約数が増加するとそれに比例してスキーマも増えるため、統計情報の更新も現在より時間がかかることになります。
今後はPostgreSQLのリソースとの兼ね合いを見ながら、並列実行などの対処も考えていく必要があると考えています。