🎮Bộ 2 · System Design ArenaTập 14 / 17

[SYSTEM DESIGN TẬP 14] ĐẠI CHIẾN QUEUE VS. WAL CDC, 'CHÂN LÝ TẦNG CUỐI' CỦA MỌI COMPONENT VÀ TUYỆT KỸ PEEK-THEN-ACK ($215.00 ★★★)! 📡🐘

Đại chiến Queue đứng trước DB vs. WAL CDC đứng sau DB, chân lý cấu trúc dữ liệu gốc và tuyệt kỹ Two-Phase Peek-then-Ack đạt 3 Sao $215.00
🎨 Đại chiến Queue đứng trước DB vs. WAL CDC đứng sau DB, chân lý cấu trúc dữ liệu gốc và tuyệt kỹ Two-Phase Peek-then-Ack đạt 3 Sao $215.00 🔍 Phóng to

Gió thu tháng 11 lùa qua khung cửa kính phòng lab, nhưng trên màn hình đấu trường System Design Arena, nhiệt độ đang nóng rẫy như một lò luyện thép.

Vừa bước chân vào Màn 23 (search_index_lag — Search vẫn hiện giá cũ) với ngân sách trần $250.00/tháng, thằng Đ đã hí hửng vỗ đùi đánh đét:

  • "Màn này dễ thở quá anh Kido ơi! Đề bài bảo chủ shop đổi giá 100 lần mỗi giây, tìm kiếm 300 req/s. Mình cứ cho App Server ghi giá mới vào Postgres DB, rồi bắn một tin nhắn vào Queue để Worker nhặt ra cập nhật sang Search Index. Kiến trúc kinh điển này em làm ở bao nhiêu dự án rồi, nhắm mắt cũng qua!"

Nói là làm, Đ thoăn thoắt kéo các hộp lên bàn cờ: 1 App Server ($15), 1 Database S ($15), 1 Queue ($10), 1 Worker ($15), và 1 Search Index ($90). Tổng chi phí chỉ $145.00. Đ tự tin ấn nút Simulate.

Nhưng chưa đầy 45 giây sau, một hồi chuông báo động đỏ rực xé toạc màn hình:

[SIMULATION DISASTER: MINUTE 01:00 - QUEUE MAINTENANCE]
Hàng đợi Queue bước vào cửa sổ bảo trì định kỳ 60 giây!
Database: Đã ghi nhận 6.000 lượt đổi giá mới thành công (Giá iPhone hạ từ 30M xuống 25M).
Queue: Từ chối nhận job (Connection Refused)! 6.000 sự kiện đồng bộ bị nuốt chửng!
Search Index: Hoàn toàn mù tịt, vẫn hiển thị giá cũ 30M!
Hậu quả: 18.000 lượt tìm kiếm trả về giá sai. Khách đặt hàng khiếu nại dữ dội. SLA thất bại 0%!

Thằng Đ há hốc mồm, mặt tái mét:

  • "Ủa... Queue bảo trì có 1 phút thôi mà toang cả hệ thống hả anh?! Chẳng lẽ trên đời này không có cách nào bắt Database và Search Index luôn nhìn thấy cùng một giá trị mà không bị chết chùm bởi Queue sao?!"

1. CẠM BẪY DUAL-WRITE VÀ ĐẠI CHIẾN TRIỆU ĐÔ: TẠI SAO CÓ CDC RỒI VẪN CẦN QUEUE? ⚔️💥

Tôi kéo ghế lại gần, chỉ cây bút vào sơ đồ của Đ:

  • *"Mày vừa dính phải Thảm họa Ghi Hai Nơi (Dual-Write Inconsistency Trap) — một trong những cái bẫy kinh điển nhất của kỹ sư phân tán! Trong thế giới thực, không có giao dịch phân tán nào (Distributed Transaction) chạy mượt mà giữa một con Database quan hệ và một con Queue ngoài mà không phải trả giá bằng 2-Phase Commit cực kỳ nặng nề. Khi mày viết lệnh:
await db.products.update({ where: { id }, data: { price } }); // Bước 1: Ghi DB thành công
await queue.send({ event: 'PRICE_UPDATED', id, price });       // Bước 2: Bắn Queue gãy cánh!

Nếu bước 2 gãy vì mạng lag, Queue bảo trì, hoặc App Server bị OOM kill ngay giữa 2 dòng code, thì Database và Search Index sẽ vĩnh viễn lệch pha nhau!"*

Đ gật gù, rồi mắt nó sáng lên khi nhìn thấy chiếc hộp CDC Stream ($80.00) và nút gạt CDC: On trên Database:

  • "Em hiểu rồi! Mình bật CDC: On trên Postgres, cắm cổng CDC sang CDC Stream rồi cho Worker sync sang Search Index! Postgres ghi vào sổ nhật ký Write-Ahead Log (WAL), đứt mạng hay bảo trì thì con trỏ LSN (Log Sequence Number) vẫn nằm yên đó, bật lại là đọc bù không mất một byte! Nhưng khoan đã anh Kido... Nếu Postgres WAL + CDC xịn xò đến mức đảm bảo dữ liệu ghi vào không bao giờ mất, thế tại sao trong các hệ thống lớn người ta không dẹp phắt luôn mấy con Queue như RabbitMQ, BullMQ hay Kafka đi cho đỡ tốn tiền? Sao không dùng CDC thay cho Queue luôn?!"

Tôi cười lớn:

  • "Đó là câu hỏi triệu đô mà ngay cả nhiều Senior Architect vẫn nhầm lẫn tai hại! Sự khác biệt sinh tử nằm ở VỊ TRÍ ĐỨNG VÀ BẢN CHẤT NHIỆM VỤ!"
     [NGƯỜI DÙNG BÃO TẢI] ──> [ 🛡️ QUEUE (ĐỨNG TRƯỚC DB) ] ──(Nhả 500 req/s)──> [ 🐘 POSTGRES DB ]
                                   (Hấp thụ xung lực Flash Sale)                     │
                                                                                (Ghi sổ WAL)
                                                                                     ▼
     [ 🔍 SEARCH INDEX ] <─── [ ⚙️ WORKER ] <─────────────────────────── [ 📡 CDC STREAM (ĐỨNG SAU DB) ]
                                (Nhân bản dữ liệu bất đồng bộ)

Tôi vạch ra 4 việc sống còn mà Queue làm được còn WAL CDC thì bó tay:

  1. Chặn lũ trước cửa (Shock Absorber): 50.000 người cùng bấm Mua Hàng trong 1 giây Flash Sale. Queue đứng TRƯỚC Database, hứng trọn 50.000 req vào RAM rồi nhỏ giọt 500 req/s xuống Postgres để DB không phát nổ. Còn CDC đứng ĐẰNG SAU Database! Muốn có WAL cho CDC đọc thì 50.000 req phải đâm xuyên qua DB trước — Postgres (trần 1.500 req/s) đã sập từ mili-giây đầu tiên trước khi kịp ghi một dòng log!
  2. Cơ chế Competing Consumers & DLX chống tắc nghẽn (Head-of-Line Blocking): Trong Queue, nếu Job #1 gửi mail bị bên thứ ba treo 30 giây, Queue tự động đẩy Job #1 vào Dead-Letter Queue (DLX) để 50 Worker khác tiếp tục bốc Job #2, #3, #4 chạy ầm ầm. Còn sổ WAL là một cuộn băng từ ghi tuần tự theo LSN. Nếu dòng LSN #1 bị lỗi, toàn bộ đường ống CDC phải dừng lại chờ, hoặc nếu bỏ qua thì mất vĩnh viễn vì WAL không có tính năng nhét ngược lại để 5 phút sau chạy lại!
  3. Hẹn giờ tương lai (Delayed Jobs): Queue có thể ra lệnh: "15 phút nữa nếu khách chưa thanh toán thì đánh thức Worker dậy hủy đơn". CDC chỉ là camera quay lén sổ WAL thời gian thực, không thể dặn cuốn sổ WAL "15 phút nữa hẵng báo"!
  4. Tác vụ tạm thời (Ephemeral Tasks): Gửi mã OTP SMS, nén video, gọi API OpenAI — xong việc là vứt. Chẳng ai rảnh nợ đi INSERT hàng triệu dòng OTP rác vào bảng Postgres chỉ để mượn CDC kích hoạt Worker!

Thằng Đ đập tay xuống bàn:

  • "Quá sáng tỏ! Queue là chiếc đập thủy điện đứng TRƯỚC để chia việc và chặn bão; còn CDC là tấm gương phản chiếu đứng SAU để nhân bản nguồn sự thật duy nhất!"

2. "CHÂN LÝ TẦNG CUỐI" CỦA MỌI COMPONENT: MAP VÀ ARRAY TÁCH KHỎI APP SERVER 🧠🌌

Thấy Đ đã thông suốt ranh giới, tôi hỏi tiếp một câu kiểm tra tư duy:

  • "Đ này, mày có bao giờ tự hỏi: Bản chất thực sự của Redis, RabbitMQ, hay Search Index là cái gì không? Dưới gầm bàn, chúng nó có phép thuật gì đặc biệt không?"

Đ ngập ngừng:

  • "Thì... chúng nó là những phần mềm mã nguồn mở được viết bằng C/C++ hay Rust tối ưu cho lưu trữ phân tán..."

  • *"Đúng, nhưng chưa chạm tới tầng nguyên lý gốc (First Principles)! Sự thật trần trụi: Mọi Component lấp lánh trên đám mây thực chất chỉ là 1 con App chuyên dụng bọc quanh 1 Cấu Trúc Dữ Liệu cơ bản mà mày hoàn toàn có thể tự viết bằng vài chục dòng code ngay trong RAM của App Server:

  • Cache (Redis): Thực chất là một biến new Map<string, any>() kèm thuật toán loại bỏ phần tử cũ nhất LRU!

  • Queue (RabbitMQ): Thực chất là một mảng const queue = [] với thao tác push() ở đuôi và shift() ở đầu!

  • Rate Limiter: Một cái Map<UserId, { tokens, lastRefill }> cộng trừ số nguyên!

  • Hot Counter: Một cái Map<PostId, number> cộng dồn rồi xả định kỳ!

  • Search Index: Một từ điển ngược Map<Từ_khóa, ProductId[]> (Inverted Index)!

  • Thậm chí cả Database: Nhúng thuật toán B-Tree vào App Server thì chính là SQLite!"*

Đ ngơ ngác:

  • "Ủa, nếu chỉ là Map với Array, và đọc trong RAM App Server nhanh gấp 1.000 lần gọi qua mạng sang Redis (0.0001ms vs 1ms), thì tại sao người ta không nhúng hết vào App Server cho tiết kiệm tiền?!"

  • *"Bởi vì khi hệ thống lớn lên và mày phải nhân bản từ 2 máy App Server trở lên, mày sẽ đâm sầm vào 3 BỨC TƯỜNG VẬT LÝ không thể vượt qua:

  1. Bức tường 1 — Split-Brain State (Ông nói gà bà nói vịt): App #1 và App #2 nằm ở 2 server vật lý, RAM tách biệt hoàn toàn. User đổi giá ở App #1, giây sau F5 trúng App #2 thì App #2 vẫn trả về giá cũ trong RAM của nó! Muốn 10 máy App nhìn thấy cùng một trạng thái, bắt buộc phải bưng cái Map đó ra một hộp độc lập đứng giữa.
  2. Bức tường 2 — Deploy Restart Wipeout (Mất trí nhớ sau mỗi lần deploy): App Server là nơi mày sửa code và git push liên tục mỗi ngày. Mỗi lần deploy, container bị kill và restart. Nếu Queue hay Cache nằm trong RAM App Server, 500 đơn hàng đang chờ xử lý sẽ bốc hơi sạch sành sanh! Tách ra hộp riêng thì mày restart App 100 lần, hàng đợi vẫn nằm im bất tử.
  3. Bức tường 3 — Noisy Neighbor & OOM Crash (Chết chùm vì ngốn tài nguyên): Nếu nhúng Search Index quét chữ hoặc Worker nén video vào App Server, chỉ cần một truy vấn nặng ngốn 100% CPU hoặc làm tràn RAM (OOM), toàn bộ API thanh toán của khách hàng trên cùng tiến trình đó sẽ lăn ra chết theo!"*

Thằng Đ tròn xoe mắt:

  • "Em hiểu rồi... Tách Component ra ngoài không phải vì sang chảnh, mà là để mua sự Cô lập lỗi (Fault Isolation) và Chia sẻ trạng thái (Shared State) khi scale ngang!"

3. TUYỆT KỸ TWO-PHASE PEEK-THEN-ACK DƯỚI 15MB RAM TRONG @KIDO/ASYNC-SYNC 🐘⚡

Tôi mở màn hình code của bộ thư viện core:

  • "Chính vì ngộ ra chân lý đó, anh em mình vừa đổi tên @kido/offline-sync thành @kido/async-sync — bởi vì nó không chỉ giải quyết offline sync ở client, mà còn gánh toàn bộ cỗ máy Asynchronous WAL Replication, Partition Archival và Logical CDC Streaming ở phía server. Và trong nhánh src/pg-cdc-stream/, anh đã thiết kế ra Tuyệt kỹ Two-Phase Peek-then-Ack thay thế hoàn toàn cỗ xe tăng Debezium + Kafka cồng kềnh ngốn hàng GB RAM của các dự án lớn!"
// Giai đoạn 1: PEEK (Xem trước dữ liệu, KHÔNG xác nhận LSN để tránh mất mát)
const peekSql = `
  SELECT lsn, xid, data 
  FROM pg_logical_slot_peek_changes('search_cdc_slot', NULL, 100, 'format-version', '2');
`;
const changes = await db.query(peekSql);

// Xử lý: LSN Burst Coalescer (Gộp 10 lần đổi giá của cùng 1 sản phẩm thành 1 lần duy nhất)
const coalescedBatch = coalesceCdcBatch(changes);

// Bắn đồng bộ sang Search Index
const success = await searchIndex.bulkUpsert(coalescedBatch);

// Giai đoạn 2: ACK (Chỉ xác nhận và đẩy con trỏ LSN khi Search Index xác nhận 200 OK!)
if (success) {
  await db.query(`
    SELECT pg_logical_slot_get_changes('search_cdc_slot', $1, NULL);
  `, [highestLsn]);
}

Tôi giải thích cặn kẽ 3 vũ khí tối thượng bên trong lát cắt này:

  1. Hai pha Peek-then-Ack an toàn tuyệt đối: Khác với việc gọi pg_logical_slot_get_changes ngay lập tức (vừa đọc là Postgres xóa luôn khỏi slot, nếu worker sập giữa chừng thì mất dữ liệu), cỗ máy dùng peek_changes để kéo lô dữ liệu về xử lý. Chỉ khi Search Index đã ghi nhận thành công, lệnh get_changes mới được kích hoạt để tiến con trỏ LSN.
  2. LSN Burst Coalescer & Watermark Guard: Khi chủ shop sửa giá 1 sản phẩm 10 lần trong 1 giây (c + u -> c, u + d -> d, c + d -> no-op), bộ lọc gom tất cả thành 1 lệnh duy nhất ở LSN cao nhất, triệt tiêu 90% tải ghi rác vào Search Index.
  3. WAL Disk-Full Circuit Breaker: Một replication slot nếu bị bỏ rơi (ví dụ Search Index chết 1 ngày) sẽ ngăn Postgres xóa các file WAL trong thư mục pg_wal, khiến ổ cứng đầy 100% và làm sập toàn bộ Database. Hàm evaluateCdcSlotDiskSafety liên tục đo khoảng cách byte giữa vị trí hiện tại và slot LSN; nếu vượt ngưỡng an toàn ($>10\text{GB}$), cỗ máy phát cảnh báo khẩn cấp hoặc tự động drop slot để bảo toàn tính mạng cho Database chính!

4. MÀN CHỐT HẠ KỶ LỤC 3 SAO $215.00 VÀ CÚ CLICK ĐỊNH MỆNH 🏆✨

Thằng Đ quay lại bàn cờ Màn 23 với một tâm thế hoàn toàn mới. Mọi nỗi mơ hồ đã biến mất, nhường chỗ cho sự sắc bén của một kỹ sư nắm vững nguyên lý gốc.

Nó lập tức dọn dẹp lại bàn cờ theo sơ đồ chuẩn không cần Load Balancer:

  • Relational DB S: $15.00/tháng, bật cờ CDC: On. Toàn bộ thay đổi giá ghi nhận thẳng vào WAL.
  • CDC Stream: $80.00/tháng, cắm trực tiếp vào cổng cdc của DB.
  • Worker S: $15.00/tháng, đón dòng CDC stream và đẩy sang Search Index.
  • Search Index S: $90.00/tháng, nhận các mẻ cập nhật và phục vụ truy vấn tìm kiếm 300 req/s.
  • App Server S: $15.00/tháng, gánh trọn vẹn $100\text{ req/s ghi} + 300\text{ req/s tìm kiếm} = 400\text{ req/s}$ (dưới trần 500 req/s, không cần tốn thêm $20 cho Load Balancer!).
┌────────────────────────────────────────────────────────────────────────┐
│               BÀN CỜ ĐỒNG BỘ CDC 3 SAO MÀN 23 ($215.00/THÁNG)          │
├────────────────────────────────────────────────────────────────────────┤
│ Users ──(100 đổi giá / 300 tìm kiếm)──> App Server S ($15)             │
│                                           │             │              │
│                 ┌─────────────────────────┘             │              │
│                 ▼ (sql ghi)                             ▼ (search đọc) │
│       Relational DB S ($15, CDC: On)               Search Index ($90)  │
│                 │ (cdc stream WAL)                      ▲              │
│                 ▼                                       │ (search ghi) │
│          CDC Stream ($80) ──(cdc)──> Worker S ($15) ────┘              │
└────────────────────────────────────────────────────────────────────────┘

Thanh chi phí dự toán hiện lên một con số hoàn hảo: $215.00/tháng (trần $250.00).

Đ hít một hơi thật sâu rồi bấm nút SIMULATE!

Các thanh đo lưu lượng bắt đầu nhảy múa:

  • 0s – 60s: 100 lượt đổi giá và 300 lượt tìm kiếm đổ vào liên tục. Latency đổi giá: 24 ms, latency tìm kiếm: 18 ms.
  • 60s – 120s: KỊCH BẢN THỬ THÁCH BẮT ĐẦU! Mạng nội bộ ngắt kết nối Worker trong 30 giây mô phỏng sự cố! Database vẫn êm đềm nhận lệnh ghi giá. Sổ WAL tích lũy các bản ghi LSN an toàn trong đĩa cứng.
  • 120s: Worker kết nối lại! Dòng chảy CDC Stream bùng nổ, Worker kéo liền tay các lô dữ liệu bằng Two-Phase Peek-then-Ack, bù đắp sạch trơn toàn bộ khoảng cách dữ liệu trong chưa đầy 4,2 giây! Search Index lập tức phản ánh đúng 100% giá mới!
  • 240s: KẾT THÚC THỜI GIAN MÔ PHỎNG!

Màn hình bùng nổ trong pháo hoa vàng rực:

╔════════════════════════════════════════════════════════════════════════╗
║             SYSTEM DESIGN ARENA - LEVEL 23: SEARCH INDEX LAG           ║
║                           KẾT QUẢ CUỐI CÙNG                            ║
╠════════════════════════════════════════════════════════════════════════╣
║  • Tỷ lệ phản hồi Đổi giá sản phẩm        : 100.0% (Latency: 24 ms)    ║
║  • Tỷ lệ phản hồi Tìm kiếm                : 100.0% (Latency: 18 ms)    ║
║  • Tuổi thọ job cũ nhất chờ cập nhật      : 4.2 giây (Yêu cầu: < 120s) ║
║  • Tỷ lệ hoàn thành Job cập nhật index    : 100.0%                     ║
║                                                                        ║
║  ★ CHI PHÍ THỰC TẾ: $215.00 / $250.00                                  ║
║  ★ XẾP HẠNG       : TOP 1% TOÀN CẦU (★★★ 3 SAO KỶ LỤC BEST COST!)      ║
╚════════════════════════════════════════════════════════════════════════╝

Thằng Đ reo vang, hai mắt long lanh:

  • "Thắng rồi anh ơi! Đúng $215.00 không lệch một xu! Không có giọt rớt dữ liệu nào, kể cả lúc đứt mạng!"

Tôi mỉm cười đóng nắp laptop. Trong thế giới phân tán đầy cạm bẫy, việc hiểu sâu sắc nguồn sự thật duy nhất của cuốn sổ WAL, bản chất của cấu trúc dữ liệu nền tảng, và ranh giới kiến trúc giữa Queue với CDC chính là tấm khiên vững chắc nhất bảo vệ hệ thống trước mọi cơn bão giông.

🔬
Kido Engineering Blog • Deep-Dive Architecture

Giải Phẫu Kỹ Thuật & Kiến Trúc Thực Chiến Của Tập 214

blog.khanhdp.com ↗

Muốn xem chi tiết mã nguồn, bảng so sánh kiến trúc Sơ khởi ➔ Điểm nghẽn khi Scale ➔ Chuẩn Big Tech Production và phân tích kỹ thuật đằng sau câu chuyện ở tập này? Đọc ngay bài viết chuyên sâu: