Hệ thống Dữ liệu Phân tán/Request routing — tìm đúng node giữ dữ liệu
18/38
Bài 18 / 38~11 phútPhân mảnh (partitioning)Miễn phí lượt xem

Request routing — tìm đúng node giữ dữ liệu

Chia dữ liệu ra nhiều node xong, client làm sao biết hỏi node nào cho một khoá? Ba cách route, dịch vụ coordination, và giữ mapping đúng khi partition di chuyển.

TL;DR: Chia dữ liệu ra nhiều node xong, câu hỏi cuối là định tuyến: client muốn khoá K, làm sao biết K nằm ở node nào — nhất là khi partition di chuyển lúc rebalancing? Có ba cách: (1) client tự biết mapping, gọi thẳng node đúng; (2) một routing tier (proxy) đứng trước, chuyển tiếp; (3) client gọi node bất kỳ, node đó tự chuyển tiếp. Cả ba đều cần một nguồn sự thật về mapping partition→node luôn cập nhật khi cụm co giãn — thường do dịch vụ coordination (ZooKeeper/etcd) giữ. Điểm khó không phải "route thế nào" mà "ai biết mapping mới nhất" và đồng bộ nó ra sao khi cụm co giãn.

Đây là mảnh cuối của partitioning: sau khi quyết chia thế nào (bài 1-2) và index thế nào (bài 3), phải trả lời tìm lại ở đâu. Và câu trả lời kéo thẳng vào một chủ đề lớn của track — làm sao nhiều thành phần thống nhất một thông tin (mapping) đang thay đổi.

Nhớ lại Service & kube-proxy

Bạn đã thấy Kubernetes giải đúng bài toán này ở tầng mạng: Service cho một tên ổn định, kube-proxy định tuyến tới pod đang sống dù pod đổi IP. Request routing ở đây là cùng ý tưởng cho dữ liệu: một lớp gián tiếp che việc "partition nào ở node nào" khỏi client, và tự cập nhật khi mapping đổi.

1. Analogy — gọi tổng đài, hỏi trực tiếp, hay được chuyển máy

Bạn cần gặp đúng người phụ trách hồ sơ của mình trong một toà nhà lớn, mà phòng ban hay đổi tầng:

  • Bạn tự có sơ đồ tầng mới nhất (client tự biết): đi thẳng đúng phòng. Nhanh, nhưng bạn phải luôn có bản sơ đồ cập nhật — tầng đổi mà bạn không biết là đi nhầm.
  • Có quầy lễ tân (routing tier): bạn hỏi lễ tân, họ chỉ/đưa tới đúng phòng. Bạn không cần biết sơ đồ; chỉ lễ tân cần cập nhật.
  • Hỏi bất kỳ ai gặp, họ chuyển tiếp (node tự forward): gặp ai cũng được, họ biết đường và dẫn bạn tới đúng phòng. Tiện cho bạn, nhưng mỗi nhân viên phải biết sơ đồ.

Điểm chung: dù cách nào, phải có một sơ đồ tầng đúng và ai đó giữ nó cập nhật khi phòng ban chuyển tầng. Đó mới là phần khó.

Toà nhàRequest routing
Sơ đồ tầngMapping partition → node
Tự có sơ đồ, đi thẳngClient tự biết mapping
Hỏi lễ tânRouting tier / proxy
Hỏi bất kỳ ai, được chuyểnNode bất kỳ tự forward
Phòng ban đổi tầngPartition di chuyển (rebalancing)
Ai giữ sơ đồ cập nhậtDịch vụ coordination

2. Ba cách định tuyến

flowchart TB
  subgraph A["(1) Client tu biet"]
    CA["Client (co mapping)"] --> NA["Node dung"]
  end
  subgraph B["(2) Routing tier"]
    CB["Client"] --> P["Proxy/router"] --> NB["Node dung"]
  end
  subgraph C["(3) Node tu forward"]
    CC["Client"] --> NX["Node bat ky"] --> NC["Node dung"]
  end

