Oisix ra daichi Creator's Blog(オイシックス・ラ・大地クリエイターズブログ)

オイシックス・ラ・大地株式会社のエンジニア・デザイナーが執筆している公式ブログです。

オンプレPostgreSQLをAmazon Aurora PostgreSQLへ移行しようとしてINDEX作成にハマりました

SREの林 aka もりはやです。


先日オンプレで稼働していたとあるPostgreSQLのDBを、Amazon Aurora PostgreSQL(以後はAurora)に移行を行ったところ CREATE INDEX が以下のエラーで作成できない問題に苦労させられたため、解決策と合わせて公開します。


これがベストな対応かは自信がないため、より良い方法をご存知の方はお気軽に教えてください。

以下はDBのリストア中に CREATE INDEX で発生したエラーです。

pg_restore: error: could not execute query: ERROR: could not write to file "base/pgsql_tmp/pgsql_tmp19647.47.fileset/1.2": No space left on device CONTEXT: parallel worker Command was: CREATE INDEX history_1 ON public.history USING btree (itemid, clock);

TL;DR

シンプルに経緯と課題と解決策を述べると以下です。

  • 経緯
    • オンプレのPostgreSQL13を利用するZabbix Serverをマイグレーションしたかった
    • Zabbixシステムを丸ごと移行するためOSとミドルを以下の様にアップグレード

      • OS: CentOS ? -> Ubuntu 22:04 *1
      • AP: Zabbix Server 5.0 -> 6.0
      • DB: PostgreSQL 13 -> 15
    • 移行手段はpg_dump -Fc <DB> で取得したダンプファイルを psql でAuroraへ流し込んだ

  • 課題
    • psql実施中にエラー ERROR: could not write to file "base/pgsql_tmp/pgsql_tmp19647.26.fileset/0.2": No space left on device
    • 原因はどうやら CREATE INDEX 中に使われるのが2つあるAuroraのボリュームのうちの、自動拡張されるクラスターボリュームではなく、一時ストレージと呼ばれる領域が溢れている
    • PostgreSQLの仕様で、大規模テーブルのINDEX作成時には一時領域を利用しているようだ*2
  • 対策
    • 対策として以下の2つを行なって無事に CREATE INDEX を行えた
      • DBインスタンスクラスのスケールアップ
      • psqlで CREATE INDEX を実行するセッションで SET work_mem = 'xxxGB'; ※maintenance_work_mem が適切だった

詳細

以下からは詳細になります。

経緯

当社はモニタリングをDatadogを主体に利用していますが、ネットワーク機器や一部のオンプレミス機器に対してはZabbixを活用してきました。

そのZabbixが動作するOSのバージョンが年季が入っており、かつ稼働環境も当社のメイン基盤であるAWSではないことから、AWSへの移行を開始しました。*3


構成はシンプルで、Zabbix Serverおよび、DBのPostgreSQLもVMで動作していました。

オンプレPostgreSQLからのバックアップはS3に日次で取得されている

Zabbix DBであるPostgreSQLのデータは、バックアップを目的として日次で pg_dump によって取得し、S3へアップロードを行っています。

以下はPostgreSQLのバックアップシェルから主要コマンド部分を抜粋したものです。 pg_dump で出力したDBを gzip で固めて、それを s3 でアップロードするだけのシンプルなスクリプトです。*4

# Data dump
$ pg_dump -Fc zabbix | gzip > /mnt/backup/pg_dump.gz;date

# Upload data to S3
$ aws s3 rm s3://hogehoge-backet/pg_dump.gz && \
$ aws s3 cp /mnt/backup/pg_dump.gz s3://hogehoge-backet/

Auroraへのリストア

新しい環境となるAWS上では、DBはAuroraを選択しています。 リストアにあたっては上述した様にZabbixのDBデータがS3に毎日アップロードされている状態でしたので、それを適当なEC2から psql を用いて流し込むだけの想定でした。


シンプルなバックアップ&リストアになりますが、これにはZabbixを利用するネットワーク担当の同僚氏から「ダンプからリストアの間に欠損するデータについては気にしなくて良いよ」とコメントをいただいていためです。これは大変ありがたい話で、RPOを短くするならストリーミングなどの手段を取る必要が出てくるため移行のハードルが下がったと感じています。*5


具体的には以下様なコマンドでAuroraへリストアを行いました。

$ aws s3 cp s3://hogehoge-backet/pg_dump.gz ./ ; echo $?
$ time gunzip -c pg_dump.gz | pg_restore -c -d zabbix -h aws-aurora-oisixradaichi-zabbix.example.com -U zabbix  

課題

DBリストア中にエラーが発生、ただし初回はスルーした

上記のリストアコマンドは当初は順調に進んでいる様に見えました。 コマンド実行の直後はオンプレからAuroraへ移行する時特有のPublic系テーブルのエラーが多発しましたが、これはRDSやAuroraの仕様と判断して無視しました。


数十時間をかけてリストアが終わり、発生したエラーは以下のみでした。 後ほどこのエラーがZabbixの動作に致命的なボトルネック要因となるのですが、このタイミングではPublic系テーブルの大量エラーを見送った直後でもあり「とりあえず動けばまあ大丈夫だろ」と判断して仮リリースを行いました。

pg_restore: error: could not execute query: ERROR:  could not write to file "base/pgsql_tmp/pgsql_tmp19647.26.fileset/0.2": No space left on device
Command was: ALTER TABLE ONLY public.trends_uint
    ADD CONSTRAINT trends_uint_pkey PRIMARY KEY (itemid, clock);


pg_restore: error: could not execute query: ERROR:  could not write to file "base/pgsql_tmp/pgsql_tmp19647.47.fileset/1.2": No space left on device
CONTEXT:  parallel worker
Command was: CREATE INDEX history_1 ON public.history USING btree (itemid, clock);


pg_restore: error: could not execute query: ERROR:  could not write to file "base/pgsql_tmp/pgsql_tmp19647.51.fileset/0.2": No space left on device
Command was: CREATE INDEX history_uint_1 ON public.history_uint USING btree (itemid, clock);


pg_restore: warning: errors ignored on restore: xxx

こうして上述のエラーはでましたが、OS、APであるZabbix Server、DBであるPostgreSQLのバージョンをそれぞれ上げ、環境を大きくオンプレからAWSへ移行しても、Zabbix Serverは綺麗に上がってきました。この結果はZabbix Server内部でのバージョンマイグレーションプロセスがしっかり作られているためと認識しておりZabbix開発関係者を大きくリスペクトします。

仮リリース中に画面が重くなり504が発生

DBリストア後、オンプレZabbixと並行する形でAWS上に移行したZabbixの運用を開始しました。ネットワーク担当の同僚氏にチェックをお願いしたところ、当初は良さそうでしたが途中からどうにも画面が重く、場合によっては504が多発する様な事態となりました。

ボトルネックの特定

当初Zabbix Server側のキャッシュチューニングを行いましたが効果はありませんでした。


DatadogやCloudWatchを含め調査したところ、Performance InsightでDBがボトネックとなっていることが見えました。 image.png (151.8 kB)

圧倒的な IO:DataFileRead (水色) の多発です。 加えて Top SQL には以下のクエリが表示されていました。

image.png (80.2 kB)

select clock,ns,value from history_uint where itemid=? and clock>?

ここまで見れば一目瞭然で、リストア時に無視したINDEXのエラーが原因と判断できました。INDEXの無い history_uint テーブルにREADアクセスが走り、詰まっていたのです。

どうしてCREATE INDEXのエラーが発生したのか

INDEXが無いことがボトルネックと特定できましたので、改めて手動で CREATE INDEX を実行しましたが、同じエラーが発生しました。

> CREATE INDEX history_uint_1 ON public.history_uint USING btree (itemid, clock);
ERROR:  could not write to file "base/pgsql_tmp/pgsql_tmpxxxx.xx.fileset/0.x": No space left on device

この時の私の認識では、ストレージが自動拡張するAuroraでどうして No space left on device のようなストレージのエラーが出るのか不思議でした。


原因はどうやら CREATE INDEX 中に使われるAuroraのディスク領域について、2つあるAuroraのボリュームのうちの、自動拡張されるクラスターボリュームではなく、ローカルストレージと呼ばれる領域が溢れているとわかりました。

