エラーが発生する理由
ERROR: cannot execute INSERT in a read-only transaction メッセージが表示される場合、通常、アプリケーションが書き込みを許可されていないデータベースノードに対してデータを書き込もうとしていることを意味します。最新のクラウド環境では、ほとんどの場合、アプリが誤って Primary (Writer) ノードではなく Standby (Replica) ノードに接続してしまったことが原因です。
PostgreSQLのレプリカは「ホットスタンバイ」モードを使用します。これにより、メインサーバーの負荷を軽減するために大量の SELECT トラフィックを処理できます。しかし、プライマリソースとのデータ整合性を保つため、INSERT、UPDATE、または DELETE コマンドは厳格にブロックされます。
ステップ1:ロールのクイックチェックを実行する
どのサーバーに接続しているか推測する必要はありません。PostgreSQLに対して、現在リカバリモードかどうかを直接問い合わせることができます。これは、レプリカに接続しているかどうかを確認する最も早い方法です。
SQLツール(psqlやDBeaverなど)で次のクエリを実行してください:
SELECT pg_is_in_recovery();
- f (false): プライマリノードにいます。それでも書き込みに失敗する場合は、セッションまたはデータベースが明示的に読み取り専用モードに設定されています。
- t (true): レプリカにいます。接続先を別の場所に向ける必要があります。
ステップ2:接続エンドポイントの監査
上記のチェックで t が返された場合、アプリケーションは間違ったドアを叩いています。.env、application.yml、または Kubernetes の ConfigMap でデータベースホストのURLを確認してください。
クラウドプラットフォーム (AWS RDS / Aurora)
マネージドサービスは通常、2つの異なるURLを提供します。例えば AWS Aurora では、次のように表示されます:
- Writer Endpoint:
mydb.cluster-xyz.us-east-1.rds.amazonaws.com(メインアプリに使用) - Reader Endpoint:
mydb.cluster-ro-xyz.us-east-1.rds.amazonaws.com(レポートや分析のみに使用)
書き込みの多いマイクロサービスが、誤って Reader アドレスを使用していないか確認してください。
ステップ3:接続文字列でスマートルーティングを使用する
Python、Go、Node.jsで一般的な libpq ベースのドライバーを使用している場合、クライアントにライターを探させることができます。複数のホストをリストし、target_session_attrs パラメータを追加します。
接続文字列を次のように更新してください:
postgresql://user:pass@host-a:5432,host-b:5432/dbname?target_session_attrs=read-write
クライアントはまず host-a を試します。もしそれがレプリカであれば、自動的に host-b にジャンプして書き込み可能なノードを確認します。これにより、データベースのフェイルオーバー中のダウンタイムを防ぐことができます。
ステップ4:PgBouncerの設定を修正する
接続プーリングに PgBouncer を使用している場合、pgbouncer.ini に間違いがある可能性があります。役割ごとに個別のプールを定義するのが一般的です。誤設定の例は次のようになります:
[databases]
# アプリは 'analytics_db' ではなく 'primary_db' に接続する必要があります
primary_db = host=10.0.0.1 port=5432 dbname=prod
analytics_db = host=10.0.0.2 port=5432 dbname=prod
アプリケーションのデータベースポート(PgBouncerの場合は通常 6432)が、正しいエイリアスにルーティングされているか再確認してください。
ステップ5:コードレベルの読み取り専用フラグを確認する
ハードウェアに問題がなくても、ソフトウェア側で制限をかけている場合があります。例えば Java の Spring Boot では、開発者が書き込みが必要なサービスメソッドに誤って @Transactional(readOnly = true) を追加している可能性があります。
次のコマンドで現在のセッションの状態を確認できます:
SHOW default_transaction_read_only;
もし on が返され、かつ pg_is_in_recovery() が false の場合、誰かがユーザー権限またはグローバル設定を制限した可能性があります。現在のセッションで強制的にオフにするには、次を使用します:
SET default_transaction_read_only = off;
修正の確認方法
ルーティングが修正されたと思ったら、アプリが使用しているのと同じ接続文字列で手動テストを実行してください。確実を期すために、小さなダミーテーブルを作成します:
CREATE TABLE connection_test (id serial PRIMARY KEY, test_val text);
INSERT INTO connection_test (test_val) VALUES ('success');
DROP TABLE connection_test;
その INSERT が成功すれば、アプリケーションは正しく書き込みノードと通信できています。
予防策
- DNSの明確化: 生のIPアドレスではなく、
db-writer.internalやdb-reader.internalのような明示的な名前を使用します。 - モニタリング: プライマリノードの
pg_is_in_recoveryステータスがtrueに変わった場合にトリガーされるアラートを Prometheus や CloudWatch で設定します。 - フレームワークの認識: ORMがパフォーマンスを「最適化」するために、デフォルトで読み取り専用トランザクションを使用していないか確認してください。

