Postgres + pgBouncer + Redis: kiến trúc data layer cho startup 5 dev

Chia sẻ bài viết

Mục lục
Postgres + pgBouncer + Redis: kiến trúc data layer cho startup 5 dev
TL;DR
  • Postgres + pgBouncer + Redis là combo data layer chuẩn cho startup 5 dev: đủ tốt cho 100k-1M user, không cần ngay Cassandra hay Vitess.
  • pgBouncer: connection pooler ở giữa app và Postgres, giảm 100x số connection thật, save RAM Postgres.
  • Redis: cache hot data (session, user profile, rate limit), giảm load Postgres 5-10x, latency dưới 1ms.
  • Topology đề xuất: 1 VPS chính chạy Postgres + pgBouncer + app, 1 VPS Redis dedicated, 1 VPS replica Postgres + backup.
  • Bài này hướng dẫn full: setup mỗi component, config tối ưu, monitor, anti-pattern phải tránh, lộ trình scale khi startup lớn lên.

Startup 5 dev mới ra Series A thường gặp tình huống: data ngày càng lớn, app slow hơn, mỗi sáng có dev hỏi "DB lại chậm rồi". Câu hỏi: nâng cấp database stack thế nào để chịu được 100k user trong 6 tháng tới mà không phải refactor toàn bộ? Câu trả lời thông thường ở thị trường VN không phải Cassandra hay Vitess, mà là combo cổ điển nhưng cực mạnh: Postgres làm source of truth, pgBouncer pool connection, Redis cache hot path.

Bài này hướng dẫn kiến trúc data layer cho startup 5 dev có 1 app web, 1 mobile app, traffic 100-500 req/s ổn định, peak 2-5k req/s. Test trên Cloud VPS 80 (8GB RAM) chạy Postgres 16 + pgBouncer + Redis 7. Có thể chạy 1 VPS cho stage MVP, tách 3 VPS khi production. Phù hợp đa số SaaS Việt Nam đang scale từ side project lên có khách trả phí.

Kiến trúc tổng thể

+------------+    +------------+    +------------+
|   App x 3  |--->|  pgBouncer |--->|  Postgres  |
|  (Node/Py) |    | (6432 port)|    |  (5432)    |
+------------+    +------------+    +------------+
       |
       v
+------------+    +------------+
|    Redis   |    | Postgres   |
|  (6379)    |    |  Replica   |
+------------+    +------------+

App connect cả Postgres (qua pgBouncer) và Redis. Postgres replica chạy read-only và backup. Redis có thể chạy cùng VPS app hoặc VPS riêng tùy traffic.

Bước 1: Cài Postgres 16 với tuning startup

sudo apt install -y postgresql-16
sudo systemctl enable postgresql

Tuning /etc/postgresql/16/main/postgresql.conf cho VPS 8GB:

listen_addresses = '*'
max_connections = 200
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 16MB
maintenance_work_mem = 512MB
wal_buffers = 16MB
max_wal_size = 2GB
checkpoint_completion_target = 0.9
random_page_cost = 1.1
effective_io_concurrency = 200
default_statistics_target = 100
log_min_duration_statement = 1000  # log query > 1s

Restart:

sudo systemctl restart postgresql

max_connections cao (200) nhưng vẫn cần pgBouncer ở giữa. Lý do giải thích bước tiếp.

Bước 2: Vì sao cần pgBouncer?

Mỗi connection Postgres tốn ~5-10MB RAM. Với 200 connection = 1-2GB RAM dedicated chỉ cho connection state. Node.js app dễ tạo connection nhanh hơn close (pool default 10-50 mỗi instance). 5 instance x 50 = 250 connection, vượt max ngay.

pgBouncer làm pool ở giữa: app connect tới pgBouncer (nhẹ, mỗi connection chỉ ~5KB), pgBouncer multiplex tới Postgres với pool nhỏ (25-50 connection thật). Kết quả: 1000 client connection chỉ cần 25 Postgres connection.

Bước 3: Cài pgBouncer

sudo apt install -y pgbouncer

Cấu hình /etc/pgbouncer/pgbouncer.ini:

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

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 5
log_connections = 0
log_disconnections = 0
ignore_startup_parameters = extra_float_digits
admin_users = pgbouncer_admin

Tạo userlist.txt:

sudo -u postgres psql -tAc "SELECT '"'||rolname||'" "'||rolpassword||'"' FROM pg_authid;" 
  | sudo tee /etc/pgbouncer/userlist.txt

Restart:

sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer

App update DATABASE_URL trỏ về port 6432 thay vì 5432:

DATABASE_URL=postgresql://myapp:pass@localhost:6432/myapp_prod

Pool mode: transaction vs session vs statement

  • session (default): client giữ connection cho cả session. An toàn nhưng pool ít hiệu quả.
  • transaction (recommended): connection trả về pool sau mỗi transaction. Hiệu quả cao, nhưng KHÔNG hỗ trợ session-level feature (LISTEN/NOTIFY, prepared statement với name, SET LOCAL).
  • statement: trả về sau mỗi query. Cực hiệu quả nhưng không có transaction multi-statement.

