Điều kiện tiên quyết: Đây là Phần 2 của Khóa Học System Design. Hãy đọc Phần 1: Tư Duy System Design trước để nắm vững các khuôn rập đánh đổi (trade-off frameworks) nền tảng.

Answer-first: Trong môi trường AI-Native 2026, các hệ thống phân tán đòi hỏi khả năng xử lý lưu lượng siêu lớn và độ trễ cực thấp. Cân bằng tải L4 (L4 load balancing) định tuyến lưu lượng dựa trên dữ liệu siêu dữ liệu tầng giao vận (IP/TCP/UDP) với chi phí CPU tối thiểu nhưng khả năng xử lý logic hạn chế. Cân bằng tải L7 (L7 load balancing) phân tích thông tin chi tiết trong HTTP headers, URL paths và cookies, cho phép định tuyến dựa trên nội dung (content-based routing) và kiểm tra sức khỏe ứng dụng, nhưng đổi lại chi phí xử lý trên từng yêu cầu cao hơn.


So Sánh Cân Bằng Tải L4 vs L7

Answer-first: Sự khác biệt cốt lõi nằm ở tầng xử lý trong mô hình mạng. L4 (Tầng Giao vận - Transport Layer) định tuyến ở cấp độ TCP/UDP dựa trên bộ IP + Port. L7 (Tầng Ứng dụng - Application Layer) định tuyến ở cấp độ HTTP dựa trên headers, URLs và payload dữ liệu.

So Sánh Kiến Trúc (Architecture Comparison)

graph TD
    subgraph l4["Cân bằng tải L4 (Tầng Giao vận)"]
        C1([Khách]) -->|"TCP SYN → dst:80"| LB1["L4 LB\n(HAProxy/IPVS)"]
        LB1 -->|"Chuyển tiếp luồng TCP\n(Ghi đè IP - IP rewrite)"| B1["Backend 1\n:8080"]
        LB1 -->|"Chuyển tiếp luồng TCP"| B2["Backend 2\n:8080"]
        LB1 -->|"Chuyển tiếp luồng TCP"| B3["Backend 3\n:8080"]
    end

    subgraph l7["Cân bằng tải L7 (Tầng Ứng dụng)"]
        C2([Khách]) -->|"HTTP GET /api/v1"| LB2["L7 LB\n(Nginx/Envoy)"]
        LB2 -->|"Đường dẫn: /api/* → service-api"| S1["API Service"]
        LB2 -->|"Đường dẫn: /static/* → CDN"| S2["Dịch vụ tĩnh (Static)"]
        LB2 -->|"Header: X-Beta → true"| S3["Bản thử nghiệm (Beta Canary)"]
    end

Ma Trận Ra Quyết Định (Decision Matrix)

Thuộc Tính (Property)L4 (TCP của HAProxy)L7 (Nginx / Envoy)
Tầng OSI (OSI Layer)Tầng Giao vận (Lớp 4)Tầng Ứng dụng (Lớp 7)
Căn cứ định tuyếnBộ IP + PortĐường dẫn URL, HTTP headers, cookies, chuỗi JWT
Chi phí độ trễ (Latency overhead)~0.1–0.3ms~0.5–2ms (do chi phí giải mã HTTP)
Thông lượng (Throughput)Hàng triệu kết nối/giâyVài trăm ngàn/giây
Giải mã TLS (TLS termination)❌ Cho phép đi qua (Pass-through)✅ Hỗ trợ giải mã TLS tại gateway
Kiểm tra sức khỏe (Health checks)Kiểm tra kết nối TCPKiểm tra HTTP 200, phản hồi tùy biến
Duy trì phiên (Sticky sessions)Băm IP nguồn (Source IP hash)Phân bổ theo Cookie (SERVERID)
Trường hợp sử dụng (Use case)Ứng dụng TCP tốc độ cao, streaming, game serverREST APIs, gRPC, triển khai Canary, A/B Testing

[!TIP] Mô hình phối hợp thực tế: Triển khai tầng L4 (HAProxy/IPVS) ở phía ngoài cùng để xử lý khối lượng lưu lượng thô ban đầu, sau đó chuyển tiếp tới tầng L7 (Envoy/Nginx) bên trong để thực hiện định tuyến nâng cao. Mô hình này giúp tầng L4 hấp thụ các đợt tăng vọt kết nối (connection burst), còn tầng L7 xử lý logic định tuyến nghiệp vụ.


Cơ Chế Direct Server Return (DSR)

Answer-first: Trong mô hình DSR (Direct Server Return), bộ cân bằng tải chỉ tiếp nhận các yêu cầu gửi vào (inbound requests), còn các máy chủ backend sẽ gửi trực tiếp dữ liệu phản hồi (response traffic) về cho client mà không qua bộ cân bằng tải. Cơ chế này loại bỏ nguy cơ bộ cân bằng tải trở thành điểm nghẽn về băng thông đầu ra.

Luồng Xử Lý Trong DSR (DSR Traffic Flow)

sequenceDiagram
    participant C as Khách (IP: 1.2.3.4)
    participant LB as L4 LB (VIP: 10.0.0.1)
    participant B as Máy chủ Backend (IP: 10.0.0.10)

    C->>LB: Gửi SYN (đích: VIP:80)
    LB->>B: Chuyển tiếp gói tin (IP đích giữ nguyên VIP:80, thay đổi MAC)
    Note over B: Backend xử lý vì đã cấu hình địa chỉ VIP trên loopback
    B->>C: Phản hồi HTTP trực tiếp tới Client (IP nguồn: VIP:80)
    Note over LB: Luồng dữ liệu phản hồi không qua bộ cân bằng tải!

Cấu Hình HAProxy DSR

# /etc/haproxy/haproxy.cfg — Cấu hình L4 DSR mode
global
    log /dev/log local0
    maxconn 500000

defaults
    mode tcp
    timeout connect 5s
    timeout client  30s
    timeout server  30s

frontend http_front
    bind *:80
    default_backend http_backend

backend http_backend
    balance leastconn
    server backend1 10.0.0.10:80 check
    server backend2 10.0.0.11:80 check
    server backend3 10.0.0.12:80 check
# Cấu hình trên từng máy chủ Backend: gán địa chỉ VIP vào giao diện loopback
sudo ip addr add 10.0.0.1/32 dev lo label lo:vip

# Cấu hình bỏ qua phản hồi ARP cho địa chỉ VIP trên giao diện mạng chính
echo 1 > /proc/sys/net/ipv4/conf/eth0/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/eth0/arp_announce

[!IMPORTANT] Cấu hình sysctl có vai trò quan trọng: arp_ignore=1 đảm bảo máy chủ backend chỉ phản hồi các yêu cầu ARP khớp với IP trên giao diện mạng nhận yêu cầu. Cấu hình này ngăn chặn các máy chủ backend tranh chấp phản hồi ARP cho địa chỉ VIP, tránh hiện tượng xung đột địa chỉ IP trong mạng.

Tầm Quan Trọng Của DSR Ở Quy Mô Lớn

Trong các hệ thống thương mại điện tử lớn với lưu lượng tới 500k+ RPS, dung lượng gói tin phản hồi (response payloads - hình ảnh, danh sách sản phẩm) thường khoảng 50–200KB. Nếu sử dụng mô hình proxy truyền thống, toàn bộ dung lượng dữ liệu này sẽ phải đi qua bộ cân bằng tải. Với cơ chế DSR, bộ cân bằng tải chỉ xử lý các gói tin SYN nhỏ ở chiều vào, giúp thông lượng phản hồi tổng thể tăng trưởng tuyến tính theo số lượng máy chủ backend.


Lựa Chọn Thuật Toán Cân Bằng Tải

Answer-first: Đội ngũ phát triển cần lựa chọn thuật toán cân bằng tải dựa trên biến động kích thước và độ trễ của request. Thuật toán Vòng Tròn (Round Robin) và Ít Kết Nối Nhất (Least Connections) tối ưu cho các request có chi phí xử lý tương đồng. Thuật toán Băm Nhất Quán (Consistent Hashing) là bắt buộc đối với các dịch vụ có trạng thái (stateful protocols như Redis hay luồng gRPC). Thuật toán Băm IP (IP Hash) giúp duy trì phiên làm việc (sticky sessions) mà không tốn chi phí quản lý cookie.

Thuật Toán (Algorithm)Độ Phức Tạp Thuật ToánTrường Hợp Sử Dụng Tối ƯuRủi Ro Khi Lỗi
Round RobinO(1)Chi phí request tương đồngRequest xử lý kéo dài gây quá tải cục bộ
Weighted Round RobinO(1)Cụm máy chủ có năng lực phần cứng khác nhauCấu hình trọng số không chính xác dẫn đến quá tải
Least ConnectionsO(log N)Độ dài thời gian xử lý request biến động lớnMáy chủ mới thêm vào nhận lượng lớn request dồn dập
Consistent HashingO(log N)Dịch vụ có trạng thái (Stateful): Redis, gRPC streamsTập trung Hot Key gây quá tải một số node
IP HashO(1)Duy trì phiên (Sticky sessions) không dùng cookiePhân bổ không đều khi lưu lượng qua địa chỉ IP NAT công cộng

Xây Dựng Middleware Rate Limiting (Token Bucket) Bằng Go

Answer-first: Thuật toán Thùng Chứa Token (Token Bucket) là chuẩn mực trong thiết kế rate limiting cho hệ thống. Thuật toán cho phép xử lý các đợt tăng vọt lưu lượng đột biến (bursts) trong giới hạn cho phép, đồng thời điều hòa tốc độ truy cập trung bình trong thời gian dài. Thư viện golang.org/x/time/rate cung cấp cấu trúc Token Bucket hiệu năng O(1) áp dụng mô hình cập nhật lười (lazy refill model).

Cơ Chế Hoạt Động Của Token Bucket

$$\text{tokens} = \min\left(\text{sức chứa (capacity)}, \text{tokens} + r \times \Delta t\right)$$

Trong đó:

  • $r$ = tốc độ nạp lại token (tokens/giây)
  • $\Delta t$ = khoảng thời gian từ lần kiểm tra gần nhất (lazy refill)
  • $\text{capacity}$ = dung lượng cho phép tăng vọt tối đa (maximum burst size)

Mỗi request tiêu tốn 1 token. Nếu số dư token < 1, hệ thống phản hồi lỗi HTTP 429 Too Many Requests.

[!NOTE] Đoạn mã bên dưới áp dụng cơ chế rate limiting trên đơn vị một máy chủ (single-node). Để triển khai rate limiting đồng bộ trên cụm gồm nhiều replicas chia sẻ chung hạn ngạch người dùng, hệ thống cần ứng dụng thuật toán cửa sổ trượt (sliding window counter) đồng bộ qua Redis — chi tiết được phân tích ở Phần 11: Bảo Mật & Giới Hạn Tốc Độ API.

package ratelimit

import (
    "context"
    "fmt"
    "net/http"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

// PerClientRateLimiter quản lý giới hạn tốc độ truy cập cho từng client IP
type PerClientRateLimiter struct {
    mu      sync.RWMutex
    clients map[string]*clientState
    r       rate.Limit    // Tốc độ nạp token mỗi giây
    burst   int           // Dung lượng cho phép tăng vọt (burst capacity)
    ttl     time.Duration // Thời gian dọn dẹp client không hoạt động
}

type clientState struct {
    limiter  *rate.Limiter
    lastSeen time.Time
}

func NewPerClientRateLimiter(rps float64, burst int, ttl time.Duration) *PerClientRateLimiter {
    rl := &PerClientRateLimiter{
        clients: make(map[string]*clientState),
        r:       rate.Limit(rps),
        burst:   burst,
        ttl:     ttl,
    }
    go rl.cleanupLoop() // Tiến trình chạy ngầm thu gom các client đã hết hạn
    return rl
}

func (rl *PerClientRateLimiter) getLimiter(clientIP string) *rate.Limiter {
    rl.mu.RLock()
    state, ok := rl.clients[clientIP]
    rl.mu.RUnlock()

    if ok {
        state.lastSeen = time.Now()
        return state.limiter
    }

    rl.mu.Lock()
    defer rl.mu.Unlock()
    limiter := rate.NewLimiter(rl.r, rl.burst)
    rl.clients[clientIP] = &clientState{limiter: limiter, lastSeen: time.Now()}
    return limiter
}

func (rl *PerClientRateLimiter) cleanupLoop() {
    ticker := time.NewTicker(rl.ttl / 2)
    defer ticker.Stop()
    for range ticker.C {
        rl.mu.Lock()
        for ip, state := range rl.clients {
            if time.Since(state.lastSeen) > rl.ttl {
                delete(rl.clients, ip)
            }
        }
        rl.mu.Unlock()
    }
}

func (rl *PerClientRateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        clientIP := r.RemoteAddr
        if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
            clientIP = xff
        }

        limiter := rl.getLimiter(clientIP)

        if !limiter.Allow() {
            w.Header().Set("Retry-After", "1")
            w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%.0f", float64(rl.r)))
            http.Error(w,
                `{"error":"rate_limit_exceeded","message":"Too many requests, please retry after 1 second"}`,
                http.StatusTooManyRequests,
            )
            return
        }

        next.ServeHTTP(w, r)
    })
}

Rate Limiting Phân Tán Với Redis

Cấu hình rate limiting trên một tiến trình đơn lẻ không thể đáp ứng khi mở rộng trên nhiều Pods. Việc sử dụng lệnh INCR kết hợp EXPIRE trong Redis cho phép khởi tạo hệ thống rate limiting tập trung (atomic cross-pod rate limiting):

package ratelimit

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

type RedisRateLimiter struct {
    rdb      *redis.Client
    limit    int64
    window   time.Duration
}

// Allow kiểm tra yêu cầu của client dựa trên thuật toán cửa sổ trượt (sliding window) với Redis
func (r *RedisRateLimiter) Allow(ctx context.Context, clientKey string) (bool, error) {
    key := fmt.Sprintf("ratelimit:%s", clientKey)
    
    // Thực thi Lua script đảm bảo tính nguyên tử (atomic) cho thao tác INCR + EXPIRE
    script := redis.NewScript(`
        local count = redis.call('INCR', KEYS[1])
        if count == 1 then
            redis.call('EXPIRE', KEYS[1], ARGV[1])
        end
        return count
    `)
    
    count, err := script.Run(ctx, r.rdb,
        []string{key},
        int64(r.window.Seconds()),
    ).Int64()
    if err != nil {
        return true, nil // Khi xảy ra lỗi Redis, áp dụng cơ chế Fail Open để tránh ảnh hưởng lưu lượng người dùng
    }
    
    return count <= r.limit, nil
}

[!WARNING] Chiến lược Fail Open vs Fail Closed: Trong đoạn mã trên, khi Redis gặp sự cố, hệ thống trả về true (Fail Open) để tiếp tục phục vụ lưu lượng đối với các API người dùng công cộng. Tuy nhiên, với các endpoint nhạy cảm về bảo mật (như đăng nhập hay thanh toán), hệ thống nên áp dụng cơ chế Fail Closed (từ chối yêu cầu và phản hồi lỗi 503) để đảm bảo an toàn.


Mẫu Thiết Kế API Gateway (Kong / Envoy)

API Gateway đóng vai trò làm điểm truy cập tập trung (single entry point) tiếp nhận mọi lưu lượng từ phía client, quản lý các chức năng chung (cross-cutting concerns) giúp các dịch vụ microservices phía sau tập trung xử lý logic nghiệp vụ.

graph LR
    Client([Mobi / Web]) -->|HTTPS| GW["API Gateway\n(Kong / Envoy)"]
    GW -->|"Kiểm tra JWT\nRate limit\nLogging"| Auth["Dịch vụ Auth"]
    GW -->|"Định tuyến: /orders/*"| Orders["Dịch vụ Order"]
    GW -->|"Định tuyến: /products/*"| Products["Dịch vụ Products"]
    GW -->|"Định tuyến: /payments/*"| Payments["Dịch vụ Payments"]
    
    style GW fill:#4a6cf7,color:#fff

