Bỏ qua đến nội dung
Quay lại

Rate Limiter Phân tán: Thiết kế và xử lý lỗi

Đã đăng:  at  10:00 SA

1. Bài toán

API gateway cần có một quyết định trước khi gọi application code: principal này có được phép tiêu thụ một đơn vị quota ngay bây giờ không? Principal có thể là user, source IP, API key hoặc OAuth token. Policy áp dụng có thể khác theo route, HTTP method, gói tenant và region.

Limiter phải hỗ trợ token bucket để kiểm soát tốc độ refill và cho phép burst có giới hạn, cùng sliding window khi sản phẩm cần một số đếm cứng trong một khoảng thời gian. Request bị từ chối trả về HTTP 429 Too Many Requests với giá trị Retry-After hữu ích. Thay đổi policy phải đến được gateway mà không cần restart.

Caller của dịch vụ là API gateway và các service nội bộ. Platform team và product team vận hành dịch vụ. Vì limiter nằm trên synchronous request path, các ràng buộc chính là latency, availability, ngữ nghĩa counter, khả năng scale, hành vi của clock và an toàn khi lỗi.

Yêu cầu [SOURCE FACT]

Yêu cầuMục tiêu
Độ trễ quyết địnhp99 dưới 1 ms tại gateway, không tính thời gian mạng đến origin
Availability của quyết định99.99% mỗi tháng cho limiter path
Ngữ nghĩa counterMột logical counter toàn cục cho mỗi key và policy, với staleness chéo region có giới hạn và được ghi rõ
Khả năng scaleKhông có hotspot trung tâm cố định; shard ownership theo chiều ngang
Hành vi của clockTính đúng không được phụ thuộc vào wall clock đã đồng bộ
An toànLimiter outage không được âm thầm biến thành traffic đắt tiền không giới hạn

Hướng thiết kế [ANALYSIS]

Tách decision plane nhanh khỏi control plane chậm hơn. Xem fail-open và fail-closed là lựa chọn policy rõ ràng, không phải hệ quả ngẫu nhiên của timeout. Lựa chọn phù hợp có thể khác theo endpoint: thao tác đắt tiền có thể fail-closed, còn read ít rủi ro có thể có đường fail-open được giới hạn.

2. Ước tính capacity

Phần dưới là mô hình capacity minh họa, không phải số đo production. Cần thay các giả định bằng telemetry của dịch vụ trước khi provision.

Giả định minh họa [ANALYSIS]

Tính toán [ANALYSIS]

Các giả định trên cho 10,000,000 x 100 = 1,000,000,000 decision mỗi ngày. Chia cho 86.400 giây cho 11,574 decision mỗi giây ở mức trung bình. Áp dụng peak 10x minh họa cho 115,740 decision mỗi giây; cộng 30% headroom cho mục tiêu lập kế hoạch khoảng 150,000 decision mỗi giây.

Ở peak đó, tỷ lệ allow/reject 90:10 tương đương khoảng 135,000 allow mỗi giây và 15,000 reject mỗi giây. Mỗi decision đọc counter và thường thực hiện một atomic update nhỏ, nên read-to-write ratio của decision plane xấp xỉ 1:1. Cached policy read nằm ngoài path này.

Với token bucket, 500,000,000 x 40 x 2 = 40 GB là hot state trước index và memory overhead. Áp dụng overhead minh họa 2.5x cho reservation 100 GB usable in-memory capacity. Expire bucket idle sau 24 giờ là activity TTL, không phải clock dùng để đảm bảo correctness.

Ở peak, 150,000 x 100 = 15 MB/s event data chưa nén, tương đương khoảng 1.296 TB/ngày. Giữ detailed decision event ở dạng sampled, nhưng giữ counter và toàn bộ reject. Với kích thước nén minh họa 30%, retention bảy ngày là khoảng 1.296 TB x 7 x 0.3 = 2.72 TB.

Policy configuration khoảng 100 MB trước replication và cache overhead (100,000 x 1 KB). Với key tăng 5% mỗi ngày, active state sẽ tăng từ 500 triệu lên khoảng 525 triệu key sau một ngày nếu expiry không bù lại mức tăng. Mục tiêu availability 99.99% mỗi tháng tương đương khoảng 4,4 phút trong tháng 30 ngày. p99 latency target phải chặt hơn nhiều so với availability budget này: limiter chậm có thể làm cạn connection hoặc thread ở origin trước khi được xem là unavailable.