90 phần trăm app web dùng transaction mode. Đảm bảo app không dùng prepared statement có name (Sequelize, Prisma đa số OK; node-pg-prepared cần đổi config).

Bước 4: Cài Redis 7

sudo apt install -y redis-server

Cấu hình /etc/redis/redis.conf:

bind 0.0.0.0
port 6379
requirepass $YOUR_STRONG_PASS
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec

maxmemory 2GB ổn cho VPS 8GB (Postgres lấy 4GB, app lấy 1GB, Redis 2GB, OS 1GB). policy allkeys-lru tự xóa key cũ khi đầy.

Restart:

sudo systemctl restart redis-server
redis-cli -a $YOUR_PASS ping  # PONG

Pattern caching: cache-aside

// Node.js cache-aside example
async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  const user = await pg.query("SELECT * FROM users WHERE id=$1", [id]);
  if (user.rows[0]) {
    await redis.set(`user:${id}`, JSON.stringify(user.rows[0]), "EX", 3600);
  }
  return user.rows[0];
}

async function updateUser(id, data) {
  await pg.query("UPDATE users SET ... WHERE id=$1", [id]);
  await redis.del(`user:${id}`);  // invalidate cache
}

Pattern này phù hợp với 80 phần trăm use case. Cache có TTL (1 giờ ở đây) để tránh stale data. Update DB là invalidate cache, không cần manual sync.

Use case Redis thường gặp

  • Session storage: express-session với connect-redis. Mỗi session 1-2KB, 10k user concurrent = 20MB.
  • Rate limit: INCR + EXPIRE. Block IP gọi quá 100 req/phút.
  • Cache hot query: user profile, product detail, news feed.
  • Queue đơn giản: LPUSH + BRPOP. Hoặc dùng BullMQ wrap Redis cho job queue chuyên nghiệp.
  • Pub/Sub: notify nhiều worker khi có event mới.
  • Leaderboard: ZADD + ZRANGEBYSCORE cho game/contest.
  • Distributed lock: SETNX + EXPIRE, khóa critical section.

Monitoring stack

Postgres metric quan trọng:

-- Connection count
SELECT count(*), state FROM pg_stat_activity GROUP BY state;

-- Slow query (cần pg_stat_statements extension)
SELECT query, total_exec_time, calls
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

-- Cache hit ratio
SELECT sum(blks_hit) * 100.0 / sum(blks_hit + blks_read) AS cache_hit_pct
FROM pg_stat_database;

Cache hit ratio nên trên 95 phần trăm. Dưới đó thì shared_buffers chưa đủ hoặc query không dùng index.

pgBouncer metric:

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

Cột cl_waiting > 0 nghĩa client đang đợi connection từ pool. Tăng default_pool_size nếu xảy ra thường xuyên.

Redis metric:

redis-cli INFO stats
redis-cli INFO memory
redis-cli SLOWLOG GET 10

Theo dõi keyspace_hits / (keyspace_hits + keyspace_misses) để biết cache hiệu quả. Dưới 80 phần trăm là cache strategy có vấn đề.

Anti-pattern phải tránh

  • Cache stampede: nhiều request cùng miss cache, đập DB đồng thời. Fix: distributed lock hoặc probabilistic early expiration.
  • Cache mọi thứ: data không hot thì không nên cache. Lãng phí RAM Redis.
  • TTL quá dài: data stale. Default 1-24 giờ tùy use case.
  • Connection leak app: không close client, dù có pgBouncer cũng max_client_conn 1000.
  • SELECT * trong loop: N+1 query. Dùng JOIN hoặc batch IN.
  • Index thiếu: Postgres slow log + EXPLAIN ANALYZE thường tìm ra.
  • Redis FLUSHALL trên production: wipe toàn bộ cache, traffic đập DB.

Topology cho từng giai đoạn startup

Giai đoạn MVP (0-1000 user)

1 VPS (Cloud VPS 50, 4GB RAM): app + Postgres + pgBouncer + Redis trên cùng máy. Backup S3. Đủ chạy ngon, low cost ~25 USD/tháng.

Giai đoạn Beta (1k-10k user)

1 VPS app + 1 VPS database (Cloud VPS 80, 8GB RAM cho DB). Tách giúp app crash không kéo DB xuống. Redis vẫn chung với DB.

Giai đoạn Growth (10k-100k user)

2 VPS app (load balance) + 1 VPS Postgres primary + 1 VPS Postgres replica + 1 VPS Redis dedicated. Tổng ~5 VPS, chi phí 150-250 USD/tháng.

Giai đoạn Scale (100k+ user)

Cluster Postgres (Patroni), Redis Sentinel hoặc Cluster, app behind LB. Bắt đầu cân nhắc tách microservice. Chi phí 500-2000 USD/tháng.

Migration khi tách Redis ra VPS riêng

  1. Spin up VPS Redis mới, cài và config.
  2. Setup VPN WireGuard giữa app và Redis (encrypt traffic).
  3. Update REDIS_URL trong app, deploy.
  4. App connect Redis mới, key trống.
  5. Cho cache rebuild dần qua traffic. Tránh maintenance window.

