Chuyện Gì Vừa Xảy Ra
Bạn đang chạy một query — hoặc đơn giản chỉ để kết nối ngồi không — thì đột nhiên kết nối bị ngắt kèm thông báo:
SSL SYSCALL error: EOF detected
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or during processing of the request.
Hiểu nôm na: lớp SSL phía server đã cắt kết nối TCP mà không gửi cảnh báo close_notify theo chuẩn TLS. PostgreSQL nhận được EOF đột ngột trên socket. Ứng dụng, psql, hay connection pooler của bạn không có chút báo trước nào — kết nối biến mất giữa chừng.
Ngắt kết nối bình thường sẽ gửi tín hiệu shutdown trước. EOF thì không. Nghĩa là có gì đó ở tầng OS đã cắt kết nối mà không thông báo cho SSL biết. Các nghi phạm thường gặp: sự cố mạng thoáng qua, server cạn kiệt tài nguyên, vấn đề với SSL cert, hoặc load balancer âm thầm chém các kết nối idle.
Tái Hiện và Xác Nhận Lỗi
Trước khi vội vàng sửa, hãy xác định chính xác khi nào lỗi xảy ra. Chạy thử kết nối nhanh:
psql "host=your-db-host dbname=mydb user=myuser sslmode=require" -c "SELECT version();"
Lỗi ngay lập tức? Vấn đề nằm ở bước thiết lập kết nối — nhiều khả năng do cert hoặc SSL mode không khớp. Lỗi sau khi ngồi idle một hai phút? Bạn đang gặp vấn đề về timeout hoặc keepalive.
Kiểm tra log PostgreSQL trên server để tìm thông tin ngắt kết nối tương ứng:
sudo grep -i "ssl\|EOF\|connection" /var/log/postgresql/postgresql-*.log | tail -50
Trên RDS, lấy log từ AWS Console hoặc qua CLI:
aws rds download-db-log-file-portion \
--db-instance-identifier mydb \
--log-file-name error/postgresql.log \
--output text
Sửa Nhanh — Thử Những Cách Này Trước
1. Kiểm tra xem server có đang chạy không
sudo systemctl status postgresql
sudo journalctl -u postgresql -n 50 --no-pager
OOM killer của Linux âm thầm kill PostgreSQL xảy ra thường xuyên hơn bạn nghĩ — đặc biệt trên các server có ít hơn 4 GB RAM:
sudo dmesg | grep -i "oom\|killed" | tail -20
2. Bật TCP keepalives ở phía client
Các kết nối idle rất dễ bị ngắt. Firewall, NAT gateway, và load balancer không thấy traffic trong 60–300 giây sẽ âm thầm cắt kết nối — và SSL ghi nhận điều đó là EOF. TCP keepalives gửi các gói probe nhỏ để giữ kết nối luôn hoạt động. Cấu hình trong connection string:
# psql connection string
psql "host=db-host dbname=mydb user=myuser \
keepalives=1 \
keepalives_idle=60 \
keepalives_interval=10 \
keepalives_count=5"
Với các ứng dụng dùng libpq (Python psycopg2, Node pg, v.v.), truyền các tham số này trong connection parameters:
# 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. Đồng bộ SSL mode giữa client và server
SSL mode không khớp là nguyên nhân kích hoạt lỗi phổ biến đến bất ngờ. Trước tiên, xem server thực sự yêu cầu gì:
psql -c "SHOW ssl;"
psql -c "SELECT name, setting FROM pg_settings WHERE name LIKE 'ssl%';"
Sau đó điều chỉnh sslmode của client cho phù hợp:
# Nếu server có ssl=on và bắt buộc SSL:
export PGSSLMODE=require
# Nếu server cho phép nhưng không bắt buộc:
export PGSSLMODE=prefer
Sửa Triệt Để — Theo Từng Nguyên Nhân Gốc
Nguyên nhân A: Idle timeout của load balancer / firewall
AWS ALB mặc định cắt kết nối idle sau hơn 60 giây. Mặc định của RDS Proxy là 1.800 giây. Upstream keepalive timeout của Nginx là 60 giây. Bất kỳ thứ nào trong số này đều có thể âm thầm cắt kết nối — và lớp SSL của PostgreSQL sẽ coi đó là EOF bất ngờ.
Cách sửa gồm hai phần: rút ngắn các timeout nội bộ của PostgreSQL để nằm dưới giới hạn của load balancer, và bật keepalives phía server để các kết nối idle không bị cắt:
-- Trong postgresql.conf hoặc per-user:
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();
Nguyên nhân B: PgBouncer hoặc pgpool-II cắt kết nối
Các connection pooler tự đóng kết nối server-side theo lịch riêng của chúng — và lịch này có thể không khớp với kỳ vọng của ứng dụng. Kiểm tra server_idle_timeout của PgBouncer:
# Trong pgbouncer.ini:
server_idle_timeout = 600 # giây, mặc định 600
server_lifetime = 3600 # thời gian sống tối đa của kết nối
client_idle_timeout = 0 # 0 = không timeout
# Reload:
psql -p 6432 pgbouncer -c "RELOAD;"
Cũng cần xác minh cấu hình SSL của PgBouncer nhất quán từ đầu đến cuối. Nếu ứng dụng kết nối đến PgBouncer với sslmode=require nhưng PgBouncer lại kết nối đến PostgreSQL qua TCP thường, bạn sẽ gặp EOF khi reconnect:
# pgbouncer.ini — cả hai phía cần phải khớp:
server_tls_sslmode = require
client_tls_sslmode = require
client_tls_cert_file = /etc/pgbouncer/client.crt
client_tls_key_file = /etc/pgbouncer/client.key
Nguyên nhân C: SSL certificate hết hạn hoặc không khớp
Cert hết hạn rất khó phát hiện. Quá trình SSL handshake bắt đầu bình thường, rồi thất bại giữa chừng — và client nhận EOF thay vì thông báo lỗi rõ ràng. Kiểm tra ngày hết hạn trước:
# Kiểm tra hạn cert trên PostgreSQL server:
openssl x509 -in /etc/postgresql/16/main/server.crt -noout -dates
# Kiểm tra từ xa:
openssl s_client -connect your-db-host:5432 -starttls postgres 2>/dev/null \
| openssl x509 -noout -dates
Nếu đã hết hạn, tạo lại và khởi động lại. Chỉ dùng cách sau cho môi trường dev/test:
# Self-signed (chỉ dùng cho dev/test):
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
Nguyên nhân D: Server hết bộ nhớ hoặc file descriptor
Khi server cạn bộ nhớ, OOM killer của Linux bắt đầu kill các tiến trình — và các backend PostgreSQL cũng không ngoại lệ. Tương tự nếu bạn chạm giới hạn file descriptor: kết nối mới thất bại, và các kết nối đang có bị ngắt mà không có cảnh báo.
# Kiểm tra giới hạn hiện tại:
cat /proc/$(pgrep -o postgres)/limits | grep -i "open files\|max"
# Tăng giới hạn trong /etc/security/limits.conf:
postgres soft nofile 65536
postgres hard nofile 65536
# Hoặc trong postgresql.service (systemd):
[Service]
LimitNOFILE=65536
Xác Nhận Đã Sửa Thành Công
Đừng chỉ giả định là đã xong. Hãy giữ một kết nối mở suốt một khoảng thời gian idle đầy đủ và xác nhận nó vẫn còn sống:
# Giữ kết nối mở trong 5 phút và kiểm tra còn hoạt động không:
psql "host=db-host dbname=mydb keepalives=1 keepalives_idle=30" \
-c "SELECT pg_sleep(300); SELECT 'still alive';"
# Theo dõi các kết nối đang hoạt động trong lúc test:
psql -c "SELECT pid, state, wait_event, query_start, state_change \
FROM pg_stat_activity WHERE datname='mydb';"
Nếu bạn nhận được still alive thay vì lỗi SSL EOF, cách sửa đã hiệu quả.
Mẹo Thêm
Làm việc trong môi trường cloud còn thêm một lớp phức tạp nữa — subnet, security group, và NACL đều ảnh hưởng đến việc các gói keepalive probe có đến được database hay không. Nếu bạn cần kiểm tra nhanh dải IP hoặc xác minh hai host có cùng network segment không, Subnet Calculator trên ToolCraft rất tiện cho việc này — chạy hoàn toàn trên trình duyệt, không gửi dữ liệu ra ngoài.
Thêm một điều đáng đưa vào monitoring: cảnh báo khi pg_stat_activity cho thấy số lượng kết nối idle tăng đột biến kèm theo state_change kéo dài. Pattern này thường xuất hiện trước 5–10 phút khi các kết nối bắt đầu bị ngắt với lỗi EOF — phát hiện sớm sẽ tránh được rất nhiều tình huống hỗn loạn.