Ba cách khác nhau ở ai giữ bản sao mappingai lo cập nhật nó khi partition di chuyển:

  • (1) Client tự biết mapping. Mỗi client giữ (hoặc cache) bảng partition→node, tự tính partition của khoá rồi gọi thẳng node đúng — ít hop nhất, chỉ một chặng mạng tới node. Cái giá: mapping bị sao thành N bản (mỗi client một bản), nên khi nó đổi phải có đường đẩy cập nhật tới mọi client; sót một nhóm client là nhóm đó ôm bản cũ, gọi nhầm node. Toàn bộ độ phức tạp "biết mapping mới nhất" dồn về phía client — bên bạn ít kiểm soát nhất.
  • (2) Routing tier (proxy). Một lớp proxy đứng trước nhận mọi request, tra mapping của riêng nó rồi chuyển tiếp tới node đúng. Client chỉ cần biết một địa chỉ (địa chỉ tier); mapping gom về một chỗ, chỉ tier phải cập nhật khi cụm co giãn. Cái giá: thêm một hop (client tới tier rồi tier tới node), và tier thành thành phần sống-còn phải scale + HA — tier chết là cả cụm mất lối vào. Thực tế chạy nhiều bản tier sau một load balancer, và các bản đó lại phải chia sẻ cùng một mapping.
  • (3) Node tự forward. Client gọi bất kỳ node; nếu node đó không giữ khoá, nó chuyển tiếp nội bộ tới node đúng rồi trả kết quả về. Client cực đơn giản (gọi ai cũng được, không cần biết mapping). Cái giá: mọi node phải biết mapping toàn cục — thường qua gossip (mỗi node định kỳ trao đổi hiểu biết "ai giữ khoảng khoá nào" với vài node lân cận, dần dần cả cụm hội tụ về cùng bức tranh) — và một request gọi lệch node phải chịu thêm một hop mạng nội bộ.
CáchMapping nằm ởAi cập nhật khi đổiSố hopĐiểm yếu chính
Client tự biếtmọi client (cache)phải đẩy tới mọi client1 (ít nhất)N bản dễ lệch nhau
Routing tiertập trung ở tierchỉ tier2 (qua proxy)tier phải HA + scale
Node tự forwardmọi node (gossip)gossip lan dần1–2 (có thể lệch)gossip hội tụ có độ trễ

Mấu chốt: cả ba chỉ dời chỗ đặt độ phức tạp (client / một tier / mọi node) và đổi số hop, chứ không xoá được câu hỏi "ai giữ mapping mới nhất" — mục sau trả lời trực tiếp câu hỏi đó.

3. Ai giữ mapping — và vì sao đó là phần khó?

Cả ba cách đều dựa trên một mapping partition→node đúng. Khi cụm ổn định, mapping tĩnh — dễ. Cái khó là lúc rebalancing: partition di chuyển, và giờ nhiều thành phần (mọi client, hoặc mọi node, hoặc routing tier) phải cùng thấy thay đổi đó, nếu không request đi tới node cũ (không còn giữ khoá) → lỗi hoặc chuyển tiếp thừa.

Đây là một bài toán thống nhất một thông tin đang thay đổi giữa nhiều node — và nó xuất hiện khắp hệ phân tán. Giải pháp phổ biến: một dịch vụ coordination chuyên trách (ZooKeeper, etcd) giữ mapping như nguồn sự thật; các thành phần đăng ký theo dõi và được thông báo khi mapping đổi. Bản thân dịch vụ đó phải nhất quán mạnh (không được cho hai câu trả lời khác nhau về "ai giữ partition P") — nên nó dùng consensus bên trong.

Nói cách khác: routing đẩy độ khó về đúng câu hỏi trung tâm của cả track — làm sao nhiều node đồng ý một sự thật — mà Module 6 (consensus) sẽ mổ. Request routing là một ứng dụng cụ thể đầu tiên của consensus mà bạn gặp.

4. Pitfall — mỗi thành phần tự đoán mapping, không có nguồn sự thật

Pitfall — cache mapping mà không có cơ chế cập nhật khi nó đổi

Cách hỏng phổ biến: client (hoặc node) cache bảng partition→node để đi thẳng cho nhanh, nhưng không có đường nhận biết khi mapping đổi.

❌ Client cache mapping luc khoi dong -> rebalancing chuyen partition P sang node moi
   -> client van goi node cu -> node cu khong con giu P -> loi / tra sai
   -> moi client cache mot phien ban khac nhau -> hanh vi loan

❌ Khong co nguon su that -> moi node tu "doan" ai giu gi -> khong dong bo

Hệ quả: sau mỗi lần co giãn, một phần request đi nhầm chỗ cho tới khi cache hết hạn — lỗi lúc-có-lúc-không, khó tái hiện.

Hướng đúng: luôn có một nguồn sự thật cho mapping (dịch vụ coordination nhất quán mạnh), và cơ chế thông báo/theo dõi để các thành phần cập nhật khi mapping đổi (watch/notify, hoặc phiên bản mapping kèm mỗi response để phát hiện cache cũ). Cache để nhanh thì được, nhưng phải có đường làm mới khi nguồn sự thật đổi — đừng để cache thành sự thật.

