Vấn đề Hiệu năng Ẩn mà Kỹ sư Backend Thường Bỏ qua
Nhiều hệ thống backend chạy khá ổn ở môi trường local/dev nhưng dần trở nên chậm, thiếu ổn định hoặc tốn kém khi lên production. Nguyên nhân thường đến từ các vấn đề hiệu năng khó thấy trong code review hoặc test dữ liệu nhỏ.
Bài viết này tập trung vào 5 vấn đề hiệu năng phổ biến nhưng dễ bị bỏ sót:
- N+1 query
- Cạn kiệt connection pool
- Chiến lược cache không phù hợp
- I/O chậm
- Logic blocking trên critical path
Với mỗi vấn đề, chúng ta sẽ đi qua: cách phát hiện, vì sao xảy ra, và cách xử lý.
1. N+1 Query
1.1 N+1 Query là gì?
N+1 xảy ra khi bạn:
- Chạy 1 query để lấy danh sách bản ghi.
- Sau đó chạy thêm N query để lấy dữ liệu liên quan cho từng bản ghi.
Ví dụ (pseudo-code):
// 1 query
const users = await db.user.findMany();
// N queries (mỗi user một query)
for (const user of users) {
user.posts = await db.post.findMany({ where: { userId: user.id } });
}Nếu có 100 users, bạn vừa chạy 101 query thay vì 1-2 query.
1.2 Vì sao nguy hiểm?
- Trông vẫn ổn trên local với dataset nhỏ
- Trở nên rất chậm với dữ liệu production thực tế
- Tạo spike tải DB và tăng latency
1.3 Cách phát hiện N+1
Kỹ thuật phát hiện
- Bật query logging (slow query log, ORM debug)
- Tìm query lặp lại trong loop
- Dùng APM (New Relic, Datadog, OpenTelemetry) để xem span DB
- Profile endpoint với khối lượng dữ liệu thực tế
1.4 Cách xử lý N+1
a) Dùng Join hoặc Include/Preload
const usersWithPosts = await db.user.findMany({
include: {
posts: true,
},
});b) Dùng truy vấn theo IN / batch loading
const users = await db.user.findMany();
const userIds = users.map((u) => u.id);
const posts = await db.post.findMany({
where: { userId: { in: userIds } },
});
// group posts theo userId trong memoryc) Dùng DataLoader pattern (GraphQL / API)
Thêm lớp batching (ví dụ DataLoader) để gộp nhiều query nhỏ thành ít query lớn hơn.
2. Cạn kiệt Connection Pool
2.1 Là gì?
Hầu hết backend dùng connection pool để giao tiếp với database/dịch vụ ngoài. Mỗi connection là tài nguyên hữu hạn.
Cạn kiệt pool xảy ra khi:
- Tất cả connection đang bị giữ
- Request mới phải chờ connection rảnh
- Cuối cùng dẫn tới timeout và có thể gây failure dây chuyền
2.2 Nguyên nhân thường gặp
- Query chạy quá lâu
- Giữ connection trong lúc chạy tác vụ blocking
- Không trả connection ở nhánh lỗi
- Pool size quá nhỏ so với traffic
2.3 Dấu hiệu
- Latency tăng dần theo thời gian
- Nhiều request kẹt ở trạng thái “waiting for connection”
- DB có nhiều session “idle in transaction”
2.4 Cách phát hiện
Cần kiểm tra gì?
- Metric DB: active connections, wait time, lỗi “too many connections”
- Log app: “pool timeout”, “connection acquisition timeout”
- Trace APM: span lấy connection kéo dài bất thường
2.5 Cách xử lý / phòng ngừa
- Luôn trả connection trong
finally:
const client = await pool.connect();
try {
await client.query('...');
} finally {
client.release();
}- Dùng transaction ngắn, không giữ connection khi:
- Chờ network call
- Chạy việc CPU nặng
- Tuning pool size theo số instance ứng dụng và năng lực DB
- Đặt timeout và circuit breaker cho query chậm
3. Chiến lược Cache Không Phù hợp
Cache có thể tăng hiệu năng rất mạnh, nhưng làm sai sẽ gây:
- Dữ liệu stale
- Cache stampede
- Tăng độ phức tạp và tốn bộ nhớ
3.1 Các lỗi cache phổ biến
a) Cache mọi thứ, không TTL
cache.set('users', users); // không TTL, không invalidationHệ quả:
- Dữ liệu nhanh stale
- Memory tăng không kiểm soát
- Khó đảm bảo tính đúng đắn
b) Cache sai tầng
- Cache per-instance thay vì shared
- Trùng lặp cache ở nhiều lớp (API, service, DB)
c) Không có chiến lược invalidation
- “Sai dữ liệu thì restart service”
- Xóa cache thủ công
3.2 Pattern cache tốt
Nguyên tắc
- Cache dữ liệu đọc nhiều, đắt, ít thay đổi
- Dùng TTL và/hoặc invalidation theo event
- Key cache nên rõ ràng, có namespace
- Với nhiều instance, ưu tiên shared cache (Redis)
Ví dụ read-through cache với TTL:
async function getUserProfile(id: string) {
const key = `user:profile:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id } });
if (user) {
await redis.set(key, JSON.stringify(user), { EX: 300 }); // 5 phút
}
return user;
}3.3 Tránh cache stampede
Khi key hot hết hạn, nhiều request cùng đập xuống DB.
Cách giảm thiểu:
- Locking/single-flight
- Stale-while-revalidate
- Thêm jitter vào TTL để tránh hết hạn đồng loạt
4. I/O Chậm (Disk, Network, External Services)
4.1 Vì sao I/O chậm nguy hiểm?
Ngay cả khi CPU thấp, app vẫn có thể bị:
- I/O-bound (chờ disk/network/API ngoài)
- Tăng latency spike do dependency chậm
4.2 Ví dụ I/O chậm khó thấy
- Ghi log lớn kiểu synchronous cho mỗi request
- Gọi API ngoài tuần tự trong loop
- Đọc file lớn/đổ report trên luồng request chính
4.3 Cách phát hiện
Observability
- Dùng APM breakdown theo DB/API/disk
- Đặt timer/metric quanh thao tác I/O
- Kiểm tra IOPS đĩa, network latency, error rate
4.4 Cách xử lý
- Parallelize I/O độc lập khi an toàn:
const [user, orders] = await Promise.all([getUser(id), getUserOrders(id)]);- Đẩy I/O nặng sang background jobs:
- Sinh báo cáo
- Export dữ liệu lớn
- Xử lý file
- Đặt timeout và circuit breaker cho dịch vụ ngoài
5. Logic Blocking Trên Critical Path
5.1 Vấn đề
Trong runtime event loop (Node.js), thao tác đồng bộ nặng trên request path sẽ block mọi request khác:
- JSON payload quá lớn
- Regex phức tạp
- Mã hóa/hash nặng chạy trực tiếp
- Vòng lặp CPU-bound
5.2 Cách phát hiện
- Event loop lag tăng cao
- CPU spike nhưng throughput giảm
- p95/p99 latency xấu dù DB không quá tải
5.3 Cách xử lý
- Chuyển tác vụ nặng sang worker/background queue
- Tránh thao tác sync nặng trong request path
- Giới hạn payload và validate sớm
- Profile bằng flamegraph để tìm hotspot CPU
Checklist Production
- [ ] Bật query log và phát hiện N+1 sớm
- [ ] Theo dõi pool metrics + timeout
- [ ] Chuẩn hóa chiến lược cache (TTL + invalidation)
- [ ] Đặt timeout/circuit breaker cho external calls
- [ ] Tránh CPU/blocking logic trên hot path
- [ ] Theo dõi p95/p99 latency và error budget
Kết luận
Vấn đề hiệu năng ẩn hiếm khi đến từ một lỗi lớn duy nhất. Thường đó là nhiều “điểm rò” nhỏ cộng dồn theo thời gian. Nếu bạn thiết kế observability tốt, áp dụng checklist định kỳ, và xử lý theo ưu tiên từ dữ liệu thực tế, hệ thống sẽ ổn định hơn đáng kể khi scale.
