SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-21

Connection pooling Postgres trên VPS 4 GB: khi nào cần?

VPS 4 GB có thể hết RAM trước khi chạm max_connections vì mỗi connection là một process. Xem pooler sửa gì, phá gì và vì sao OOM làm Postgres restart.

Vì sao một VPS hết RAM trước khi chạm đến max_connections

Connection pooling cho Postgres trên VPS không phải là mẹo tăng tốc. Đây là cách giữ cho máy 4 GB hoạt động ổn định, vì mỗi connection PostgreSQL là một process riêng của operating system và giữ phần memory riêng. Pooler đặt một số lượng nhỏ, cố định các backend process thực phía sau một số lượng lớn connection từ client với chi phí thấp.

Giá trị mặc định của max_connections là 100. Đây là một giới hạn, không phải ngân sách memory. PostgreSQL không kiểm tra xem máy của bạn có thực sự đủ khả năng chạy 100 backend đang xử lý query hay không, nên máy thường fail trước. OOM killer của kernel sẽ chọn một process. Nếu process được chọn là backend, PostgreSQL sẽ restart toàn bộ cluster để bảo đảm shared memory an toàn. Log hiển thị server process (PID 1234) was terminated by signal 9: Killed, sau đó là terminating any other active server processes. Mọi connection đang mở đều bị ngắt, kể cả các connection vẫn hoạt động bình thường.

Máy hết memory vì mỗi connection là một process, đồng thời work_mem được cấp cho từng thao tác sort hoặc hash chứ không phải cho từng connection. Hai yếu tố này đều làm mức sử dụng memory tăng theo cấp số nhân.

Mỗi kết nối là một process và mỗi process đều tốn memory

PostgreSQL dùng một process cho mỗi kết nối. postmaster fork một backend khi client kết nối, và backend đó tồn tại cho đến khi client ngắt kết nối. Đây không phải là thread. Nó có page table riêng, catalog cache riêng và cached query plan riêng. Các cache này tăng dần khi kết nối truy cập thêm bảng và chạy thêm query khác nhau. Vì vậy, một kết nối tồn tại lâu trong ứng dụng ORM bận rộn sẽ tốn nhiều memory hơn một kết nối mới.

Shared memory thực sự được dùng chung. shared_buffers là một vùng cấp phát cho toàn bộ cluster và được map vào mọi backend. Private memory không được dùng chung. Vì vậy, top có thể gây hiểu nhầm trong trường hợp này: resident set size (RSS) của một backend bao gồm các shared page mà backend đó đã truy cập. Do đó, cộng RSS của 50 backend sẽ tính shared_buffers 50 lần.

Thay vào đó, hãy đo phần private. PSS (proportional set size) chia mỗi shared page theo số process đang map page đó. USS (unique set size) chỉ tính các page thuộc riêng process đó.

sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'

Cột USS là lượng memory sẽ được giải phóng nếu backend đó thoát. Đây mới là chi phí thực của mỗi kết nối. Các số liệu được công bố thường cho thấy một backend idle dùng vài MB, còn backend đã chạy các query ORM lớn có thể dùng nhiều hơn vài lần mức đó. Hãy xem đây là các số liệu điển hình được công bố, không phải số liệu của hệ thống bạn. Con số duy nhất đáng dùng để lập kế hoạch là số đo từ chính máy chủ của bạn với workload thực tế.

Có một vùng memory cấp phát cho mỗi session rất dễ bị bỏ sót. temp_buffers mặc định là 8MB và được cấp phát cho từng session vào lần đầu session đó truy cập temporary table. Vùng này không được trả lại cho đến khi session kết thúc.

work_mem được cấp cho từng operation, không phải từng connection

Đây là chỗ nhiều người tính sai. work_mem mặc định là 4MB, và tài liệu PostgreSQL nói rõ ý nghĩa của việc này: “một query phức tạp có thể thực hiện đồng thời nhiều operation sort và hash, trong đó mỗi operation thường được phép sử dụng lượng memory tối đa bằng giá trị này trước khi bắt đầu ghi dữ liệu vào temporary file.” Một plan có ba sort node có thể dùng lượng work_mem gấp ba trong cùng một backend, tại cùng một thời điểm.