Nếu cache lớn không thể rebuild nhanh: dùng SCAN + DUMP/RESTORE script migrate qua. Hoặc replication Redis với MIGRATE command.

Backup combo

  • Postgres: pg_dump cron daily + WAL archive với pgBackRest cho PITR.
  • pgBouncer: chỉ là proxy stateless, không cần backup config (commit trong git).
  • Redis: RDB snapshot daily + AOF append every second. Backup .rdb file lên S3.

Khi disaster, Postgres recover trước (source of truth). Redis chỉ là cache, có thể rebuild dần dần.

Chọn đúng thứ tự triển khai

Giai đoạnCần gìChưa cần gì
Sản phẩm mới, dưới 100 người dùngMột cơ sở dữ liệu, sao lưu hằng ngàyChưa cần lớp gộp kết nối, chưa cần bộ nhớ đệm
Có người dùng thật, vài nghìn lượt mỗi ngàyThêm lớp gộp kết nối, thêm chỉ mục đúngBộ nhớ đệm chỉ khi đo thấy truy vấn lặp lại nhiều
Nhiều tiến trình ứng dụng, tải tăng theo giờLớp gộp kết nối bắt buộc, bộ nhớ đệm cho truy vấn nóngChưa cần tách máy riêng
Nhiều máy chủ ứng dụngTách lớp gộp kết nối và bộ nhớ đệm ra riêngNhân bản đọc chỉ khi đo thấy nghẽn ở đọc

Sai lầm phổ biến của đội nhỏ: dựng cả ba tầng ngay từ ngày đầu. Kết quả là ba thứ phải vận hành, ba nguồn sự cố, trong khi tải chưa tới mức cần tầng nào. Dựng theo thứ tự trên và chỉ thêm khi đo thấy cần.

Kiểm hệ thống đang ở đâu

# So ket noi hien tai va gioi han
psql -c "SHOW max_connections;"
psql -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state;"

# Truy van cham nhat
psql -c "SELECT calls, round(mean_exec_time::numeric,1) AS tb_ms, left(query,70)
         FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"

# Ty le doc trung bo nho dem, duoi 95% la dang xuong o dia nhieu
psql -c "SELECT round(100.0*sum(blks_hit)/nullif(sum(blks_hit)+sum(blks_read),0),2)
         FROM pg_stat_database;"

# Neu da co bo nho dem: ty le trung
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"

# Nghen o dia hay o vi xu ly
vmstat 2 30

Ba điều hay làm sai

  • Đặt số kết nối trong bể quá lớn. Nhiều kết nối làm chậm chứ không nhanh hơn. Với máy 4 nhân, khoảng chục kết nối là hợp lý.
  • Đệm cả nội dung riêng của từng người dùng. Đây là lỗi gây hậu quả nặng nhất: người này thấy dữ liệu của người kia.
  • Dùng bộ nhớ đệm để che truy vấn tệ. Sửa truy vấn và thêm chỉ mục trước; đệm sau. Đệm chỉ giấu vấn đề cho tới lúc nó hết hạn đồng loạt.

Về phần cứng: cả ba tầng đều nhạy với ổ đĩa và bộ nhớ hơn là với số nhân. Thứ tự nâng cấp đúng là bộ nhớ trước, ổ NVMe sau, số nhân cuối cùng. Xem VPS NVMe.

FAQ

pgBouncer có làm slow query không?

Overhead pgBouncer cực thấp, dưới 0.5ms mỗi request. Cảm nhận không có. Lợi ích pool > overhead nhiều lần. Trừ khi app rất đơn giản dùng <10 connection, mọi production đều dùng pgBouncer.

Khi nào nên dùng PgCat hay Supavisor thay pgBouncer?

PgCat (Rust) và Supavisor (Elixir) là thế hệ mới hỗ trợ multi-tenant, load balance, sharding. Phù hợp khi có nhiều DB và nhiều cluster. Startup 5 dev với 1 DB chính thì pgBouncer vẫn ổn.

Redis có thể thay luôn Postgres làm primary DB không?

Không cho data quan trọng. Redis là in-memory, dù có AOF/RDB thì độ bền vẫn kém Postgres ACID. Dùng Redis làm cache + queue, Postgres làm source of truth.

Stack này có chạy được trên Docker compose không?

Có. Tạo docker-compose.yaml với 3 service: postgres, pgbouncer, redis. Mỗi service có healthcheck. App connect qua service name. Phù hợp dev/staging. Production thì cài native trên VPS để performance tốt.

Khi nào cần thêm Memcached?

Hiếm. Redis làm tốt mọi việc Memcached làm, lại có data structure phong phú. Trừ trường hợp legacy app yêu cầu Memcached cụ thể, không cần.

Cloud VPS cho vibe coder

Cloud VPS đa cấu hình cho data layer startup: 8GB-32GB cho mọi giai đoạn

Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Nâng cấp gói trong vài phút khi startup scale từ MVP lên Growth, không cần migrate VPS thủ công.

Xem 8 cấu hình Cloud VPS →