1. Tự động hóa khởi tạo Database riêng cho Tenant
Mục tiêu là xây dựng một cơ chế để ứng dụng hoặc script có thể gọi một lệnh duy nhất, từ đó tạo ra một database mới với cấu hình chuẩn hóa cho tenant mới mà không cần can thiệp thủ công vào server.
Chúng ta sẽ sử dụng vai trò superuser của PostgreSQL để cấp quyền tạo database cho một vai trò trung gian (ví dụ: tenant_admin), đồng thời chuẩn bị một template database chứa các extension và cấu hình cơ bản.
Trước tiên, hãy đảm bảo vai trò tenant_admin đã tồn tại và có quyền CREATEDB nhưng bị giới hạn quyền truy cập vào các database hệ thống.
sudo -u postgres psql -c "CREATE ROLE tenant_admin WITH LOGIN CREATEDB PASSWORD 'strong_password_here';"
Kết quả mong đợi: PostgreSQL trả về CREATE ROLE và không báo lỗi.
Để đảm bảo mỗi database mới đều có sẵn các extension cần thiết (như pg_cron, uuid-ossp), chúng ta tạo một database mẫu (template) và cấu hình nó không thể bị xóa trực tiếp.
sudo -u postgres psql -c "CREATE DATABASE tenant_template OWNER tenant_admin TEMPLATE template1;"
sudo -u postgres psql -d tenant_template -c "CREATE EXTENSION IF NOT EXISTS pg_cron;"
sudo -u postgres psql -d tenant_template -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
sudo -u postgres psql -d tenant_template -c "SELECT datname FROM pg_database WHERE datname = 'tenant_template' AND datistemplate = true;"
Kết quả mong đợi: Database tenant_template được tạo và các extension đã được cài đặt sẵn bên trong.
Bây giờ, tạo một hàm SQL an toàn để khởi tạo database mới. Hàm này sẽ tạo database từ template, cấp quyền cho user tenant, và gán các resource limit cơ bản.
sudo -u postgres psql -c "
CREATE OR REPLACE FUNCTION create_tenant_db(tenant_name text, tenant_user_name text, tenant_password text)
RETURNS void AS \$\$
DECLARE
db_name text := 'db_' || tenant_name;
user_name text := tenant_user_name;
pass text := tenant_password;
BEGIN
-- Tạo database từ template
EXECUTE format('CREATE DATABASE %I OWNER %I TEMPLATE tenant_template', db_name, user_name);
-- Tạo user nếu chưa tồn tại (nếu muốn user riêng cho tenant)
EXECUTE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', user_name, pass);
-- Cấp quyền đầy đủ cho user vào database của họ
EXECUTE format('GRANT ALL PRIVILEGES ON DATABASE %I TO %I', db_name, user_name);
-- Set limit mặc định (sẽ chi tiết hơn ở phần sau)
EXECUTE format('ALTER DATABASE %I SET work_mem = \'64MB\'', db_name);
EXECUTE format('ALTER DATABASE %I SET statement_timeout = \'60s\'', db_name);
-- Grant quyền vào các schema public trong db mới
EXECUTE format('GRANT ALL ON SCHEMA public TO %I', user_name);
END;
\$\$ LANGUAGE plpgsql SECURITY DEFINER;
"
Kết quả mong đợi: Hàm create_tenant_db được tạo thành công. Khi gọi hàm này, một database mới sẽ được tạo ngay lập tức với cấu hình chuẩn.
Test hàm bằng cách tạo một tenant mẫu:
sudo -u postgres psql -c "SELECT create_tenant_db('acme_corp', 'acme_user', 'AcmePass123!');"
sudo -u postgres psql -c "SELECT datname FROM pg_database WHERE datname LIKE 'db_acme_corp';"
Kết quả mong đợi: Database db_acme_corp xuất hiện trong danh sách và có thể kết nối bằng user acme_user.
2. Quản lý Task định kỳ với pg_cron cho từng Tenant
Với mô hình Database riêng, mỗi tenant có thể có lịch chạy task khác nhau. Extension pg_cron cho phép chúng ta chạy query SQL theo lịch, nhưng cần cấu hình cẩn thận để task của tenant A không ảnh hưởng đến tenant B.
Quan trọng nhất là thiết lập biến môi trường PGRON_CONNECTION hoặc sử dụng CONNECTION trong job để đảm bảo task chỉ chạy trong database cụ thể của tenant đó.
Đầu tiên, khởi động service pg_cron nếu chưa chạy (thường cần cấu hình trong postgresql.conf load extension).
sudo systemctl status pg_cron
Nếu chưa chạy, hãy đảm bảo pg_cron được load trong postgresql.conf và khởi động lại PostgreSQL.
Để tạo job cho tenant cụ thể, ta cần kết nối vào database của tenant đó và chạy lệnh SELECT cron.schedule(). Job này sẽ chạy query trong chính context của database đó.
Thí dụ: Tạo job backup log hàng ngày cho tenant acme_corp.
psql -U acme_user -d db_acme_corp -c "SELECT cron.schedule('acme_daily_cleanup', '0 2 * * *', 'DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL '30 days';');"
Kết quả mong đợi: Job được tạo và trả về ID job. Query này chỉ chạy trong db_acme_corp.
Để quản lý tập trung từ một script, ta có thể tạo hàm wrapper để thêm job vào database tenant cụ thể.
sudo -u postgres psql -c "
CREATE OR REPLACE FUNCTION add_tenant_job(tenant_db text, job_name text, schedule_expr text, command text)
RETURNS void AS \$\$
BEGIN
EXECUTE format('SELECT cron.schedule(%L, %L, %L)', job_name, schedule_expr, command)
USING tenant_db;
END;
\$\$ LANGUAGE plpgsql SECURITY DEFINER;
"
Lưu ý: Để hàm EXECUTE chạy query trên một database khác, ta cần kết nối động. Cách trên có thể gặp vấn đề nếu không có dblink hoặc pg_cron không hỗ trợ trực tiếp. Cách tốt nhất là dùng dblink hoặc script bên ngoài kết nối vào từng DB.
Phương án thực tế nhất là dùng dblink trong PostgreSQL để gọi hàm cron.schedule từ database quản lý sang database tenant.
sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS dblink;"
sudo -u postgres psql -c "
CREATE OR REPLACE FUNCTION add_tenant_job(tenant_db text, job_name text, schedule_expr text, command text)
RETURNS void AS \$\$
BEGIN
EXECUTE format('SELECT dblink(%L, %L)',
format('host=127.0.0.1 dbname=%I user=%I password=%L', tenant_db, 'postgres', 'postgres_password'),
format('SELECT cron.schedule(%L, %L, %L)', job_name, schedule_expr, command)
);
END;
\$\$ LANGUAGE plpgsql SECURITY DEFINER;
"
Kết quả mong đợi: Hàm được tạo. Khi gọi, nó sẽ tự động kết nối vào DB tenant và tạo job.
Verify job đã được tạo trong database tenant:
psql -U acme_user -d db_acme_corp -c "SELECT jobname, schedule FROM cron.job;"
Kết quả mong đợi: Danh sách job hiện thị với task đã tạo.
3. Cấu hình Giới hạn Tài nguyên (Resource Limit) cho Tenant
Khi có hàng trăm tenant trên cùng một server, một tenant "hungry" (tiêu thụ nhiều tài nguyên) có thể làm sập toàn bộ hệ thống. PostgreSQL 16 hỗ trợ giới hạn tài nguyên ở mức database thông qua các ALTER DATABASE SET và ALTER ROLE SET.
Chúng ta sẽ giới hạn work_mem, statement_timeout và max_connections (gián tiếp qua connection pool).
Thiết lập giới hạn mặc định cho tất cả database tenant mới tạo bằng cách sửa file postgresql.conf hoặc dùng hàm wrapper đã tạo ở phần 1.
Thêm các tham số sau vào lệnh CREATE DATABASE hoặc ALTER DATABASE của mỗi tenant.
sudo -u postgres psql -c "ALTER DATABASE db_acme_corp SET work_mem = '64MB';"
sudo -u postgres psql -c "ALTER DATABASE db_acme_corp SET statement_timeout = '30s';"
sudo -u postgres psql -c "ALTER DATABASE db_acme_corp SET lock_timeout = '10s';"
Kết quả mong đợi: Các tham số được áp dụng. Nếu query của tenant chạy quá 30s hoặc lock quá 10s, nó sẽ bị cắt (timeout) và trả về lỗi, không làm treo server.
Để quản lý giới hạn CPU và RAM ở mức hệ thống (OS) cho từng tenant, PostgreSQL tự thân không làm trực tiếp. Ta cần kết hợp với systemd (nếu tách service) hoặc cgroups v2. Tuy nhiên, trong môi trường đơn server, cách hiệu quả nhất là giới hạn số lượng connection và memory trong Postgres.
Cấu hình pg_hba.conf để giới hạn số lượng connection tối đa cho mỗi tenant role.
sudo nano /etc/postgresql/16/main/pg_hba.conf
Thêm dòng sau vào cuối file (trước dòng local all all peer):
host db_acme_corp acme_user 127.0.0.1/32 md5
Và trong postgresql.conf, thiết lập max_connections cho toàn server, nhưng phân bổ logic cho từng tenant qua PgBouncer (sẽ nói ở phần sau).
Tuy nhiên, để giới hạn memory cho từng query của tenant, ta dùng work_mem như trên. Để giới hạn memory cho toàn bộ connection của tenant, ta có thể dùng shared_buffers (không khả thi cho từng tenant) nên phải dựa vào work_mem và maintenance_work_mem.
sudo -u postgres psql -c "ALTER DATABASE db_acme_corp SET maintenance_work_mem = '256MB';"
Verify các giới hạn đã áp dụng:
psql -U acme_user -d db_acme_corp -c "SHOW work_mem; SHOW statement_timeout;"
Kết quả mong đợi: Trả về giá trị 64MB và 30s.
4. Tối ưu hóa Kết nối với PgBouncer cho hàng nghìn Database
Với mô hình Database riêng, nếu có 1000 tenant, việc tạo 1000 connection pool riêng biệt hoặc để PostgreSQL mở 1000 connection trực tiếp sẽ làm quá tải max_connections (mặc định 100) và tốn nhiều RAM.
Giải pháp là sử dụng PgBouncer với chế độ Pool Mode: Transaction hoặc Session. PgBouncer hoạt động như một proxy, giữ các connection thực sự đến PostgreSQL và phân phối chúng cho các client (tenant) theo nhóm.
Trong môi trường Multi-Tenant Database riêng, chúng ta cấu hình PgBouncer để tạo pool cho mỗi database riêng biệt, nhưng giới hạn số lượng connection thực tế trong mỗi pool.
Cài đặt PgBouncer trên Ubuntu 24.04:
sudo apt update
sudo apt install -y pgbouncer
Sửa file cấu hình PgBouncer để hỗ trợ hàng nghìn database. File cấu hình nằm ở /etc/pgbouncer/pgbouncer.ini.
sudo nano /etc/pgbouncer/pgbouncer.ini
Thay đổi nội dung như sau:
[databases]
; Định nghĩa pool cho từng tenant (có thể dùng wildcard hoặc list)
; Để tự động phát hiện, dùng:
* = host=127.0.0.1 port=5432 dbname=$name
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
auth_query = SELECT usename, passwd FROM pg_shadow WHERE usename IN (SELECT usename FROM pg_roles);
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
min_pool_size = 0
reserve_pool = 5
reserve_pool_timeout = 3
server_connect_timeout = 15
server_login_retry = 3
Cấu hình quan trọng: max_client_conn = 10000 cho phép PgBouncer nhận 10000 request từ client, nhưng mỗi tenant chỉ dùng default_pool_size = 20 connection thực tế đến Postgres. Điều này giảm tải cho Postgres từ 10000 xuống khoảng 20 * số lượng tenant đang hoạt động đồng thời.
Tạo file userlist.txt chứa thông tin đăng nhập của tất cả tenant. PgBouncer cần file này để xác thực. Ta sẽ dùng script để tự động cập nhật file này khi tạo tenant mới.
sudo nano /etc/pgbouncer/userlist.txt
Nội dung mẫu (sẽ được script tự động update):
"acme_user" "AcmePass123!"
"tenant_user" "password"
Để tự động hóa việc thêm user vào userlist.txt khi tạo tenant, ta cần hook vào hàm create_tenant_db hoặc dùng script bên ngoài. Ở đây, ta giả định dùng script shell để update file này.
echo '"acme_user" "AcmePass123!"' >> /etc/pgbouncer/userlist.txt
Khởi động lại PgBouncer để áp dụng cấu hình:
sudo systemctl restart pgbouncer
Kiểm tra trạng thái các pool:
sudo pgbouncer -c /etc/pgbouncer/pgbouncer.ini show pools
Kết quả mong đợi: PgBouncer chạy, hiển thị các pool cho các database đã kết nối. Khi tenant kết nối qua port 6432, PgBouncer sẽ tự động tạo pool cho database đó nếu chưa tồn tại (nhờ cấu hình wildcard *).
Để test kết nối qua PgBouncer:
psql -h 127.0.0.1 -p 6432 -U acme_user -d db_acme_corp
Kết quả mong đợi: Kết nối thành công. PgBouncer sẽ re-use connection từ pool.
5. Quản lý Backup riêng biệt cho từng Database Tenant
Với hàng trăm database, việc backup toàn bộ server (pg_dumpall) sẽ tạo file quá lớn, khó phục hồi (restore) riêng lẻ và tốn thời gian. Chiến lược tối ưu là backup từng database riêng biệt dưới dạng file hoặc lưu trữ trên object storage (S3).
Chúng ta sẽ cấu hình script backup tự động chạy định kỳ (cron) để backup từng tenant đang tồn tại.
Tạo thư mục lưu backup:
sudo mkdir -p /var/backups/postgres/tenants
sudo chown postgres:postgres /var/backups/postgres/tenants
Viết script shell backup_tenant.sh để quét danh sách database và backup từng cái một.
sudo nano /usr/local/bin/backup_tenant.sh
Nội dung script:
#!/bin/bash
# Script backup từng tenant database riêng biệt
BACKUP_DIR="/var/backups/postgres/tenants"
PG_USER="postgres"
PG_PASS="postgres_password"
DATE=$(date +%Y%m%d_%H%M%S)
# Lấy danh sách database tenant (bỏ qua hệ thống)
# Giả định tên tenant bắt đầu bằng 'db_'
DB_LIST=$(psql -U $PG_USER -h 127.0.0.1 -t -c "SELECT datname FROM pg_database WHERE datname LIKE 'db_%' AND datistemplate = false;")
for DB in $DB_LIST; do
echo "Backing up database: $DB"
# Tạo file backup riêng cho từng DB
FILE_NAME="${BACKUP_DIR}/${DB}_${DATE}.sql.gz"
# Chạy pg_dump với định dạng custom (-Fc) để nén và restore nhanh hơn
# Dùng -Z 9 để nén mạnh
PGPASSWORD=$PG_PASS pg_dump -U $PG_USER -h 127.0.0.1 -Fc -Z 9 -f "$FILE_NAME" "$DB"
if [ $? -eq 0 ]; then
echo "Backup successful for $DB: $FILE_NAME"
else
echo "Backup FAILED for $DB"
fi
# (Tùy chọn) Xóa backup cũ hơn 7 ngày của tenant này
find "$BACKUP_DIR" -name "${DB}_*.sql.gz" -mtime +7 -delete
done
Cấp quyền thực thi cho script:
sudo chmod +x /usr/local/bin/backup_tenant.sh
Cấu hình Cron để chạy script này hàng ngày lúc 2 AM:
sudo crontab -u postgres -e
Thêm dòng sau vào file crontab:
0 2 * * * /usr/local/bin/backup_tenant.sh >> /var/log/postgres/backup.log 2>&1
Kết quả mong đợi: Script chạy tự động vào 2 giờ sáng mỗi ngày. Mỗi tenant sẽ có một file backup riêng biệt db_tenant_name_YYYYMMDD_HHMMSS.sql.gz.
Verify backup đã được tạo:
ls -lh /var/backups/postgres/tenants/
Kết quả mong đợi: Danh sách các file backup cho từng tenant xuất hiện.
Test restore một tenant cụ thể (giả sử muốn restore db_acme_corp):
ls /var/backups/postgres/tenants/ | grep db_acme_corp
Lấy file mới nhất và restore:
LATEST_BACKUP=$(ls -t /var/backups/postgres/tenants/db_acme_corp_*.sql.gz | head -n 1)
PGPASSWORD=postgres_password pg_restore -U postgres -h 127.0.0.1 -d db_acme_corp -c "$LATEST_BACKUP"
Kết quả mong đợi: Dữ liệu của tenant acme_corp được khôi phục từ file backup riêng mà không ảnh hưởng đến các tenant khác.
Điều hướng series:
Mục lục: Series: Triển khai Database Multi-Tenant với PostgreSQL và Ubuntu 24.04
« Phần 4: Triển khai mô hình Schema riêng: Tạo Template và quản lý Schema
Phần 6: Cấu hình Backup và Disaster Recovery cho môi trường Multi-Tenant »