Hash operation được cấp nhiều hơn. hash_mem_multiplier mặc định là 2.0, nên một hash join hoặc hash aggregate có thể dùng work_mem nhân hai, tức 8MB với các setting mặc định. Parallel query tiếp tục nhân lượng memory này, vì mỗi parallel worker là một process khác có allowance riêng.

Hãy tính thử với một VPS 4 GB. Đặt shared_buffers thành 1 GB, giữ work_mem ở mức 4MB, rồi cho 100 connection chạy mỗi query có hai hash node. Khi đó là 100 nhân 16MB, tức 1.6 GB private memory, cộng thêm 1 GB shared buffer, chưa tính page cache và các thành phần khác trên máy. Bây giờ tăng work_mem lên 64MB vì server còn dư RAM. Cùng 100 connection khi đó tương đương 100 nhân 256MB. Không có cảnh báo nào. Bạn chỉ phát hiện ra khi OOM killer xử lý.

Bạn có thể kiểm tra work_mem có quá nhỏ hay không thay vì đoán. Đặt log_temp_files = 0 trong postgresql.conf rồi reload. Mỗi lần spill ra disk sẽ ghi một dòng chứa tên file và kích thước của file, chẳng hạn temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Spill thường xuyên cho thấy việc tăng work_mem có thể hữu ích. Nếu không có spill, việc tăng giá trị này không đem lại lợi ích mà chỉ tiêu tốn lượng memory bạn không có.

Phép tính pool thực sự gây sự cố

Không ai chủ động mở 240 connection. Họ cấu hình pool có 20 connection, rồi chạy application ở nhiều nơi.

ChartBackends requested per app topology, pool size 20 per worker (arithmetic)
The data behind this chart
[
  {
    "config": "1 worker",
    "backends": 20
  },
  {
    "config": "4 web workers",
    "backends": 80
  },
  {
    "config": "4 web + 2 background",
    "backends": 120
  },
  {
    "config": "2 hosts x 4 workers",
    "backends": 160
  },
  {
    "config": "3 hosts x 4 workers",
    "backends": 240
  }
]

Bốn Gunicorn worker, mỗi worker giữ một pool có 20 connection, yêu cầu 80 backend. Thêm hai background job worker thì con số này là 120. Tăng lên 3 hosts x 4 workers, application sẽ yêu cầu 240 backend trên tổng max_connections là 100. Không có setup nào trong số 5 setup đó bị cấu hình sai tại một vị trí riêng lẻ. Pool được tạo riêng cho từng process, và không thành phần nào của application nhìn thấy tổng số connection.

Các giá trị mặc định của library cũng đẩy hệ thống theo hướng này. QueuePool của SQLAlchemy mặc định là pool_size=5 với max_overflow=10, tức 15 connection cho mỗi process. HikariCP mặc định là 10. Django không có pool tích hợp trước phiên bản 5.1 và dùng một connection cho mỗi worker process. Vì vậy, các application Django thường chỉ gặp vấn đề này về sau, rồi gặp đồng loạt khi ai đó đặt CONN_MAX_AGE hoặc bật tùy chọn "pool": True mới hơn. Nếu bạn chạy application Django phía sau Gunicorn và nginx, số cần nhân là số Gunicorn worker, không phải số server.

Thực tế connection pooling cho Postgres trên VPS thay đổi điều gì

Pooler là một tiến trình giao tiếp với ứng dụng bằng PostgreSQL wire protocol ở một phía và duy trì một nhóm nhỏ các connection thực đến server ở phía kia. Pooler không làm query chạy nhanh hơn. Nó thay đổi bên chịu chi phí tạo connection và số lượng backend thực sự tồn tại.

Có 2 điểm được cải thiện. Việc kết nối không còn tốn chi phí fork cùng các lần tra cứu catalog để làm đầy cache của backend mới, vì pooler tự xử lý connect của client. Quan trọng hơn, số lượng backend thực không còn tỷ lệ trực tiếp với số connection của ứng dụng. Vì vậy, 500 client có thể dùng chung 20 backend.

