Điều kiện tiên quyết: Đây là Phần 5 của Khóa Học System Design. Quẹo lội lại Phần 4: Phình To Cơ Sở Dữ Liệu để thấu cái hang ổ lưu trữ (storage tier) nơi đám rác sự kiện lỳ lợm (persisted events) bị quăng xuống.
Answer-first: Cấu Trúc Bám Đuôi Sự Kiện (Event-Driven Architecture) giật đứt tung mớ xiềng xích gò bó (decouples services) bằng cái luồng chửi thề chéo hông (asynchronous communication) qua một quyển sổ thù vặt trâu bò (durable message log). Ở xứ Go, mớ goroutines và ống xả có nắp (buffered channels) đẻ ra cái luồng nhịn nhục xả áp bẩm sinh (natural backpressure) — bao giờ tụi bóc rác (consumers) lết lếch lê gối hụt hơi nhặt không kịp đống rác của lũ vãi rác (producers), cái ống xả đầy ụ ứ họng sẽ tự bịt mõm (blocks) thằng xả rác, khóa hầu bao bóp họng dội ngược tốc độ ăn rác (throttling the ingest rate) một cách điêu luyện.
So Găng Kafka vs RabbitMQ — Đứa Nào Xài Vào Việc Nào?
Answer-first: Kafka là một quyển sổ phong thần phân tán (distributed commit log) — mớ tin rác được ngâm dấm sống dai nhách (retained indefinitely), bọn ăn rác tự nhớ lấy địa chỉ nhặt rác (consumers manage their own offsets), và rảnh rỗi lôi băng ra tua lại chửi lộn tiếp (replay is possible). Thằng RabbitMQ rặt là một tên bưu tá cò mồi (message broker) — tin tức quăng xong gật đầu (ACK) là bị bóp cổ xóa sổ luôn (deleted), thằng cò lái gom hết đống bùi nhùi lắt léo dẫn đường (broker handles routing complexity), tọng rác tận họng theo kiểu đẩy xuống (push-based delivery). Tụi nó giải quyết rác khác hệ nhau.
Lên Bàn Cân Kiến Trúc (Architectural Comparison)
| Đồ Nghề (Property) | Xưởng Gỗ Apache Kafka | Cò Mồi RabbitMQ |
|---|---|---|
| Cốt Lõi Mớ Rác (Message Model) | Quyển sổ chốt đơn tứ tán (Phập thêm đuôi chẳng cho sửa - append-only, immutable) | Cò mồi bưu tá (Ngã tư/cống rãnh - queue/exchange, chọt sửa tá lả - mutable) |
| Hạn Ngâm Giấm (Message Retention) | Vặn núm tùy thích (Mặc định vứt xó 7 ngày, có thể ngâm mục mọt - indefinite) | Đớp xong gật (ACK) là bốc hơi luôn |
| Lối Trả Hàng (Delivery Model) | Trộm Chó (Pull) — tụi ăn rác đi rình mò bóc, tự nhớ lấy số nhà (manage offsets) | Tọng Họng (Push) — bưu tá bới rác nhét tận mồm (delivers to consumer) |
| Lời Thề Xếp Hàng (Ordering Guarantee) | Chỉ ngoan ngoãn nội trong một hẻm (Within a partition only) | Mạnh ai nấy giẫm đạp (Không có cửa nếu bu vào 1 đống mồm - multiple consumers) |
| Xả Rác Cỡ (Throughput) | Cả triệu cục rác/giây (Tuyệt kỹ húp thẳng luồn lõi nhân zero-copy kernel optimization) | Sương sương ~100k cục/giây |
| Tua Lại (Replay) | ✅ Bao xài — xoay núm (rewind offset) nhảy múa tung tăng | ❌ Ngậm bồ hòn — Gật (ACK’d) là bốc cháy (gone) |
| Chỉ Đường Cò Mồi (Routing) | Máng rác (Topic) + hẻm vạch (partition) (Ngô nghê đơn giản) | Đủ kiểu ngã tư (Exchange types): nhắm thẳng (direct), văng miểng (fanout), bám trend (topic), ngửi mùi (headers) |
| Ghế VIP Hầu Xài (Best Use Case) | Truy vết ngọn nguồn (Event sourcing), suối rác (stream processing), sổ phong thần (audit log), rải thảm (fan-out) | Cống đùn rác (Task queue), hò hét sai vặt (RPC), mò mẫm mê cung (complex routing), bóc vác (work queues) |
[!NOTE] Hốt cả cặp trói chung. Shopee xài mẻ Kafka để tháo cống luồng rác đơn hàng (sổ truy vết, rải thảm gặm thống kê, tua lại khi ngáo) và xách bưu tá RabbitMQ cho cái kho lính lác đẩy rác hàng tồn kho (quăng lính lác bóc vác, tống giam rác cùi bắp vô dead-letter queue retry). Hai thằng ăn hai mâm trị bệnh khác nhau — chả đâm chém giành nồi cơm.
Mổ Bụng Tuyệt Kỹ Zero-Copy Của Kafka — Sao Nó Bắn Rác Nhanh Quỷ Khốc Thần Sầu
Answer-first: Kafka cắn rứt bơm máu tốc độ bàn thờ nhờ gọi bùa sendfile() gọi mớm ruột gan nhân hệ điều hành (system call) — dọn mâm vác dữ liệu xẹt thẳng không dây dưa (zero-copy) từ bộ nhớ tạm của OS (OS page cache) phang chóc lọt xuống tận mồm cái lỗ cắm mạng (NIC socket buffer), ngó lơ đạp gãy mặt khu dân sự (bypassing user space completely). Kẹp xích chung chiêu viết đĩa chổng mông nối đuôi (sequential disk writes) và màn chọt mục lục thủng lỗ chỗ thưa thớt (sparse index lookups), Kafka vặt trụi thùi lụi mớ cặn bã hút máu CPU với trò vác bộ nhớ bưng qua bưng lại (memory copy overhead).
Đi Cày Cuốc Nông Dân I/O vs Phi Thân Zero-Copy
graph LR
subgraph traditional["Nông Dân Cày Bừa I/O (4 bận vác nặng, 4 nháy thay đồ context switches)"]
D1[Ổ Cứng Disk] -->|"Vác DMA"| KC1[RAM Ruột Kernel Page Cache]
KC1 -->|"CPU còng lưng vác"| US1[Sân Dân Sự User Buffer]
US1 -->|"CPU vác tiếp"| SK1[Hố Rác Cáp Mạng Socket Buffer]
SK1 -->|"DMA xúc"| NIC1[Lỗ Cắm Mạng NIC]
end
subgraph zerocopy["Độn Thổ Zero-Copy sendfile() (2 nháy DMA vác, 0 CPU động móng tay)"]
D2[Ổ Cứng Disk] -->|"DMA múc"| KC2[RAM Ruột Kernel Page Cache]
KC2 -->|"DMA quăng xẹt rải thảm (scatter-gather)"| NIC2[Lỗ Cắm Mạng NIC]
end
- Lối cũ lề mề (Traditional): 4 bận vác bộ nhớ + 4 chập cởi áo khoác user/kernel (context switches).
- Luồn lách đứt đuôi (Zero-copy): chẳng mượn CPU cõng cắn nào + 2 bận bưng DMA + 2 nháy cởi áo.
- Tiếng gầm thực địa (Real-world impact): Dội bomb tốc độ lồng lộn 2–4× mớ đống lùng nhùng hít khói IO (I/O-bound workloads).
Lưới Mục Lục Thủng (Sparse Index) — Mò Số Nhà Lụi Nhanh chẳng cần Sục Sạo
Kafka chả rảnh háng ngồi băm nát đánh số từng hột rác (index every message). Nó chơi vạch một cái mục lục thủng (sparse index) chăng lưới lốm đốm cứ cách $X$ byte vũng rác lại thả ghim 1 cái số nhà bùn (file offset):
Cuốn sổ thủng .index file (sparse):
Số nhà 0 → Tọa độ hố rác 0
Số nhà 1,234 → Tọa độ hố rác 4,096
Số nhà 2,468 → Tọa độ hố rác 8,192
Bãi mìn .log file:
[Rác ở số=0]
[Rác ở số=1]
...
[Rác ở số=1,234] ← Bay nhảy phi thân xuống lỗ này nhờ trò cưa đôi bói bóc lụi (binary search) cuốn sổ .index
Mò Rác (Lookup): Cưa đôi cuốn sổ (binary search) .index lôi chóc ra địa chỉ hẻm → ngước mặt đi quét dọn (sequential scan) từ mớ rác gần xịt đó. Đâm chém chiêu O(log N) mò mục lục gộp chiêu O(M) quét chổi chà, với rác M thì muỗi mọt (bị rào lại hãm họng bởi mật độ nhét cọc index density).
Trồng Cây Xả Áp (Backpressure) Xứ Go
Answer-first: Đập đê xả áp (Backpressure) ở lãnh thổ Go được rèn đúc mướt mượt bẩm sinh từ mớ ống khói có thùng chứa (buffered channels) — lúc nào lồng ấp nhồi ứ họng (buffer is full), kẻ tuồn rác (sender) bị khóa mồm (blocks), dội nguyên cơn bão ứ đọng ngược về (propagating pressure back) mõm thằng ném rác thượng nguồn (upstream producer). Nắn lại đút họng cho mớ bầy tôi tớ lao công đóng hộp (bounded worker pool), nguyên xưởng máy tự thắt cổ ợ chua nắn dòng (throttles ingest) lúc bọn bốc rác chổng mông lết bò (slower) trễ nhịp lũ đẻ rác.
Đúc Lò Bày Binh Lao Công Đóng Hộp (Bounded Worker Pool Pattern)
package kafka
import (
"context"
"fmt"
"log"
"sync"
"time"
)
type Message struct {
Key string
Value []byte
Partition int32
Offset int64
}
// StartWorkerPool dọn xưởng nhốt lính gồng cái gông xả áp bẩm sinh (natural backpressure)
// workers: quân số lính lác (concurrent goroutines)
// bufferSize: mồm thùng rác — ứ họng phát, bít cửa dội nhét jobChan block (thắt cổ backpressure)
func StartWorkerPool(
ctx context.Context,
workers int,
bufferSize int,
process func(ctx context.Context, msg Message) error,
) chan<- Message {
jobChan := make(chan Message, bufferSize) // Cái ống đong đầy rác = cục phanh xả áp (backpressure mechanism)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case msg, ok := <-jobChan:
if !ok {
return
}
if err := process(ctx, msg); err != nil {
log.Printf("lính %d: nghẹn ứ ợ mửa rác số=%d: %v",
workerID, msg.Offset, err)
// Bãi xịn Production: đá cút rác vào máng lợn chết dead-letter queue
}
}
}
}(i)
}
go func() { wg.Wait() }()
return jobChan
}
// KafkaConsumerLoop móc mõm mớm rác vào xưởng bóc vác
func KafkaConsumerLoop(ctx context.Context, jobChan chan<- Message) {
msgOffset := int64(0)
for {
select {
case <-ctx.Done():
return
default:
// Phép giả lập rình rác (Kafka poll batch)
for i := 0; i < 10; i++ {
msg := Message{
Key: fmt.Sprintf("order-%d", msgOffset),
Value: []byte(`{"event":"order_created"}`),
Offset: msgOffset,
}
select {
case jobChan <- msg:
msgOffset++
case <-ctx.Done():
return
default:
// Ứ họng thùng chứa rác (Buffer full) → phanh gấp backpressure: đình công húp rác (pause consumption)
// Ra trận Production: giảm bớt tần suất hít rác Kafka (Kafka poll rate), CẤM gật đầu đóng sổ (don't commit offset)
log.Printf("WARN: Kéo phanh xả áp ợ trào ngay cục rác số %d", msgOffset)
time.Sleep(10 * time.Millisecond)
}
}
}
}
}
[!IMPORTANT] Trói cổ xếp hàng hẻm partition (Partition ordering constraint): Kafka thề thốt giữ trật tự xếp hàng CỤC BỘ chui rúc nội trong một hẻm (single partition only). Cả bầy công nhân lộn xộn bu bám (generic worker pool) vạch rác nhai nuốt hỗn loạn bừa bãi (arbitrary order - thằng lính nào hở mõm rảnh rỗi là thò tay húp). Nhỡ bám víu đâm sống đâm chết đòi xếp hàng đều tay (e.g., TẠO_ĐƠN phải lòi phèo dọn trước mớ HỦY_ĐƠN cho chung chóc 1 cái đơn hàng), ới gọi ngay cục bầy tôi dán nhãn mốc hẻm (partition-aware pool) tròng cổ giam đúng nháy 1 con goroutine đẻ bóc rác cho mỗi cái hẻm partition.
Lò Lính Lác Bám Trụ Hẻm Xếp Hàng Ngay Ngắn (Partition-Aware Ordered Worker Pool)
// OrderedPartitionWorkerPool: Mỗi ngách hẻm (partition) → đẻ dính 1 thằng đệ (dedicated goroutine)
// Lời thề đinh đóng cột bốc vác xếp hàng thẳng tắp (in-order processing) nội trong chóp mỗi hẻm
type OrderedPartitionWorkerPool struct {
mu sync.RWMutex
partitionChans map[int32]chan Message
}
func (p *OrderedPartitionWorkerPool) Submit(
ctx context.Context,
msg Message,
process func(ctx context.Context, msg Message) error,
) {
p.mu.Lock()
ch, exists := p.partitionChans[msg.Partition]
if !exists {
ch = make(chan Message, 100)
p.partitionChans[msg.Partition] = ch
// Tống đẻ ra nháy một con goroutine độc tài đớp rác cắm chốt cho riêng cái hẻm partition này
go func(partCh <-chan Message) {
for m := range partCh {
process(ctx, m) // Đớp rác ngay hàng thẳng lối (Sequential processing) — đinh đóng cột ordering guaranteed
}
}(ch)
}
p.mu.Unlock()
ch <- msg
}
Bùa Ngải Chốt Đơn Đét 1 Lần (Exactly-Once Semantics) Trấn Yểm Kafka
Answer-first: Chiêu ngải Chốt Đơn 1 Lần (Exactly-Once Semantics) ở Kafka đòi phải cắm sừng một thằng ném rác lỳ lợm miễn nhiễm đúp (idempotent producer - chặn ngửa ngực nôn mửa rác đôi duplicate publishes) kẹp nách một thằng lụm rác có cái nết khắc bia đá đánh dấu số nhà (commits the Kafka offset) gộp dính chùm cùng một hố sống chết bện chặt (atomically) với cái nồi cơm gõ việc (business operation). Muốn đú true exactly-once đi tới bến nhức nách ra mấy cái bãi đáp ngoại lai (side effects outside Kafka), bắt tròng cổ ôm lấy một cái chìa khóa miễn tróc (idempotency key).
Ăn Vạ Ít Nhất 1 Miếng (At-Least-Once) Là Trò Ngầm Định Xưa Rồi Diễm
graph LR
Consumer -->|"1. Trát ghi DB THƠM BƠ (SUCCESS)"| DB
Consumer -->|"2. Tạch ngỏm củ tỏi (Crash) trước bận đóng dấu số nhà (offset commit)"| X[💥 Oẳng Củ Tỏi (Crash)]
Consumer -->|"3. Gọi hồn bò dậy (Restart): húp nhai lại đúng y cục rác mốc khô (re-reads same offset)"| Kafka
Consumer -->|"4. Mâm DB gồng họng ói mửa KÉP (DUPLICATE)!"| DB
style X fill:#f8d7da,stroke:#dc3545
Bùa 1 Phát Chốt Đơn Qua Màn Cột Chặt Số Nhà Vô DB (Exactly-Once via Transactional Offset Commit)
Miếng nghề bãi rác xịn xò (production pattern): nhét chôn lấp luôn cái tọa độ Kafka offset dính cục chung chóc trong lòng DB transaction của mớ nùi ghi chép việc kinh doanh (business write):
package consumer
import (
"context"
"database/sql"
"fmt"
"log"
)
type OrderEventConsumer struct {
db *sql.DB
}
// ProcessOrderEvent — Xơi 1 Chát (Exactly-Once) qua cái kho bấu chặt mốc nhà (transactional offset storage)
func (c *OrderEventConsumer) ProcessOrderEvent(
ctx context.Context,
partition int32,
offset int64,
orderJSON []byte,
) error {
tx, err := c.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("tịt ngòi bới tx (begin tx): %w", err)
}
defer tx.Rollback()
// 1. Soi mói bùa cản đúp (idempotency) — cào bới coi cái số nhà (offset) mốc khô này lỡ bị thò mồm bóc chưa?
var exists bool
err = tx.QueryRowContext(ctx,
`SELECT EXISTS(
SELECT 1 FROM kafka_offsets
WHERE topic='order-events' AND partition=$1 AND offset=$2
)`, partition, offset,
).Scan(&exists)
if err != nil {
return fmt.Errorf("cào mù mắt bới offset: %w", err)
}
if exists {
log.Printf("Mốc nhà %d đớp nát rồi, lơ nhảy hố (skipping - idempotent)", offset)
return nil
}
// 2. Mớ bòng bong nhét nồi cơm (Business logic) — nhét mẹ đơn vô
_, err = tx.ExecContext(ctx,
`INSERT INTO orders (data, created_at) VALUES ($1, NOW())`, orderJSON,
)
if err != nil {
return fmt.Errorf("lọt hố đẻ order: %w", err)
}
// 3. Khắc bia đá dính tịt (Commit) cái mốc Kafka TRÓI CHUNG (SAME transaction) nháy
_, err = tx.ExecContext(ctx,
`INSERT INTO kafka_offsets (topic, partition, offset)
VALUES ('order-events', $1, $2)
ON CONFLICT (topic, partition) DO UPDATE SET offset = EXCLUDED.offset`,
partition, offset,
)
if err != nil {
return fmt.Errorf("giấu xác offset: %w", err)
}
// 4. Nhảy lầu ôm nhau cùng đứt (Atomic commit): mớ rác nồi cơm (business data) + bia đá (offset) thề sống chết (committed) hoặc dắt díu đi thọt hết trọi (rolled back)
return tx.Commit()
}
[!TIP] Khuôn đúc chôn dấu (Schema) giấu lố offset của Kafka:
CREATE TABLE kafka_offsets ( topic VARCHAR(255) NOT NULL, partition INT NOT NULL, offset BIGINT NOT NULL, PRIMARY KEY (topic, partition) );
Mổ Xẻ Sâu Sân Chơi (Case Study): Vạt Đầu Cơn Lốc Đòi Mạng Chốt Đơn Flash Sale Shopee (Shopee Flash Sale Peak Shaving)
🔥 [Miếng Nghề Bãi Xịn Production Pattern]: Ông Vua Lò Bơm Sự Kiện Đơn Hàng Của Shopee Ổ Căn Bệnh (Problem): Cơn Lốc ồ ạt nửa đêm săn Flash sale: đè cổ 500,000 nháy chốt đơn/phút (orders/minute). Vựa Database sùi bọt mép há hốc mồm từ chối tiêu thụ ngậm cục rác ứ đọng nã pháo dập cùng lúc (synchronous write volume). Xếp Hàng (Architecture):
Cái ngón tay khách chọt (User Request) → Chắn Cửa API Gateway → Nhét Kafka (Cái Máng Order Topic) → Bầy Lính Bóc Vác Worker Pool (Go) → Ghi lụi vô DB WriteĂn Hôi (Result): Lỗ cắm DB nhấm nháp ọc ạch nuốt trôi ~5,000 mớ chọt/giây đều như vắt chanh (steady) mặc xác ở ngoài bão dông cỡ nào (regardless of burst size). Pháo đài Kafka giơ mông hứng đạn trọn cục lốc (absorbs the spike); đám lính lác lụi hì hục rút nước hốt rác dưới tốc độ gông cổ bóp lại (controlled rate). Vặn Ốc (Config): 50 thằng cu li (workers) × 10 hẻm ngách (partitions) = 500 cái vòi mút concurrent DB writes. Bọng đái rác (Buffer size) = 10,000 nháy hộc. Vòi Phanh Backpressure: Hễ bụng chứa no căng phình lồi nức họng, máy nhai Kafka (consumer) đứng sững lại tự kỷ (pauses automatically). Rác đơn hàng dồn cục trong bụng Kafka (ngâm dấm 7 ngày - 7-day retention) — chẳng lòi văng ra giọt nào (zero data loss). (Mò hố từ: Shopee Engineering Blog, 2021)
Hỏi Nhanh Đáp Gọn (FAQ)
Giữa Kafka với cha RabbitMQ rốt cục lòi le xào qua lộn lại có gì khác mọe nhau?
Kafka là một quyển sổ phong thần tứ tán (distributed log) — mớ rác quăng vào sống dính ngắc mục ruỗng (persist indefinitely), tụi ăn bám tự khâu nhớ mốc nhà mình (consumers manage offsets), xé ra tua lộn lại như chơi, xả đạn vãi hàng mốc cỡ triệu chọt/giây (millions/s). Gã RabbitMQ mang kiếp cò mồi vác thư (message broker) — ọc cắn xong là chùi mép ném rác bốc hơi (deleted sau ACK), cò lái bù đầu phân chác bòng bong vạch đường (complex routing), dồn ép mớ rác. Bốc Kafka khi ôm ấp mò truy vết đầu nguồn sự kiện (event sourcing), soi vết sổ đỏ (audit trails), và văng miểng rải thảm tống họng (fan-out) tới mớ bầy ruồi muỗi consumers. Túm đầu RabbitMQ trị ba cái cống rãnh đùn việc rác (task queues), quăng đi đá lại xin chỏ (request-reply), và xơi cái mớ vạch đường chia rẽ loằng ngoằng (complex routing patterns).
Giở chiêu hãm phanh xả áp (backpressure) trong Go cái kiểu lầy gì?
Cầm lấy một cái ống khói bịt bọng đái rác (bounded buffered channel). Quả lồng ấp mọng no lết bánh văng (full), kẻ quăng rác tịt mồm (sender blocks) — đấy gọi là món xả áp bẩm sinh. Bế nhét kẹp vớ select { case jobChan <- msg: default: // múa búa xả áp backpressure handling } mớm đường né chọt nghẽn đâm chém (non-blocking sends) xài rành mạch lối rẽ dọn xả áp (e.g., khóa họng xơi rác Kafka consumption, đôn số đẩy gạch metrics counter).
Nhúng bùa épKafka cắn chặt lời nguyền Chốt Đơn Đét 1 Lần (Exactly-Once) sao đây?
Đú tới nái chơi nhức nách rặt dòng exactly-once tạt mớ ngoại đạo (external side effects - như bắn DB writes, quăng ném API calls) đòi phải túm mỏ 1 đứa ăn bám trơ mặt kháng đúp (idempotent consumer): chôn lấp cái mốc tọa độ Kafka offset kẹp chung mâm nồi cơm chó (business data) gộp lặn 1 cái hố chôn chung tử huyệt DB transaction. Nhỡ xui thằng lính hụt hơi ngỏm củ tỏi gọi hồn dựng dậy, cục rác cũ nhão nhẹt (duplicate message) lộ trần lòi mặt qua bùa săm soi vạch vú mốc nhà (offset check) rồi gõ đầu sút văng (safely skipped).
🔗 Bay Sang Bài Tới: Phần 6: Khóa Tứ Tán Nhốt Bọn Phá Bĩnh — Bùa Redlock, etcd & Dẹp Loạn Đạp Giẫm Đua Nhau (Race Condition Prevention) Trong Go — Phân tích lạch cạch độ lệch đồng hồ Redlock clock drift math, gõ búa dựng redsync implementation, và tới khi nào cút Redis mà bợ etcd.