何が起きたのか
クエリを実行中、あるいはただアイドル状態で待っていただけなのに、突然こんなエラーで接続が切れた:
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_activityでidle接続がstate_changeの時間が長いままスパイクするアラートを設定しよう。このパターンは通常、EOFエラーで接続が切れ始める5〜10分前に現れる — 早期に気づくことで、慌てた対応を避けられる。