Việc phải chờ chính là tính năng, và đây là điểm nhiều người không thích. Không có pooler, 500 query chạy đồng thời đều được cấp một backend và cùng chạy trên 2 CPU core. Vì vậy, query nào cũng chậm và toàn bộ memory bị sử dụng cùng lúc. Có pooler, 20 query chạy còn số còn lại chờ vài mili giây. Mỗi query đang chạy nhận được phần CPU thực tế phù hợp và hoàn tất sớm hơn. Một queue phía trước pool nhỏ hiệu quả hơn việc không có queue phía trước pool lớn.

Pooler không giới hạn những tài nguyên khác trên máy. Nếu Postgres dùng chung VPS với app server hoặc với một vector database trên cùng VPS, pooler chỉ bảo vệ Postgres khỏi ứng dụng của bạn, không làm gì hơn. Hãy đặt hard ceiling cho các service lân cận: bạn có thể giới hạn memory và CPU mà một service được phép sử dụng bằng systemd để một tiến trình chạy mất kiểm soát không kéo database sập theo. Vị trí chạy chính database sẽ quyết định cách bạn đặt các giới hạn này. Đây là khác biệt thực tế giữa chạy Postgres trong Docker hoặc chạy trực tiếp trên host.

Phân biệt session pooling và transaction pooling

Một thiết lập quyết định mọi thứ còn lại, đó là pool_mode.

Trong session pooling, một kết nối đến server được cấp cho client trong toàn bộ thời gian tồn tại của kết nối client đó và được giải phóng khi client ngắt kết nối. Mọi thứ đều hoạt động vì pooler chỉ là một proxy thông thường. Bạn tiết kiệm chi phí thiết lập kết nối, ngoài ra không có lợi ích nào khác. Nếu ứng dụng mở 200 kết nối, bạn vẫn cần 200 backend.

Trong transaction pooling, một kết nối đến server chỉ được cấp cho client trong thời gian của một transaction. Tại COMMIT hoặc ROLLBACK, kết nối đó được trả về pool và client tiếp theo đang chờ sẽ nhận nó. Đây là cơ chế biến 500 client thành 20 backend. Đây cũng là nguyên nhân làm hỏng một số chức năng, và đó là hành vi có chủ ý: statement tiếp theo có thể chạy trên một backend khác với backend đã chạy statement trước đó.

Giá trị mặc định của PgBouncer là pool_mode = session. Cài đặt nó mà không thay đổi gì sẽ chỉ giúp bạn có phần tiết kiệm chi phí, nhưng không có lợi ích chính. Chế độ thứ ba, statement, trả kết nối về pool sau từng statement và từ chối các transaction có nhiều statement. Không nên dùng chế độ này trừ khi bạn biết chính xác lý do cần dùng.

Những gì bị lỗi trong transaction mode và lý do

Mọi thứ dưới đây đều lỗi vì cùng một lý do. Đó là state nằm bên trong một backend duy nhất, trong khi transaction pooling không đảm bảo bạn sẽ nhận lại cùng backend.

  • SETRESET ở session level. SET search_path, SET statement_timeout, SET TIME ZONESET ROLE được thực thi trên backend đã phục vụ statement đó và sẽ mất trước transaction tiếp theo của bạn. Hãy dùng SET LOCAL bên trong một explicit transaction. Nó được giới hạn trong transaction đó nên an toàn.
  • LISTEN. Việc gửi notification thuộc về backend đã chạy LISTEN, và backend đó được giao cho client khác ngay khi transaction kết thúc. NOTIFY vẫn hoạt động trong transaction mode, nên lỗi này khó nhận ra: gửi thành công nhưng không bao giờ nhận được. Nếu cần LISTEN, hãy mở thêm một connection trực tiếp đến port 5432 để bỏ qua pooler.
  • Advisory lock ở session level. pg_advisory_lock() được giữ bởi session và được giải phóng khi session kết thúc. Khi dùng transaction pooling, lệnh unlock của bạn chạy trên backend khác, nên lock vẫn bị giữ cho đến khi PgBouncer thu hồi server connection đó. Theo mặc định, việc này xảy ra sau server_lifetime, tức 1 giờ. Hãy dùng pg_advisory_xact_lock(). Lock này được backend đã tạo nó giải phóng khi transaction kết thúc.
  • PREPAREDEALLOCATE, là các SQL statement. Chúng không bao giờ khả dụng trong transaction mode.
  • Các cursor WITH HOLD và mọi server-side cursor được kỳ vọng vẫn tồn tại sau khi transaction kết thúc.
  • Temporary table cần tồn tại qua commit. CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWS đặt table vào temporary schema của một backend, nhưng transaction tiếp theo của bạn có thể chạy trên backend khác.
  • LOAD.