5. Tự thiết kế routing — đến lượt bạn

Bạn đã có ba cách route + hiểu phần khó nằm ở giữ mapping đồng bộ. Giờ tự thiết kế cho một tình huống cụ thể: đọc kỹ rồi tự quyết (a) chọn cách route nào, (b) đặt nguồn sự thật của mapping ở đâu, (c) biện minh — viết ra (trong đầu hoặc ra giấy) trước khi mở đáp án.

Kho key-value cho một game mobile. Khoảng 8 triệu client là app chạy trên điện thoại người dùng, mạng chập chờn, và bạn không kiểm soát được vòng đời chúng (không thể đẩy cập nhật tức thì tới mọi máy — người dùng cập nhật app khi nào tuỳ họ). Cụm 30 node giữ trạng thái người chơi và co giãn mạnh quanh giờ cao điểm/sự kiện — thêm/bớt node vài lần mỗi ngày, nên partition di chuyển thường xuyên. Yêu cầu: đọc/ghi độ trễ thấp, và không để client gọi nhầm node sau mỗi lần co giãn.

Tự kiểm tra
Q1
Với kho key-value game mobile trên: chọn cách route nào trong ba cách, đặt nguồn sự thật của mapping partition→node ở đâu, và vì sao? Biện minh dựa trên đặc điểm client (8 triệu app mobile không đẩy cập nhật được) và tần suất rebalancing.

Loại ngay cách (1) client tự biết: 8 triệu client mobile trên mạng chập chờn, không đẩy cập nhật tức thì được → không thể giữ 8 triệu bản mapping cùng tươi. Rebalancing vài lần mỗi ngày sẽ để lại một biển client ôm mapping cũ, gọi nhầm node — đúng anti-pattern ở mục 4.

Chọn routing tier (2): để client không phải giữ mapping — client mobile chỉ gọi một địa chỉ ổn định (tier sau load balancer), việc theo dõi mapping dồn về phía server nơi bạn kiểm soát được. Ở đây tier hợp hơn cách (3) node tự forward vì client là bên ngoài không đáng tin và mỗi hop mạng mobile đều đắt: đẩy một hop nội bộ server (tier tới node) rẻ hơn nhiều so với để client mobile gọi lệch rồi bị forward.

Nguồn sự thật của mapping: một dịch vụ coordination nhất quán mạnh (ZooKeeper/etcd) giữ mapping partition→node. Tier watch dịch vụ này để được thông báo và nạp lại mapping ngay khi rebalancing đổi partition. Vì partition di chuyển thường xuyên, cơ chế watch/notify (thay vì để cache tự hết hạn theo thời gian) mới đủ nhanh để tier không route nhầm giữa hai lần co giãn.

Vì sao phải nhất quán mạnh: nếu hai bản tier đọc mapping và nhận hai câu trả lời khác nhau về "ai giữ partition P", cùng một khoá sẽ đi hai node khác nhau → đọc/ghi loạn. Nguồn sự thật phải cho một câu trả lời duy nhất kể cả khi có lỗi mạng — nên bên trong nó dùng consensus.

6. 📚 Deep Dive

📚 Deep Dive — Request routing
  • Designing Data-Intensive Applications (Kleppmann) — Chương 6, mục "Request Routing" — ba cách route, vai trò của dịch vụ coordination (ZooKeeper) giữ mapping và thông báo thay đổi.
  • Nhiều hệ dùng ZooKeeper/etcd làm nguồn mapping (và cả membership); một số (kiểu Cassandra) dùng gossip để mọi node cùng biết ai giữ khoảng khoá nào, cho phép cách (3) node tự forward mà không cần tier riêng.

Ghi chú: DDIA Ch6 nhấn: routing không khó ở "gửi đi đâu" mà ở "giữ mapping đồng bộ khi nó đổi" — chính là consensus.

7. Liên hệ các bài khác

8. Tóm tắt

  • Request routing: client cần khoá K, làm sao tới node giữ K — khó vì mapping partition→node đổi khi rebalancing.
  • Ba cách: client tự biết (ít hop, client phức tạp) · routing tier (client đơn giản, thêm hop) · node tự forward (client đơn giản, mọi node phải biết mapping).
  • Cả ba cần một nguồn sự thật về mapping luôn cập nhật — thường do dịch vụ coordination (ZooKeeper/etcd) giữ và thông báo thay đổi.
  • Phần khó thật sự = thống nhất mapping đang đổi giữa nhiều node → chính là consensus (Module 6).
  • Anti-pattern: cache mapping mà không có đường làm mới khi nó đổi → request đi nhầm node sau mỗi lần co giãn.

9. Tự kiểm tra

Tự kiểm tra
Q1
Nêu ba cách request routing và điểm đánh đổi của mỗi cách (độ phức tạp đặt ở đâu, số hop).
  • Client tự biết mapping: client tính partition rồi gọi thẳng node đúng — ít hop nhất, nhưng độ phức tạp (giữ + cập nhật mapping) dồn vào mọi client.
  • Routing tier (proxy): client chỉ gọi một địa chỉ, proxy tra mapping và chuyển tiếp — client đơn giản, mapping tập trung; đổi lại thêm một hop và tier phải scale/HA.
  • Node tự forward: client gọi node bất kỳ, node không giữ khoá thì chuyển tiếp — client rất đơn giản, nhưng mọi node phải biết mapping và có thể thêm hop mạng nội bộ.

Chọn theo muốn đặt độ phức tạp ở đâu (client / một tier / mọi node) và ngân sách hop.

Q2
Vì sao 'phần khó' của routing không phải cách gửi mà là giữ mapping đồng bộ? Điều gì xảy ra lúc rebalancing?

Khi cụm ổn định, mapping tĩnh — cả ba cách gửi đều dễ. Cái khó xuất hiện lúc rebalancing: partition di chuyển sang node mới, nên nhiều thành phần (mọi client, hoặc mọi node, hoặc routing tier) phải cùng thấy mapping mới.

Nếu không đồng bộ: request đi tới node cũ (không còn giữ khoá) → lỗi, hoặc phải chuyển tiếp thừa; và nếu mỗi thành phần có phiên bản mapping khác nhau → hành vi loạn. Đây là bài toán thống nhất một thông tin đang thay đổi giữa nhiều node — khó đúng như mọi bài toán đồng thuận khác của track.

Q3
Vai trò của một dịch vụ coordination (ZooKeeper/etcd) trong routing là gì, và vì sao nó phải nhất quán mạnh?

Nó giữ nguồn sự thật về mapping partition→node: các thành phần đăng ký theo dõi và được thông báo khi mapping đổi, nên tất cả cùng cập nhật sau rebalancing thay vì mỗi nơi tự đoán.

Phải nhất quán mạnh vì nó không được cho hai câu trả lời khác nhau về "ai giữ partition P" tại cùng thời điểm — nếu hai client hỏi và nhận hai node khác nhau, request sẽ đi lung tung và có thể ghi vào hai chỗ. Để đảm bảo một câu trả lời duy nhất dù có lỗi/partition mạng, dịch vụ đó dùng consensus bên trong.

Q4
Một hệ cache bảng mapping ở client để đi thẳng cho nhanh. Rủi ro gì khi rebalancing, và thiết kế đúng là gì?

Rủi ro: sau rebalancing, partition P chuyển sang node mới, nhưng client vẫn dùng mapping cache cũ → gọi node cũ (không còn giữ P) → lỗi hoặc trả sai, cho tới khi cache hết hạn. Mỗi client cache một phiên bản khác nhau → lỗi lúc-có-lúc-không, khó tái hiện.

Thiết kế đúng: cache để nhanh được, nhưng phải có đường làm mới khi nguồn sự thật đổi — ví dụ theo dõi (watch) dịch vụ coordination để nhận thông báo, hoặc gắn phiên bản mapping vào mỗi response để node phát hiện client dùng mapping cũ và bảo nó nạp lại. Đừng để cache trở thành sự thật.

Q5
Request routing được gọi là 'ứng dụng đầu tiên của consensus' trong track. Giải thích mối liên hệ đó.

Routing cần một mapping partition→node mà mọi thành phần đồng ý, kể cả khi nó thay đổi và kể cả khi có lỗi/partition mạng. "Nhiều node thống nhất một giá trị/thông tin dù có lỗi" chính là định nghĩa của consensus.

Nên dù bài routing nằm ở Module 3, lời giải rốt ráo của nó (dịch vụ coordination nhất quán mạnh) lại là consensus — chủ đề Module 6. Đây là ví dụ cụ thể cho thấy vì sao consensus là "hạ tầng" mà nhiều tính năng phân tán (routing, bầu leader, khoá) dựa vào; các bài sau sẽ tháo chính cơ chế đó ra.

Bài tiếp theo: Module 3 — Tổng kết & cheat sheet

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên