📖 Bản tiếng Anh (English Edition)
Bài trước: Bài 2 — Động Cơ Flash Sale: Redis Lua & Chống Bán Quá Kho | Mục lục Series | Bài tiếp theo: Bài 4 — Mở Rộng Cơ Sở Dữ Liệu: Từ Sharded MySQL Sang TiDB NewSQL
Answer-first: Shopee bảo vệ hạ tầng trong các đợt cao điểm bằng tấm khiên kết hợp giới hạn tốc độ WAF, phòng chờ ảo và peak shaving với Apache Kafka. Việc tách rời tiếp nhận đơn khỏi cơ sở dữ liệu giúp san phẳng đỉnh tải, duy trì phản hồi dưới 50ms và bảo đảm không thất thoát đơn hàng.
Điều kiện tiên quyết: Nắm vững kiến trúc luồng sự kiện (event streaming), cấu trúc partition Kafka, các cấp độ cam kết chuyển phát tin nhắn, giải thuật giới hạn tốc độ Token Bucket và cơ chế tự động mở rộng theo độ trễ hàng đợi với KEDA.
1. Phễu Lọc Lưu Lượng Đa Tầng (Progressive Traffic Filtering Funnel)
Vào thời điểm giao thừa mở màn chiến dịch Siêu Sale 11.11, lượng tương tác của người dùng trên toàn hệ thống tăng vọt hàng trăm lần chỉ trong vài giây ngắn ngủi. Hàng chục triệu người mua sắm trên khắp bảy quốc gia Đông Nam Á đồng loạt tải lại trang chủ khuyến mãi, áp mã giảm giá vận chuyển và nhấn nút đặt hàng liên tục.
Nếu làn sóng khổng lồ hàng triệu yêu cầu này đánh thẳng vào các dịch vụ phía sau, các switch mạng nội bộ sẽ tắc nghẽn, các kết nối cơ sở dữ liệu sẽ bị ngắt và toàn bộ hệ thống đặt hàng sẽ sụp đổ dây chuyền.
Để bảo vệ các ranh giới giao dịch cốt lõi, Shopee triển khai một phễu lọc lưu lượng đa tầng theo lũy tiến. Mỗi tầng trong phễu được thiết kế để loại bỏ các yêu cầu không hợp lệ hoặc không có tính hành động với tốc độ thực thi nhanh nhất và chi phí tính toán thấp nhất:
flowchart TD
subgraph VanhDaiMangBien ["Vành Đai Mạng Biên (Tầng 1 & 2)"]
direction TB
L1["Tầng 1: Mạng Anycast Edge CDN (15.000.000 Lượt Click/giây)"] -->|Tài Nguyên Tĩnh Đệm Tại Mạng Biên| L2["Tầng 2: Cloudflare / Envoy WAF (3.000.000 Req/giây)"]
L2 -->|Bẫy Bot & Xác Thực Bằng Chứng Công Việc PoW| L3["Tầng 3: Phòng Chờ Ảo Virtual Waiting Room (1.200.000 Req/giây)"]
end
subgraph VanhDaiCongDichVu ["API Gateway & Giới Hạn Tốc Độ (Tầng 3 & 4)"]
direction TB
L3 -->|Phát Vé Vào Cửa Có Chữ Ký Điện Tử| L4["Tầng 4: Giới Hạn Tốc Độ Rate Limiting (500.000 Req/giây)"]
L4 -->|Hạ Cấp Tính Năng Không Cốt Lõi| L5["Tầng 5: Giữ Chỗ Tồn Kho Trong RAM Redis (100.000 Req/giây)"]
end
subgraph VanhDaiLuuTru ["Hồ Chứa Đệm & Cơ Sở Dữ Liệu (Tầng 5 & 6)"]
direction TB
L5 -->|Trừ Kho Nguyên Tử Thành Công| L6["Tầng 6: Hồ Chứa Giảm Tải Apache Kafka (50.000 Msg/giây)"]
L6 -->|Gom Batch 200 Đơn Hàng Ghi Đồng Loạt| L7["Tầng 7: Lưu Sổ Cái TiDB Phân Tán (15.000 Writes/giây)"]
end
classDef edge fill:#ffebee,stroke:#c62828,stroke-width:2px;
classDef gw fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
classDef storage fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
class VanhDaiMangBien edge;
class VanhDaiCongDichVu gw;
class VanhDaiLuuTru storage;
Toán Học Kiến Trúc Của Phễu Lọc Lũy Tiến
Phễu hoạt động tương tự như một chuỗi các bộ lọc thông thấp (low-pass filter) giúp triệt tiêu áp lực ghi:
- Mạng Biên CDN (Từ 15 triệu xuống 3 triệu QPS): Hơn 80% lượt click của người dùng là các tài nguyên tĩnh (hình ảnh sản phẩm, file mã nguồn JavaScript, từ điển ngôn ngữ bản địa). Toàn bộ khối dữ liệu này được hoàn trả ngay tại các trạm biên CDN (PoP) gần người dùng nhất mà không cần chạm tới trung tâm dữ liệu của Shopee.
- Hệ Thống WAF & Tường Lửa Chống Bot (Từ 3 triệu xuống 1,2 triệu QPS): Các tập lệnh tự động (crawler) và các công cụ bot tìm cách gom sạch hàng tồn kho giảm giá bị chặn đứng thông qua phân tích hành vi, nhận diện dấu vân tay TLS và kiểm tra giải mã PoW.
- Phòng Chờ Ảo (Từ 1,2 triệu xuống 500 ngàn QPS): Lượng khách hàng hợp lệ đến cùng một giây được đưa vào hàng đợi ảo trên mạng biên. Ứng dụng điện thoại thăm dò trạng thái với khoảng thời gian ngẫu nhiên (jitter), tránh hiện tượng bão yêu cầu đồng loạt.
- Giới Hạn Tốc Độ Tại API Gateway (Từ 500 ngàn xuống 100 ngàn QPS): Bộ lọc thuật toán cửa sổ trượt loại bỏ các hành vi spam yêu cầu bất thường theo từng tài khoản, địa chỉ IP và mã định danh thiết bị, trả về mã lỗi
HTTP 429 Too Many Requests. - Động Cơ Trừ Kho Trong RAM (Từ 100 ngàn xuống 50 ngàn QPS): Kịch bản Redis Lua nguyên tử thực hiện trừ kho trong chưa đầy 1ms. Yêu cầu mua hàng cho các sản phẩm đã cháy hàng bị ngắt mạch ngay tại bộ nhớ RAM.
- Hồ Chứa Đệm Apache Kafka (Từ 50 ngàn xuống 15 ngàn lượt ghi/giây): Các giao dịch thành công được đẩy vào các hàng đợi Kafka bền vững, chuyển hóa các đỉnh sóng áp lực nhọn hoắt thành một dòng chảy êm đềm, ổn định cho cơ sở dữ liệu phía sau.
2. Giới Hạn Tốc Độ Token Bucket Và Cơ Chế Phòng Chờ Ảo
Tại tầng cổng dịch vụ (API Gateway), cơ chế giới hạn tốc độ (rate limiting) bắt buộc phải đạt độ trễ siêu thấp, có tính phân tán và hoàn toàn dự đoán được. Các giải pháp đếm theo cửa sổ cố định (Fixed-Window) bộc lộ nhược điểm chết người gọi là hiện tượng bùng nổ ranh giới (boundary burst): kẻ tấn công có thể xả toàn bộ hạn ngạch vào 10ms cuối cùng của khung giờ $N$ và tiếp tục xả hạn ngạch mới vào 10ms đầu tiên của khung giờ $N+1$, khiến hệ thống phải gánh gấp đôi lưu lượng cho phép.
flowchart LR
subgraph ThuatToanTokenBucket ["Mô Hình Thuật Toán Token Bucket"]
direction TB
TB_Refill["Máy Bơm Token (Tốc độ: r token/giây)"] --> TB_Bucket["Thùng Chứa Token (Dung lượng: b token)"]
TB_Req["Yêu Cầu Truy Cập"] --> TB_Check{"Còn Token Trong Thùng Không?"}
TB_Bucket --> TB_Check
TB_Check -->|Còn: Tiêu Thụ 1 Token| TB_Allow["Cho Phép Đi Vào Microservice"]
TB_Check -->|Hết: Thùng Rỗng| TB_Reject["Chuyển Vào Phòng Chờ Ảo (HTTP 429 / 202)"]
end
subgraph VongDoiPhongChoAo ["Vòng Đời Phòng Chờ Ảo Tại Mạng Biên"]
direction TB
WR_Enter["Người Dùng Vào Hàng Đợi"] --> WR_Cookie["Cấp Cookie Có Chữ Ký Điện Tử (HMAC-SHA256)"]
WR_Cookie --> WR_Poll["App Thăm Dò Có Chèn Độ Lệch Ngẫu Nhiên (Jitter)"]
WR_Poll --> WR_Gate["Mạng Biên Cho Phép K Người/Giây Đi Tiếp"]
WR_Gate --> WR_Admit["Chuyển Hướng Tới Màn Hình Thanh Toán Với Vé Dùng 1 Lần"]
end
classDef token fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
classDef room fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
class ThuatToanTokenBucket token;
class VongDoiPhongChoAo room;
So Sánh Thuật Toán Generic Cell Rate (GCRA) Với Token Bucket Truyền Thống
Shopee hiện thực hóa giải thuật Generic Cell Rate Algorithm (GCRA) thông qua Redis và Envoy Gateway. Không giống như Token Bucket truyền thống đòi hỏi phải lưu đồng thời cả số lượng token hiện có và dấu thời gian lần cập nhật cuối, GCRA chỉ cần duy trì một giá trị duy nhất cho mỗi khách hàng: Thời Điểm Đến Lý Thuyết (Theoretical Arrival Time - TAT).
Khi một yêu cầu gửi tới tại thời điểm $t$:
- Nếu $t > \text{TAT}$, thùng chứa đã được làm đầy hoàn toàn: $\text{TAT}_{\text{mới}} = t + T$, với $T = 1 / \text{rate}$.
- Nếu $t \le \text{TAT}$, yêu cầu này đến sớm hơn dự kiến. Nếu $\text{TAT} - t \le \tau$ (với $\tau$ là tham số dung sai chịu tải bùng nổ), yêu cầu được chấp thuận, và $\text{TAT}_{\text{mới}} = \text{TAT} + T$.
- Nếu $\text{TAT} - t > \tau$, năng lực chịu tải đã cạn kiệt. Yêu cầu bị từ chối ngay lập tức mà không cần cập nhật TAT.
Giải thuật GCRA thu nhỏ trạng thái lưu trong Redis xuống chỉ còn một số nguyên 64-bit cho mỗi key, loại bỏ hoàn toàn các xung đột đọc ghi tương tranh và hoàn tất tính toán dưới 100 microsecond.
Tích Hợp Phòng Chờ Ảo Trên Mạng Biên
Khi bộ điều tiết từ chối yêu cầu trong đêm 11.11, Shopee tuyệt đối không hiển thị một trang lỗi vô cảm. Thay vào đó, cổng biên phản hồi mã HTTP 202 Accepted kèm dữ liệu chỉ dẫn ứng dụng kích hoạt giao diện Phòng Chờ Ảo (Virtual Waiting Room):
- Cookie Cấp Quyền Có Chữ Ký Điện Tử: Khách hàng nhận được một chuỗi JWT hoặc HMAC-SHA256 mã hóa thông tin gồm
user_id, vị trí trong hàng đợi và thời gian phát hành. - Thăm Dò Trạng Thái Kèm Độ Lệch Ngẫu Nhiên (Jitter): Ứng dụng điện thoại sẽ thăm dò đường dẫn
/api/v1/queue/statussau khoảng thời gian $T_{\text{chờ}} = \text{KhoảngThờiGianGốc} + \text{rand}(0, \text{Jitter})$. Việc chèn tham số ngẫu nhiên ngăn ngừa tình trạng hàng triệu máy cùng lúc gửi gói tin vào đúng ranh giới của từng giây. - Điều Tiết Xả Hàng Đợi Động: Hệ thống giám sát theo dõi liên tục chỉ số sức khỏe của cơ sở dữ liệu phía sau (mức sử dụng CPU, độ trễ nhân bản, lưu lượng I/O ổ cứng). Khi tải cơ sở dữ liệu hạ xuống dưới 70%, cổng biên sẽ mở dần, cấp vé vào cửa một lần cho người mua theo đúng thứ tự đến trước được phục vụ trước (FIFO).
3. Apache Kafka Làm Hồ Chứa Giảm Áp Lực Đỉnh (Peak Shaving)
Các hệ quản trị cơ sở dữ liệu quan hệ và NewSQL rất mạnh mẽ trong việc truy vấn phức tạp và đảm bảo tính toàn vẹn giao dịch ACID, nhưng tốc độ ghi đĩa vật lý của chúng luôn bị giới hạn bởi độ trễ fsync và cơ chế ghi log tuần tự Write-Ahead Log (WAL). Một cụm cơ sở dữ liệu hiệu năng cao có thể duy trì an toàn 20.000 lệnh commit mỗi giây, nhưng sẽ ngay lập tức rơi vào trạng thái bế tắc (deadlock) nếu phải gánh một đợt sóng 250.000 đơn hàng mỗi giây.
Shopee xem Apache Kafka như một hồ chứa nước điều hòa áp lực khổng lồ. Kiến trúc commit log tuần tự chỉ nối tiếp dữ liệu (append-only) của Kafka tận dụng triệt để cơ chế ánh xạ bộ nhớ Page Cache của nhân Linux và tối ưu hóa I/O đĩa tuần tự, cho phép một cụm Kafka gồm 3 máy chủ broker có thể hấp thụ hơn 1.500.000 lượt ghi mỗi giây với độ trễ chưa đầy 1 mili-giây.
sequenceDiagram
autonumber
actor MobileClient as App Shopee Trên Điện Thoại
participant EdgeGW as Envoy API Gateway
participant OrderSvc as Dịch Vụ Tạo Đơn Hàng (Go)
participant KafkaProducer as Sarama Async Producer
participant KafkaBroker as Cụm Kafka (64 Partition)
participant ConsumerFleet as Nhóm Consumer (Co Giãn KEDA)
participant DLQTopic as Hàng Đợi Chết (orders.checkout.dlq)
participant TiDB as Cơ Sở Dữ Liệu Phân Tán TiDB
MobileClient->>EdgeGW: POST /api/v1/order/create
EdgeGW->>OrderSvc: Gọi RPC Tạo Đơn (vtprotobuf)
OrderSvc->>KafkaProducer: Đẩy Tin Nhắn Đơn Hàng Vào Hàng Đợi (Tái Sử Dụng sync.Pool)
KafkaProducer-->>OrderSvc: Xác Nhận Đã Đưa Vào Bộ Đệm RAM
OrderSvc-->>EdgeGW: Hoàn Tất HTTP 200 (Kèm Mã Theo Dõi Đơn Hàng)
EdgeGW-->>MobileClient: Phản Hồi Tức Thì (Độ Trễ p99 Dưới 35ms)
Note over KafkaProducer,KafkaBroker: Đẩy Mẻ Tin Nhắn Mỗi 50ms Hoặc Gom Đủ 500 Tin
KafkaProducer->>KafkaBroker: Ghi Dữ Liệu Lên Kafka Broker (Nén Snappy)
Note over KafkaBroker,ConsumerFleet: Tiêu Thụ Bất Đồng Bộ San Phẳng Đỉnh Sóng
ConsumerFleet->>KafkaBroker: Đọc Từng Mẻ 200 Tin Nhắn
alt Tin Nhắn Hợp Lệ
ConsumerFleet->>TiDB: Thực Thi Batch INSERT Đơn Hàng (Ghi Đồng Loạt)
TiDB-->>ConsumerFleet: Commit Thành Công
ConsumerFleet->>KafkaBroker: Ghi Nhận Offset Của Partition
else Gặp Tin Nhắn Độc (Poison Pill) / Sai Schema
ConsumerFleet->>DLQTopic: Chuyển Vào Hàng Đợi DLQ Sau 3 Lần Thử Lại
ConsumerFleet->>KafkaBroker: Vẫn Commit Offset (Không Gây Nghẽn Hàng Đợi)
end
Chiến Lược Phân Vùng (Partitioning) Và Cam Kết Thứ Tự Xử Lý
Trong ngành thương mại điện tử, các giao dịch của cùng một người mua bắt buộc phải tuân thủ thứ tự thời gian nghiêm ngặt (người dùng không thể thanh toán đơn hàng trước khi đơn hàng đó được tạo ra), nhưng các đơn hàng của những người mua khác nhau thì hoàn toàn độc lập với nhau.
Shopee cấu hình từ 64 đến 128 partition cho mỗi topic đặt hàng và thiết lập giá trị khóa phân vùng (partition key) chính là user_id:
$$\text{Partition} = \text{MurmurHash2}(\text{user_id}) \pmod{\text{TổngSốPartition}}$$
Mô hình phân vùng này mang lại các lợi ích kiến trúc then chốt:
- Mọi sự kiện đơn hàng của cùng một khách hàng luôn luôn đi vào cùng một partition theo đúng trình tự thời gian phát sinh.
- Các tiến trình tiêu thụ (consumer) duy trì tính tuần tự nghiêm ngặt cho từng khách hàng mà không cần phải dùng tới các cơ chế khóa phân tán toàn cục.
- Thông lượng xử lý đơn hàng tổng thể có thể mở rộng tuyến tính bằng cách bổ sung thêm hàng trăm pod consumer chạy song song độc lập.
4. Cài Đặt Thực Tế Kafka Producer Và Consumer Bằng Golang
Đoạn mã nguồn Golang dưới đây hiện thực hóa toàn bộ luồng đẩy tin nhắn bất đồng bộ và nhóm worker tiêu thụ hàng đợi theo mẻ sử dụng thư viện Shopify/Sarama. Mã nguồn bao gồm bể tái sử dụng cấu trúc bộ nhớ sync.Pool, kỹ thuật gom mẻ ghi cơ sở dữ liệu, theo dõi độ trễ hàng đợi và tự động cách ly tin nhắn độc hại vào hàng đợi lỗi Dead Letter Queue (DLQ).
package messaging
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/IBM/sarama"
)
// OrderEvent đại diện cho thông tin đơn hàng bất biến được đệm vào Kafka.
type OrderEvent struct {
OrderID string `json:"order_id"`
UserID int64 `json:"user_id"`
SKUID int64 `json:"sku_id"`
Quantity int32 `json:"quantity"`
TotalAmount float64 `json:"total_amount"`
IdempotencyKey string `json:"idempotency_key"`
Timestamp int64 `json:"timestamp"`
RetryCount int `json:"retry_count"`
}
// OrderProducer bao bọc AsyncProducer của Sarama với bể tái sử dụng bộ nhớ.
type OrderProducer struct {
producer sarama.AsyncProducer
topic string
pool sync.Pool
enqueued int64
dropped int64
}
func NewOrderProducer(brokers []string, topic string) (*OrderProducer, error) {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForLocal // Leader xác nhận nhằm cân bằng độ bền và thông lượng
config.Producer.Compression = sarama.CompressionSnappy // Giảm 40% băng thông với chi phí CPU không đáng kể
config.Producer.Flush.Messages = 500 // Gom mẻ 500 tin nhắn
config.Producer.Flush.Frequency = 50 * time.Millisecond // Xả đệm tối đa sau 50 mili-giây
config.Producer.Return.Successes = false
config.Producer.Return.Errors = true
producer, err := sarama.NewAsyncProducer(brokers, config)
if err != nil {
return nil, fmt.Errorf("khong the tao async producer: %w", err)
}
p := &OrderProducer{
producer: producer,
topic: topic,
pool: sync.Pool{
New: func() interface{} {
return new(OrderEvent)
},
},
}
// Lắng nghe lỗi ngầm để tránh nghẽn channel nội bộ
go func() {
for err := range producer.Errors() {
atomic.AddInt64(&p.dropped, 1)
log.Printf("[ERROR] Ghi tin vao Kafka that bai: %v", err)
}
}()
return p, nil
}
func (p *OrderProducer) PublishOrder(event *OrderEvent) error {
payload, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("loi marshal json: %w", err)
}
msg := &sarama.ProducerMessage{
Topic: p.topic,
Key: sarama.StringEncoder(fmt.Sprintf("%d", event.UserID)), // Phân vùng theo UserID
Value: sarama.ByteEncoder(payload),
}
select {
case p.producer.Input() <- msg:
atomic.AddInt64(&p.enqueued, 1)
return nil
default:
atomic.AddInt64(&p.dropped, 1)
return errors.New("err_queue_full: bo dem kafka producer da bi tran")
}
}
// Close xả nốt tin nhắn đang chờ và ngắt kết nối an toàn.
func (p *OrderProducer) Close() error {
return p.producer.Close()
}
// BatchOrderConsumer quản lý vòng đời nhóm consumer và gom mẻ ghi cơ sở dữ liệu.
type BatchOrderConsumer struct {
db *sql.DB
dlqTopic string
producer sarama.SyncProducer
}
func NewBatchOrderConsumer(db *sql.DB, producer sarama.SyncProducer, dlqTopic string) *BatchOrderConsumer {
return &BatchOrderConsumer{
db: db,
dlqTopic: dlqTopic,
producer: producer,
}
}
func (c *BatchOrderConsumer) Setup(sarama.ConsumerGroupSession) error { return nil }
func (c *BatchOrderConsumer) Cleanup(sarama.ConsumerGroupSession) error { return nil }
func (c *BatchOrderConsumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
const batchSize = 200
const flushInterval = 100 * time.Millisecond
batch := make([]*OrderEvent, 0, batchSize)
messages := make([]*sarama.ConsumerMessage, 0, batchSize)
ticker := time.NewTicker(flushInterval)
defer ticker.Stop()
flush := func() {
if len(batch) == 0 {
return
}
if err := c.persistBatch(session.Context(), batch); err != nil {
log.Printf("[WARN] Ghi ca me don hang that bai: %v. Chuyen sang xu ly tung don", err)
c.handleBatchFailure(session, messages, batch)
} else {
for _, m := range messages {
session.MarkMessage(m, "")
}
}
batch = batch[:0]
messages = messages[:0]
}
for {
select {
case msg, ok := <-claim.Messages():
if !ok {
flush()
return nil
}
event := new(OrderEvent)
if err := json.Unmarshal(msg.Value, event); err != nil {
// Tin nhắn sai định dạng: đẩy ngay sang DLQ và đánh dấu xong
c.routeToDLQ(msg.Value, "unmarshal_error")
session.MarkMessage(msg, "")
continue
}
batch = append(batch, event)
messages = append(messages, msg)
if len(batch) >= batchSize {
flush()
}
case <-ticker.C:
flush()
case <-session.Context().Done():
flush()
return nil
}
}
}
// persistBatch thực thi câu lệnh SQL gom mẻ tốc độ cao xuống TiDB.
func (c *BatchOrderConsumer) persistBatch(ctx context.Context, batch []*OrderEvent) error {
tx, err := c.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO orders (order_id, user_id, sku_id, quantity, total_amount, idempotency_key, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE total_amount = total_amount
`)
if err != nil {
return err
}
defer stmt.Close()
for _, o := range batch {
if _, err := stmt.ExecContext(ctx, o.OrderID, o.UserID, o.SKUID, o.Quantity, o.TotalAmount, o.IdempotencyKey); err != nil {
return err
}
}
return tx.Commit()
}
// handleBatchFailure cách ly tin nhắn độc hại và chuyển hướng lỗi sang DLQ.
func (c *BatchOrderConsumer) handleBatchFailure(session sarama.ConsumerGroupSession, msgs []*sarama.ConsumerMessage, batch []*OrderEvent) {
for i, o := range batch {
o.RetryCount++
if o.RetryCount > 3 {
c.routeToDLQ(msgs[i].Value, "vuot_qua_so_lan_thu_lai")
session.MarkMessage(msgs[i], "")
} else {
log.Printf("[RETRY] Don hang %s se duoc thu lai (lan %d)", o.OrderID, o.RetryCount)
}
}
}
func (c *BatchOrderConsumer) routeToDLQ(payload []byte, reason string) {
msg := &sarama.ProducerMessage{
Topic: c.dlqTopic,
Value: sarama.ByteEncoder(payload),
Headers: []sarama.RecordHeader{
{Key: []byte("dlq_reason"), Value: []byte(reason)},
},
}
_, _, err := c.producer.SendMessage(msg)
if err != nil {
log.Printf("[CRITICAL] Khong the ghi tin vao DLQ: %v", err)
}
}
5. Cơ Chế Hạ Cấp Tính Năng (Graceful Degradation Switches)
Khi lưu lượng truy cập toàn sàn chạm ngưỡng giới hạn vật lý của cụm máy chủ, Shopee sẽ kích hoạt Các Công Tắc Hạ Cấp Tính Năng Chủ Động. Thay vì để hệ thống sập tự do và trả về các mã lỗi HTTP 500 ngẫu nhiên, nền tảng sẽ cắt bỏ có hệ thống các tính năng phụ trợ, giải phóng hơn 60% năng lực xử lý CPU của cơ sở dữ liệu để phục vụ duy nhất luồng đặt hàng thanh toán.
flowchart TD
subgraph TangCotLoi ["Tầng Cốt Lõi 0 (Được Bảo Vệ 100% Khả Dụng)"]
C1["Thanh Toán Giỏ Hàng & Áp Mã Giảm Giá"]
C2["Trừ Tồn Kho Nguyên Tử Trong Bộ Nhớ RAM"]
C3["Tiếp Nhận Webhook Ngân Hàng & Kết Toán"]
end
subgraph TangHaCap1 ["Tầng Hạ Cấp 1 (Tắt Khi Tải Vượt 80% Năng Lực)"]
D1["Gợi Ý Sản Phẩm Cá Nhân Hóa (Thay Bằng File JSON Tĩnh)"]
D2["Tìm Kiếm Lịch Sử Đơn Hàng (Chỉ Cho Phép Tìm Trong 7 Ngày Gần Nhất)"]
D3["Đẩy Thông Báo Voucher Qua Ứng Dụng (Hoãn Lại 30 Phút)"]
end
subgraph TangHaCap2 ["Tầng Hạ Cấp 2 (Tắt Khi Tải Vượt 95% Năng Lực)"]
E1["Gửi Đánh Giá Sản Phẩm (Lưu Tạm Vào Hàng Đợi Chờ Xử Lý Sau)"]
E2["Thông Báo Chat Trực Tiếp Với Shop (Giới Hạn 1 Tin / 5 Giây)"]
E3["Mini-game Tích Xu & Vòng Quay May Mắn (Đóng Cửa Kèm Bảng Thông Báo Bảo Trì)"]
end
TangCotLoi -.->|Hưởng Trọn Năng Lượng CPU Cơ Sở Dữ Liệu Được Giải Phóng| TangHaCap1
TangCotLoi -.->|Hưởng Trọn Băng Thông Bộ Đệm Được Giải Phóng| TangHaCap2
classDef core fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
classDef t1 fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
classDef t2 fill:#ffebee,stroke:#c62828,stroke-width:2px;
class TangCotLoi core;
class TangHaCap1 t1;
class TangHaCap2 t2;
Ba Cấp Độ Hạ Cấp Nghiêm Ngặt
Shopee phân loại toàn bộ hệ thống microservice thành ba tầng khả dụng:
- Tầng 0 (Giao Dịch Sống Còn): Giữ chỗ tồn kho, gửi đơn đặt hàng, giao tiếp cổng thanh toán và kiểm tra gian lận. Các dịch vụ này được bảo lưu tài nguyên cố định và không bao giờ bị hạ cấp trong bất kỳ hoàn cảnh nào.
- Tầng 1 (Phụ Thuộc Mềm): Danh sách sản phẩm gợi ý tại trang chi tiết sản phẩm được thay thế bằng các tệp JSON tĩnh lưu trên CDN. Các câu lệnh tìm kiếm toàn văn được chuyển hướng chỉ quét trên tiêu đề thay vì quét toàn bộ mô tả chi tiết.
- Tầng 2 (Tính Năng Phụ): Tính năng chat trực tiếp người mua - người bán, gửi nhận xét và các trò chơi săn xu tích điểm tạm thời đóng cửa. Các lệnh gọi tới các API này lập tức nhận về mã HTTP 200 kèm giao diện thông báo hệ thống đang bảo trì định kỳ.
Các công tắc hạ cấp được điều khiển tập trung thông qua cụm cấu hình Apollo dựa trên thuật toán đồng thuận Raft. Khi các kỹ sư SRE bật công tắc, quy tắc cấu hình mới sẽ lan truyền tới hơn 50.000 pod microservice chỉ trong vòng 500 mili-giây.
6. Ma Trận Đánh Đổi Kỹ Thuật Và Các Giải Pháp Bị Bác Bỏ
Việc áp dụng giải pháp peak shaving bất đồng bộ đòi hỏi sự đánh đổi kỹ lưỡng giữa độ trễ, tính nhất quán và trải nghiệm người dùng:
| Quyết Định Kiến Trúc | Phương Án Bị Bác Bỏ | Lý Do Đánh Đổi & Căn Cứ Thực Tế |
|---|---|---|
| Phân chia Topic riêng theo loại sự kiện | Dùng một Topic tổng hợp duy nhất | Topic dùng chung trộn lẫn đơn hàng ưu tiên với các sự kiện phân tích, gây tắc nghẽn toàn bộ khi hàng đợi bị trễ. |
| Thuật toán GCRA Token Bucket | Đếm theo khung giờ cố định (Fixed-Window) | Khung giờ cố định cho phép xả gấp đôi lưu lượng tại ranh giới chuyển giao, làm nghẽn kết nối cơ sở dữ liệu. |
| Gom mẻ 200 đơn hàng ghi đồng loạt | Ghi từng đơn hàng độc lập vào Database | Ghi từng đơn tốn 200 lượt khứ hồi mạng và 200 lần ép ghi đĩa WAL, làm giảm hơn 85% thông lượng cơ sở dữ liệu. |
| Phòng chờ ảo có trải nghiệm thân thiện | Trả về trang lỗi HTTP 429 thô | Trang lỗi thô khiến người dùng bức xúc bấm F5 liên tục, tạo ra một đợt tấn công từ chối dịch vụ DDoS tự phát. |
Câu Hỏi Thường Gặp (FAQ)
Làm thế nào Shopee đảm bảo người mua không bị chậm trễ nhận thông báo đặt hàng khi hàng đợi Kafka bị trễ?
order_id và mã xác nhận giữ chỗ thành công ngay trong phản hồi HTTP 200 đồng bộ ban đầu. Ứng dụng điện thoại hiển thị ngay màn hình chúc mừng đặt hàng thành công mà không cần chờ dữ liệu xuất hiện trong cơ sở dữ liệu. Độ trễ vài giây của hàng đợi Kafka phía sau hoàn toàn vô hình đối với người dùng, trong lúc họ đang bận thao tác chọn phương thức thanh toán tiền mặt hoặc quét mã QR.Thế nào là một 'tin nhắn độc hại' (poison pill) trong Kafka, và cơ chế xử lý lỗi ngăn chặn tắc nghẽn ra sao?
recover(): nếu tin nhắn lỗi cú pháp hoặc đã thử lại quá 3 lần, nó sẽ được chuyển hướng sang hàng đợi chết Dead Letter Queue (orders.checkout.dlq) và tiến hành commit offset để cho phép các đơn hàng hợp lệ phía sau tiếp tục được xử lý bình thường.Tại sao chuẩn nén Snappy được ưu tiên sử dụng thay cho Gzip hay Zstandard trong các topic đặt hàng?
Cơ chế KEDA mở rộng số lượng pod consumer tự động dựa trên độ trễ hàng đợi hoạt động như thế nào?
orders.checkout.v1 vượt mốc 10.000 tin, KEDA sẽ ngay lập tức kích hoạt mở rộng số lượng pod consumer từ 10 lên 64 pod chỉ trong vòng 30 giây, giải tỏa nhanh chóng lượng hàng đợi ứ đọng trước khi độ trễ cơ sở dữ liệu bị suy thoái.Tài Liệu Tham Khảo Kỹ Thuật
Để tìm hiểu sâu hơn về kiến trúc hàng đợi phân tán và các bài toán chịu tải cao:
- Kiến trúc giao dịch chịu tải đỉnh cao Alipay Double 11
- Bản đồ đọc và mục lục hệ thống kiến trúc
- Thiết kế hệ thống vi dịch vụ hiệu năng cao với Golang
Bước Tiếp Theo
Mời bạn tiếp tục theo dõi Bài 4: Mở Rộng Cơ Sở Dữ Liệu — Từ Sharded MySQL Sang TiDB NewSQL Phân Tán để tìm hiểu cách Shopee xóa bỏ hoàn toàn gánh nặng vận hành phân mảnh cơ sở dữ liệu truyền thống bằng giải pháp NewSQL dựa trên thuật toán đồng thuận Multi-Raft.
