0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Tối ưu hiệu suất cho API Node.js với Redis và CDN

Đăng vào 7 tháng trước

• 6 phút đọc

Tại sao Hiệu Suất Quan Trọng cho API Node.js

Nếu bạn là một kỹ sư full-stack, có lẽ bạn đã cảm nhận rõ sự khó chịu khi một endpoint hoạt động chậm: người dùng bỏ đi, tỷ lệ chuyển đổi giảm, và toàn đội ngũ phải vội vàng khắc phục sự cố. Các API hiện đại cần phải xử lý những đợt lưu lượng truy cập đột biến, giữ cho độ trễ ở mức hợp lý và kiểm soát chi phí đám mây. Trong hướng dẫn thực tiễn này, chúng ta sẽ đi qua một quy trình tối ưu hóa gồm tám bước kết hợp giữa caching Redis, lập chỉ mục cơ sở dữ liệu thông minh, hàng đợi nền và lớp CDN. Cuối cùng, bạn sẽ có một cơ sở hiệu suất có thể đo lường và một bộ điều chỉnh cụ thể để điều chỉnh.


Bước 1: Ghi Nhận Cơ Sở Dữ Liệu

Trước khi bạn có thể cải thiện bất cứ điều gì, bạn cần dữ liệu. Sử dụng một công cụ kiểm tra tải nhẹ mô phỏng các mẫu lưu lượng thực tế.

Các công cụ bạn có thể sử dụng

  • autocannon – công cụ CLI đơn giản để kiểm tra HTTP.
  • k6 – kiểm tra tải có thể lập trình với khả năng xuất số liệu.
  • Postman/Newman – kiểm tra nhanh.
javascript Copy
# Cài đặt autocannon toàn cầu
npm i -g autocannon

# Chạy một bài kiểm tra 30 giây trên endpoint /users
autocannon -d 30 -c 50 https://api.example.com/users

Ghi lại độ trễ trung bình, p99, và băng thông. Lưu những con số này trong một bảng markdown hoặc bảng điều khiển Grafana để bạn có thể so sánh sau này.


Bước 2: Thêm Lớp Cache Redis

Mô hình cache-aside là cách linh hoạt nhất để giới thiệu Redis mà không cần viết lại logic nghiệp vụ.

Cài đặt và cấu hình

javascript Copy
npm i ioredis
javascript Copy
// redisClient.js
const Redis = require('ioredis')
const redis = new Redis({ host: process.env.REDIS_HOST, port: 6379 })
module.exports = redis

Triển khai cache-aside trong một endpoint

javascript Copy
// userController.js
const redis = require('./redisClient')
const db = require('./db') // giả sử là một client pg

async function getUser(req, res) {
  const id = req.params.id
  const cacheKey = `user:${id}`

  // 1️⃣ Thử Redis trước
  const cached = await redis.get(cacheKey)
  if (cached) {
    return res.json(JSON.parse(cached))
  }

  // 2️⃣ Rơi về DB
  const { rows } = await db.query('SELECT * FROM users WHERE id = $1', [id])
  const user = rows[0]

  // 3️⃣ Điền cache với TTL hợp lý (ví dụ, 5 phút)
  await redis.setex(cacheKey, 300, JSON.stringify(user))
  res.json(user)
}

Các thực tiễn tốt nhất

  • Chọn TTL phù hợp với yêu cầu độ tươi mới của dữ liệu.
  • Sử dụng hash cho các đối tượng liên quan để tránh tình trạng stampede cache.
  • Ghi lại các lần truy cập cache/hỏng để phân tích sau này.

Bước 3: Tối ưu Truy Cập Cơ Sở Dữ Liệu

Ngay cả với Redis, API của bạn vẫn sẽ phải truy cập cơ sở dữ liệu cho các thao tác ghi và các lần truy cập không có trong cache. Đảm bảo rằng các truy vấn đó càng nhẹ càng tốt.

Lập chỉ mục các cột đúng cách

sql Copy
-- Lập chỉ mục trên cột tìm kiếm chính
CREATE INDEX idx_users_id ON users(id);

-- Chỉ mục tổng hợp cho các bộ lọc phổ biến
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

Chọn chỉ các cột cần thiết

javascript Copy
// Xấu – chọn tất cả các cột
await db.query('SELECT * FROM users WHERE id = $1', [id])

// Tốt – chỉ lấy những gì bạn cần
await db.query('SELECT id, name, email FROM users WHERE id = $1', [id])

Sử dụng câu lệnh đã chuẩn bị & kết nối pooling

javascript Copy
const { Pool } = require('pg')
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
module.exports = pool

Bước 4: Chuyển Giao Công Việc Nặng cho Hàng Đợi

Các tác vụ kéo dài (gửi email, xử lý hình ảnh, phân tích) không bao giờ nên chặn chu trình yêu cầu-phản hồi. BullMQ hoạt động tốt với Redis.

