PostgreSQLリードレプリカでのクエリ強制キャンセルエラーの対応

株式会社GxPの@rasahina-gxpです。
本記事はグロースエクスパートナーズアドベントカレンダーの21日目の記事です。

リードレプリカ構成のRDS for PostgreSQLで時間のかかるクエリが強制的に終了させられるエラーについての原因と対応についてご紹介します。

# 前提条件
### Postgresqlバージョン
11.4
### 動作環境
Amazon RDS for PostgreSQL(以下RDS)
### システムの概要
Webアプリがプライマリにデータを書き込み、オンラインバッチシステムがリードレプリカからデータを取得し分析を行うシステムです。Webアプリから分析対象データをRDSに登録し、APIにてバッチアプリに分析実行を指示します。ざっくりと以下の構成になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/963532/5a6cef85-12d0-3469-f460-b2b51e727622.png)



# 事象
バッチアプリがリードレプリカに参照クエリの実行中、以下のエラーが発生しクエリが強制的に終了させられます。

```
### Error querying database.  Cause: org.postgresql.util.PSQLException: ERROR: canceling statement due to conflict with recovery
Detail: User query might have needed to see row versions that must be removed.
```

# 原因
PostgresqlではWAL(Write Ahead Log)を利用したストリーミングレプリケーションによるホットスタンバイ構成をとることができます。RDSのリードレプリカも同様の機構で実現されており、WALをプライマリからリードレプリカに送信し、WALの内容をすべてリードレプリカ側で実行することでプライマリの変更をリードレプリカに反映させます。
(WALの詳細については[こちら](https://www.kimullaa.com/entry/2019/10/28/000000)が分かりやすいです。)
このとき、リードレプリカへの参照とプライマリでのデータ更新などで競合が起きると、リードレプリカのクエリは強制的に終了させられます。
例えば、リードレプリカでレコードAを参照するようなSELECT文を実行中にプライマリにてレコードAをDELTEするようなクエリを実行した場合、プライマリでのレコードAのDELTEを含むWALがリードレプリカに適用される時、リードレプリカに対するSELECT文と競合しているため、SELECT文が強制的に終了させられます。(その他には排他ロックの取得やVACCUMの実行タイミングなど、WALの書き込みタイミングも関係します。)
クエリが適切な速度で実行されている場合は通常は問題になりませんが、例えば分析系のクエリ等どうしても実行時間が長くなってしまうような場合は問題になります。

# 対策
本事象の対策として以下パラメータの調整が考えられます。

|No| パラメータ名 | 説明 |デフォルト値 |
|:-|:-----------|:------------|:-----------|
|1| max_standby_streaming_delay     | WALがリードレプリカに到着してから何秒後にWAL適用するか。-1だとクエリが動いている間はWAL適用しない。| 30 |
|2| hot_standby_feedback | リードレプリカでクエリ実行中、プライマリ側でリードレプリカで参照している行のVACUUMの実行を待たせる。| OFF |
|3| vacuum_defer_cleanup_age| 通常その行をみているトランザクションがいなくなるとVACUUMが実行されるが、その実行を設定したトランザクション数分遅延させる。| 0 |

これらの詳細については参考に記載したリンク先が詳しいためご参照ください。
どのパラメータも調整が難しくどの設定を行うかはケースバイケースとなります。
今回のケースではどれが適用可能か見ていきます。
#### max_standby_streaming_delayの変更
-1に設定することでクエリ実行と完全に同期させることができますが、バッチ側ではAPIでリアルタイムにリクエストを受け付けているため、こちらを適用すると、バッチにリクエストは届ているが分析対象データが存在しないという状況が発生します。
-1以外の設定も考えられますが具体的な値を決定するのは難しそうです。今回は適用を見送りました。
#### hot_standby_feedbackの設定
WALの書き込みにはvaccumの実行も一つのトリガーになります。こちらを適用することでvaccumを遅延させることは可能ですが、プライマリ側でストレージの逼迫等に注意する必要があります。なお今回はAutoVaccumによる事象と推測し、こちらを適用したところ事象解決し安定稼働させることができました。
#### vacuum_defer_cleanup_ageの設定
マスタ側でデータの物理削除をトランザクション数分遅延させることができますが、このパラメータも具体的な値を決定するのは難しそうです。今回は値の決定に至ることができなさそうなため、適用を見送りました。

# まとめ
今回はパラメータの調整のみで事なきを得ましたが、リードレプリカ構成ではプライマリにデータを登録する側はデータ登録を分散させる、リードレプリカを参照する側はクエリを分散させる、などを行い処理の単位を小さくすることなどに気を配る必要性がありました。
また、性能試験はWebアプリ、バッチアプリ単独は実施していましたが、同時に稼働させながら性能試験を行う等を検討する必要性もありそうです。
RDSのようなマネージドサービスは気軽にリードレプリカなどの構成をとることができますが、実際にアプリケーションを運用していく中ではこのような細かい知識も重要となっていきますね。
それでは良いリードレプリカライフをお送りください。


# 参考
[Amazon RDS PostgreSQL レプリケーションのベストプラクティス](https://aws.amazon.com/jp/blogs/news/best-practices-for-amazon-rds-postgresql-replication/)
[PostgreSQLによるデータ分析ことはじめ](https://www.slideshare.net/ooyamams/postgresql-69896127)
[PostgreSQLのレプリケーションのコンフリクトについて](https://soudai.hatenablog.com/entry/2017/12/25/104556)
[PostgreSQLのレプリケーションのコンフリクトについて言いたい](https://qiita.com/sawada_masahiko/items/16a30b2df52331c86483)
[PostgreSQL WALログの仕組みとタイミングを理解したい](https://www.kimullaa.com/entry/2019/10/28/000000)