0
0
Lập trình
Admin Team
Admin Teamtechmely

Bảo vệ API Node.js của bạn: Giới hạn Tốc độ với 3 Chiến lược

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

• 6 phút đọc

Giới thiệu

Giới hạn tốc độ (Rate Limiting) là một chiến lược quan trọng để kiểm soát số lượng yêu cầu mà một khách hàng hoặc người dùng có thể gửi đến một mạng, ứng dụng hoặc API trong một khoảng thời gian nhất định (mỗi phút, mỗi giây). Trong bài viết này, chúng ta sẽ khám phá ba chiến lược phổ biến để triển khai giới hạn tốc độ: Fixed Window, Sliding Window và Token Bucket.

Tại sao Giới hạn Tốc độ lại Quan trọng?

1. Bảo vệ Tài nguyên khỏi Lạm dụng

Không có giới hạn tốc độ, một khách hàng (hoặc bot) có thể gửi hàng ngàn yêu cầu trong vài giây. Điều này có thể làm sập máy chủ của bạn, tăng chi phí (nếu bạn phải trả theo yêu cầu API hoặc thời gian tính toán) và giảm hiệu suất cho những người dùng khác. Với giới hạn tốc độ, bạn chặn bất kỳ khách hàng nào khỏi việc chiếm đoạt tài nguyên của hệ thống.

2. Ngăn Chặn Các Cuộc Tấn Công Denial-of-Service (DoS)

Các kẻ tấn công thường cố gắng làm ngập máy chủ bằng lưu lượng truy cập để làm cho dịch vụ trở nên không khả dụng. Giới hạn tốc độ giúp giảm thiểu tác động của một cuộc tấn công như vậy bằng cách chặn các yêu cầu lạm dụng trước khi chúng tiêu tốn hết băng thông, bộ nhớ hoặc CPU của bạn. Mặc dù đây không phải là giải pháp hoàn hảo đối với các cuộc tấn công Distributed DoS (DDoS) lớn, nhưng nó là một hàng rào bảo vệ cần thiết.

3. Đảm Bảo Sử Dụng Công Bằng

Trong các hệ thống đa người dùng hoặc API công cộng, bạn muốn mọi người có một phần truy cập công bằng. Giới hạn tốc độ đảm bảo rằng một khách hàng không chiếm dụng dịch vụ của người khác. Ví dụ, trong một API cho phép 100 yêu cầu mỗi phút, mọi người dùng đều có cùng một cơ hội, ngăn chặn việc lạm dụng và duy trì trải nghiệm nhất quán cho tất cả.

Các Chiến lược Giới hạn Tốc độ

Trong phần này, chúng ta sẽ tìm hiểu ba phương pháp phổ biến để triển khai giới hạn tốc độ:

  1. Fixed Window
  2. Sliding Window
  3. Token Bucket / Leaky Bucket

1. Fixed Window

Chiến lược Fixed Window đếm số yêu cầu trong một khoảng thời gian cố định (như giây, phút hoặc giờ). Khi khoảng thời gian này kết thúc, số đếm cũng được thiết lập lại.

Ví dụ: Nếu một yêu cầu API bị giới hạn ở 5 yêu cầu mỗi phút, API không thể nhận quá 5 yêu cầu trong mỗi phút và ngưỡng này sẽ thiết lập lại sau mỗi 1 phút.

javascript Copy
import { Request, Response, NextFunction } from 'express';

// Cấu hình
const WINDOW_SIZE_IN_MS = 60_000; // 1 phút
const MAX_REQUESTS = 5; // mỗi IP mỗi khoảng thời gian

// Lưu trữ: { ip -> { count, windowStart } }
type Entry = { count: number; windowStart: number };
const store = new Map<string, Entry>();

export function fixedWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const ip = req.ip || req.connection.remoteAddress || 'unknown';
  const now = Date.now();

  let entry = store.get(ip);

  if (!entry) {
    // Yêu cầu đầu tiên của IP này
    store.set(ip, { count: 1, windowStart: now });
    return next();
  }

  // Nếu khoảng thời gian hiện tại đã hết → thiết lập lại bộ đếm
  if (now - entry.windowStart >= WINDOW_SIZE_IN_MS) {
    entry = { count: 1, windowStart: now };
    store.set(ip, entry);
    return next();
  }

  // Vẫn trong khoảng thời gian
  entry.count++;

  if (entry.count > MAX_REQUESTS) {
    const retryAfter = Math.ceil((entry.windowStart + WINDOW_SIZE_IN_MS - now) / 1000);

    res.setHeader('Retry-After', retryAfter.toString());
    return res.status(429).json({
      success: false,
      message: `Quá nhiều yêu cầu. Vui lòng thử lại sau ${retryAfter}s`
    });
  }

  return next();
}

2. Sliding Window

