Giới thiệu
Nếu bạn là một kỹ sư full-stack phụ trách API sử dụng Node.js, chắc hẳn bạn đã từng trải qua cảm giác khó chịu khi endpoint chạy chậm. Trong môi trường sản xuất, một vài mili giây độ trễ có thể dẫn đến doanh thu bị mất, hóa đơn đám mây cao hơn, và người dùng cảm thấy thất vọng. Hướng dẫn này sẽ chỉ cho bạn những bước cụ thể, ít rủi ro mà bạn có thể thực hiện ngay hôm nay để tăng tốc độ dịch vụ của mình: caching dữ liệu đúng cách, tối ưu hóa chỉ mục cơ sở dữ liệu, và áp dụng các mẫu async cùng với hàng đợi. Tất cả các ví dụ đều sử dụng Node.js thuần túy (không có phép thuật từ framework) để bạn có thể sao chép và dán chúng vào bất kỳ mã nguồn nào của mình.
Hiểu rõ mức độ thời gian sử dụng
Trước khi bạn bắt đầu tối ưu hóa, bạn cần có một cơ sở.
Đo lường độ trễ
- Bật tính năng theo dõi yêu cầu trong middleware của Express (hoặc Fastify).
- Ghi lại thời gian và tên route.
- Liên hệ những bản ghi này với thời gian truy vấn DB và các cuộc gọi HTTP bên ngoài.
javascript
// middleware theo dõi thời gian đơn giản cho Express
app.use((req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const diff = Number(process.hrtime.bigint() - start) / 1e6; // ms
console.log(`${req.method} ${req.originalUrl} → ${res.statusCode} (${diff.toFixed(2)} ms)`);
});
next();
});
Thu thập một vài phút lưu lượng trong môi trường staging, sau đó sắp xếp các route chậm nhất. Những route này là mục tiêu ưu tiên cho các phần tiếp theo.
1️⃣ Chiến lược Caching
Caching là cách hiệu quả nhất để giảm thời gian phản hồi khi dữ liệu được đọc nhiều và thay đổi ít.
Cache tại bộ nhớ với Redis
Redis cung cấp cho bạn một kho lưu trữ nhanh, có thể mạng, tồn tại qua các lần khởi động lại quá trình. Sử dụng nó cho:
- Các truy vấn tìm kiếm thường xuyên (ví dụ: thông tin sản phẩm).
- Các tổng hợp tính toán mà nếu không sẽ phải truy vấn DB trong mỗi yêu cầu.
javascript
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
await client.connect();
async function getUserProfile(userId) {
const cacheKey = `user:profile:${userId}`;
const cached = await client.get(cacheKey);
if (cached) return JSON.parse(cached);
// Giới thiệu vào DB
const profile = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
await client.set(cacheKey, JSON.stringify(profile), { EX: 300 }); // TTL 5 phút
return profile;
}
Mẹo:
- Giữ TTL ngắn để tránh dữ liệu cũ.
- Sử dụng mẫu
SETNXđể ngăn chặn hiện tượng cache stampedes.
Tiêu đề Cache HTTP
Khi phản hồi là không thay đổi trong một khoảng thời gian, hãy để các trình duyệt và CDN thực hiện công việc nặng nhọc.
javascript
app.get('/public/terms', (req, res) => {
res.set('Cache-Control', 'public, max-age=86400, immutable');
res.json({ version: '2024-09', content: '...' });
});
Một tiêu đề public, max-age cho biết bất kỳ cache downstream nào (Cloudflare, Fastly, v.v.) rằng payload có thể được lưu trữ trong khoảng thời gian xác định.
2️⃣ Tối ưu hóa Chỉ mục Cơ sở dữ liệu
Ngay cả mã Node.js nhanh nhất cũng sẽ chậm lại nếu truy vấn cơ sở dữ liệu quét hàng triệu hàng.
Xác định các chỉ mục thiếu
Chạy EXPLAIN (ANALYZE, BUFFERS) trên các truy vấn chậm của bạn. Tìm kiếm Seq Scan nơi mà Index Scan sẽ được mong đợi.
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = $1 AND status = 'shipped';
Nếu kế hoạch hiển thị một quét tuần tự, hãy thêm một chỉ mục tổng hợp:
sql
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
Giữ cho Chỉ mục nhẹ
- Tránh việc lập chỉ mục quá mức – mỗi chỉ mục thêm overhead cho việc ghi.
- Sử dụng
INCLUDEcho các chỉ mục bao phủ khi bạn cần thêm cột mà không làm phình to khóa.
sql
CREATE INDEX idx_orders_customer_status_inc ON orders (customer_id, status) INCLUDE (order_date, total_amount);
Giờ đây, truy vấn có thể được hoàn thành hoàn toàn từ chỉ mục, giảm thiểu mili giây trong thời gian phản hồi.
3️⃣ Các Mẫu Async & Hàng đợi Nền
Công việc dài hạn (xử lý hình ảnh, gửi email, tạo PDF) không bao giờ nên chặn luồng yêu cầu.
Fire-and-Forget với setImmediate
Đối với các tác vụ nhỏ không cần độ bền, bạn có thể hoãn thực hiện:
javascript
app.post('/upload', async (req, res) => {
// Lưu tệp một cách đồng bộ
const fileId = await saveFile(req.file);
// Ngay lập tức phản hồi cho khách hàng
res.status(202).json({ fileId });
// Xử lý tệp trong nền
setImmediate(() => generateThumbnail(fileId));
});
Hàng đợi bền vững với BullMQ
Đối với bất kỳ việc gì phải tồn tại qua sự cố, hãy sử dụng hàng đợi dựa trên Redis như BullMQ.
javascript
const { Queue, Worker } = require('bullmq');
const emailQueue = new Queue('email', { connection: { host: 'redis', port: 6379 } });
// Nhà sản xuất – thêm công việc vào hàng đợi
app.post('/send-welcome', async (req, res) => {
await emailQueue.add('welcome', { userId: req.body.id });
res.status(202).send('Email chào mừng đã được xếp hàng');
});
// Người tiêu dùng – xử lý công việc
const worker = new Worker('email', async job => {
if (job.name === 'welcome') {
const user = await db.query('SELECT email FROM users WHERE id = $1', [job.data.userId]);
await sendEmail(user.email, 'Chào mừng!', 'Cảm ơn bạn đã tham gia cùng chúng tôi.');
}
});
Lợi ích:
- Tự động thử lại và giảm thiểu có sẵn.
- Workers có thể được mở rộng theo chiều ngang mà không cần chạm vào mã API.
4️⃣ CDN & Caching Tại Edge
Các tài sản tĩnh (JS bundles, hình ảnh, CSS) nên được lưu trữ trên CDN. Ngay cả phản hồi API cũng có thể được cached tại edge khi chúng là idempotent.
- Triển khai một CDN (Cloudflare, AWS CloudFront) ở phía trước proxy ngược Nginx của bạn.
- Bật
stale-while-revalidateđể phục vụ nội dung hơi cũ trong khi nguồn gốc được làm mới. - Tận dụng các chức năng edge cho kiểm tra xác thực giá rẻ, độ trễ thấp hoặc A/B testing.
nginx
# Ví dụ đoạn mã Nginx cho caching nhận thức edge
location /api/ {
proxy_pass http://upstream_app;
proxy_cache my_cache;
proxy_cache_valid 200 30s;
add_header Cache-Control "public, max-age=30, stale-while-revalidate=60";
}
5️⃣ Danh sách Kiểm tra Hiệu suất Nhanh
- Đo lường trước: Ghi lại độ trễ cơ bản với middleware theo dõi yêu cầu.
- Caching mạnh mẽ: Redis cho dữ liệu động, tiêu đề HTTP cho payload tĩnh.
- Chỉ mục khôn ngoan: Chạy
EXPLAINtrên mọi truy vấn chậm và thêm chỉ mục tổng hợp. - Chuyển giao công việc: Sử dụng
setImmediatecho fire-and-forget, BullMQ cho công việc bền. - Đẩy ra edge: Phục vụ tài sản tĩnh qua CDN, thêm tiêu đề cache edge cho các API GET.
- Giám sát liên tục: Thiết lập cảnh báo Grafana/Prometheus cho thời gian phản hồi phần trăm 99.
Kết luận
Tối ưu hóa hiệu suất là một quá trình lặp đi lặp lại. Bằng cách bắt đầu với các phép đo chính xác, sau đó thêm caching, chỉ mục, xử lý async và giao hàng edge, bạn có thể thường xuyên giảm thời gian phản hồi trung bình xuống 50% hoặc hơn mà không cần viết lại lớn. Giữ danh sách kiểm tra bên cạnh, xem lại các chỉ số của bạn sau mỗi thay đổi, và để dữ liệu dẫn dắt bạn.
Nếu bạn cần hỗ trợ trong việc triển khai những tối ưu hóa này quy mô lớn, đội ngũ tại RamerLabs có thể giúp đỡ bạn.