Chuyển đến nội dung chính

Bài 10: Rate Limiting & Throttling — Kiểm soát tốc độ gửi

Tại sao cần rate limiting cho email system. Token bucket, sliding window, leaky bucket. Multi-level throttling theo provider, domain, IP. Adaptive rate limiting dựa trên bounce rate. Redis-based distributed limiter.

🏗️ Kiến trúc — Bài 10 Bài 10: Rate Limiting & Throttling — Kiểm soát tốc độ gửi

Thiết kế Hệ thống Notification gửi hàng triệu Email

Phần 4: Xử lý quy mô — Scaling to Millions

xdev.asia

Giới thiệu

Một hệ thống có thể render và enqueue hàng triệu email chưa chắc đã có thể gửi chúng an toàn. Điểm nghẽn thực sự thường nằm ở tốc độ gửi hợp lệ: giới hạn từ ESP, reputation của domain/IP, và khả năng hấp thụ của downstream systems.

Bài này tập trung vào lớp điều tiết lưu lượng, nơi quyết định chiến dịch 10 triệu email sẽ thành công hay tự đẩy cả domain vào spam folder.


1. Vì sao rate limiting là bắt buộc?

Các giới hạn thực tế

Nguồn giới hạnVí dụHậu quả nếu vượt ngưỡng
ESP send quotaSES 200 email/giây mặc địnhThrottlingException, queue backlog
Domain reputationDomain mới gửi đột ngột 1M emailSpam placement tăng mạnh
IP reputationDedicated IP chưa warm-upBounce và complaint tăng
ISP limitsGmail, Yahoo throttling theo domainDeferred delivery, soft bounce
Internal systemsWorker quá nhiều, DB update quá dàyCPU cao, lock contention

Mục tiêu của throttling layer

  • Bảo vệ reputation của sender.
  • Bảo vệ email providers khỏi traffic burst.
  • Ưu tiên transactional email hơn marketing email.
  • Duy trì throughput ổn định thay vì spike ngắn rồi sập.
  • Giữ queue backlog trong phạm vi kiểm soát.

2. Các thuật toán phổ biến

So sánh nhanh

Thuật toánĐiểm mạnhĐiểm yếuPhù hợp cho
Fixed WindowDễ implementBurst ở ranh giới windowCounter đơn giản
Sliding WindowChính xác hơnTốn state hơnPer-domain limits
Leaky BucketOutput đềuHơi cứng với traffic burst hợp lệSmoothing traffic
Token BucketCho phép burst có kiểm soátCần đồng bộ stateESP/API throttling

Khuyến nghị cho email system

  • Dùng token bucket cho giới hạn theo provider và theo IP pool.
  • Dùng sliding window cho complaint rate, bounce rate, open rate theo campaign/domain.
  • Dùng priority-aware scheduler để email quan trọng không bị marketing chiếm hết quota.

3. Kiến trúc multi-level throttling

Campaign Queue
    │
    ▼
Priority Scheduler
    │
    ├── Check provider quota (SES / SendGrid / Mailgun)
    ├── Check domain quota (gmail.com / yahoo.com / outlook.com)
    ├── Check IP pool quota
    ├── Check campaign quota
    └── Check suppression / reputation guard
    │
    ▼
Dispatch Queue
    │
    ▼
Workers -> ESP

Thứ tự kiểm tra hợp lý

  1. Kiểm tra suppression list và bounce history.
  2. Kiểm tra priority class của message.
  3. Kiểm tra quota theo provider.
  4. Kiểm tra quota theo recipient domain.
  5. Kiểm tra quota theo sender IP / dedicated IP pool.
  6. Nếu fail, reschedule thay vì drop.

Priority policy ví dụ

PriorityUse caseSLAQuy tắc
criticalOTP, reset password< 30sLuôn có reserved capacity
highOrder confirmation< 2 phútKhông bị block bởi marketing
normalProduct updates< 15 phútChia sẻ quota động
lowNewsletter, drip campaignTheo scheduleBị cắt trước khi reputation xấu

4. Distributed rate limiter với Redis

Token bucket model

import time
import redis

class RedisTokenBucket:
    def __init__(self, client: redis.Redis, key: str, rate: int, burst: int):
        self.client = client
        self.key = key
        self.rate = rate
        self.burst = burst

    def allow(self, tokens: int = 1) -> bool:
        now_ms = int(time.time() * 1000)
        script = """
        local key = KEYS[1]
        local now_ms = tonumber(ARGV[1])
        local rate = tonumber(ARGV[2])
        local burst = tonumber(ARGV[3])
        local requested = tonumber(ARGV[4])

        local data = redis.call('HMGET', key, 'tokens', 'updated_at')
        local tokens = tonumber(data[1])
        local updated_at = tonumber(data[2])

        if tokens == nil then
          tokens = burst
          updated_at = now_ms
        end

        local elapsed = math.max(0, now_ms - updated_at)
        local refill = (elapsed / 1000.0) * rate
        tokens = math.min(burst, tokens + refill)

        local allowed = 0
        if tokens >= requested then
          tokens = tokens - requested
          allowed = 1
        end

        redis.call('HMSET', key, 'tokens', tokens, 'updated_at', now_ms)
        redis.call('PEXPIRE', key, 60000)
        return allowed
        """

        result = self.client.eval(script, 1, self.key, now_ms, self.rate, self.burst, tokens)
        return result == 1

Các key nên theo dõi

rate:provider:ses
rate:provider:sendgrid
rate:domain:gmail.com
rate:domain:yahoo.com
rate:ip-pool:warm-01
rate:campaign:camp_flash_sale_april

Điểm quan trọng là limiter phải được share giữa tất cả workers, nếu không mỗi worker đều tưởng mình còn quota và hệ thống sẽ vượt ngưỡng ngay lập tức.


5. Adaptive throttling dựa trên tín hiệu deliverability

Tốc độ gửi không nên cố định. Nó phải phản ứng với tín hiệu từ thực tế.

Tín hiệu cần theo dõi

SignalÝ nghĩaHành động
Soft bounce tăngISP đang defer trafficGiảm 20-40% send rate
Complaint rate tăngNội dung/list quality xấuGiảm mạnh, pause campaign
Open rate giảm bất thườngSpam placement tăngGiảm tốc, đổi IP/domain mix
Queue delay quá caoHệ thống thiếu worker/quotaTăng worker hoặc kéo dài ETA

Policy engine ví dụ

def compute_send_rate(base_rate, metrics):
    rate = base_rate

    if metrics.soft_bounce_rate > 0.03:
        rate *= 0.7

    if metrics.complaint_rate > 0.001:
        rate *= 0.5

    if metrics.provider_throttling_rate > 0.05:
        rate *= 0.8

    if metrics.critical_queue_depth > 1000:
        rate = max(rate, metrics.reserve_for_critical)

    return max(int(rate), 10)

Nguyên tắc

  • Marketing traffic là nơi phải hy sinh đầu tiên.
  • Transactional traffic nên có quota reserve cố định.
  • Không giảm quota về 0 ngay lập tức trừ khi complaint spike rất cao hoặc ESP yêu cầu pause.

6. IP warming và domain warm-up

Một sai lầm phổ biến là domain/IP mới nhưng gửi full load ngay ngày đầu.

Lộ trình warm-up mẫu cho dedicated IP mới

NgàyTối đa email/ngàyĐối tượng
15,000Users engaged cao
210,000Segment sạch, recent active
320,000Mở rộng nhẹ
440,000Bắt đầu mixed traffic
580,000Giữ theo complaint/bounce
6160,000Tiếp tục scale
7+Theo reputationChỉ tăng nếu metrics ổn

Guardrails khi warm-up

  • Chỉ gửi đến recipients đã opt-in rõ ràng.
  • Ưu tiên segment có open/click rate tốt.
  • Không reuse list cũ chất lượng thấp.
  • Theo dõi Gmail Postmaster Tools và Microsoft SNDS nếu có.

7. Failure modes thường gặp

Khi thiết kế limiter sai

  1. Local limiter per worker: tổng throughput vượt quota dù từng worker đều "đúng".
  2. Không reserve capacity: OTP bị chậm vì campaign marketing đang ăn hết token.
  3. Throttle theo provider nhưng quên domain: ESP vẫn nhận, nhưng Gmail bắt đầu defer.
  4. Retry vô hạn: throttle bị khuếch đại do retry storm.

Checklist production

  • Có quota theo provider, domain, IP, campaign.
  • Có reserve cho critical traffic.
  • Có cơ chế pause campaign thủ công.
  • Có adaptive throttling dựa trên feedback loop.
  • Có dashboard hiển thị send rate hiện tại và quota còn lại.

Tổng kết

Rate limiting không chỉ là bài toán kỹ thuật mà là lớp bảo vệ sống còn cho deliverability. Hệ thống tốt phải biết gửi nhanh khi được phép, giảm tốc khi có tín hiệu xấu, và luôn ưu tiên traffic quan trọng nhất.

Bài tiếp theo: Chúng ta sẽ tổ chức batch processing và worker pool để xử lý hàng triệu recipients hiệu quả mà không làm cạn memory hay nghẽn database.