Khác với giới hạn tốc độ Fixed Window (điều chỉnh số đếm tại các khoảng thời gian cố định), phương pháp Sliding Window liên tục đánh giá các yêu cầu dựa trên một khoảng thời gian di động.

Ví dụ: Nếu một API có giới hạn 100 yêu cầu mỗi phút:

  • Nếu một người dùng gửi 90 yêu cầu trong 50 giây vừa qua, họ chỉ có thể gửi thêm 10 yêu cầu trong 10 giây tiếp theo.

Mỗi giây, khoảng thời gian sẽ tiến lên, loại bỏ các yêu cầu cũ và bao gồm các yêu cầu mới.

javascript Copy
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis();
const WINDOW_SIZE = 60; // giây
const MAX_REQUESTS = 100;

export async function slidingWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const key = `sliding:${req.ip}`;
  const now = Date.now();

  const windowStart = now - WINDOW_SIZE * 1000;

  // Xóa các yêu cầu cũ ngoài khoảng thời gian
  await redis.zremrangebyscore(key, 0, windowStart);

  // Đếm số yêu cầu trong khoảng thời gian
  const count = await redis.zcard(key);

  if (count >= MAX_REQUESTS) {
    res.setHeader('Retry-After', String(WINDOW_SIZE));
    return res.status(429).json({ message: 'Quá nhiều yêu cầu' });
  }

  // Thêm thời gian yêu cầu hiện tại
  await redis.zadd(key, now, now.toString());
  await redis.expire(key, WINDOW_SIZE);

  next();
}

3. Token Bucket / Leaky Bucket

Phương pháp này cho phép các đợt yêu cầu lớn đến một giới hạn nhất định và sau đó dần dần bổ sung. Nó thực thi một tỷ lệ xử lý nghiêm ngặt và không thay đổi, làm mượt lưu lượng.

Cách thức hoạt động:

  1. Các yêu cầu được thêm vào hàng đợi (bucket).
  2. Bucket sẽ rò rỉ với một tỷ lệ cố định (các yêu cầu được xử lý từng cái một theo thời gian đều).
  3. Nếu bucket bị tràn (quá nhiều yêu cầu), các yêu cầu dư thừa sẽ bị loại bỏ.
javascript Copy
import { Request, Response, NextFunction } from 'express';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';

const redis = new Redis();

const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'bucket',
  points: 150, // dung lượng bucket
  duration: 60, // khoảng thời gian bổ sung (60s → 150 token mỗi phút)
  execEvenly: true // làm mượt đều
});

export async function tokenBucketLimiter(req: Request, res: Response, next: NextFunction) {
  try {
    await limiter.consume(req.ip, 1);
    next();
  } catch (rejRes) {
    const retrySecs = Math.ceil(rejRes.msBeforeNext / 1000) || 1;
    res.set('Retry-After', String(retrySecs));
    res.status(429).json({ message: 'Quá nhiều yêu cầu, vui lòng thử lại sau' });
  }
}

Mẹo Tối Ưu Hiệu Suất

  • Sử dụng Redis: Với các phương pháp Sliding Window và Token Bucket, sử dụng Redis để lưu trữ thông tin yêu cầu sẽ giúp tối ưu hóa hiệu suất và giảm tải cho server của bạn.
  • Giới hạn số lượng yêu cầu linh hoạt: Tùy chỉnh ngưỡng giới hạn dựa trên hành vi người dùng, ví dụ, cho phép nhiều yêu cầu hơn cho những người dùng có hoạt động tích cực.

Nhận Diện và Xử Lý Lỗi

  • Thông báo rõ ràng: Khi một người dùng vượt quá giới hạn yêu cầu, hãy cung cấp một thông báo rõ ràng về lý do và thời gian chờ đợi.
  • Theo dõi và phân tích: Theo dõi các yêu cầu và phân tích để tìm ra các mẫu lạm dụng và điều chỉnh giới hạn cho phù hợp.

Kết luận

Giới hạn tốc độ không chỉ là một mẹo hiệu suất — nó là một biện pháp bảo vệ chống lại lạm dụng, thời gian chết và sử dụng không công bằng. Chúng ta đã khám phá ba chiến lược mạnh mẽ: Fixed Window, Sliding Window và Token Bucket/Leaky Bucket. Mỗi chiến lược có ưu điểm riêng, và việc chọn lựa phương pháp phù hợp sẽ phụ thuộc vào mẫu lưu lượng của ứng dụng và nhu cầu mở rộng.

👉 Hãy theo dõi video của tôi trên YouTube, nơi tôi sẽ hướng dẫn bạn cách thiết lập giới hạn tốc độ thực tế trong ứng dụng Node.js, từng bước với các ví dụ mã hóa trực tiếp và các mẹo tốt nhất để giữ cho API của bạn an toàn và hiệu quả.

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