Protocol-level prepared statement là trường hợp duy nhất đã thay đổi. PgBouncer 1.21.0 bổ sung hỗ trợ cho chúng trong transaction mode. PgBouncer 1.24.0 bật tính năng này theo mặc định bằng cách đặt max_prepared_statements thành 200. Các bản build cũ giữ giá trị này ở 0, tức là tắt. Ubuntu 24.04 đi kèm PgBouncer 1.22.0, nên tính năng đã có nhưng bạn phải tự đặt max_prepared_statements. Nếu không chắc bản build của mình đang hoạt động thế nào, hãy dùng thiết lập an toàn ở phía client: psycopg 3 sẽ ngừng sử dụng server-side prepared statement khi bạn đặt prepare_threshold thành None.

Django có cách gọi riêng cho vấn đề này. Tài liệu ghi rằng “dùng connection pooler ở transaction pooling mode (ví dụ PgBouncer) yêu cầu tắt server-side cursor cho connection đó”, vì “server-side cursor chỉ có thể truy cập trên connection đã tạo ra nó”. Đặt DISABLE_SERVER_SIDE_CURSORS thành True trong entry của database đó. Nếu không, mọi lệnh gọi .iterator() sẽ lỗi không ổn định và chỉ xuất hiện khi có tải.

Transaction mode đáng để sử dụng, nhưng đây là một contract. Hãy đọc danh sách này, kiểm tra ORM và background job library của bạn để bảo đảm tương thích, rồi chuyển sang sử dụng.

Cài đặt PgBouncer và trỏ ứng dụng vào đó

Cấu hình dưới đây dùng để chạy trên server của bạn: Ubuntu 24.04, trong đó PostgreSQL đã listening trên 127.0.0.1 port 5432.

sudo apt update
sudo apt install -y pgbouncer
pgbouncer --version

Ubuntu 24.04 cung cấp PgBouncer 1.22.0. Tính đến August 2026, upstream đang ở bản 1.25.2. Hãy kiểm tra bạn đang dùng bản nào, vì hành vi của prepared statement ở phần trên phụ thuộc vào phiên bản này.

Tạo một role chỉ dùng để đăng nhập vào admin console của PgBouncer, sau đó tạo password file. PgBouncer cần các SCRAM (salted challenge response authentication mechanism) secret trong pg_authid, và chỉ superuser mới đọc được table đó.

sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
  'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
  > /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txt

Việc sao chép secret thay vì nhập lại password là yếu tố giúp cách này hoạt động. PgBouncer chỉ có thể dùng lại SCRAM secret để đăng nhập vào PostgreSQL khi client cũng đã xác thực bằng SCRAM, khi secret trong file giống từng byte với secret trong pg_authid (cùng salt và iteration count, không chỉ cùng password), và khi dòng [databases] không cố định một user=. Thêm user=appuser vào dòng đó thì PgBouncer sẽ cần plaintext password. Xác nhận owner của file khớp với account mà service chạy bằng systemctl show pgbouncer -p User. Khi đổi password trong PostgreSQL, bạn phải tạo lại file này. Nếu không, lần connect tiếp theo sẽ trả về password authentication failed.

Bây giờ viết /etc/pgbouncer/pgbouncer.ini.

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digits

listen_addr = 127.0.0.1 giữ pooler ngoài public Internet. Điều này quan trọng vì pooler có thể truy cập từ bên ngoài là một authentication endpoint mà bạn không định public. max_client_conn là số application connection mà PgBouncer chấp nhận, và chi phí của chúng thấp nên có thể đặt lớn. default_pool_size là số backend thực mà một cặp database và user có thể giữ, và đây mới là giới hạn tốn tài nguyên. max_db_connections giới hạn toàn bộ database ở mức 80, chừa khoảng dưới max_connections cho psql, backup và monitoring. ignore_startup_parameters = extra_float_digits ngăn PgBouncer từ chối các driver, trong đó có JDBC driver, gửi parameter đó ngay khi connect.

sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pager

Nếu khởi động thành công, log sẽ có một dòng cho biết PgBouncer đang listening trên 127.0.0.1:6432. Nếu khởi động thất bại do password file, log sẽ ghi path mà PgBouncer không thể đọc. Nguyên nhân gần như luôn là mode hoặc ownership, không phải lỗi cú pháp. Sau đó đổi connection string của ứng dụng từ port 5432 sang port 6432 và restart ứng dụng. Không cần thay đổi phần nào khác trong ứng dụng.

Cách kiểm tra pool có hoạt động đúng không

PgBouncer có admin console được truy cập thông qua một virtual database có tên pgbouncer.

psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncer
SHOW POOLS;
SHOW STATS;
SHOW CLIENTS;

SHOW POOLS là chỉ số cần theo dõi. cl_active là số client hiện đang gắn với một server connection, cl_waiting là số client đang xếp hàng chờ connection, sv_activesv_idle là số backend thực tế đang được sử dụng và đang rảnh, còn maxwait là thời gian client ở đầu hàng đợi đã phải chờ, tính bằng giây. Ở mức tải bình thường, trạng thái ổn định nghĩa là cl_waiting bằng 0 và maxwait bằng 0. Nếu maxwait tăng quá một hoặc hai giây, pool có thể quá nhỏ hoặc query quá chậm. Hai nguyên nhân này cần cách xử lý khác nhau.

Hãy kiểm tra chỉ số này trước khi tăng default_pool_size.

SELECT state, count(*) FROM pg_stat_activity
  WHERE backend_type = 'client backend' GROUP BY state;

Nếu phần lớn backend ở trạng thái idle in transaction, kích thước pool không phải vấn đề. Ứng dụng đang mở một transaction rồi thực hiện thao tác chậm bên trong, chẳng hạn gọi HTTP, nên mỗi backend bị giữ lại mà không chạy query. idle_in_transaction_session_timeout sẽ ngắt các transaction đó, nhưng cách sửa thực sự nằm ở code của ứng dụng. Nếu ngược lại, toàn bộ backend đều ở trạng thái active, pool thực sự đã bão hòa. Khi đó nên tối ưu query bằng EXPLAIN (ANALYZE, BUFFERS) trước khi cấp thêm connection.

Về việc định cỡ, một điểm bắt đầu được nhắc đến nhiều nhất là heuristic của HikariCP: xấp xỉ gấp đôi số core cộng một. Với VPS 2 core, giá trị này là 5. Hãy xem đó là giá trị khởi điểm được công bố, đặt default_pool_size gần mức này, rồi điều chỉnh dựa trên maxwait. Pool nhỏ có vẻ không hợp lý nhưng thường cho kết quả đo tốt hơn, vì một backend đang xếp hàng không tốn tài nguyên, còn một backend đang chạy sẽ tiêu tốn CPU, memory và lock contention.

Chọn giữa PgBouncer, PgDog và Pgpool-II

PgBouncer là lựa chọn cho trường hợp thông thường: một PostgreSQL server, một VPS và một ứng dụng mở nhiều connection hơn khả năng máy chủ có thể giữ. Nó chỉ làm một việc, cấu hình nằm trong một file ini duy nhất, và có package cho Debian và Ubuntu. Nó xử lý connection bằng một thread duy nhất. Cách này đủ cho workload cỡ VPS và chỉ trở thành giới hạn trên các máy lớn hơn nhiều.

PgDog đáng xem xét khi quyết định định tuyến cần diễn ra trong cùng network hop với pooling. Dự án tự mô tả là một proxy để scale PostgreSQL, được viết bằng Rust, hỗ trợ transaction pooling và session pooling, read/write splitting bằng cách parse query, cùng sharding với multi-shard routing và two-phase commit. Hãy chọn nó khi bạn có một primary và một hoặc nhiều replica, đồng thời muốn gửi read request đến replica mà không phải để ứng dụng biết replica tồn tại. Có 2 điểm cần lưu ý. PgDog dùng giấy phép AGPLv3, vì vậy điều khoản network-use là vấn đề giấy phép cần được thống nhất với người chịu trách nhiệm quyết định đó tại công ty trước khi đưa vào production; quan điểm của chính dự án là việc sử dụng nội bộ và sửa đổi riêng tư không tạo ra nghĩa vụ cung cấp source. Dự án cũng còn mới, có release hằng tuần và số version 0.x, nên hãy pin vào một release tag thay vì theo main.

git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.toml

Build từ source cần Rust toolchain stable hiện tại, CMake và compiler C/C++. Trang releases cũng có binary Linux dựng sẵn và package Debian, cùng một container image tại ghcr.io/pgdogdev/pgdog. Cấu hình được chia thành 2 file. File đầu tiên chứa các setting chung và một entry cho mỗi database. Ở đây, các entry được viết dưới dạng TOML array gồm các inline table để dễ phân biệt 2 dạng này.

databases = [
  { name = "appdb", host = "127.0.0.1" },
]

[general]
port = 6432
default_pool_size = 10

File thứ 2 chứa một entry cho mỗi user, cũng dùng cùng dạng array.

users = [
  { name = "appuser", database = "appdb", password = "change-this" },
]

Theo mặc định, PgDog listen trên cổng 6432, cùng cổng với PgBouncer, nên 2 phần mềm này không thể cùng bind cổng mặc định trên một host.

Pgpool-II, ở version 4.7.2 tính đến tháng 6 năm 2026, hỗ trợ pooling và load balancing, cùng watchdog để tự động failover. Các tính năng bổ sung kéo theo thêm các failure mode, và cần hiểu rõ mô hình pooling của nó trước khi chọn. Pgpool-II pre-fork num_init_children child process, mỗi child cache tối đa max_pool server connection, nên giới hạn số backend là num_init_children nhân với max_pool. Mỗi child phục vụ một client tại một thời điểm, nên số client có thể accept bằng num_init_children và được cố định khi khởi động. Một client idle vẫn chiếm một child. Nếu đặt num_init_children là 100 và max_pool là 4, bạn đã cho phép 400 backend. Đây chính xác là vấn đề mà bạn cài pooler để giải quyết. Hãy chọn Pgpool-II khi cần failover và query routing của nó, rồi tính phép nhân đó thật cẩn thận. Nếu mục tiêu duy nhất là giảm số backend, Pgpool-II có nhiều thành phần hơn mức cần thiết cho công việc này.

Câu hỏi về proxy managed và phương án tương đương khi tự host

Các nền tảng managed bán tính năng này như một sản phẩm riêng. AWS đặt RDS Proxy trước RDS, còn Supabase đặt pooler riêng của họ, Supavisor, trước Supabase Postgres. Cả hai đều thực hiện đúng công việc được mô tả ở đây: giữ các client connection với chi phí thấp và phân phối một số lượng nhỏ hơn các backend thực tế. Supavisor là open source và có thể tự host, nên lựa chọn này không phải là proprietary đối lập với miễn phí.

Phương án tương đương khi tự host không phải là một ý tưởng khác. Đó là cùng một ý tưởng, nhưng file cấu hình nằm trong tay bạn: PgBouncer chạy ở transaction mode, trên cùng VPS với database và listening trên 127.0.0.1. Có 2 khác biệt thực sự. Managed proxy nằm cách database một network hop, nên tạo thêm latency và tiếp tục giữ các client connection trong khi database restart bên dưới nó. PgBouncer trên chính host của database chỉ thêm một loopback hop, gần như không đáng kể, và sẽ dừng khi host đó dừng. Nếu muốn có khả năng tồn tại qua một lần restart, bạn cũng cần cơ chế failover. Khi đó, watchdog của Pgpool-II hoặc health check của PgDog mới bắt đầu đáng với độ phức tạp của chúng.

