Sự cố Production lúc 3:00 sáng
Quý trước, hệ thống dashboard phân tích của chúng tôi bất ngờ bị tê liệt ngay trong khung giờ cao điểm thanh toán. Độ trễ phản hồi API tăng vọt từ mức trung bình 42ms lên thẳng hơn 4.500ms. CPU của cụm MongoDB replica set chạm ngưỡng 98%, khiến kỹ sư trực on-call nhận cảnh báo khẩn cấp lúc 3:14 sáng.
Thủ phạm nhìn bên ngoài thì hoàn toàn vô hại:
db.orders.find({
status: "COMPLETED",
createdAt: { $gte: ISODate("2026-09-01T00:00:00Z") }
}).sort({ totalAmount: -1 }).limit(20);
Trên môi trường staging với 12.000 documents mẫu, lệnh này trả về kết quả trong chưa đầy 4ms. Nhưng trên production, với hơn 8,5 triệu bản ghi cùng dung lượng collection lên tới 62GB, nó đã làm cạn kiệt bộ đệm WiredTiger cache và nghẽn toàn bộ I/O ổ đĩa. Nếu bạn seed dữ liệu staging bằng các công cụ tiện ích như toolcraft.app/vi/tools/data/csv-to-json, hãy luôn duy trì quy mô dữ liệu sát với thực tế. Lượng dữ liệu quá nhỏ sẽ luôn che giấu các lỗi sai thứ tự index.
Chẩn đoán điểm nghẽn với explain()
Đừng ngồi đoán cách MongoDB thực thi bộ lọc của bạn nữa. Hãy truyền trực tiếp "executionStats" vào explain() để xem các chỉ số thực tế từ kế hoạch thực thi:
db.orders.find({
status: "COMPLETED",
createdAt: { $gte: ISODate("2026-09-01T00:00:00Z") }
}).sort({ totalAmount: -1 }).limit(20).explain("executionStats");
Đọc hiểu các dấu hiệu cảnh báo
Có ba tín hiệu bất thường lộ rõ ngay trong kết quả phân tích của chúng tôi:
stage: "COLLSCAN": Quét toàn bộ collection (full table scan). Cơ sở dữ liệu phải đọc tuần tự 8.520.412 documents trực tiếp từ ổ cứng NVMe vì không có index nào chứa prefix phù hợp.totalDocsExamined: 8520412so vớinReturned: 20: Một truy vấn tối ưu thường có tỷ lệ giữa số bản ghi kiểm tra và số bản ghi trả về xấp xỉ 1:1. Việc duyệt qua hơn 8,5 triệu bản ghi chỉ để lấy về 20 phần tử payload gây lãng phí nghiêm trọng bộ nhớ và băng thông CPU.stage: "SORT"không có index hỗ trợ: MongoDB mặc định giới hạn dung lượng sort trên bộ nhớ RAM ở mức 100MB (hoặc 32MB ở các phiên bản cũ MongoDB <4.4). Nếu việc sắp xếp các bản ghi đã lọc vượt quá giới hạn bộ đệm này mà không bậtallowDiskUse: true, truy vấn sẽ văng lỗi ngay lập tức:Executor error during find command: Sort exceeded memory limit.
So sánh ba chiến lược đánh Index
Khi các truy vấn kết hợp nhiều điều kiện bị nghẽn, các đội ngũ kỹ thuật thường thử áp dụng một trong ba cấu trúc index sau. Dưới đây là cách chúng hoạt động dưới tải thực tế trên production:
1. Tạo nhiều Single-Field Index riêng lẻ
db.orders.createIndex({ status: 1 });
db.orders.createIndex({ createdAt: 1 });
db.orders.createIndex({ totalAmount: -1 });
MongoDB có thể thử cơ chế giao nhau giữa các index (AND_SORTED), nhưng cách này hiếm khi chịu tải tốt. Database engine phải quét các cây B-tree của từng index riêng biệt, gộp các mã định danh bản ghi trong bộ nhớ, và sau đó vẫn phải thực hiện blocking sort trên RAM. Đồng thời, mỗi thao tác insert hoặc update giờ đây phải chịu chi phí ghi gấp ba lần thay vì một lần.
2. Compound Index ngây thơ (Đặt Range lên đầu)
// Thứ tự sai: trường range được đặt trước equality và sort
db.orders.createIndex({ createdAt: 1, status: 1, totalAmount: -1 });
Đặt createdAt ở vị trí đầu tiên sẽ phá hỏng hoàn toàn hiệu quả của index. Một khi MongoDB quét qua toán tử dải ($gte), nó không thể đi sâu hơn vào cây B-tree phức hợp để tìm kiếm chính xác trường status. Tệ hơn nữa, nó làm mất tính tuần tự theo thứ tự của totalAmount, dẫn đến việc lại phải sort tốn kém trên bộ nhớ.
3. Compound Index chuẩn quy tắc ESR
// Thứ tự chuẩn: Equality (status) -> Sort (totalAmount) -> Range (createdAt)
db.orders.createIndex({ status: 1, totalAmount: -1, createdAt: 1 });
Cấu trúc này ăn khớp hoàn hảo với tổ chức vật lý của cây B-tree. Bộ lập kế hoạch truy vấn (query planner) sẽ nhảy thẳng tới các bản ghi thỏa mãn status: "COMPLETED", duyệt qua các bản ghi theo đúng thứ tự đã sắp xếp, và dừng quét ngay khi chạm mốc giới hạn ngày hoặc số lượng limit.
Cách áp dụng quy tắc ESR
Quy tắc ESR là viết tắt của Equality (Bằng), Sort (Sắp xếp), và Range (Dải phạm vi). Hãy sắp xếp các trường trong compound index theo đúng trình tự này bất cứ khi nào truy vấn kết hợp nhiều kiểu lọc khác nhau.
1. Đặt các trường Equality lên đầu
Đưa các điều kiện so khớp chính xác (như tenantId, status, hoặc userId) vào phần đầu của index key. Điều này ngay lập tức thu hẹp phạm vi đánh giá về một phân vùng nhỏ, liền kề trên cây B-tree.
2. Đặt các trường Sort ở vị trí thứ hai
Tiếp theo, hãy định nghĩa các trường được chỉ định trong .sort(). Bằng cách khớp đúng chiều sắp xếp yêu cầu hoặc chiều đảo ngược hoàn toàn của nó, MongoDB có thể truyền dữ liệu đã sắp xếp thẳng từ index ra ngoài. Cơ sở dữ liệu sẽ tránh được việc phải gom bản ghi vào RAM để sort, loại bỏ áp lực bộ nhớ.
3. Đặt các trường Range ở cuối cùng
Đặt các toán tử so sánh dải ($gt, $gte, $lt, $lte) hoặc bộ lọc kiểm tra tập hợp ở cuối index. Sau khi engine bắt đầu quét một dải phạm vi qua các khối index không liền mạch, các trường index đứng sau sẽ không còn khả năng phục vụ thao tác sắp xếp hay tìm kiếm điểm cố định nữa.
Kiểm chứng kết quả trên Production
Chúng tôi đã tạo index theo quy tắc ESR ở chế độ background để tránh làm khóa các thao tác ghi dữ liệu:
// 1. Khởi tạo index đã tối ưu
db.orders.createIndex(
{ status: 1, totalAmount: -1, createdAt: 1 },
{ background: true }
);
// 2. Xác thực đường dẫn thực thi truy vấn
const plan = db.orders.find({
status: "COMPLETED",
createdAt: { $gte: ISODate("2026-09-01T00:00:00Z") }
}).sort({ totalAmount: -1 }).limit(20).explain("executionStats");
const stats = plan.executionStats;
print("Root Stage: " + stats.executionStages.stage);
print("Keys Examined: " + stats.totalKeysExamined);
print("Docs Examined: " + stats.totalDocsExamined);
print("Returned: " + stats.nReturned);
print("Latency (ms): " + stats.executionTimeMillis);
Các chỉ số vận hành đã thay đổi ngoạn mục:
totalKeysExaminedgiảm sâu từ 8.520.412 xuống chỉ còn 20.totalDocsExaminedgiảm từ 8.520.412 xuống chỉ còn 20 (tỷ lệ chuẩn tuyệt đối 1:1).executionTimeMillisgiảm từ 4.510ms xuống chỉ còn 1ms.- Giai đoạn
SORTngốn bộ nhớ hoàn toàn biến mất, thay vào đó là luồng quét index (IXSCAN) đi thẳng vàoFETCH.
Mức sử dụng CPU của replica set đã giảm về mức 14%. Hãy luôn kiểm tra kết quả từ explain() mỗi khi độ trễ có dấu hiệu tăng vọt, và thiết kế compound index tuân thủ chặt chẽ các nguyên tắc ESR. Đối với các tác vụ tổng hợp và biến đổi dữ liệu phức tạp hơn, bạn có thể áp dụng các nguyên tắc tối ưu tương tự khi xây dựng MongoDB Aggregation Pipeline.

