Tập 18 đã giúp deploy không downtime, nhưng một server chạy 3 worker Gunicorn sớm muộn cũng chạm giới hạn: CPU bão hòa, request xếp hàng, trang thống kê tải chậm dần. Tập này chỉ cách mở rộng theo từng nấc — thêm worker, cân bằng tải qua Nginx, tách read replica cho PostgreSQL — và dấu hiệu cho biết khi nào mới thật sự cần scale.
Nấc 1: thêm worker trước khi thêm máy
Trước khi mua thêm server, hãy vắt hết máy hiện tại. Công thức kinh điển cho Gunicorn là hai lần số CPU cộng một, nhưng đó chỉ là điểm khởi đầu — hãy đo thực tế bằng CPU và độ trễ phản hồi. Nếu CPU còn dư mà request vẫn chậm, vấn đề thường nằm ở query hoặc thiếu cache — quay lại tập 12 và tập 9 trước khi scale.
# Kiem tra tai that cua app truoc khi quyet dinh scaleuptimecurl -s http://localhost:8000/health/ -w "\n%{time_total}s\n"
Nấc 2: load balancing với Nginx
Khi một máy không còn đủ, chạy app trên hai máy và để Nginx chia đều request. Nginx đã làm reverse proxy từ tập 3, giờ chỉ cần thêm server vào upstream và chọn thuật toán chia tải. least_conn gửi request tới máy ít kết nối nhất, hợp với các request nặng như thống kê theo tháng. Lưu ý: app phải stateless — session và file upload không được nằm trên đĩa cục bộ.
upstream expense_app { least_conn; server 10.0.0.11:8000; server 10.0.0.12:8000;}server { listen 80; server_name chi-tieu.example.com; location / { proxy_pass http://expense_app; proxy_set_header Host $host; }}
Nấc 3: read replica cho PostgreSQL
Hầu hết app đọc nhiều hơn ghi rất nhiều: danh sách giao dịch, thống kê theo tháng, báo cáo đều là đọc. Tách một read replica để gánh các query đọc, còn database chính chỉ lo ghi. Django hỗ trợ sẵn nhiều database: khai báo thêm replica trong DATABASES, rồi dùng router nhỏ để đẩy query đọc sang replica, ghi vẫn về database chính.
# settings.pyDATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "expense", "HOST": "db-primary.internal", }, "replica": { "ENGINE": "django.db.backends.postgresql", "NAME": "expense", "HOST": "db-replica.internal", },}DATABASE_ROUTERS = ["expense.routers.ReadReplicaRouter"]
# expense/routers.pyclass ReadReplicaRouter: def db_for_read(self, model, **hints): return "replica" def db_for_write(self, model, **hints): return "default" def allow_migrate(self, db, app_label, model_name=None, **hints): return db == "default"
Khi nào mới cần scale
Đừng scale theo cảm giác. Scale khi có ít nhất một dấu hiệu rõ ràng kéo dài nhiều ngày: CPU trên 70% trong giờ cao điểm, độ trễ p95 tăng gấp đôi so với bình thường, hoặc pool kết nối database cạn kiệt dù đã dùng PgBouncer ở tập 4. Ngược lại, nếu lỗi chỉ xuất hiện trong một khung giờ ngắn, hãy xem đó là traffic bất thường hoặc một query chậm. Mỗi nấc mở rộng thêm một thứ phải giám sát — chỉ mở rộng khi số liệu tập 8 chứng minh bạn đã chạm trần.
Áp dụng vào dự án của bạn
Với ứng dụng quản lý chi tiêu, bắt đầu bằng việc đo: ghi lại CPU, độ trễ p95 và số kết nối database trong một tuần. Nếu CPU dưới 50% thì chưa cần làm gì — hãy tối ưu query chậm trước. Khi cần, thêm worker rồi tới load balancing hai máy; chỉ tách read replica khi query đọc đang đè nặng database chính.
Bài viết liên quan
- Django vận hành production #18: Deploy không downtime — blue-green/rolling, thứ tự chạy migrations và restart
- Django vận hành production #4: PostgreSQL trên production — kết nối, CONN_MAX_AGE, pool với PgBouncer, backup cơ bản
- Django vận hành production #9: Cache với Redis — cache framework, pattern invalidation, tránh cache sai dữ liệu
Câu hỏi thường gặp
Bao nhiêu worker Gunicorn là đủ?
Bắt đầu với hai lần số CPU cộng một, rồi đo thực tế. Thêm worker chỉ giúp khi CPU còn dư; nếu database hoặc query là nút thắt, thêm worker chỉ làm mọi thứ chậm hơn.
Read replica có làm dữ liệu đọc bị cũ không?
Có, replica đồng bộ trễ vài chục mili giây đến vài giây. Đừng đọc dữ liệu vừa ghi từ replica — sau khi tạo giao dịch mới, đọc lại từ database chính hoặc dùng using("default").
Load balancing có cần sticky session không?
Không, nếu app đã stateless: session lưu trong Redis hoặc database, file lưu trên S3. Sticky session chỉ là miếng vá tạm khi app còn giữ trạng thái trên đĩa cục bộ.
