Tại sao lỗi này xảy ra
Khi thấy thông báo ERROR: cannot execute INSERT in a read-only transaction, điều đó thường có nghĩa là ứng dụng của bạn đang cố gắng ghi dữ liệu vào một node cơ sở dữ liệu không được phép thực hiện thao tác đó. Trong các thiết lập đám mây hiện đại, lỗi này hầu như luôn xảy ra do ứng dụng vô tình kết nối với một Standby (Replica) thay vì node Primary (Writer).
Các bản sao (replica) của PostgreSQL sử dụng chế độ 'hot standby'. Điều này cho phép chúng xử lý lưu lượng SELECT lớn để giảm tải cho máy chủ chính. Tuy nhiên, chúng chặn nghiêm ngặt các lệnh INSERT, UPDATE, hoặc DELETE để giữ cho dữ liệu nhất quán với nguồn chính.
Bước 1: Kiểm tra nhanh vai trò của Server
Đừng đoán xem bạn đang ở trên máy chủ nào. Bạn có thể hỏi trực tiếp PostgreSQL xem nó có đang ở chế độ phục hồi (recovery mode) hay không. Đây là cách nhanh nhất để xác nhận xem bạn có đang truy cập vào một replica hay không.
Thực thi truy vấn này trong công cụ SQL của bạn (như psql hoặc DBeaver):
SELECT pg_is_in_recovery();
- f (false): Bạn đang ở node Primary. Nếu việc ghi vẫn thất bại, có thể session hoặc cơ sở dữ liệu của bạn đã được thiết lập rõ ràng ở chế độ chỉ đọc (read-only).
- t (true): Bạn đang ở một Replica. Bạn cần trỏ kết nối của mình sang nơi khác.
Bước 2: Kiểm tra các Endpoint kết nối
Nếu kết quả kiểm tra ở trên trả về t, ứng dụng của bạn đang gõ nhầm cửa. Hãy kiểm tra các tệp .env, application.yml, hoặc Kubernetes ConfigMaps để xem URL máy chủ cơ sở dữ liệu.
Nền tảng đám mây (AWS RDS / Aurora)
Các dịch vụ được quản lý thường cung cấp hai URL riêng biệt. Ví dụ, trong AWS Aurora, bạn có thể thấy:
- Writer Endpoint:
mydb.cluster-xyz.us-east-1.rds.amazonaws.com(Sử dụng cái này cho ứng dụng chính của bạn). - Reader Endpoint:
mydb.cluster-ro-xyz.us-east-1.rds.amazonaws.com(Chỉ sử dụng cái này cho báo cáo hoặc phân tích).
Hãy đảm bảo các microservice có nhiều tác vụ ghi không vô tình sử dụng địa chỉ Reader.
Bước 3: Sử dụng định tuyến thông minh trong chuỗi kết nối
Nếu bạn sử dụng các driver dựa trên libpq—phổ biến trong Python, Go và Node.js—bạn có thể để client tự tìm node writer cho bạn. Hãy liệt kê nhiều host và thêm tham số target_session_attrs.
Cập nhật chuỗi kết nối của bạn trông giống như thế này:
postgresql://user:pass@host-a:5432,host-b:5432/dbname?target_session_attrs=read-write
Client sẽ thử host-a trước. Nếu nó nhận thấy đó là một replica, nó sẽ tự động nhảy sang host-b để kiểm tra node có quyền ghi. Điều này giúp ngăn chặn thời gian gián đoạn (downtime) trong quá trình failover cơ sở dữ liệu.
Bước 4: Sửa cấu hình PgBouncer
Nếu bạn sử dụng PgBouncer để quản lý kết nối (connection pooling), lỗi có thể nằm trong tệp pgbouncer.ini. Việc định nghĩa các pool riêng biệt cho các vai trò khác nhau là rất phổ biến. Một cấu hình sai có thể trông như thế này:
[databases]
# Ứng dụng nên truy cập 'primary_db', không phải 'analytics_db'
primary_db = host=10.0.0.1 port=5432 dbname=prod
analytics_db = host=10.0.0.2 port=5432 dbname=prod
Kiểm tra kỹ xem cổng cơ sở dữ liệu của ứng dụng (thường là 6432 đối với PgBouncer) có đang định tuyến đến đúng bí danh (alias) hay không.
Bước 5: Kiểm tra các cờ Read-Only ở cấp độ mã nguồn
Đôi khi phần cứng vẫn ổn, nhưng phần mềm lại tự giới hạn chính nó. Ví dụ trong Java Spring Boot, một lập trình viên có thể đã thêm @Transactional(readOnly = true) vào một phương thức service mà giờ đây cần thực hiện thao tác ghi.
Bạn có thể kiểm tra trạng thái của session hiện tại bằng lệnh sau:
SHOW default_transaction_read_only;
Nếu kết quả trả về là on, nhưng pg_is_in_recovery() là false, có thể ai đó đã hạn chế quyền người dùng hoặc cấu hình toàn cục. Bạn có thể buộc tắt nó cho session hiện tại bằng cách sử dụng:
SET default_transaction_read_only = off;
Cách xác minh bản sửa lỗi
Khi bạn tin rằng việc định tuyến đã được khắc phục, hãy chạy một thử nghiệm thủ công trên chính chuỗi kết nối mà ứng dụng của bạn sử dụng. Tạo một bảng tạm nhỏ để chắc chắn:
CREATE TABLE connection_test (id serial PRIMARY KEY, test_val text);
INSERT INTO connection_test (test_val) VALUES ('success');
DROP TABLE connection_test;
Nếu lệnh INSERT đó hoạt động, ứng dụng của bạn cuối cùng đã kết nối đúng node.
Phòng ngừa chủ động
- DNS rõ ràng: Sử dụng các tên tường minh như
db-writer.internalvàdb-reader.internalthay vì địa chỉ IP thô. - Giám sát: Thiết lập cảnh báo trong Prometheus hoặc CloudWatch để kích hoạt nếu trạng thái
pg_is_in_recoverycủa node primary chuyển sangtrue. - Nhận thức Framework: Đảm bảo ORM của bạn không mặc định các transaction là read-only để "tối ưu hóa" hiệu suất.