3. API Design

Decision endpoint [PROPOSED DESIGN]

Gateway gửi một request identity ổn định. Dùng mTLS để xác thực traffic giữa gateway và limiter. Không tin tenant identifier do caller gửi nếu chưa validate gateway identity và quyền của identity đó.

POST /v1/decisions
Authorization: mTLS
Content-Type: application/json
X-Request-Id: 01J...
Idempotency-Key: gateway-01J...-attempt-1

{
  "principal_type": "api_key",
  "principal_id": "key_7f3",
  "route": "POST:/v1/payments",
  "region": "sg",
  "cost": 1
}

Response cho phép:

HTTP/1.1 200 OK
Content-Type: application/json

{"allowed":true,"remaining":39,"limit":40,"reset_at":"2026-08-15T03:01:00Z","policy_version":812}

Response bị từ chối:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{"allowed":false,"remaining":0,"limit":40,"retry_after_ms":1840,"policy_version":812}

cost cho phép thao tác đắt tiền tiêu thụ nhiều hơn một token. Gateway chuyển tiếp cùng X-Request-Id; retry dùng lại cùng Idempotency-Key. Deduplicate decision record trong một horizon ngắn được ghi rõ để response timeout không tiêu quota hai lần. Key này không thay thế idempotency key của business operation: tạo payment vẫn cần contract idempotency durable riêng.

Quản trị policy [PROPOSED DESIGN]

PUT /v1/policies/{policy_id}
If-Match: "policy-version-811"

{"match":{"route":"POST:/v1/payments","plan":"standard"},"algorithm":"token_bucket","rate_per_second":20,"burst":40,"scope":"api_key"}

PUT là idempotent và If-Match ngăn lost update. Chỉ operator có quyền mới được thay đổi policy. Lưu policy dưới dạng các version bất biến. Trong thời gian propagation, gateway có thể tạm thời dùng version trước; hãy xuất propagation delay tối đa thành metric.

4. Data Model

Control plane [PROPOSED DESIGN]

Dùng relational control database làm source of truth cho policy configuration. Giữ mutable bucket state trong sharded in-memory decision store. Định kỳ summarize state này; không copy đồng bộ mọi mutation vào SQL.

CREATE TABLE rate_policy (
  policy_id        BIGINT PRIMARY KEY,
  version          BIGINT NOT NULL,
  route_pattern    TEXT NOT NULL,
  method           TEXT NOT NULL,
  principal_scope  TEXT NOT NULL,
  algorithm        TEXT NOT NULL CHECK (algorithm IN ('token_bucket', 'sliding_window')),
  rate_per_second  NUMERIC,
  burst            INTEGER,
  window_seconds   INTEGER,
  limit_count      INTEGER,
  state            TEXT NOT NULL CHECK (state IN ('active', 'disabled')),
  updated_at       TIMESTAMPTZ NOT NULL
);

CREATE UNIQUE INDEX rate_policy_version
  ON rate_policy (policy_id, version);

CREATE INDEX rate_policy_match
  ON rate_policy (method, route_pattern, principal_scope, state);

CREATE TABLE policy_audit (
  policy_id BIGINT NOT NULL,
  version BIGINT NOT NULL,
  actor TEXT NOT NULL,
  change_json JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (policy_id, version)
);

Decision state [PROPOSED DESIGN]

Dùng key hash(tenant_id | principal_type | principal_id | route | policy_id). Hash partitioning phân phối traffic customer tùy ý và tránh range hotspot do ID tuần tự. Token-bucket value chứa {tokens, last_elapsed_ns, policy_version, dedupe_entries}.

Dùng elapsed time từ monotonic source cho phép tính refill. Wall-clock timestamp có thể trả về trong các field hướng đến client như reset_at, nhưng clock synchronization không được quyết định request có được phép hay không. Policy version trong value giúp quan sát thay đổi cấu hình và hỗ trợ cache invalidation an toàn.


Chia sẻ bài viết này trên:

Bài trước
Thiết kế hệ thống thanh toán với sổ cái kép
Bài tiếp theo
Thiết kế hệ thống chat thời gian thực bền vững