Vấn đềBạn vừa triển khai bản vá scaling bằng cách thêm PgBouncer, và mọi thứ trông hoàn hảo trong năm phút đầu. Rồi đột nhiên, log bắt đầu la hét với lỗi này:
ERROR: prepared statement "S_1" already exists
Lỗi này thường xảy ra khi bạn chuyển connection pooler sang chế độ transaction. Đây là lỗi kinh điển "chạy được trên máy tôi" vì nó hiếm khi xuất hiện trong quá trình phát triển local. Bạn chỉ thấy nó khi đạt đến một ngưỡng concurrency nhất định—thường chỉ khoảng 10–20 request mỗi giây—khi nhiều client bắt đầu dùng chung các kết nối database backend.
Tại sao điều này xảy raNguyên nhân gốc rễ là sự bất đồng cơ bản giữa ORM của ứng dụng và connection pooler về việc ai "sở hữu" một phiên database.
Sự xung độtMột prepared statement là đối tượng phía server được lưu trong bộ nhớ của một kết nối cụ thể. Khi ORM gửi một truy vấn, nó thường "chuẩn bị" truy vấn đó trước để tiết kiệm thời gian cho các lần thực thi sau. PostgreSQL đặt tên cho kế hoạch này, ví dụ như S_1.
Trong chế độ transaction, PgBouncer giống như một người phục vụ bận rộn. Nó cho bạn một bàn (kết nối) cho một món (transaction), rồi ngay lập tức đưa bàn đó cho khách khác. Nhưng người phục vụ không dọn bàn. Đây là cách crash xảy ra:
- App Instance A lấy một kết nối, tạo statement
S_1, hoàn thành công việc rồi rời đi.- PgBouncer đưa kết nối đó trở lại pool, nhưngS_1vẫn còn nằm trong bộ nhớ của kết nối đó.- App Instance B lấy đúng kết nối đó. Nó cố định nghĩa statementS_1của riêng mình.- PostgreSQL thấy tênS_1đã tồn tại và ném ra lỗi, làm hỏng truy vấn của bạn.## Quy trình debugTrước khi thay đổi code, hãy xác nhận cài đặt pooler của bạn. Kiểm tra file cấu hình hoặc chạy lệnh này trên server:
grep "pool_mode" /etc/pgbouncer/pgbouncer.ini
Nếu bạn thấy pool_mode = transaction, bạn đã tìm ra nguồn gốc của xung đột. Chế độ này cho phép bạn xử lý hàng nghìn kết nối chỉ với 50–100 slot database thực tế, nhưng nó phá vỡ giả định rằng một phiên chỉ thuộc về một client.
Các giải pháp### 1. Flag 'pgbouncer' của PrismaNgười dùng Prisma có cách đơn giản nhất. Bạn không cần tắt tính năng nào; chỉ cần báo cho Prisma biết nó không hoạt động một mình. Thêm ?pgbouncer=true vào connection string trong file .env:
DATABASE_URL="postgresql://user:pass@db-host:6432/mydb?pgbouncer=true"
Flag này buộc Prisma dùng anonymous prepared statements. Vì chúng không có tên như S_1, chúng không thể xung đột.
2. Tắt Prepared Statements trong ORMVới hầu hết ứng dụng, lợi ích hiệu năng từ prepared statements thường chỉ dưới 3–5%. Hiếm khi đáng đánh đổi sự ổn định. Tắt chúng ở tầng ORM là giải pháp bền vững nhất.
- TypeORM: Đặt
prepareThreshold: 0trong tùy chọn data source.- Sequelize: ĐặtminifyAliases: truetrong cấu hình.- Go (pgx): ĐặtPreferSimpleProtocol: truetrong cấu hình kết nối.Đây là cách sửa tiêu chuẩn cho TypeORM:
{
type: "postgres",
host: "localhost",
port: 6432,
extra: {
max_prepared_statements: 0,
prepareThreshold: 0
}
}
3. Giải pháp PgBouncer hiện đại (v1.22.0+)Nếu bạn có thể nâng cấp hạ tầng, PgBouncer 1.22.0 đã giải quyết vấn đề này một cách tự nhiên. Nó có thể theo dõi prepared statements cho bạn. Thêm dòng này vào pgbouncer.ini:
max_prepared_statements = 100
Điều này cho phép PgBouncer xử lý việc dọn dẹp một cách minh bạch. Nếu bạn đang dùng LTS cũ như Ubuntu 22.04, bạn có thể có phiên bản 1.16, vì vậy hãy dùng cách sửa ở tầng ORM thay thế.
4. Giải pháp cuối cùng: Session ModeNếu ứng dụng của bạn phụ thuộc nhiều vào trạng thái session và bạn không thể thay đổi code, hãy chuyển sang chế độ session:
[pgbouncer]
pool_mode = session
Lưu ý: Điều này loại bỏ lợi ích "multiplexing". Nếu bạn có 500 app instance mà chỉ có 100 kết nối DB, 400 instance trong số đó sẽ không thể kết nối ngay lập tức.
Kiểm traSau khi áp dụng bản sửa, đừng chỉ hy vọng nó hoạt động. Theo dõi log trong một bài load test ngắn. Dùng công cụ như wrk để tấn công endpoint tốn nhiều database nhất:
wrk -t12 -c40 -d30s http://localhost:3000/api/users
Trong khi đó chạy, theo dõi log của Postgres:
tail -f /var/log/postgresql/postgresql-main.log | grep "already exists"
Nếu bài test kết thúc và log im lặng, vấn đề pooling của bạn đã được giải quyết.

