Cách tôi xây dựng bộ giới hạn tốc độ (Rate Limiter) theo cơ chế Cửa sổ trượt (Sliding Window) trong Redis

API video của chúng tôi từng bị sập vào mỗi buổi tối lúc 8 giờ tối UTC.

Đó không phải là lưu lượng truy cập thực tế. Một vài công cụ quét dữ liệu (scrapers) đã tìm thấy endpoint xu hướng của chúng tôi và tấn công dồn dập. Các truy vấn cơ sở dữ liệu bị dồn ứ. Người dùng thực tế chỉ thấy biểu tượng đang tải thay vì video.

Chúng tôi cần cơ chế giới hạn tốc độ (rate limiting). Hầu hết các phương pháp đơn giản đều làm vấn đề trở nên tồi tệ hơn.

Vấn đề với bộ đếm Cửa sổ cố định (Fixed Window):

Hầu hết mọi người đều bắt đầu với cửa sổ cố định. Bạn đếm số lượng yêu cầu trong một khối thời gian nhất định. Nếu giới hạn của bạn là 100 yêu cầu mỗi phút, một người dùng có thể gửi 100 yêu cầu vào lúc 11:59:59 và thêm 100 yêu cầu nữa vào lúc 12:00:00. Như vậy là 200 yêu cầu chỉ trong một giây. Các công cụ quét dữ liệu lợi dụng các ranh giới này để vượt qua giới hạn của bạn.

Giải pháp: Cửa sổ trượt (Sliding Window).

Cửa sổ trượt đếm số lượng yêu cầu trong N giây gần nhất tính từ thời điểm hiện tại. Không có ranh giới nào để bị lợi dụng.

Chúng tôi sử dụng hai phương pháp khác nhau tùy theo nhu cầu:

1. Nhật ký Cửa sổ trượt (Sliding Window Log) (Để đạt độ chính xác cao)

Chúng tôi sử dụng Redis Sorted Sets (ZSET) cho những người dùng đã xác thực. Chúng tôi lưu trữ một dấu thời gian (timestamp) cho mỗi yêu cầu.

  • Chúng tôi sử dụng ZREMRANGEBYSCORE để loại bỏ các mục cũ.
  • Chúng tôi sử dụng ZCARD để đếm số lượng yêu cầu hiện tại.
  • Chúng tôi sử dụng ZADD để ghi lại các yêu cầu mới.

Để ngăn chặn tình trạng tranh chấp (race conditions), chúng tôi chạy logic này trong một script Lua duy nhất. Điều này giúp thao tác trở nên nguyên tử (atomic). Một script xử lý mọi thứ chỉ trong một lần kết nối tới Redis. Điều này ngăn chặn việc hai yêu cầu cùng lúc cho rằng chúng vẫn đang nằm trong giới hạn cho phép.

2. Bộ đếm Cửa sổ trượt (Sliding Window Counter) (Để mở rộng quy mô)

Đối với lưu lượng truy cập ẩn danh, chúng tôi có hàng triệu khóa (keys). Chúng tôi không thể lưu trữ mọi dấu thời gian. Thay vào đó, chúng tôi sử dụng một bộ đếm có trọng số cho cửa sổ hiện tại và cửa sổ trước đó. Phương pháp này tiêu tốn ít bộ nhớ hơn nhiều và đạt hiệu suất cao.

Những bài học quan trọng từ môi trường thực tế:

  • Sử dụng script Lua. Điều này đảm bảo việc kiểm tra và ghi dữ liệu diễn ra như một hành động duy nhất.
  • Phát hành các header tiêu chuẩn. Luôn gửi X-RateLimit-RemainingRetry-After. Điều này giúp các client tuân thủ quy tắc tự động giảm tốc độ truy cập.
  • Fail open (Cho phép khi lỗi). Nếu Redis gặp sự cố, hãy cho phép yêu cầu đi qua. Một API bị quá tải vẫn tốt hơn là một API hoàn toàn ngừng hoạt động.
  • Nhận diện client chính xác. Sử dụng API key cho người dùng và IP đã được băm (hashed IPs) cho khách.
  • Áp dụng giới hạn phù hợp với từng endpoint. Một endpoint tìm kiếm nặng cần giới hạn chặt chẽ hơn so với một endpoint danh sách đơn giản.

Kết quả có được ngay lập tức. Các đợt tăng vọt vào buổi tối đã biến mất. Hiệu suất cơ sở dữ liệu của chúng tôi đã trở lại bình thường.

Nguồn: https://dev.to/ahmet_gedik778845/how-i-built-a-sliding-window-rate-limiter-for-our-video-api-in-redis-4k1c