Các chức năng cốt lõi của API Gateway (cross-cutting concerns):

  • Xác thực và phân quyền (Authentication/Authorization): Kiểm tra JWT validation, xác minh token OAuth2.
  • Giới hạn tốc độ (Rate Limiting): Áp dụng hạn ngạch cho từng người dùng (per-user) hoặc khóa API (per-API-key).
  • Biến đổi yêu cầu/phản hồi (Request/Response Transformation): Bổ sung HTTP headers, biến đổi định dạng dữ liệu (body transformation).
  • Giám sát (Observability): Thu thập nhật ký truy cập tập trung (centralized access logging), chèn thông tin tracing phân tán.
  • Giải mã TLS (TLS Termination): Quản lý chứng chỉ SSL/TLS tại gateway, cho phép truyền dữ liệu nội bộ qua giao thức HTTP để tối ưu hiệu năng.

[!NOTE] Chi phí độ trễ của Gateway (Gateway latency overhead): Các API Gateway như Envoy bổ sung khoảng 0.5–1ms độ trễ cho mỗi yêu cầu để xử lý phân tích HTTP, chạy plugin và thu thập metric. Với mục tiêu SLO P99 < 50ms, mức chi phí này nằm trong giới hạn chấp nhận được. Với các hệ thống yêu cầu độ trễ cực thấp (như giao dịch tài chính thời gian thực hay game streaming), đội ngũ kỹ thuật có thể bỏ qua tầng L7 Gateway và kết nối trực tiếp qua L4 DSR.


Câu Hỏi Thường Gặp (FAQ)

Sự khác biệt cốt lõi giữa Cân bằng tải L4 và L7 là gì?

L4 hoạt động ở tầng giao vận (Transport Layer) dựa trên địa chỉ IP và Port mà không phân tích nội dung gói tin HTTP. Chi phí xử lý thấp (~0.1ms) và khả năng đáp ứng hàng triệu kết nối/giây. Hạn chế là chỉ có thể ra quyết định ở cấp độ kết nối TCP (như IP hash hay least-connections), không thể định tuyến dựa trên đường dẫn URL hay HTTP headers.

L7 hoạt động ở tầng ứng dụng (Application Layer), phân tích chi tiết HTTP headers, URL paths và cookies. Chi phí xử lý cao hơn (~0.5–2ms), nhưng hỗ trợ các tính năng định tuyến nâng cao như định tuyến theo đường dẫn, triển khai Canary theo header và kiểm tra sức khỏe ở cấp độ ứng dụng.

Cơ chế DSR tăng thông lượng phản hồi như thế nào?

Trong mô hình proxy truyền thống, cả chiều yêu cầu (request) và chiều phản hồi (response) đều phải truyền qua bộ cân bằng tải. Với mô hình DSR, bộ cân bằng tải chỉ xử lý các gói tin yêu cầu ở chiều vào, còn máy chủ backend sẽ gửi trực tiếp dữ liệu phản hồi tới client bằng cách sử dụng địa chỉ VIP làm IP nguồn. Do kích thước dữ liệu phản hồi thường lớn hơn nhiều so với dữ liệu yêu cầu, DSR giúp giảm tới 90% lưu lượng qua bộ cân bằng tải và nâng cao thông lượng tổng thể của hệ thống.

Khi nào nên áp dụng IP Hash và Consistent Hashing?

IP Hash: Đơn giản, chi phí tính toán O(1), phù hợp để duy trì phiên làm việc (sticky sessions) mà không cần cookie. Tuy nhiên có rủi ro phân bổ tải không đều khi nhiều người dùng cùng truy cập qua một địa chỉ IP NAT công cộng (như mạng văn phòng).

Consistent Hashing: Phù hợp với các dịch vụ có trạng thái (stateful backends như phân vùng Redis hay luồng gRPC), đảm bảo yêu cầu từ một client luôn được điều hướng tới cùng một máy chủ backend cố định kể cả khi số lượng node trong cụm thay đổi. Giải pháp này giúp giảm thiểu việc tính toán lại vị trí lưu trữ (remapping) khi thêm hoặc bớt node. Chi tiết xem tại Phần 9: Băm Nhất Quán (Consistent Hashing).


🔗 Bay Sang Bài Tới: Phần 3: Bài Toán Caching & Hội Chứng Voi Giẫm Đạp Caching Trong Go — Thuật toán XFetch, phân tích bộ nhớ Redis LRU/LFU, và chống trùng lặp truy vấn bằng singleflight (singleflight deduplication).