Mục lục

Module 7: Độ Tin Cậy - Timeout/Retry/Idempotency/Rate Limit

Ngăn chặn các lỗi lan truyền với timeout, retry logic, idempotency key và rate limiting để bảo vệ hệ thống.

Thời lượng: 1 tuần
Cấp độ: Intermediate
Yêu cầu: Đã hoàn thành Modules 1-6

Tổng quan Module

Module này dạy bạn cách xây dựng hệ thống backend có khả năng phục hồi (resilient). Bạn sẽ học cách thiết lập timeout đúng cách, retry logic không gây bão traffic, idempotency để chống duplicate, và rate limiting để bảo vệ hệ thống.

Kết quả học tập

Sau module này, bạn sẽ:

  • Thiết lập timeout và retry policy đúng cách
  • Implement idempotency cho các thao tác quan trọng
  • Thiết kế rate limiting để chống abuse
  • Hiểu cách các pattern này ngăn cascading failures

Bài 7.1: Timeout & Retry Logic

Lý thuyết

Timeout là bắt buộc:

  • Nếu không set timeout, request có thể "treo" mãi mãi
  • Database query timeout: 5-10 giây
  • HTTP client timeout: 30 giây (read), 10 giây (connect)
  • Background job timeout: tùy nghiệp vụ

Retry Logic:

  • Chỉ retry các lỗi tạm thời (5xx, network timeout)
  • Không retry lỗi client (4xx - lỗi người dùng)
  • Phải có backoff (delay tăng dần) và jitter (random hóa)

Exponential Backoff:

Code:
Attempt 1: wait 1s
Attempt 2: wait 2s
Attempt 3: wait 4s
Attempt 4: wait 8s
Max attempts: 3-5

Nguyên tắc then chốt

"Timeout luôn phải được set. Retry chỉ dùng cho lỗi tạm thời, không phải lỗi logic."

Retry sai cách sẽ biến 1 service chậm thành cả hệ thống sập.

Thực hành

Bài tập: Thiết kế retry policy cho HTTP client

typescript:
interface RetryPolicy {
  maxAttempts: number;        // 3-5 lần
  initialDelay: number;       // 1000ms
  maxDelay: number;           // 30000ms
  backoffMultiplier: number;  // 2
  retryableStatusCodes: number[];  // [502, 503, 504]
  retryableErrors: string[];  // ['ECONNRESET', 'ETIMEDOUT']
}

const policy: RetryPolicy = {
  maxAttempts: 3,
  initialDelay: 1000,
  maxDelay: 10000,
  backoffMultiplier: 2,
  retryableStatusCodes: [502, 503, 504, 429],
  retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED']
};

// Ví dụ implementation
async function fetchWithRetry(url: string, policy: RetryPolicy) {
  let lastError;
  
  for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
    try {
      const response = await fetch(url, { 
        signal: AbortSignal.timeout(30000) // 30s timeout
      });
      
      if (response.ok || !policy.retryableStatusCodes.includes(response.status)) {
        return response;
      }
      
      lastError = new Error(`HTTP ${response.status}`);
      
    } catch (error) {
      lastError = error;
      
      // Không retry nếu không phải lỗi tạm thời
      if (!isRetryableError(error, policy)) {
        throw error;
      }
    }
    
    // Tính delay với jitter
    if (attempt < policy.maxAttempts) {
      const baseDelay = Math.min(
        policy.initialDelay * Math.pow(policy.backoffMultiplier, attempt - 1),
        policy.maxDelay
      );
      const jitter = baseDelay * 0.1 * Math.random();
      await sleep(baseDelay + jitter);
    }
  }
  
  throw lastError;
}

Nhiệm vụ:

  • Thiết lập timeout cho: DB query, HTTP client, Redis
  • Viết retry policy cho external API calls
  • Test với service bị chậm để xem retry có giúp ích không

Câu hỏi thảo luận

  1. Thảm họa lan truyền: Giải thích tại sao retry có thể khiến 1 service chậm → toàn hệ thống sập?
  2. Circuit Breaker: Khi nào cần circuit breaker thay vì retry đơn thuần?
  3. User Experience: Retry làm response time tăng. Làm sao balance giữa reliability và speed?

Bài 7.2: Idempotency (Chống Duplicate)

Lý thuyết

Idempotency là gì?

  • Gọi API nhiều lần = kết quả giống gọi 1 lần
  • Ví dụ: Tạo đơn hàng 2 lần (do user double-click) chỉ tạo 1 đơn

Tại sao quan trọng?

  • Network có thể retry tự động (TCP retransmission)
  • User có thể click nhiều lần do UX chậm
  • Payment/Order/Deposit: thiếu idempotency = mất tiền

Cách implement:

http:
POST /orders
Idempotency-Key: unique-key-123
Content-Type: application/json

{
  "items": [...],
  "amount": 100000
}

Lần 1: Tạo order mới → Return 201
Lần 2 (cùng key): Trả order cũ → Return 200 (không tạo mới)

Nguyên tắc then chốt

"Payment, Order, Deposit là những chỗ 'chết người' nếu thiếu idempotency. Phải implement ngay từ đầu."

Thực hành

Bài tập: Implement idempotency cho API tạo deposit

typescript:
// 1. Database schema
CREATE TABLE idempotency_keys (
  key VARCHAR(255) PRIMARY KEY,
  resource_type VARCHAR(50),
  resource_id VARCHAR(255),
  response_body JSON,
  status_code INT,
  created_at TIMESTAMP,
  expires_at TIMESTAMP
);

CREATE INDEX idx_expires ON idempotency_keys(expires_at);

// 2. Middleware
async function idempotencyMiddleware(req, res, next) {
  const idempotencyKey = req.headers['idempotency-key'];
  
  if (!idempotencyKey) {
    return res.status(400).json({
      error: {
        code: 'MISSING_IDEMPOTENCY_KEY',
        message: 'Idempotency-Key header is required for this endpoint'
      }
    });
  }
  
  // Check cache
  const cached = await redis.get(`idem:${idempotencyKey}`);
  if (cached) {
    const { statusCode, body } = JSON.parse(cached);
    return res.status(statusCode).json(body);
  }
  
  // Check database
  const record = await db.idempotencyKeys.findOne({ key: idempotencyKey });
  if (record) {
    return res.status(record.statusCode).json(record.responseBody);
  }
  
  // Proceed with request
  req.idempotencyKey = idempotencyKey;
  next();
}

// 3. Save result
async function saveIdempotencyResult(key, resourceId, statusCode, body) {
  const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24h
  
  await db.idempotencyKeys.create({
    key,
    resourceType: 'deposit',
    resourceId,
    responseBody: body,
    statusCode,
    expiresAt
  });
  
  // Cache for 5 minutes
  await redis.setex(`idem:${key}`, 300, JSON.stringify({ statusCode, body }));
}

Nhiệm vụ:

  • Implement idempotency middleware
  • Thêm Idempotency-Key header validation
  • Set TTL hợp lý (24h thường đủ)
  • Test: gửi cùng key 2 lần, phải trả cùng kết quả

Câu hỏi thảo luận

  1. Scope: Key nào cần idempotency? Chỉ POST hay cả PUT/DELETE?
  2. Deduplication vs Idempotency: Khác nhau ở đâu so với event deduplication?
  3. Key Generation: Client generate key hay server? Trade-offs?

Bài 7.3: Rate Limiting (Giới Hạn Request)

Lý thuyết

Tại sao cần Rate Limit?

  • Chống brute force (login attack)
  • Chống DoS/DDoS
  • Bảo vệ infrastructure cost (API abuse)
  • Đảm bảo fair usage cho mọi user

Chiến lược Rate Limit:

  1. Per IP: Giới hạn theo IP address
  2. Per User: Giới hạn theo user ID (nếu authenticated)
  3. Per API Key: Cho external integrations
  4. Global: Giới hạn toàn hệ thống

Algorithms:

  • Token Bucket: Cho phép burst traffic
  • Leaky Bucket: Smooth out traffic
  • Fixed Window: Đơn giản nhưng có edge cases
  • Sliding Window: Chính xác hơn, phức tạp hơn

Nguyên tắc then chốt

"Rate limit không chỉ để chống hack. Nó bảo vệ hệ thống khỏi cả lỗi code (infinite loop gọi API)."

Thực hành

Bài tập: Thiết kế rate limit cho các endpoint

Endpoint Policies:

EndpointRate LimitWindowScope
POST /auth/login5 requests15 minutesPer IP
GET /search100 requests1 minutePer User
POST /upload10 requests1 hourPer User
GET /products1000 requests1 minutePer API Key
POST /payments10 requests10 minutesPer User

Implementation với Redis:

typescript:
async function checkRateLimit(key: string, limit: number, windowSeconds: number) {
  const now = Date.now();
  const windowKey = `ratelimit:${key}:${Math.floor(now / (windowSeconds * 1000))}`;
  
  const current = await redis.incr(windowKey);
  
  if (current === 1) {
    await redis.expire(windowKey, windowSeconds);
  }
  
  if (current > limit) {
    throw new Error('RATE_LIMIT_EXCEEDED');
  }
  
  return {
    limit,
    remaining: Math.max(0, limit - current),
    reset: Math.ceil(now / (windowSeconds * 1000)) * windowSeconds * 1000
  };
}

// Middleware
app.use(async (req, res, next) => {
  const key = req.user?.id || req.ip;
  
  try {
    const rateLimit = await checkRateLimit(key, 100, 60);
    
    res.setHeader('X-RateLimit-Limit', rateLimit.limit);
    res.setHeader('X-RateLimit-Remaining', rateLimit.remaining);
    res.setHeader('X-RateLimit-Reset', rateLimit.reset);
    
    next();
  } catch (error) {
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests. Please try again later.',
        retryAfter: 60
      }
    });
  }
});

Nhiệm vụ:

  • Chọn algorithm phù hợp (token bucket vs sliding window)
  • Thiết lập limit cho từng loại endpoint
  • Trả headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • Return 429 với Retry-After header

Câu hỏi thảo luận

  1. Gateway vs Service: Rate limit nên đặt ở API Gateway hay ở service? Trade-offs?
  2. Dynamic Limits: Có nên tăng limit cho premium users không?
  3. Distributed Systems: Làm sao sync rate limit counter giữa nhiều server instances?

Tổng kết Module

Bạn đã học:

  • Timeout và retry policy để xử lý lỗi tạm thời
  • Idempotency để chống duplicate operations
  • Rate limiting để bảo vệ hệ thống

Bước tiếp theo

Module 8 sẽ dạy về Database Standards: schema conventions, migrations không downtime, và transaction boundaries.

Tiếp tục Module 8 →


Tài liệu tham khảo

Quảng cáo
mdhorizontal