Còn một lựa chọn khác cũng cần được đưa vào danh sách. Nếu connection count là nguyên nhân chính khiến deployment của bạn phức tạp, embedded database không có connection model để pool, vì nó là một library nằm trong process của bạn thay vì một server listening trên một port. Với một application server đơn lẻ có write volume vừa phải, chạy SQLite trong production trên VPS sẽ loại bỏ toàn bộ vấn đề này thay vì buộc bạn phải quản lý nó. Khi thực sự cần một server, hãy xác định kích thước pool trước khi xác định cấu hình máy chủ.

FAQ

Tôi vẫn cần PgBouncer nếu ứng dụng đã có connection pool phải không?

Thông thường là có, vì pool của ứng dụng chạy theo từng process và không nhìn thấy các process khác. 4 worker Gunicorn, mỗi worker giữ một pool gồm 20 backend 80, sẽ dùng tổng cộng 120 backend nếu thêm 2 background worker. Chỉ PgBouncer nhìn thấy tổng số này và có thể giới hạn nó. Cách bố trí phù hợp là dùng cả hai: pool nhỏ bên trong từng worker để request không phải trả chi phí cho việc tạo kết nối TCP, và PgBouncer chạy ở transaction mode để giới hạn số backend thực sự đứng sau các pool đó.

Chính xác thì điều gì bị hỏng khi chuyển PgBouncer sang transaction mode?

Mọi thứ giữ state trên cùng một backend qua nhiều transaction đều có thể bị hỏng. Các SETRESET ở cấp session, cursor LISTENWITH HOLD, các câu lệnh SQL PREPAREDEALLOCATE, advisory lock ở cấp session, temporary table cần tồn tại sau khi commit, và LOAD. NOTIFY vẫn hoạt động, khiến lỗi LISTEN trông giống lỗi delivery hơn là lỗi pooling. Với Django, đặt DISABLE_SERVER_SIDE_CURSORS thành True. Với psycopg 3, đặt prepare_threshold thành None hoặc chạy PgBouncer 1.22 trở lên với max_prepared_statements lớn hơn 0. Thay pg_advisory_lock() bằng pg_advisory_xact_lock().

default_pool_size nên là bao nhiêu trên VPS 2 core?

Nhỏ hơn mức bạn nghĩ là phù hợp. Heuristic HikariCP được công bố rộng rãi là khoảng gấp đôi số core cộng 1, tức khoảng 5 trên máy có 2 core. Đây chỉ là điểm bắt đầu, không phải giá trị cố định. Hãy đặt giá trị này, sau đó đọc maxwaitcl_waiting trong SHOW POOLS khi hệ thống chịu tải thực tế. Nếu cả hai đều bằng 0 thì pool đã đủ lớn. maxwait tăng cho biết client đang phải xếp hàng. Trước khi tăng giá trị, hãy kiểm tra pg_stat_activity: các backend bị kẹt trong idle in transaction là lỗi của ứng dụng, và thêm connection chỉ che giấu lỗi đó.

PgBouncer hay PgDog?

Dùng PgBouncer khi có một PostgreSQL server trên một VPS. Đây là mô hình của phần lớn deployment. PgBouncer có package trong Ubuntu, hành vi được document rõ, và toàn bộ cấu hình nằm trong một file ini. Dùng PgDog khi read/write splitting giữa các replica hoặc sharding cần nằm cùng hop với pooling, để ứng dụng không phải biết topology. Trước khi chọn PgDog, hãy làm rõ vấn đề AGPLv3 với người phụ trách licensing tại nơi bạn làm việc, đồng thời pin một release cụ thể, vì project vẫn dùng version 0.x và release hàng tuần.

#postgres#pgbouncer#pgdog#connections#hiệu năng