javascript Copy
npm i bullmq
javascript Copy
// queue.js
const { Queue, Worker } = require('bullmq')
const connection = { host: process.env.REDIS_HOST }
const emailQueue = new Queue('email', { connection })

// Nhà sản xuất – gọi từ một endpoint
async function enqueueWelcomeEmail(userId) {
  await emailQueue.add('welcome', { userId })
}

// Người tiêu dùng – chạy trong một quy trình riêng
const worker = new Worker('email', async job => {
  const { userId } = job.data
  // gửi email qua nhà cung cấp của bạn
}, { connection })

Bằng cách di chuyển các công việc này vào một worker nền, bạn giữ độ trễ API thấp và có khả năng tự động lặp lại/điều chỉnh tự nhiên.


Bước 5: Tận Dụng Lớp CDN

Tài sản tĩnh (JS bundles, hình ảnh) nên được lưu trữ trên CDN, nhưng bạn cũng có thể cache các phản hồi API ở rìa cho các endpoint chỉ đọc.

Thiết lập nhanh với Cloudflare Workers

  1. Tạo một script Worker mà proxy /public/* tới nguồn gốc của bạn.
  2. Thêm một header Cache-Control: public, max-age=300 vào các phản hồi JSON có thể cache.
  3. Sử dụng quy tắc Cache-Everything của Cloudflare cho /v1/products nếu dữ liệu thay đổi không thường xuyên.
javascript Copy
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const url = new URL(request.url)
  if (url.pathname.startsWith('/public/')) {
    // Để Cloudflare cache tệp tĩnh
    return fetch(request)
  }
  // Đối với các tuyến API, thêm cache rìa ngắn hạn
  const response = await fetch(request)
  const newHeaders = new Headers(response.headers)
  newHeaders.set('Cache-Control', 'public, max-age=60')
  return new Response(response.body, { ...response, headers: newHeaders })
}

Mẹo caching rìa

  • Giữ TTL thấp cho dữ liệu thay đổi thường xuyên.
  • Biến thể theo Accept-Encoding để tránh phục vụ nội dung nén cho các khách hàng không tương thích.
  • Sử dụng stale-while-revalidate để phục vụ dữ liệu hơi lỗi thời trong khi một lần lấy mới đang diễn ra.

Bước 6: Giám Sát & Cảnh Báo Liên Tục

Hiệu suất là một mục tiêu di động. Tích hợp khả năng quan sát từ ngày đầu tiên.

  • Prometheus thu thập số liệu từ express-prom-bundle hoặc fastify-metrics.
  • Grafana bảng điều khiển cho độ trễ, tỷ lệ lỗi, tỷ lệ hit cache và độ sâu hàng đợi.
  • Alertmanager kích hoạt Slack/PagerDuty khi độ trễ p99 tăng > 200 ms.
javascript Copy
// metrics.js (ví dụ Express)
const promBundle = require('express-prom-bundle')
app.use(promBundle({ includeMethod: true, includePath: true }))

Các số liệu chính cần theo dõi

  • Độ trễ p99 – mục tiêu SLA của bạn.
  • Tỷ lệ hit cache – nhắm đến > 80% cho các endpoint nóng.
  • Độ sâu hàng đợi – nên giữ dưới một ngưỡng cấu hình.
  • Sử dụng pool kết nối DB – tránh bão hòa.

Bước 7: Tự Động Triển Khai với Chiến Lược Không Ngừng Hoạt Động

Khi bạn đẩy một phiên bản mới bao gồm thay đổi khóa cache hoặc di chuyển chỉ mục, bạn không muốn người dùng thấy lỗi 500.

  • Sử dụng triển khai xanh-lục trong Kubernetes hoặc Docker Swarm.
  • Chạy di chuyển DB trực tuyến với các công cụ như pg_rollback hoặc flyway có thể thêm cột trước khi xóa các cột cũ.
  • Làm ấm cache Redis sau khi triển khai bằng cách lấy trước các khóa được sử dụng nhiều nhất.
javascript Copy
# Ví dụ: script làm ấm cache
node -e "
const redis = require('./redisClient')
;[1,2,3,4,5].forEach(async id => {
  const { rows } = await db.query('SELECT id, name FROM users WHERE id=$1', [id])
  await redis.setex(`user:${id}`, 300, JSON.stringify(rows[0]))
})
" 

Kết Luận

Bằng cách đo lường, caching, lập chỉ mục, xếp hàng và caching ở rìa, bạn có thể giảm hàng chục đến hàng trăm mili giây cho mỗi yêu cầu, giảm tải cho cơ sở dữ liệu và giữ cho hóa đơn đám mây của bạn có thể dự đoán. Hãy nhớ tích hợp giám sát vào quy trình để phát hiện các suy giảm sớm.

Nếu bạn cần hỗ trợ triển khai điều này, đội ngũ tại https://ramerlabs.com có thể giúp.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào