Lỗi Gặp Phải
MongoServerError: Topology was destroyed
Bạn thực hiện một câu truy vấn, thao tác insert, hoặc aggregation — nhưng thay vì nhận được kết quả, bạn nhận được lỗi này. Thao tác chưa hề được thực thi. Topology kết nối nội bộ của MongoDB đã bị hủy trước (hoặc trong khi) code của bạn cố sử dụng nó.
Nguyên Nhân
MongoDB driver duy trì một topology: một đối tượng nội bộ theo dõi các kết nối đang hoạt động, tình trạng server và connection pool. Lỗi "Topology was destroyed" có nghĩa là driver đã đóng đối tượng đó trong khi code của bạn vẫn đang trỏ vào nó.
Các nguyên nhân thường gặp:
- Bạn đã gọi
client.close()(hoặcmongoose.disconnect()) rồi sau đó tiếp tục chạy truy vấn — lỗi timing async điển hình. - Tiến trình nhận tín hiệu
SIGINT/SIGTERMvà shutdown handler đóng kết nối trong khi các truy vấn đang thực thi vẫn còn đang chờ. serverSelectionTimeoutMShết hạn — driver không thể tìm thấy server khả dụng trong khoảng thời gian timeout (mặc định: 30 giây) và hủy topology thay vì thử lại.- Một
MongoClientđược tạo bên trong một hàm hoặc request handler rồi bị đóng ở cuối — lần gọi tiếp theo đôi khi vẫn tham chiếu đến instance cũ đã bị đóng. - Một unhandled promise rejection làm crash ứng dụng giữa chừng, khiến các kết nối đang hoạt động bị đóng đột ngột.
Cách Sửa 1 — Không Đóng Client Quá Sớm (Phổ Biến Nhất)
Script và các hàm kiểu Lambda là thủ phạm lớn nhất ở đây. Thiếu await khiến client.close() chạy trước khi truy vấn kịp bắt đầu:
// SAI — client đóng trước khi findOne hoàn thành
const client = new MongoClient(uri);
await client.connect();
client.db('mydb').collection('users').findOne({ id: 1 }); // thiếu await!
await client.close(); // topology biến mất trước khi findOne hoàn tất
// ĐÚNG — await mọi thao tác trước khi đóng
const client = new MongoClient(uri);
try {
await client.connect();
const user = await client.db('mydb').collection('users').findOne({ id: 1 });
console.log(user);
} finally {
await client.close();
}
Mọi lời gọi đến database đều cần có await. Nếu bạn dùng chuỗi .then(), thì client.close() phải được nối vào cuối cùng — không phải chạy song song với các thao tác khác.
Cách Sửa 2 — Khắc Phục Race Condition Khi Shutdown
Các server chạy lâu dài gặp lỗi này khi tiến trình tắt giữa chừng một request. Một handler SIGTERM đơn giản đóng MongoDB ngay lập tức, bỏ rơi các truy vấn đang thực thi:
// SAI — đóng DB trong khi các HTTP request vẫn có thể đang chạy
process.on('SIGTERM', async () => {
await mongoose.disconnect();
process.exit(0);
});
// ĐÚNG — drain HTTP trước, sau đó mới đóng DB
process.on('SIGTERM', async () => {
server.close(async () => { // dừng nhận HTTP request mới
await mongoose.disconnect(); // lúc này mới đóng MongoDB
process.exit(0);
});
});
Dừng HTTP server → chờ các kết nối đang hoạt động hoàn tất → đóng MongoDB. Bỏ qua bất kỳ bước nào sẽ gây ra lỗi này trong điều kiện traffic thực tế.
Cách Sửa 3 — Tăng serverSelectionTimeoutMS
Khi tải cao hoặc mạng chậm, driver có thể timeout khi tìm kiếm server khả dụng. Khi bỏ cuộc, nó hủy topology. Hãy tăng giá trị timeout:
// Node.js — mongodb driver
const client = new MongoClient(uri, {
serverSelectionTimeoutMS: 10000, // tăng từ giá trị mặc định 30000 nếu fail nhanh
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
});
# mongoose
mongoose.connect(uri, {
serverSelectionTimeoutMS: 10000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
});
Trước khi điều chỉnh timeout, hãy xác minh MongoDB server có thực sự truy cập được không:
mongosh "mongodb://user:pass@host:27017/dbname" --eval "db.adminCommand({ ping: 1 })"
Ping thành công sẽ loại trừ hoàn toàn các vấn đề về mạng.
Cách Sửa 4 — Tái Sử Dụng Một MongoClient (Không Tạo Mới Mỗi Request)
Tạo một MongoClient mới cho mỗi request — rồi đóng nó — là một anti-pattern khá phổ biến. Trong điều kiện có concurrency, các request chồng lên nhau sẽ dùng chung một topology mà một trong số chúng vừa đóng:
// SAI — client mới cho mỗi request, race condition chực chờ xảy ra
app.get('/users', async (req, res) => {
const client = new MongoClient(uri);
await client.connect();
const users = await client.db('mydb').collection('users').find().toArray();
await client.close(); // request chồng lên tiếp theo vẫn có thể đang tham chiếu topology này
res.json(users);
});
// ĐÚNG — một client cho toàn bộ tiến trình
const client = new MongoClient(uri);
await client.connect();
app.get('/users', async (req, res) => {
const users = await client.db('mydb').collection('users').find().toArray();
res.json(users);
});
Connection pool tích hợp của MongoDB (mặc định 100 kết nối) xử lý toàn bộ concurrency cho bạn. Một MongoClient cho mỗi tiến trình là mô hình đúng đắn.
Cách Sửa 5 — Thêm Logic Retry Cho Các Thao Tác Quan Trọng
Sự cố mạng thoáng qua là điều không tránh khỏi. Bọc các truy vấn quan trọng trong logic retry giúp ngăn lỗi topology tạm thời hiển thị ra cho người dùng:
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt setTimeout(r, 1000 * attempt)); // backoff 1s, 2s, 3s
continue;
}
throw err;
}
}
}
// Cách dùng
const result = await withRetry(() =>
db.collection('orders').findOne({ _id: orderId })
);
Backoff theo cấp số nhân (1 giây, 2 giây, 3 giây) cho driver thời gian phục hồi mà không liên tục tấn công server đang gặp sự cố.
Cách Sửa 6 — pymongo (Python)
Python biểu hiện lỗi này khác đi — thường là pymongo.errors.InvalidOperation: Cannot use MongoClient after close — nhưng nguyên nhân gốc rễ giống hệt nhau. Cách sửa là dùng context manager:
from pymongo import MongoClient
# SAI — client đóng trước khi find_one chạy
client = MongoClient(uri)
db = client['mydb']
client.close()
db.users.find_one() # topology đã bị hủy
# ĐÚNG — context manager đóng client chỉ sau khi khối lệnh kết thúc
with MongoClient(uri) as client:
db = client['mydb']
result = db.users.find_one({'id': 1})
print(result)
Xác Nhận Đã Sửa Xong
Đừng chỉ restart rồi hy vọng mọi thứ ổn. Hãy xác nhận lỗi thực sự đã biến mất:
- Kích hoạt lại đoạn code trước đây gặp lỗi và kiểm tra log — không còn mục
Topology was destroyednào nữa. - Kiểm tra tình trạng connection pool trong MongoDB Atlas metrics hoặc trực tiếp:
db.serverStatus().connections
// { current: 5, available: 195, totalCreated: 12 }
- Chạy load test với
autocannonhoặck6(50–200 concurrent user là phù hợp cho hầu hết API) và xác nhận không có lỗi topology nào trong toàn bộ các request.
Phòng Ngừa
- Một client cho mỗi tiến trình — khởi tạo
MongoClientmột lần khi khởi động, dùng chung trong toàn bộ ứng dụng. - Thứ tự shutdown đúng chuẩn — drain HTTP connection trước, sau đó mới gọi
mongoose.disconnect()hoặcclient.close(). Không bao giờ làm ngược lại. - Await mọi thứ — không có thao tác MongoDB nào được thực thi mà thiếu
awaithoặc chuỗi.then()đầy đủ. Một quy tắc linter (no-floating-promisestrong TypeScript) sẽ tự động phát hiện điều này. - Điều chỉnh timeout phù hợp thực tế — nếu MongoDB Atlas cluster của bạn ở
us-east-1nhưng ứng dụng chạy ởap-southeast-1, giá trịserverSelectionTimeoutMSlà 5 giây sẽ gây hại cho bạn. Hãy đo latency round-trip thực tế trước. - Theo dõi connection pool — cảnh báo khi
connections.availablegiảm xuống dưới 20. Pool exhaustion thường chính là nguyên nhân kích hoạt việc hủy topology khi tải cao.

