Hệ thống Dữ liệu Phân tán/Secondary index phân mảnh — local (document) vs global (term)
17/38
Bài 17 / 38~12 phútPhân mảnh (partitioning)Miễn phí lượt xem

Secondary index phân mảnh — local (document) vs global (term)

Index phụ khó phân mảnh vì nó cắt ngang cách chia dữ liệu chính. Hai cách: local (document-partitioned) rẻ ghi đắt đọc, global (term-partitioned) ngược lại.

TL;DR: Secondary index (tra theo trường khác ngoài khoá chính, ví dụ tìm mọi xe màu đỏ) khó phân mảnh vì trường phụ cắt ngang cách chia dữ liệu theo khoá chính. Có hai cách trái ngược: local index (document-partitioned) đẩy chi phí sang đọc — mỗi partition tự index dữ liệu của mình nên ghi rẻ, nhưng một truy vấn phải hỏi mọi partition rồi gộp (scatter/gather). Global index (term-partitioned) đẩy chi phí sang ghi — index phân mảnh theo giá trị trường phụ nên đọc chỉ chạm một partition, nhưng một document phải cập nhật nhiều partition index, thường bất đồng bộ.

Hai bài trước chia dữ liệu theo khoá chính và lo co giãn. Nhưng "tìm theo khoá chính" chỉ là một nửa nhu cầu; nửa kia là "tìm theo thuộc tính" — và đó là lúc secondary index vào cuộc, mang theo một đánh đổi mới đặc trưng cho hệ phân tán.

Nhớ lại Inverted index

Bạn đã học inverted index: ánh xạ từ một term (giá trị) tới danh sách document chứa nó — nền của tìm kiếm theo nội dung. Secondary index phân mảnh chính là câu hỏi "đặt inverted index đó ở đâu khi document rải trên nhiều node": cùng chỗ với document (local) hay gom theo term (global)?

1. Analogy — thư viện: mỗi phòng tự làm mục lục vs một mục lục chung

Sách chia ra nhiều phòng theo mã sách (khoá chính). Độc giả muốn "mọi sách chủ đề lịch sử" — một thuộc tính khác. Hai cách làm mục lục theo chủ đề:

  • Mỗi phòng tự làm mục lục của phòng mình (local): phòng nào cũng có sổ "sách lịch sử trong phòng này". Thêm một cuốn vào phòng → chỉ sửa mục lục phòng đó (rẻ). Nhưng độc giả hỏi "mọi sách lịch sử toàn thư viện" → phải đi hỏi tất cả các phòng rồi gộp.
  • Một mục lục chung, chia theo chủ đề (global): có "quầy Lịch sử" giữ danh sách mọi sách lịch sử ở mọi phòng. Độc giả hỏi → tới đúng quầy Lịch sử, xong (rẻ). Nhưng thêm một cuốn có nhiều chủ đề → phải chạy cập nhật nhiều quầy (quầy Lịch sử, quầy Chiến tranh...), và quầy có thể ở phòng khác.
Thư việnSecondary index
Mỗi phòng tự làm mục lụcLocal (document-partitioned) index
Thêm sách chỉ sửa mục lục phòng đóGhi rẻ (1 partition)
Hỏi "mọi sách LS" = đi mọi phòngĐọc = scatter/gather
Quầy chung theo chủ đềGlobal (term-partitioned) index
Hỏi tới đúng quầy, xongĐọc rẻ (1 partition/term)
Thêm sách cập nhật nhiều quầyGhi đắt (nhiều partition, async)
💡 Cách nhớ

Local = mục lục theo phòng (thêm rẻ, tìm phải đi khắp). Global = quầy theo chủ đề (tìm nhanh, thêm phải cập nhật nhiều quầy). Đọc-nhiều → global; ghi-nhiều → local.

Ba mui ten toa ra moi partition so voi mot mui ten trung dich

2. Local index — vì sao ghi rẻ mà đọc phải scatter?

Mỗi partition giữ secondary index chỉ trên các document của chính partition đó. Index "đồng vị" (co-located) với dữ liệu.

Ghi rẻ. Thêm/sửa một document chỉ đụng partition chứa nó: cập nhật dữ liệu + cập nhật index cục bộ, tất cả trong một partition. Không cần nói chuyện với partition khác.

Thử đoán trước

Local index: mỗi partition chỉ index document của chính nó, mà dữ liệu chia theo khoá chính (car_id), không theo màu. Vậy nếu cụm có 20 partition thay vì 3, một truy vấn "mọi xe đỏ" phải hỏi bao nhiêu trong số 20 partition đó — lần theo lý do "màu không liên quan gì tới cách chia theo car_id" để tự suy ra con số, đừng đoán chung chung "nhiều hay ít".

Đọc theo trường phụ đắt. Truy vấn "mọi xe màu đỏ" — xe đỏ rải khắp mọi partition (vì partition chia theo khoá chính, không theo màu). Nên phải gửi truy vấn tới mọi partition, mỗi partition tra index cục bộ của nó, rồi gộp kết quả (scatter/gather). Chậm và chịu tail latency (chờ partition chậm nhất).

Local index (partition theo car_id):
  P1: index color: {red: [c3, c8], blue: [c1]}
  P2: index color: {red: [c11], green: [c5]}
  P3: index color: {red: [c20, c22]}
  Query "red" -> hoi CA P1,P2,P3 -> gop [c3,c8,c11,c20,c22]

Đây là cách phổ biến nhất (Elasticsearch, nhiều store) vì ghi cục bộ đơn giản và bền.

3. Global index — phân mảnh theo term (đọc rẻ, ghi đắt)

Index được coi như dữ liệu riêng và phân mảnh theo giá trị trường phụ (term), độc lập với cách chia dữ liệu chính. Ví dụ: mọi entry index cho color=red nằm trên một partition (chọn theo hash(red)), color=blue trên partition khác.

Thử đoán trước

Với global index phân theo term, truy vấn "mọi xe đỏ" chỉ chạm một partition (đọc rẻ). Bạn thêm một xe mới có 3 trường được index (color=red, brand=toyota, city=hanoi). Đoán con số chính xác: nó phải cập nhật bao nhiêu partition của index (gợi ý: đếm theo số trường được index, không phải trả lời chung chung "nhiều"), và lần theo việc các partition đó nằm ở node khác nhau để đoán vì sao cập nhật thường KHÔNG thể gói gọn trong một transaction đồng bộ.

Đọc rẻ. Truy vấn "mọi xe đỏ" tới đúng partition giữ term red — một chỗ, không scatter. Range query trên term cũng có thể nhanh nếu index sắp theo term.

Ghi đắt và phức tạp. Một document có nhiều trường được index (color=red, brand=toyota), và các term đó nằm trên các partition khác nhau. Nên một lần ghi document phải cập nhật nhiều partition index — thường ở các node khác với node chứa document. Vì cập nhật đồng bộ nhiều partition là 2PC đắt (Module 4), thực tế thường cập nhật bất đồng bộ: index có thể trễ so với dữ liệu (đọc index ngay sau ghi có thể chưa thấy) — một dạng replication lag (Module 2) cho index.

Global index (partition theo term):
  P_red:    red -> [c3, c8, c11, c20, c22]   (moi xe do o day)
  P_blue:   blue -> [c1]
  P_toyota: toyota -> [c8, c22]
  Query "red" -> chi hoi P_red (nhanh).
  Ghi 1 xe do+toyota -> cap nhat P_red VA P_toyota (nhieu node, async).

4. Pitfall — chọn global index rồi giả định index luôn khớp dữ liệu

Pitfall — đọc global index ngay sau ghi và tin nó đã cập nhật

Vì global index cập nhật bất đồng bộ, có một khoảng index chưa khớp với dữ liệu.