AWSドキュメント - Amazon Aurora ストレージと信頼性 より「Aurora は、非永続的な一時ファイル用に、分離したローカルストレージを使用します。これには、クエリ処理中の大きなデータセットのソートや、インデックスの作成などの目的に使用するファイルが含まれます。」

上の引用の様に、PostgreSQLでは大規模テーブルのINDEX作成時には一時ファイルを作成し、それはローカルストレージを利用しているようです。*6


実際にCloudWatchのグラフでも、ローカルストレージが CREATE INDEX 実行後に減り続けて枯渇し、エラー後に急激に回復している様子が見えました。

image.png (34.3 kB)

対策

対策1: Auroraのローカルストレージ(一時領域)はスケールアップで増える

AWSドキュメント - Amazon Aurora PostgreSQL の管理 - Aurora PostgreSQL 用の一時ストレージの制限 にあるように、Auroraのローカルストレージはインスタンスのサイズによって変動します。


例を挙げると以下の様に倍々で増えていきます。

DB instance class Maximum temporary storage available (GiB)
db.r7g.large 32
db.r7g.xlarge 63
db.r7g.2xlarge 126
db.r7g.4xlarge 252

これを受けて CREATE INDEX が失敗した場合はスケールアップをすることでエラーを回避することにしました。

対策2: セッションで SET work_memによる作業メモリの拡大(本来適切なのはmaintenance_work_mem)

(2024-02-01追記)本記事の公開当初は work_mem のチューニングを対策として述べておりましたが、それは不適切でした。正しくは maintenance_work_mem のチューニングが CREATE IDNEX の一時ファイルに対しては有効です。謹んで訂正します。


----ここからの work_mem についての記事は公開当初の内容です----

加えてPostgreSQLにはセッション単位で作業メモリを変更することができる work_mem というパラメタがあります。

PostgreSQL 15.4文書 - 第20章 サーバの設定 - 20.4. 資源の消費 より引用

work_mem (integer) 一時ディスクファイルに書き込むようになる前に、問い合わせ操作(たとえば並べ替えとハッシュテーブル操作)が使用する基本的な最大のメモリ容量を指定します。

具体的には psql で接続後、以下を実施しました。

zabbix=> SET work_mem = 'xxxGB';            
SET
zabbix=>
zabbix=> show work_mem;
work_mem   
----------
 xxxGB   
(1 row)  


----ここまでの work_mem についての記事は公開当初の内容です----


(2024-02-01追記)上記の様に本記事の公開当初は work_mem のチューニングを対策として述べておりましたが、それは不適切でした。正しくは maintenance_work_mem のチューニングが CREATE IDNEX の一時ファイルに対しては有効です。謹んで訂正します。


PostgreSQL 15.4文書 - 第20章 サーバの設定 - 20.4. 資源の消費 より引用

maintenance_work_mem (integer) VACUUM、CREATE INDEX、およびALTER TABLE ADD FOREIGN KEYの様な保守操作で使用されるメモリの最大容量を指定します。 この値が単位なしで指定された場合は、キロバイト単位であるとみなします。 デフォルト値は64メガバイト(64MB)です。 1つのデータベースセッションでは、一度に1つしか上記操作はできませんし、通常インストレーションでこうした操作が同時に非常に多く発生することはありませんので、これをwork_memよりもかなり多めの値にしても安全です。 大きい値を設定することでvacuum処理と、ダンプしたデータベースのリストア性能が向上します。

あとはもうトライ&エラー

ここからは泥臭くてダサい話になります。


こうして対策はわかりましたが、その時の私はINDEXを作成するためにどの程度の一時領域が必要になるかがわからず、とりあえずワンサイズあげればいけるだろうと甘い見込みでリトライを行いました。


結果として以下の"FreeLocalStorage"のグラフがさす様に、何度も失敗してはスケールアップを繰り返してしまいました。


流れとしては以下の3つをリトライのたびに繰り返したのです。

  1. グラフのメトリクスは CREATE INDEX 実行開始で消費され右肩下がり減っていきます。
  2. そして枯渇してエラーとなり急げきに上にあがります。
  3. その後Auroraをスケールアップすることで約2倍の空き容量になります。


ここまで説明すれば、読まれた方には半分涙目になりながら CREATE INDEXSET work_mem (本来適切なのは maintenance_work_mem )とAuroraスケールアップをするSREの姿がグラフから浮かんでくるでしょうか...。*7

