PostgreSQL「SSL SYSCALL error: EOF detected」接続が突然切断される問題を修正する

intermediate🐘 PostgreSQL2026-05-07| PostgreSQL 12〜16、Linux/Ubuntu/CentOS、AWS RDS、コネクションプーラー(PgBouncer、pgpool-II)、SSL有効なPostgreSQL環境全般

Error Message

SSL SYSCALL error: EOF detected
#postgresql#ssl#接続#ネットワーク#タイムアウト

何が起きたのか

クエリを実行中、あるいはただアイドル状態で待っていただけなのに、突然こんなエラーで接続が切れた:

SSL SYSCALL error: EOF detected
server closed the connection unexpectedly
  This probably means the server terminated abnormally
  before or during processing of the request.

つまり:サーバー側のSSLレイヤーが、適切なTLSのclose_notifyアラートを送らないままTCP接続を強制終了した。PostgreSQLはソケット上で突然のEOFを受け取った。アプリ、psql、コネクションプーラーのいずれも一切警告なし — 接続は処理の途中で消えてしまった。

正常な切断であれば、まずシャットダウンシグナルが送られる。EOFはそうではない。つまり、OSレベルで何かがSSLに通知せず接続を強制終了したということだ。よくある原因は、ネットワークの瞬断、サーバーのリソース枯渇、SSL証明書の問題、またはロードバランサーがアイドル接続を静かに切断していることだ。

エラーの再現と確認

修正に飛びつく前に、いつこれが起きるのかを正確に特定しよう。まず簡単な接続テストを行う:

psql "host=your-db-host dbname=mydb user=myuser sslmode=require" -c "SELECT version();"

即座に失敗する場合は、接続の確立段階の問題だ — 証明書またはSSLモードの不一致が原因である可能性が高い。1〜2分のアイドル後に失敗する場合は、タイムアウトまたはキープアライブの問題だ。

サーバー上のPostgreSQLログで対応する切断を確認する:

sudo grep -i "ssl\|EOF\|connection" /var/log/postgresql/postgresql-*.log | tail -50

RDSの場合は、AWSコンソールまたはCLIからログを取得する:

aws rds download-db-log-file-portion \
  --db-instance-identifier mydb \
  --log-file-name error/postgresql.log \
  --output text

まず試すべき簡単な修正

1. サーバーが実際に動いているか確認する

sudo systemctl status postgresql
sudo journalctl -u postgresql -n 50 --no-pager

LinuxのOOMキラーがPostgreSQLを静かに終了させることは、特に4GB未満のRAMしかないサーバーでは、思っている以上によくある:

sudo dmesg | grep -i "oom\|killed" | tail -20

2. クライアント側でTCPキープアライブを有効にする

アイドル接続は脆弱だ。ファイアウォール、NATゲートウェイ、ロードバランサーは60〜300秒間トラフィックがないと、接続を静かに切断する — SSLはそれをEOFとして認識する。TCPキープアライブは小さなプローブパケットを送って接続を維持する。接続文字列に設定しよう:

# psql 接続文字列
psql "host=db-host dbname=mydb user=myuser \
  keepalives=1 \
  keepalives_idle=60 \
  keepalives_interval=10 \
  keepalives_count=5"

libpqベースのアプリ(Python psycopg2、Node pgなど)では、接続パラメータとして渡す:

# Python psycopg2
import psycopg2
conn = psycopg2.connect(
    host="db-host",
    dbname="mydb",
    user="myuser",
    keepalives=1,
    keepalives_idle=60,
    keepalives_interval=10,
    keepalives_count=5
)

3. クライアントとサーバーのSSLモードを合わせる

SSLの期待値の不一致は、意外に多いトリガーだ。まずサーバーが実際に何を要求しているか確認する:

psql -c "SHOW ssl;"
psql -c "SELECT name, setting FROM pg_settings WHERE name LIKE 'ssl%';"

そしてクライアントのsslmodeを合わせる:

# サーバーが ssl=on でSSLを必須とする場合:
export PGSSLMODE=require

# サーバーがSSLを許可するが必須ではない場合:
export PGSSLMODE=prefer

根本原因に基づく恒久的な修正

根本原因A:ロードバランサー/ファイアウォールのアイドルタイムアウト

AWS ALBはデフォルトで60秒以上アイドル状態の接続を切断する。RDS Proxyのデフォルトは1,800秒。Nginxのアップストリームキープアライブタイムアウトは60秒。これらのいずれかが接続を静かに切断し、PostgreSQLのSSLレイヤーはそれを予期しないEOFとして認識する。

修正は2段階だ:PostgreSQLの内部タイムアウトをLBの上限より短く設定し、サーバー側でキープアライブを有効にしてアイドル接続を維持する:

-- postgresql.conf またはユーザーごとに設定:
ALTER SYSTEM SET tcp_keepalives_idle = 60;
ALTER SYSTEM SET tcp_keepalives_interval = 10;
ALTER SYSTEM SET tcp_keepalives_count = 5;
SELECT pg_reload_conf();

根本原因B:PgBouncer または pgpool-II が接続を切断している

コネクションプーラーは独自のスケジュールでサーバー側のアイドル接続を閉じる — それがアプリの期待と合わないことがある。PgBouncerのserver_idle_timeoutを確認する:

# pgbouncer.ini の設定:
server_idle_timeout = 600     # 秒、デフォルト600
server_lifetime = 3600        # 最大接続寿命
client_idle_timeout = 0       # 0 = タイムアウトなし

# リロード:
psql -p 6432 pgbouncer -c "RELOAD;"

また、PgBouncerのSSL設定がエンドツーエンドで一貫していることも確認しよう。アプリがsslmode=requireでPgBouncerに接続しているのに、PgBouncerがPostgreSQLとプレーンTCPで通信している場合、再接続時にEOFが発生する:

# pgbouncer.ini — 両側の設定を合わせること:
server_tls_sslmode = require
client_tls_sslmode = require
client_tls_cert_file = /etc/pgbouncer/client.crt
client_tls_key_file = /etc/pgbouncer/client.key

根本原因C:SSL証明書の期限切れまたは不一致

期限切れの証明書はやっかいだ。SSLハンドシェイクは正常に始まるが、途中で崩壊する — クライアントには分かりやすいエラーではなくEOFが見える。まず有効期限を確認しよう:

# PostgreSQLサーバーで証明書の有効期限を確認:
openssl x509 -in /etc/postgresql/16/main/server.crt -noout -dates

# リモートから確認:
openssl s_client -connect your-db-host:5432 -starttls postgres 2>/dev/null \
  | openssl x509 -noout -dates

期限切れの場合は再生成して再起動する。開発/テスト環境のみで使用する場合:

# 自己署名証明書(開発/テスト環境のみ):
openssl req -new -x509 -days 365 -nodes \
  -out /etc/postgresql/16/main/server.crt \
  -keyout /etc/postgresql/16/main/server.key
chmod 600 /etc/postgresql/16/main/server.key
chown postgres:postgres /etc/postgresql/16/main/server.*
sudo systemctl restart postgresql

根本原因D:サーバーのメモリまたはファイルディスクリプタが枯渇した

サーバーのメモリが不足すると、LinuxのOOMキラーがプロセスを強制終了し始める — PostgreSQLのバックエンドも例外ではない。ファイルディスクリプタの上限に達した場合も同様だ:新しい接続が失敗し、既存の接続も警告なしに切断される。

# 現在の制限を確認:
cat /proc/$(pgrep -o postgres)/limits | grep -i "open files\|max"

# /etc/security/limits.conf で制限を引き上げる:
postgres soft nofile 65536
postgres hard nofile 65536

# または postgresql.service(systemd)で設定:
[Service]
LimitNOFILE=65536

修正が効いたか確認する

修正できたと思い込まないように。アイドル期間を通して接続を保持し、生き残ることを確認しよう:

# 5分間接続を開いたままにして、まだ生きているか確認:
psql "host=db-host dbname=mydb keepalives=1 keepalives_idle=30" \
  -c "SELECT pg_sleep(300); SELECT 'still alive';"

# テスト中にアクティブな接続を監視:
psql -c "SELECT pid, state, wait_event, query_start, state_change \
         FROM pg_stat_activity WHERE datname='mydb';"

SSLのEOFエラーではなくstill aliveが返ってきたら、修正は成功だ。

補足

クラウド環境での作業は、さらに複雑な要素を加える — サブネット、セキュリティグループ、NACLのすべてがキープアライブのプローブがデータベースに届くかどうかに影響する。IPレンジの確認や2つのホストが同じネットワークセグメントにあるかを検証する必要があれば、ToolCraftのSubnet Calculatorが便利だ — ブラウザ上で完全に動作し、データはどこにも送信されない。

監視に追加する価値があるもう一つのこと:pg_stat_activityidle接続がstate_changeの時間が長いままスパイクするアラートを設定しよう。このパターンは通常、EOFエラーで接続が切れ始める5〜10分前に現れる — 早期に気づくことで、慌てた対応を避けられる。

Related Error Notes