❌ Ghi xe moi (color=red) -> bao "da luu" -> ngay sau doc index "moi xe do"
   -> global index P_red CHUA duoc cap nhat (async) -> xe vua them KHONG hien
   -> tuong mat du lieu / bug

❌ Chon LOCAL index cho bang doc-nhieu theo truong phu, tra van nong
   -> moi query scatter toi MOI partition -> cham + tai lon vo ich

Hệ quả: hoặc kết quả tìm kiếm thiếu tạm thời (global async) bị hiểu là mất dữ liệu, hoặc đọc chậm triền miên (local cho workload đọc-nhiều).

Hướng đúng: chọn theo tỉ lệ đọc/ghi và yêu cầu độ tươi. Ghi-nhiều, đọc theo trường phụ thưa → local (ghi rẻ, chịu scatter khi đọc). Đọc theo trường phụ rất nhiều, chịu được index trễ chút → global (đọc rẻ, chấp nhận ghi phức tạp + eventual consistency của index). Và với global, thiết kế app biết index có thể trễ — đừng giả định vừa ghi là tìm thấy ngay.

5. 📚 Deep Dive

📚 Deep Dive — Partitioning and secondary indexes

Ghi chú: DDIA Ch6 nhấn: đây là cùng một đánh đổi đọc/ghi lặp lại — chỉ khác lần này áp lên chính index. Không có index phân tán "vừa ghi rẻ vừa đọc rẻ".

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

  • Bài 01 — Range vs hash: secondary index khó vì trường phụ cắt ngang cách chia chính (theo khoá chính) — nối thẳng từ cách partition dữ liệu.
  • Module 2 — Replication lag: global index async có "index lag" giống replication lag — đọc index ngay sau ghi có thể chưa thấy.
  • Module 4 — Transaction phân tán: vì sao global index không cập nhật đồng bộ mọi partition — đồng bộ nhiều partition là 2PC đắt.
  • Inverted index (Thuật toán): cấu trúc index bên dưới — bài này quyết đặt nó ở đâu khi phân tán.

7. Tóm tắt

  • Giá trị một trường phụ không tôn trọng ranh giới partition vốn chia theo khoá chính — đây là gốc rễ khiến secondary index khó.
  • Local (document-partitioned) = mỗi phòng tự làm mục lục riêng: thêm sách rẻ, nhưng tìm theo chủ đề phải lục khắp mọi phòng rồi gộp lại.
  • Global (term-partitioned) = một quầy chung theo chủ đề: tìm nhanh (đúng một quầy), nhưng thêm sách phải chạy cập nhật nhiều quầy — thường trễ vài giây tới vài phút.
  • Chọn local khi ghi nhiều/đọc theo trường phụ thưa; chọn global khi đọc theo trường phụ dày, chấp nhận độ trễ cập nhật index.
  • Anti-pattern: tin global index luôn khớp ngay sau ghi (async → có khoảng trễ tạm thời); hoặc dùng local cho workload đọc-nhiều theo trường phụ (scatter/gather chậm triền miên).

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao secondary index khó phân mảnh, trong khi partition theo khoá chính thì đơn giản?
  2. Q2
    Trong local (document-partitioned) index: vì sao ghi rẻ nhưng đọc theo trường phụ đắt?
  3. Q3
    Trong global (term-partitioned) index: giải thích vì sao đọc rẻ nhưng ghi đắt, và vì sao ghi thường bất đồng bộ.
  4. Q4
    Một app rao vặt: đăng tin rất nhiều (ghi cao), người xem thỉnh thoảng lọc theo 'khu vực'. Chọn local hay global index cho trường khu vực? Vì sao?
  5. Q5
    Vì sao 'index lag' của global index là họ hàng của replication lag (Module 2), và app cần lưu ý gì?

Bài tiếp theo: Request routing — tìm đúng node giữ dữ liệu

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Bài tiếp theo

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