Screenshot 2024-01-31 at 20.56.27.png (150.1 kB)

無事にINDEXを作成でき、Zabbixは安定稼働中

こうして、5回目のトライでなんとか CREATE INDEX に成功した後は、コストの観点からすぐにAuroraのインスタンスを適正サイズに変更しました。

改めて謝罪と共にネットワーク担当の同僚氏に引渡しを行い、彼からは見違える様にサクサクで安定稼動していると評価いただいています。


諸々のバージョンを上げているため、今後運用しながら細かなトラブルシュートは発生しそうですが、それはまた別の機会でナレッジが公開されるかもしれません。

まとめと今後への反省

以上で無事にZabbix Serverを移行できましたが、落ち着いてからいくつかの反省点があるため自戒とともに記載します。

Auroraのインスタンスのチョイスはもっと熟慮すべきだった

後から気づいたことで、当初はコストと性能のバランスから db.r6g 系を採用していましたが、2023年11月ごろにリリースされていたOptimized Readsな db.r6gd を採用していれば起きない問題だった可能性があります。


New – Amazon Aurora Optimized Reads for Aurora PostgreSQL with up to 8x query latency improvement for I/O-intensive applications


NVMEベースの一時ストレージを有する Optimized Reads については、上述した一時ストレージ制限のリストにも記載がなく、もしかするとクラスターボリュームと同じく可変スケールなのかもしれませんが、これは未確認な情報です。

テーブルサイズからある程度のIndexサイズの見積もりができたはず

既存のZabbixのDBは稼動しているため、そちらのテーブルとIndexのサイズを確認することで、どの程度の一時領域が必要になるか見積もれた可能性があります。事前の見積もりを正しく行えば5回もトライせずに少ないリトライで解決できたかもしれません。

work_memではなくmaintenance_work_memをチューニングすべきだった

(2024-02-01追記) 上の記述についても追記訂正をしましたが、 CREATE INDEX への対策としては work_mem ではなく maintenance_work_mem が適切でした... トライアンドエラー時にも、AWSやPostgreSQLのドキュメントをちゃんと読めば辿り着けていたはずですが、目についた work_mem を盲信してしまっていました。大反省ですが、もう忘れることはないでしょう。

PostgreSQL 15.4文書 - 第20章 サーバの設定 - 20.4. 資源の消費 より引用

maintenance_work_mem (integer) VACUUM、CREATE INDEX、およびALTER TABLE ADD FOREIGN KEYの様な保守操作で使用されるメモリの最大容量を指定します。 この値が単位なしで指定された場合は、キロバイト単位であるとみなします。 デフォルト値は64メガバイト(64MB)です。 1つのデータベースセッションでは、一度に1つしか上記操作はできませんし、通常インストレーションでこうした操作が同時に非常に多く発生することはありませんので、これをwork_memよりもかなり多めの値にしても安全です。 大きい値を設定することでvacuum処理と、ダンプしたデータベースのリストア性能が向上します。

最後に

時間も気力も消費しましたが、結果として無事に移行できたのは大変よかったです。加えてトライアンドエラー中に目を通した多くのドキュメントやブログやAIによる回答は、自分の知見として蓄えることができたと言えます。


システムの移行は骨の折れる作業ですが、誰かがやらなければならない重要な作業です。同じ様な立場の皆さんにエールと一緒にがんばろうぜの念を送って本記事の結びとします。

*1:CentOSのバージョンはあえて伏せます...mm

*2:公式ドキュメント等から正確な言及は見つけきれてないですが、動作としてそうなっているように見えた

*3:検討にあたって幾つかの考慮ポイントがありましたが、それは別記事に書くとして本記事では割愛します

*4:細かいローテーションなどやエラー時の通知処理なども実際にはあります

*5:この同僚氏も歴戦の猛者のため、色々察した上での提案だったとでしょう

*6:公式ドキュメント等から正確な言及は見つけきれてないですが、動作としてそうなっているように見えた

*7:正直にいいまして5回目の頃には心が折れかけていました

Oisix ra daichi Creator's Blogはオイシックス・ラ・大地株式会社のエンジニア・デザイナーが執筆している公式ブログです。

オイシックス・ラ・大地株式会社では一緒に働く仲間を募集しています