- Giới hạn kích thước payload để tránh request khổng lồ làm nghẽn băng thông và bộ nhớ.
- Bật nén (Gzip/Brotli) cho phản hồi để giảm dung lượng truyền qua mạng.
- Phân trang đúng cách khi trả về tập dữ liệu lớn, thay vì đổ hết một lần.
- Cắt bớt xử lý và truy vấn thừa bằng cache và tối ưu ORM.
Bạn từng gặp cảnh API chạy êm lúc test, lên production vài nghìn người dùng thì bắt đầu ì ạch chưa? Phần lớn thời gian, thủ phạm không phải là thuật toán tệ hay CPU yếu, mà là những chi tiết nhỏ ở tầng vận chuyển dữ liệu: payload quá to, phản hồi không nén, dữ liệu trả về không phân trang, hoặc server tính đi tính lại một thứ đáng lẽ chỉ cần tính một lần. Bài này gom 4 việc tôi hay kiểm tra đầu tiên khi review hiệu năng backend, kèm cấu hình Nginx và Django cụ thể để bạn áp dụng ngay.
Áp Đặt Giới Hạn Kích Thước Payload Hợp Lý
Giới hạn kích thước payload request để tránh gây áp lực lên băng thông và tài nguyên máy chủ.
Hiệu năng backend phụ thuộc rất nhiều vào tốc độ máy chủ nhận, xử lý và lưu trữ dữ liệu. Khi một payload quá lớn được gửi lên, server phải đọc hết dữ liệu đó vào bộ nhớ trước khi xử lý, dù request đó có hợp lệ hay không. Nếu không đặt giới hạn, một request vài trăm MB (dù vô tình hay cố ý) cũng đủ chiếm hết bộ nhớ đệm, kéo chậm mọi request khác đang chờ, và trong trường hợp xấu là mở đường cho tấn công từ chối dịch vụ (DoS) bằng payload khổng lồ.
Hãy hình dung bưu cục không đặt giới hạn cân nặng cho mỗi gói hàng: chỉ cần vài người gửi tủ lạnh qua đường bưu điện thường, cả dây chuyền phân loại phía sau tắc nghẽn ngay, dù phần lớn gói hàng còn lại chỉ là thư từ vài chục gram.
Giới hạn kích thước payload nên được cấu hình ở nhiều tầng, tốt nhất là cả proxy (Nginx) lẫn ứng dụng backend (Django), vì proxy chặn được request thô bạo trước khi nó chạm tới code Python, còn giới hạn trong Django giúp bạn kiểm soát chi tiết hơn theo từng loại dữ liệu (JSON, file upload).
Cấu hình Nginx
# Giới hạn kích thước body request
client_max_body_size 10M; # Giới hạn 10MB
client_body_buffer_size 128k;
Cấu hình Django
# settings.py
# Giới hạn kích thước payload upload
DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB cho dữ liệu form/JSON
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB cho file upload
# views.py
from django.core.exceptions import SuspiciousOperation
from django.http import HttpResponseBadRequest
def handle_upload(request):
try:
# Kiểm tra kích thước file trước khi xử lý
if request.FILES['file'].size > 10 * 1024 * 1024:
raise SuspiciousOperation("File quá lớn")
# Xử lý upload ở đây
except SuspiciousOperation as e:
return HttpResponseBadRequest(str(e))
Kích Hoạt Nén Cho Phản Hồi
Bật nén (Gzip hoặc Brotli) cho phản hồi để giảm dung lượng dữ liệu truyền qua mạng.
Nén phản hồi (response compression) dùng thuật toán như Gzip hoặc Brotli để thu nhỏ dữ liệu văn bản (HTML, CSS, JS, JSON) trước khi gửi qua mạng, rồi trình duyệt tự giải nén khi nhận. Với dữ liệu dạng văn bản, tỷ lệ nén thường khá cao vì nội dung lặp lại nhiều (thẻ HTML, key JSON, khoảng trắng). Đổi lại, server tốn thêm một chút CPU để nén, nhưng thường vẫn rẻ hơn nhiều so với chi phí băng thông và thời gian chờ tải.
Cứ nghĩ như đóng gói hành lý bằng túi hút chân không: bạn tốn thêm vài giây bơm hút khí trước khi lên đường, nhưng đổi lại vali gọn hơn hẳn, dễ mang vác hơn nhiều so với việc nhét đồ nguyên khối vào vali.
Cấu hình Nginx
Nén Gzip thường được cấu hình ở tầng proxy (Nginx) thay vì trực tiếp trong backend service như Django, vì Nginx được viết để nén/giải nén rất nhanh và không tốn tài nguyên của tiến trình ứng dụng.
server {
# Kích hoạt Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
# Các loại MIME được phép nén
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml;
# Mức độ nén (1-9, càng cao càng nén tốt nhưng càng tốn CPU)
gzip_comp_level 6;
# Kích thước tối thiểu để nén (dưới ngưỡng này nén không đáng)
gzip_min_length 1100;
}
Cấu hình Django
Nếu không kiểm soát được tầng proxy, bạn vẫn có thể bật nén ngay trong Django:
# settings.py
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
# Các middleware khác
]
# Hoặc nén riêng cho một view cụ thể
from django.views.decorators.gzip import gzip_page
@gzip_page
def large_data_view(request):
# View trả về dữ liệu lớn, ví dụ JSON danh sách sản phẩm
return JsonResponse(large_data)
GZipMiddleware cho các trang chứa dữ liệu nhạy cảm: nén nội dung có cả phần bí mật (như token) và phần do người dùng kiểm soát được từng là kẽ hở cho kiểu tấn công khai thác độ dài bản nén (như BREACH). Nếu không chắc, cứ để nén ở tầng Nginx/CDN, hạn chế bật GZipMiddleware cho các endpoint xử lý dữ liệu nhạy cảm.Phân Trang Hiệu Quả Cho Tập Dữ Liệu Lớn
Phân trang khi trả về tập dữ liệu lớn thay vì đổ hết một lần trong một response.
Khi một truy vấn cơ sở dữ liệu trả về hàng chục nghìn bản ghi trong một lần gọi, cả server lẫn client đều phải gánh: server tốn bộ nhớ và thời gian gom dữ liệu, client tốn băng thông tải về và thời gian render. Phân trang (pagination) giải quyết việc này bằng cách chỉ trả về một phần dữ liệu (ví dụ 20 bản ghi) mỗi lần gọi, kèm thông tin để client xin trang tiếp theo khi cần.
Giống như đọc sách: bạn không cần ai đó fax nguyên cả cuốn sách 500 trang một lúc cho bạn đọc, mà chỉ cần lật từng chương một, chương nào cần thì đọc, xong rồi lật tiếp.
# views.py
from django.core.paginator import Paginator
def list_products(request):
product_list = Product.objects.all()
paginator = Paginator(product_list, 20) # 20 sản phẩm mỗi trang
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'products.html', {'page_obj': page_obj})
<!-- Template -->
{% for product in page_obj %}
{{ product }}
{% endfor %}
{{ page_obj.has_previous }}
{{ page_obj.has_next }}
LIMIT/OFFSET phía sau). Nó dễ dùng nhưng càng vào trang sâu thì database càng phải quét qua nhiều bản ghi bị bỏ qua trước khi lấy được trang cần, nên chậm dần theo số trang. Với bảng vài triệu dòng trở lên, hoặc API kiểu "cuộn vô tận" (infinite scroll), nên cân nhắc phân trang theo con trỏ (cursor-based/keyset pagination): dùng giá trị của bản ghi cuối cùng đã lấy (ví dụ id hoặc timestamp) làm điểm bắt đầu cho trang kế tiếp, thay vì đếm offset.Giảm Xử Lý Thừa Và Truy Vấn Tốn Kém
Giảm thiểu xử lý không cần thiết hoặc các phép tính tốn kém trên máy chủ.
Nhiều API chậm không phải vì thuật toán tệ, mà vì server tính đi tính lại đúng một kết quả cho nhiều người dùng, hoặc truy vấn database nhiều hơn mức cần thiết. Hai công cụ giải quyết phần lớn trường hợp này là cache (lưu tạm kết quả) và tối ưu truy vấn ORM (chỉ lấy đúng dữ liệu cần).
Tưởng tượng một quán phở: nếu mỗi khách gọi món mà đầu bếp lại ninh nước dùng từ đầu, quán chắc chắn vỡ trận giờ cao điểm. Cách làm thực tế là ninh sẵn một nồi nước dùng lớn (cache), ai gọi món cũng múc ra dùng ngay, chỉ cần ninh lại khi nồi cạn (cache hết hạn).
Sử dụng cache
# Sử dụng cache
from django.core.cache import cache
def get_expensive_computation(param):
# Kiểm tra cache trước
cache_key = f'computation_{param}'
result = cache.get(cache_key)
if result is None:
# Tính toán nếu chưa có trong cache
result = expensive_calculation(param)
# Lưu vào cache trong 1 giờ
cache.set(cache_key, result, 3600)
return result
# Sử dụng select_related() và prefetch_related() để giảm số query
def optimized_query(request):
products = Product.objects.select_related('category').prefetch_related('tags').all()
Tối ưu truy vấn cơ sở dữ liệu
# Sử dụng only() và defer() để chỉ lấy trường cần thiết
def efficient_query(request):
# Chỉ lấy các trường cần thiết
users = User.objects.only('username', 'email')
# Bỏ qua các trường tốn kém (ví dụ text dài, blob)
products = Product.objects.defer('description', 'long_text_field')
select_related() (dùng JOIN, hợp với quan hệ một-một hoặc khóa ngoại) và prefetch_related() (chạy query riêng rồi ghép ở Python, hợp với quan hệ nhiều-nhiều) là hai cách khác nhau để tránh vấn đề N+1 query: lặp qua danh sách đối tượng mà mỗi vòng lặp lại bắn thêm một query database. Còn với cache, đừng quên vế xóa cache khi dữ liệu gốc thay đổi, cache không tự biết dữ liệu đã cũ, chỉ bạn mới biết khi nào cần cache.delete() hoặc đặt thời gian hết hạn (TTL) hợp lý.Tóm Tắt
- Giới hạn kích thước payload ở cả Nginx và Django, giữ hai ngưỡng khớp nhau để tránh lỗi 413 khó hiểu.
- Bật nén Gzip/Brotli cho phản hồi văn bản, nhưng đừng nén file đã nén sẵn và cẩn trọng với dữ liệu nhạy cảm.
- Phân trang mọi tập dữ liệu lớn; cân nhắc cursor-based pagination khi offset bắt đầu chậm ở trang sâu.
- Dùng cache cho kết quả tính toán lặp lại, và tối ưu ORM (
select_related,prefetch_related,only,defer) để giảm số query cũng như dữ liệu tải về không cần thiết.
- Kai