DDIA · Martin Kleppmann · Chương 6DDIA · Martin Kleppmann · Chương 6DDIA · Martin Kleppmann · Chapter 6
DDIA · Chương 6 · Phân mảnhDDIA · Chương 6 · Phân mảnhDDIA · Chapter 6 · Partitioning

Mỗi bản ghi thuộc đúng một mảnhChia dữ liệu ra nhiều ngănEvery record lives in exactly one partition

Chương 5 hỏi làm sao giữ cùng một dữ liệu trên nhiều máy. Chương 6 hỏi câu ngược lại: khi tập dữ liệu và tải quá lớn cho một máy, ta cắt nó ra thế nào. Kleppmann gọi mỗi mảnh là một partition, và định nghĩa nó rất chặt: mỗi bản ghi (mỗi hàng, mỗi tài liệu) thuộc về đúng một phân mảnh, và mỗi phân mảnh vận hành như một cơ sở dữ liệu nhỏ của riêng nó12. Điều quan trọng cần nói ngay: phân mảnh không thay thế nhân bản — hai thứ gần như luôn đi cùng nhau, mỗi phân mảnh vẫn được giữ trên nhiều node để chịu lỗi.

Khi tủ hồ sơ đầy quá một cái tủ không chứa nổi, ta mua thêm tủ và chia hồ sơ ra. Mỗi tờ hồ sơ chỉ nằm trong một ngăn duy nhất, nhưng để phòng cháy thì mỗi ngăn vẫn được photo ra vài bản để ở chỗ khác. Chương này nói về việc chia thế nào cho đều — và chia xong thì tìm lại bằng cách nào.

Chapter 5 asked how to keep the same data on several machines. Chapter 6 asks the opposite: when the dataset and the load are too big for one machine, how do we cut it up. Kleppmann calls each piece a partition, and defines it tightly: every record (every row, every document) belongs to exactly one partition, and each partition behaves like a small database of its own12. One thing to say up front: partitioning does not replace replication — the two almost always travel together, with each partition still kept on several nodes for fault tolerance.

∴
Luận đề của chương Điều quan trọng nhất The thesis of the chapter

Phân mảnh là bài toán vẽ ranh giới, và chỉ có hai câu hỏi thật sự: vẽ thế nào để tải chia đều, và sau khi ranh giới dịch chuyển thì tìm lại dữ liệu bằng cách nào. Mọi kỹ thuật trong chương — dải khóa hay băm, chỉ mục cục bộ hay toàn cục, tái cân bằng, định tuyến — đều là câu trả lời cho đúng hai câu hỏi ấy. Và mọi câu trả lời đều là một đánh đổi, không có phương án nào thắng ở cả hai đầu.

Chia dữ liệu ra nhiều ngăn thì phải trả lời hai câu. Một: chia sao cho không ngăn nào quá tải? Hai: chia xong rồi, muốn lấy một tờ ra thì biết nó ở ngăn nào? Nhớ nhé: cả chương chỉ xoay quanh hai câu hỏi đó, và cách nào cũng có cái được cái mất.

Partitioning is the problem of drawing boundaries, and there are really only two questions: how do we draw them so the load spreads evenly, and once the boundaries move, how do we find the data again. Every technique in the chapter — key range or hash, local or global index, rebalancing, routing — answers exactly those two. And every answer is a trade-off; nothing wins at both ends.

Trục 1 · Partitioning of key-value dataAxis 1 · Partitioning of key-value data

Vẽ ranh giới: theo dải khóa hay theo bămDrawing the line: by key range or by hash

Mục tiêu của phân mảnh là chia đều — cả dữ liệu lẫn tải truy vấn. Nếu chia không đều, Kleppmann gọi đó là skew (lệch), và mảnh phải gánh phần bất cân xứng là một hot spot (điểm nóng)1. Có một cách chia đều tuyệt đối: ném mỗi bản ghi vào một mảnh ngẫu nhiên. Nó đều thật, và cũng vô dụng thật — vì sau đó không ai biết bản ghi nằm ở đâu, nên mọi truy vấn đều phải hỏi tất cả các mảnh. Từ đó sinh ra hai cách vẽ ranh giới thực dụng, và cả chương sau này là hệ quả của việc bạn chọn cách nào.

Chia hồ sơ ra nhiều ngăn thì phải chia cho đều, không để một ngăn phồng lên còn các ngăn khác trống trơn. Ngăn bị nhét quá nhiều gọi là "điểm nóng". Có thể chia đều bằng cách bốc thăm ngẫu nhiên, nhưng rồi tìm lại thì phải lục hết mọi ngăn. Nhớ nhé: chia đều thôi chưa đủ — phải chia sao cho còn tìm lại được.

The goal of partitioning is an even spread — of data and of query load. When the spread is uneven, Kleppmann calls it skew, and a partition carrying a disproportionate share is a hot spot1. There is one way to get a perfectly even spread: throw each record at a random partition. It really is even, and it really is useless — nobody then knows where a record lives, so every query must ask every partition. Out of that come two workable ways to draw the boundary, and the rest of the chapter follows from which one you pick.

Skew · hot spot · partition = a small database of its own

Một phân mảnh là một lát cắt của tập dữ liệu mà mỗi bản ghi chỉ thuộc về một lát duy nhất. Vẽ ranh giới tốt nghĩa là không mảnh nào phải gánh phần bất cân xứng — và vẫn còn cách để biết một khóa thuộc mảnh nào mà không phải hỏi khắp nơi.

Mỗi tờ hồ sơ chỉ nằm ở một ngăn. Chia giỏi là chia sao cho không ngăn nào quá đầy, mà nhìn vào tên hồ sơ là biết ngay nó ở ngăn nào. Nhớ nhé: chia đều và tìm nhanh phải đi cùng nhau.

A partition is a slice of the dataset such that every record belongs to exactly one slice. Drawing the boundary well means no partition carries a disproportionate share — and you can still tell which partition a key is in without asking everywhere.

Một lưu ý về thuật ngữ, vì nó gây nhầm lẫn thật sự khi đọc tài liệu sản phẩm: cái mà DDIA gọi là partition thì mỗi hệ đặt một tên khác. Elasticsearch và MongoDB gọi là shard209, HBase gọi là region — một dải liên tục trong không gian khóa, không chồng lấn, và một khóa thuộc đúng một region tại mỗi thời điểm15, Cassandra và Riak gọi là vnode1317, còn Couchbase gọi là vBucket19. Cùng một khái niệm, năm cái tên — nên khi so sánh hai hệ, việc đầu tiên phải làm là dịch từ vựng về cùng một mẫu số.

Đúng một mảnh Mỗi bản ghi thuộc về một phân mảnh duy nhất; mỗi phân mảnh là một CSDL nhỏ của riêng nó.
Vẫn cần nhân bản Phân mảnh giải bài toán dung lượng, nhân bản giải bài toán chịu lỗi. Hai thứ chồng lên nhau, không thay nhau.
Lệch sinh điểm nóng Chia không đều thì một mảnh gánh phần bất cân xứng, và chính nó trở thành trần hiệu năng của cả hệ.
Một tờ, một ngăn Mỗi tờ hồ sơ chỉ nằm ở đúng một ngăn, không nằm hai chỗ.
Vẫn phải photo Chia ngăn để chứa được nhiều; photo thêm bản để phòng cháy. Hai việc khác nhau.
Ngăn quá đầy là hỏng Một ngăn phồng lên thì cả tủ chậm theo nó.
Exactly one Each record belongs to exactly one partition; each partition is a small database of its own.
Replication stays Partitioning solves capacity, replication solves fault tolerance. They stack; they don't substitute.
Skew makes hot spots An uneven split leaves one partition carrying a disproportionate share — and it becomes the whole system's ceiling.
  1. Partitioning by key range

    Theo dải khóa — quét theo dải rẻ, nhưng dễ nóngBy key range — cheap scans, easy hot spots

    Cách thứ nhất là giao cho mỗi mảnh một dải khóa liên tục, như các tập của một bộ bách khoa toàn thư. Vì khóa được giữ theo thứ tự bên trong mảnh, quét theo dải trở nên rẻ. MongoDB gọi đây là ranged sharding và mô tả đúng cái được: những tài liệu có giá trị khóa "gần nhau" nhiều khả năng nằm cùng một chunk hoặc shard, nên các truy vấn đọc trong một dải liên tục là truy vấn hiệu quả8. HBase thì gọi mỗi mảnh là một region: một dải liên tục trong không gian khóa, các region không chồng lấn nhau, và mỗi khóa thuộc về đúng một region tại một thời điểm15.

    Cách thứ nhất là chia như tủ hồ sơ theo bảng chữ cái: ngăn A–C, ngăn D–F, ngăn G–K… Ai cần tra một loạt hồ sơ liền nhau thì chỉ phải mở một ngăn. Nhớ nhé: chia theo thứ tự thì tra một dải rất nhanh.

    The first way gives each partition a contiguous range of keys, like the volumes of an encyclopaedia. Because keys stay sorted within a partition, range scans are cheap. MongoDB calls this ranged sharding and describes exactly that benefit: documents with "close" shard key values are likely to sit in the same chunk or shard, which makes reads over a contiguous range efficient8. HBase calls each piece a region: a continuous range within the key space, non-overlapping, with a single row key belonging to exactly one region at any point in time15.

    Được gìTruy vấn kiểu "mọi số đo của cảm biến X trong tháng 5" chỉ chạm một mảnh, vì các khóa liền kề nằm cạnh nhau và đã sắp sẵn theo thứ tự.
    Được gìTra một loạt hồ sơ liền nhau chỉ cần mở đúng một ngăn.
    What you gainA query like "all readings from sensor X in May" touches one partition only, because adjacent keys sit together and already sorted.

    Ranh giới không chia đều

    Các dải khóa thường không cách đều nhau, vì dữ liệu không phân bố đều: phần "A" của một danh bạ luôn dày hơn phần "X". Ranh giới phải được chọn theo phân bố thật, hoặc do người vận hành đặt, hoặc do hệ tự điều chỉnh. HBase thừa nhận thẳng điều này: nó không thể tự đoán điểm tách vì việc ấy phụ thuộc nặng vào phân bố khóa trong dữ liệu của bạn15.

    Cái giá: khóa tăng đơn điệu

    Nếu khóa là dấu thời gian, mọi lần ghi mới đều rơi vào đúng mảnh cuối. MongoDB mô tả cơ chế này rất cụ thể: luôn tồn tại một chunk có biên trên là MaxKey, và nếu khóa luôn tăng thì mọi lần chèn đều được định tuyến vào chunk ấy, khiến shard chứa nó trở thành nút cổ chai của mọi thao tác ghi10. Cùng một cái bẫy đã được Ikai Lan viết lại cho App Engine Datastore từ năm 20117.

  2. Partitioning by hash of key

    Theo băm của khóa — đều hơn, nhưng mất thứ tựBy hash of key — more even, but order is gone

    Cách thứ hai là băm khóa trước, rồi giao cho mỗi mảnh một dải của không gian băm. Vì hàm băm xáo tung mọi trật tự, những khóa gần nhau bị ném đi khắp nơi — và đó vừa là thuốc vừa là độc. MongoDB nói rõ cả hai mặt: băm cho phân bố dữ liệu đều hơn, nhưng cái giá là giảm số thao tác "trúng đích", và mongos nhiều khả năng phải phát tán (broadcast) tới mọi shard để trả lời một truy vấn theo dải9. Đổi lại, băm đúng là thuốc cho cái bẫy ở thẻ trước: tài liệu MongoDB khuyên dùng chỉ mục băm cho những khóa thay đổi đơn điệu như ObjectId hay dấu thời gian9.

    Cách thứ hai là bốc thăm có quy tắc: đưa tên hồ sơ qua một cái máy xáo, máy ra số nào thì bỏ vào ngăn đó. Cùng một tên thì luôn ra cùng một số, nên tra một tờ vẫn nhanh. Nhưng hai tờ tên gần nhau lại rơi vào hai ngăn xa nhau, nên tra "một loạt liền nhau" thì phải mở hết mọi ngăn. Nhớ nhé: xáo lên thì đều hơn, nhưng mất mất cái thứ tự.

    The second way hashes the key first, then gives each partition a range of the hash space. Because a hash function destroys ordering, adjacent keys get scattered — which is both the cure and the poison. MongoDB states both sides: hashing gives a more even distribution at the cost of fewer targeted operations, and mongos is more likely to broadcast to every shard to answer a ranged query9. In exchange, hashing is the cure for the previous card's trap: MongoDB's docs recommend hashed indexes precisely for shard keys that change monotonically, such as ObjectId values or timestamps9.

    Đánh đổiBạn mua sự phân bố đều bằng đúng cái mình vừa được ở cách thứ nhất: khả năng quét theo dải. Không có cách nào giữ cả hai cùng một lúc trên cùng một khóa.
    Đánh đổiChọn "chia đều" thì mất "tra theo dải". Chọn "tra theo dải" thì phải canh chừng ngăn quá đầy.
    The trade-offYou buy the even spread with exactly what the first way gave you: range scans. There is no way to keep both on the same key.

    Hàm băm nào cũng được?

    Không. Kleppmann đã viết hẳn một bài về chuyện này trước khi viết sách: hàm băm phục vụ những mục đích rất khác nhau, và hàm dùng cho bảng băm trong một ngôn ngữ lập trình không phải hàm dùng để chia việc cho các máy trong một cụm — hashCode của Java không an toàn cho hệ phân tán6. Về phía sản phẩm, Couchbase dùng CRC32 để ánh xạ một mục vào vBucket19, còn Riak băm cặp bucket/key thành một giá trị nhị phân 160 bit rồi ánh xạ nó lên một vòng có thứ tự17.

    Phân mảnh ≠ truy vấn

    Băm không xóa mất khả năng lọc theo dải — nó chỉ khiến việc lọc ấy không còn rẻ. Hệ vẫn trả đúng kết quả, nhưng bằng cách hỏi mọi mảnh rồi gộp lại, tức là trả bằng thông lượng và độ trễ đuôi thay vì bằng một lần đọc trúng đích. Đây chính là lý do MongoDB nói mongos "nhiều khả năng phải broadcast" chứ không nói là "không làm được"9.

  3. Compound primary key

    Khóa tổ hợp — đường trung dungThe compound key — a middle road

    Không nhất thiết phải chọn một trong hai. Cassandra tách khóa chính thành hai phần có vai trò khác nhau: partition key — thành phần đầu tiên, có thể gồm nhiều cột trong một cặp ngoặc — là thứ duy nhất quyết định bản ghi thuộc mảnh nào; các cột còn lại là clustering columns, chúng định thứ tự sắp xếp bên trong mảnh14. Kết quả là một mô hình một–nhiều rất đẹp: nếu cố định thành phần đầu, việc quét theo dải trên phần còn lại vẫn rẻ như ở cách thứ nhất. Kleppmann lấy ví dụ một mạng xã hội với khóa (user_id, update_timestamp): mọi bài của một người nằm chung một mảnh và đã sắp theo thời gian, nên "lấy các bài của người này trong khoảng thời gian kia" chỉ là một lần quét gọn1.

    Có cách đi giữa: lấy phần đầu của tên hồ sơ để chọn ngăn, rồi trong ngăn thì xếp theo phần sau. Ví dụ tất cả bài viết của cùng một bạn nằm chung một ngăn, và trong ngăn thì xếp theo ngày. Muốn xem bài của bạn ấy tháng trước? Mở đúng một ngăn, lật đúng một đoạn. Nhớ nhé: chọn ngăn bằng phần đầu, xếp trong ngăn bằng phần sau.

    You don't have to pick just one. Cassandra splits the primary key into two parts with different jobs: the partition key — the first component, possibly several columns inside one set of parentheses — is the only thing that decides which partition a row lands in; the remaining clustering columns define the sort order within that partition14. The result is a neat one-to-many model: fix the first component, and range scans over the rest stay as cheap as in the first approach. Kleppmann's example is a social network keyed by (user_id, update_timestamp): all of one user's updates sit in one partition, already sorted by time, so "this user's posts in that time window" is one tidy scan1.

    Ở đâu bạn gặp nóBất cứ chỗ nào có quan hệ một–nhiều tự nhiên: bài viết theo người dùng, sự kiện theo thiết bị, đơn hàng theo khách. Thành phần đầu chia tải, các thành phần sau giữ thứ tự.
    Ở đâu bạn gặp nóBài viết của một người, sự kiện của một máy, đơn hàng của một khách — cái gì "của ai" thì lấy "ai" làm ngăn.
    Where you see itAnywhere there's a natural one-to-many: posts per user, events per device, orders per customer. The first component spreads the load; the rest keep the order.

    Cái giá vẫn còn đó

    Khóa tổ hợp không xóa bỏ đánh đổi — nó chỉ thu nhỏ phạm vi của nó. Bạn vẫn không thể quét theo dải trên toàn bộ tập dữ liệu, chỉ quét được trong phạm vi một partition key đã cố định. Và nếu một người dùng quá nổi tiếng, cả mảnh của người ấy phồng lên: đây đúng là bài toán lệch tải ở thẻ tiếp theo, chỉ đổi lớp áo.

    Cú pháp nói lên thiết kế

    Tài liệu CQL viết rất gọn: PRIMARY KEY (a, b, c) thì a là partition key, b và c là clustering columns; còn PRIMARY KEY ((a, b), c) thì a và b hợp thành partition key tổ hợp, còn c là clustering column14. Cặp ngoặc thừa ấy không phải cú pháp trang trí — nó là ranh giới phân mảnh, viết thẳng vào lược đồ.

  4. Skewed workloads & relieving hot spots

    Tải lệch: chỗ mà băm không cứu đượcSkewed workloads: where hashing can't help

    Đây là chỗ hay bị hiểu nhầm nhất. Băm rải đều khóa, chứ không rải đều tải. Nếu hàng triệu lượt đọc và ghi cùng đổ vào đúng một khóa — chẳng hạn một tài khoản siêu nổi tiếng vừa làm điều gì đó — thì mọi yêu cầu vẫn rơi trọn vào một mảnh, bất kể bạn đã băm kỹ đến đâu. Kleppmann nói thẳng rằng phần lớn hệ dữ liệu ngày nay không tự bù được sự lệch này, và cách chữa phải nằm ở tầng ứng dụng: thêm một số ngẫu nhiên hai chữ số vào đầu khóa để tách một khóa nóng thành 100 khóa nằm trên 100 mảnh1.

    Xáo hồ sơ ra các ngăn cho đều là một chuyện. Nhưng nếu cả trường cùng lúc muốn xem đúng một tờ hồ sơ, thì cái ngăn chứa nó vẫn kẹt cứng. Cách chữa: photo tờ đó ra 100 bản, đánh số từ 01 đến 100, rải ra 100 ngăn. Chỉ có điều lúc muốn xem đầy đủ thì lại phải gom cả 100 bản. Nhớ nhé: chia đều đồ không có nghĩa là chia đều việc.

    This is the most misread part. Hashing spreads keys evenly, not load. If millions of reads and writes all land on one key — some celebrity account just did something — every request still falls into a single partition, no matter how well you hashed. Kleppmann says plainly that most data systems today can't compensate for this skew automatically, and the fix has to live in the application: prefix the key with a random two-digit number to split one hot key into 100 keys across 100 partitions1.

    Cái giá đi kèmGhi thì rải được, nhưng đọc thì từ nay phải hỏi cả 100 nơi rồi gộp lại. Vì thế Kleppmann chỉ khuyên áp cách này cho một số ít khóa thật sự nóng, không phải cho toàn bộ khóa — và phải tự ghi sổ khóa nào đã bị tách1.
    Cái giá đi kèmPhoto ra 100 bản thì cất dễ, nhưng lúc cần xem đủ lại phải đi gom 100 chỗ. Chỉ nên làm với vài tờ nóng nhất thôi.
    The priceWrites spread, but reads now have to ask all 100 places and merge. That's why Kleppmann recommends it only for a small number of genuinely hot keys, and only with bookkeeping of which keys were split1.

    Lệch dữ liệu ≠ lệch tải

    Hai loại lệch trông giống nhau trên biểu đồ nhưng có cách chữa khác hẳn. Lệch dữ liệu (một mảnh chứa nhiều byte hơn) chữa được bằng cách vẽ lại ranh giới — tách mảnh, dời mảnh. Lệch tải (một khóa bị hỏi quá nhiều) thì vẽ lại ranh giới không giúp gì cả, vì bản thân khóa là đơn vị nhỏ nhất không tách được. Khi chẩn đoán một điểm nóng, việc đầu tiên là xác định mình đang gặp loại nào.

    Vì sao chương này nhắc lại Chương 1

    Bài toán tài khoản siêu nổi tiếng chính là bài toán fan-out của dòng thời gian Twitter trong Chương 1, nhìn từ một góc khác: ở đó nó biểu hiện thành số lượt ghi bùng nổ, ở đây nó biểu hiện thành một điểm nóng phân mảnh. Cùng một sự thật vật lý — phân bố người dùng có đuôi rất dài — chỉ đổi chỗ ta gặp nó trong kiến trúc.

Trục 2 · Partitioning and secondary indexesAxis 2 · Partitioning and secondary indexes

Chỉ mục phụ phá vỡ sự gọn gàngSecondary indexes break the tidiness

Một chỉ mục phụ không xác định duy nhất một bản ghi; nó là cách tìm mọi bản ghi có một giá trị nào đó — mọi xe màu đỏ, mọi bài viết chứa một từ. Vấn đề là nó không ăn khớp với ranh giới phân mảnh: các bản ghi "màu đỏ" nằm rải khắp mọi mảnh. Kleppmann chỉ ra rằng có đúng hai cách xử lý, và chúng là hình ảnh phản chiếu của nhau: một cách làm ghi rẻ và đọc đắt, cách kia làm ngược lại1.

Cho tới giờ ta chỉ tìm hồ sơ bằng tên. Nhưng đời thật hay hỏi kiểu khác: "cho tôi tất cả hồ sơ màu đỏ". Những tờ màu đỏ thì nằm rải khắp mọi ngăn, nên phải có một cuốn sổ mục lục. Câu hỏi là: mỗi ngăn tự giữ sổ của mình, hay cả tủ dùng chung một cuốn sổ?

A secondary index doesn't identify a record uniquely; it's a way to find all records with some value — every red car, every post containing a word. The trouble is that it doesn't line up with partition boundaries: the "red" records are scattered across every partition. Kleppmann shows there are exactly two ways to handle it, and they are mirror images: one makes writes cheap and reads expensive, the other the reverse1.

Document-partitioned (local) vs term-partitioned (global)

Chỉ mục phụ theo document chỉ biết về dữ liệu của chính mảnh chứa nó — ghi gọn trong một mảnh, nhưng đọc phải hỏi tất cả rồi gộp. Chỉ mục phụ theo term được phân mảnh theo chính giá trị được đánh chỉ mục — đọc chỉ chạm một mảnh, nhưng một lần ghi có thể phải chạm nhiều mảnh chỉ mục.

Kiểu một: mỗi ngăn tự giữ sổ mục lục của riêng mình. Tìm thì phải mở hết mọi sổ. Kiểu hai: cả tủ dùng chung một bộ sổ chia theo chủ đề. Tìm thì mở đúng một cuốn, nhưng mỗi lần thêm hồ sơ lại phải đi sửa mấy cuốn sổ.

A document-partitioned secondary index only knows about the data in its own partition — writes stay local, but reads must ask everyone and merge. A term-partitioned index is partitioned by the indexed value itself — reads touch one partition, but a single write may have to touch several index partitions.

Hai cái tên này là cùng một sự phân biệt mà thế giới CSDL quan hệ đã đặt tên từ lâu. Tài liệu Oracle gọi chỉ mục trên bảng đã phân mảnh là local khi nó được gắn tự động theo cách phân mảnh của bảng, và global khi nó được phân mảnh độc lập với bảng — và đưa ra đúng lời khuyên mà lý thuyết dự đoán: dùng global cho ứng dụng OLTP cần thời gian phản hồi nhanh, dùng local cho kho dữ liệu và DSS nơi người ta quan tâm tới thông lượng22. Đọc nhanh thì trả bằng ghi phức tạp; đó là cùng một định luật bảo toàn dưới hai bộ từ vựng.

Local: ghi gọn Mỗi mảnh tự giữ chỉ mục của mình; một lần ghi chỉ chạm một mảnh, không cần phối hợp.
Local: đọc tán Truy vấn theo chỉ mục phụ phải phát tới mọi mảnh rồi gộp kết quả — Kleppmann gọi là scatter/gather.
Global: đảo ngược Chỉ mục chia theo term: đọc chỉ chạm một mảnh, nhưng ghi phải phối hợp nhiều mảnh, thường là bất đồng bộ.
Sổ riêng: ghi dễ Thêm hồ sơ vào ngăn nào thì chỉ sửa sổ của ngăn đó.
Sổ riêng: tìm mệt Muốn tìm thì phải mở sổ của tất cả các ngăn.
Sổ chung: ngược lại Tìm thì mở đúng một cuốn, nhưng mỗi lần thêm hồ sơ phải đi sửa nhiều cuốn.
Local: cheap writes Each partition keeps its own index; a write touches one partition, no coordination needed.
Local: scattered reads A secondary-index query must go to every partition and merge — Kleppmann's scatter/gather.
Global: the mirror Index partitioned by term: reads hit one partition, writes must coordinate several, usually asynchronously.
  1. Document-partitioned index · scatter/gather

    Chỉ mục cục bộ và cái giá của việc hỏi khắp nơiThe local index and the cost of asking everyone

    Cách phổ biến nhất: mỗi mảnh giữ một chỉ mục phụ chỉ bao phủ dữ liệu của chính nó và không biết gì về các mảnh khác. Ghi thì tuyệt vời — sửa một tài liệu chỉ đụng đến một mảnh. Đọc thì phải trả giá. Tài liệu Elasticsearch mô tả đúng luồng ấy: node nhận một yêu cầu đọc có trách nhiệm chuyển tiếp nó tới các node đang giữ những shard liên quan, thu gom các phản hồi rồi trả lời client — node ấy được gọi là coordinating node; và vì phần lớn truy vấn tìm kiếm phải đọc từ nhiều shard, mỗi shard giữ một tập con dữ liệu khác nhau, nên bước cuối luôn là gộp các kết quả lại rồi mới trả về21. Bài viết của Elastic minh họa cái giá ấy bằng một con số: với một chỉ mục 20 shard, vì “Elasticsearch không biết tài liệu của bạn nằm ở đâu” — tài liệu được rải ngẫu nhiên khắp cụm — nó không có lựa chọn nào ngoài phát tán yêu cầu tới cả 20 shard20. Kleppmann gọi kiểu đọc này là scatter/gather1.

    Kiểu phổ biến nhất: mỗi ngăn tự ghi sổ mục lục cho riêng nó. Bỏ thêm một tờ hồ sơ vào ngăn số 3 thì chỉ phải sửa sổ của ngăn số 3 — nhanh gọn. Nhưng muốn hỏi "tất cả hồ sơ màu đỏ" thì phải mở sổ của tất cả các ngăn rồi gom lại. Nhớ nhé: cất thì dễ, tìm thì phải chạy khắp tủ.

    The most common approach: each partition keeps a secondary index covering only its own data and knowing nothing about other partitions. Writes are great — updating a document touches one partition. Reads pay for it. The Elasticsearch docs describe exactly that flow: the node receiving a read request is responsible for forwarding it to the nodes holding the relevant shards, collating the responses and replying to the client — that node is called the coordinating node; and because most searches have to read from several shards, each holding a different subset of the data, the final step is always to combine the results before responding21. Elastic’s own post puts a number on the cost: with a 20-shard index, because “Elasticsearch has no idea where to look for your document” — the docs were randomly distributed around the cluster — it has no choice but to broadcast the request to all 20 shards20. Kleppmann calls this read pattern scatter/gather1.

    Vì sao vẫn phổ biếnVì ghi là thao tác thường xuyên hơn và cần đơn giản hơn. Giữ chỉ mục cục bộ nghĩa là không bao giờ phải phối hợp giữa các mảnh khi ghi — không giao dịch phân tán, không hàng đợi, không trạng thái nửa vời.
    Vì sao vẫn phổ biếnVì ta bỏ hồ sơ vào tủ nhiều hơn là đi tra cứu. Ghi đơn giản thì cả hệ đơn giản.
    Why it's still the defaultBecause writes are more frequent and need to stay simple. A local index means no cross-partition coordination on write — no distributed transaction, no queue, no half-finished state.

    Scatter/gather khuếch đại đuôi

    Một truy vấn scatter/gather chỉ nhanh bằng mảnh chậm nhất mà nó phải hỏi. Với 20 mảnh, xác suất chạm phải một mảnh đang bận cao gấp bội so với một lần đọc trúng đích. Đây chính là hiện tượng khuếch đại độ trễ đuôi mà Chương 1 đã dựng nền từ vựng để nói, nay xuất hiện lại như một hệ quả trực tiếp của lựa chọn phân mảnh chỉ mục.

    Ai dùng kiểu này

    Ngoài Elasticsearch, Riak cung cấp chỉ mục phụ (2i) cho phép gắn nhãn cho object ngay lúc ghi bằng một hoặc nhiều giá trị có thể truy vấn, rồi dùng chúng để tìm nhiều object18. Bản thân tài liệu Riak hiện khuyến nghị dùng Riak Search thay cho 2i cho các truy vấn không dựa trên khóa chính, vì Search có API truy vấn rộng hơn và dùng được với mọi backend lưu trữ18.

  2. Custom routing · dodging the broadcast

    Cách né scatter/gather mà không đổi kiến trúcDodging scatter/gather without changing architecture

    Có một lối thoát nửa đường, và nó đáng biết vì rẻ. Mặc định, giá trị định tuyến _routing của một tài liệu chính là _id của nó, và Elasticsearch chọn shard bằng công thức shard_num = hash(_routing) % num_primary_shards28. Nhưng bạn có thể tự đặt giá trị định tuyến cho từng tài liệu — user_id, mã bưu chính, bất cứ thứ gì ứng dụng của bạn thường lọc theo. Tài liệu nói thẳng cái lợi: thay vì phải phát tán một yêu cầu tìm kiếm tới mọi shard của chỉ mục, yêu cầu có thể được gửi tới đúng shard khớp với giá trị định tuyến ấy28 — bài viết của Elastic nói gọn hơn: định tuyến tùy biến bảo đảm chỉ một shard bị truy vấn20, bất kể chỉ mục của bạn có 20 hay 100 shard. Về bản chất, bạn đang mượn ý tưởng khóa tổ hợp ở trục 1 và áp nó lên tầng chỉ mục.

    Có một mẹo đi tắt. Thay vì để máy xáo tự chọn ngăn, bạn tự quy ước: "mọi hồ sơ của lớp 6A luôn để ở ngăn số 3". Thế thì hỏi "hồ sơ lớp 6A" chỉ phải mở một ngăn. Nhớ nhé: nếu bạn biết trước mình hay tìm theo cái gì, hãy lấy chính cái đó để chọn ngăn.

    There's a halfway escape, and it's worth knowing because it's cheap. By default a document’s _routing value is its _id, and Elasticsearch picks the shard with shard_num = hash(_routing) % num_primary_shards28. But you can set a custom routing value per document — user_id, a postcode, whatever your application usually filters on. The docs state the payoff outright: instead of having to fan out a search request to all the shards in an index, the request can be sent to just the shard matching that routing value28 — Elastic’s post says it more bluntly: custom routing ensures only one shard is queried20, no matter whether your index has 20 shards or 100. In essence you're borrowing the compound-key idea from axis 1 and applying it at the index layer.

    Điều kiện để dùng đượcBạn phải biết trước chiều truy vấn chủ đạo của mình, và chiều ấy phải đủ nhiều giá trị để không dồn tất cả vào một shard. Nói cách khác: bạn đã tự tay tạo ra nguy cơ lệch tải để đổi lấy tốc độ đọc.
    Điều kiệnPhải biết trước mình hay tìm theo gì, và cái đó phải có nhiều loại — nếu ai cũng ở "lớp 6A" thì ngăn số 3 lại kẹt.
    When it appliesYou must know your dominant query dimension in advance, and it must have enough distinct values not to pile onto one shard. Put differently: you've deliberately created skew risk in exchange for read speed.

    Ghi vẫn đi một đường

    Tài liệu Elasticsearch mô tả mọi thao tác lập chỉ mục đều được phân giải về một nhóm nhân bản thông qua routing, thường là dựa trên ID tài liệu, rồi mới chuyển tiếp nội bộ tới shard chính hiện thời của nhóm21. Định tuyến tùy biến chỉ thay cái đầu vào của phép băm ấy — nó không thay đổi hình dạng đường ghi, nên không thêm chi phí phối hợp nào.

    Vì sao đây không phải chỉ mục toàn cục

    Định tuyến tùy biến không tạo ra một chỉ mục chia theo term — nó chỉ khiến việc phân mảnh tài liệu trùng khớp với chiều truy vấn của bạn. Khi ứng dụng cần hỏi theo một chiều khác, bạn lại rơi về scatter/gather như cũ. Một chỉ mục toàn cục thật sự mới là thứ giải được bài toán ở mọi chiều — và đó là nội dung thẻ tiếp theo.

  3. Term-partitioned (global) index

    Chỉ mục toàn cục: đọc gọn, ghi phức tạpThe global index: tidy reads, complicated writes

    Cách thứ hai lật ngược mọi thứ: thay vì để mỗi mảnh dữ liệu tự giữ sổ, ta dựng một chỉ mục bao phủ toàn bộ dữ liệu rồi phân mảnh chính cái chỉ mục ấy — theo term, tức theo giá trị được đánh chỉ mục. Tất cả mục "màu đỏ" nằm chung một mảnh chỉ mục, tất cả mục "màu xanh" nằm ở mảnh khác. Đọc trở nên gọn: chỉ hỏi đúng mảnh giữ term mình cần. Nhưng ghi thì đắt và phức tạp, vì một lần ghi một tài liệu có thể phải chạm tới nhiều mảnh chỉ mục — mỗi term một mảnh. Vì thế trong thực tế các chỉ mục toàn cục thường được cập nhật bất đồng bộ, nghĩa là chỉ mục có thể trễ so với dữ liệu1.

    Cách thứ hai làm ngược lại: thay vì mỗi ngăn một sổ, cả tủ dùng chung một bộ sổ chia theo chủ đề — sổ "màu đỏ", sổ "màu xanh". Hỏi "hồ sơ màu đỏ"? Mở đúng một cuốn là xong. Nhưng bỏ thêm một tờ hồ sơ vào tủ thì phải đi sửa mấy cuốn sổ cùng lúc, nên người ta hay sửa sổ sau — và trong lúc đó sổ hơi cũ so với tủ. Nhớ nhé: sổ chung thì tra nhanh, nhưng ghi mệt và sổ dễ trễ.

    The second approach inverts everything: instead of each data partition keeping its own index, you build an index covering all the data and then partition that index — by term, the indexed value itself. All the "red" entries live in one index partition, all the "blue" ones in another. Reads become tidy: ask only the partition holding your term. But writes get expensive and complicated, because writing one document may touch several index partitions — one per term. So in practice global indexes are usually updated asynchronously, which means the index can lag behind the data1.

    Ở đâu bạn gặp nóTrong thế giới quan hệ, đây chính là chỉ mục global partitioned của Oracle: chỉ mục được phân mảnh độc lập với cách phân mảnh của bảng, trong khi chỉ mục local được gắn tự động theo bảng22.
    Ở đâu bạn gặp nóCác cơ sở dữ liệu lớn dùng cho báo cáo hay có kiểu "sổ chung" này, bên cạnh kiểu "mỗi ngăn một sổ".
    Where you see itIn the relational world this is Oracle's global partitioned index: partitioned independently of the table's own partitioning, while a local index is automatically linked to it22.

    Lời khuyên của Oracle trùng với lý thuyết

    Tài liệu Oracle nói thẳng: nói chung nên dùng chỉ mục toàn cục cho ứng dụng OLTP và chỉ mục cục bộ cho kho dữ liệu hoặc hệ hỗ trợ quyết định (DSS)22. Đọc kỹ thì đó chính là phát biểu của Kleppmann bằng từ vựng khác: OLTP cần thời gian phản hồi ngắn nên trả giá ở đường ghi; DSS quan tâm thông lượng nên chấp nhận quét rộng. Cùng một đánh đổi, hai cộng đồng, hai bộ thuật ngữ.

    Vì sao trễ là chấp nhận được

    Cập nhật chỉ mục toàn cục đồng bộ sẽ đòi hỏi một giao dịch phân tán trên mọi mảnh chỉ mục liên quan cho mỗi lần ghi — một cái giá rất ít hệ sẵn sàng trả. Chấp nhận cập nhật bất đồng bộ nghĩa là chấp nhận một cửa sổ thời gian mà chỉ mục chưa thấy bản ghi mới nhất. Với tìm kiếm và báo cáo, cửa sổ ấy thường vô hại; với "vừa ghi xong phải đọc thấy ngay", nó là một cái bẫy thật.

  4. The trade-off, not the right answer

    Không có cách đúng, chỉ có chỗ trả giáNo right answer, only where you pay

    Hai cách trên là một cặp phản chiếu hoàn hảo, và cấu trúc của nó lặp lại đúng cấu trúc đã gặp ở Chương 1 giữa fan-out on write và fan-out on read: bạn có thể làm việc lúc ghi hoặc làm việc lúc đọc, nhưng tổng công việc thì không biến mất. Chọn cái nào phụ thuộc vào tỉ lệ đọc/ghi thật của hệ bạn và vào việc bạn chịu được bao nhiêu độ trễ của chỉ mục. Đó là một quyết định về tải, không phải một quyết định về công nghệ.

    Hai cách trên giống hệt hai kiểu làm việc nhà: dọn dẹp ngay khi bày ra, hay để đó rồi dọn một lần lúc cần dùng. Cách nào cũng phải dọn — chỉ khác là dọn lúc nào. Nhớ nhé: đừng hỏi "cách nào đúng", hãy hỏi "hệ của mình đọc nhiều hay ghi nhiều".

    The two approaches are a perfect mirror pair, and the structure repeats what Chapter 1 showed between fan-out on write and fan-out on read: you can do the work at write time or at read time, but the total work doesn't vanish. Which to choose depends on your system's real read/write ratio and on how much index staleness you can live with. That's a decision about load, not about technology.

    Câu hỏi thực dụngHệ của bạn đọc theo chỉ mục phụ bao nhiêu lần cho mỗi lần ghi? Nếu tỉ lệ đó lớn, cái giá của scatter/gather sẽ lớn theo đúng tỉ lệ ấy — và chỉ khi đó, sự phức tạp của chỉ mục toàn cục mới đáng.
    Câu hỏi thực dụngBạn tra cứu nhiều hơn hay cất hồ sơ nhiều hơn? Trả lời được câu đó là chọn được cách.
    The practical questionHow many secondary-index reads does your system do per write? If that ratio is large, the cost of scatter/gather scales with it — and only then does a global index's complexity earn its keep.

    Một chi tiết dễ bỏ sót

    Đánh đổi này không cố định theo thời gian. Một hệ bắt đầu ở tỉ lệ ghi cao rồi dần chuyển sang đọc cao khi có người dùng thật; lựa chọn đúng của năm ngoái có thể là lựa chọn sai của năm nay. Vì thế điều đáng ghi lại trong tài liệu kiến trúc không phải là "chúng tôi dùng chỉ mục cục bộ" mà là "chúng tôi dùng chỉ mục cục bộ vì tỉ lệ đọc/ghi hiện là X" — để người sau biết khi nào nên xem lại.

    Nối với chương sau

    Câu "cập nhật chỉ mục toàn cục một cách đồng bộ đòi hỏi giao dịch phân tán" không phải một câu nói cho qua — nó là cửa ngõ sang Chương 7 và Chương 9. Cái giá của tính nhất quán xuyên mảnh chính là chủ đề mà phần còn lại của Part II dành để bóc tách.

Trục 3 · Rebalancing partitionsAxis 3 · Rebalancing partitions

Dời mảnh mà không dời cả thế giớiMoving partitions without moving the world

Mọi thứ đều đổi: thông lượng truy vấn tăng nên cần thêm CPU, dữ liệu phình nên cần thêm đĩa, máy hỏng nên máy khác phải gánh việc của nó. Việc chuyển tải và dữ liệu từ node này sang node khác gọi là tái cân bằng, và Kleppmann đặt ra ba yêu cầu tối thiểu cho nó: sau khi xong, tải phải được chia công bằng; trong lúc đang chạy, cơ sở dữ liệu phải tiếp tục nhận đọc và ghi; và không được dời nhiều dữ liệu hơn mức cần thiết1. Yêu cầu thứ ba nghe hiển nhiên, nhưng chính nó loại thẳng cách làm mà ai cũng nghĩ ra đầu tiên.

Tủ hồ sơ không đứng yên: hồ sơ ngày một nhiều, có hôm một cái tủ gãy chân. Lúc đó phải dọn lại — chuyển bớt hồ sơ sang tủ mới. Dọn thì phải: chia lại cho đều, vẫn cho người ta tra hồ sơ trong lúc dọn, và đừng bê nhiều hơn mức cần thiết. Nhớ nhé: cái khó không phải là dọn, mà là dọn ít nhất có thể.

Everything changes: query throughput grows so you need more CPU, the dataset swells so you need more disk, a machine fails so another must take over its work. Moving load and data from one node to another is called rebalancing, and Kleppmann sets three minimum requirements for it: afterwards the load must be fairly shared; while it runs the database must keep accepting reads and writes; and no more data than necessary should move1. The third sounds obvious, but it's precisely what rules out the approach everyone thinks of first.

Rebalancing · the three minimum requirements

Tái cân bằng là việc dời phân mảnh giữa các node, chứ không phải việc tính lại xem khóa nào thuộc mảnh nào. Mọi chiến lược tốt trong chương đều tuân theo cùng một nguyên tắc: giữ nguyên ánh xạ khóa → mảnh, chỉ đổi ánh xạ mảnh → node.

Dọn tủ là bê nguyên cả ngăn sang tủ mới, chứ không phải lôi từng tờ ra xếp lại. Nhớ nhé: dời ngăn, đừng dời từng tờ.

Rebalancing moves partitions between nodes; it does not recompute which key belongs to which partition. Every good strategy in the chapter follows the same principle: keep the key → partition mapping fixed, and change only the partition → node mapping.

Nguyên tắc ấy là chìa khóa để hiểu vì sao hash mod N hỏng còn các chiến lược khác thì không. hash mod N gắn chặt việc chọn mảnh vào số node — nên đổi số node là đổi luôn ánh xạ khóa → mảnh, và mọi khóa phải đi tìm nhà mới. Bốn chiến lược còn lại đều chèn một tầng gián tiếp vào giữa: khóa ánh xạ tới một mảnh cố định, mảnh ánh xạ tới một node có thể đổi. Đây là phiên bản phân tán của một câu cũ trong khoa học máy tính — thêm một tầng gián tiếp thì giải được gần như mọi bài toán.

Sau: công bằng Tải và dữ liệu phải được chia đều giữa các node sau khi tái cân bằng xong.
Trong: vẫn phục vụ Cơ sở dữ liệu phải tiếp tục nhận đọc và ghi trong suốt quá trình dời.
Tối thiểu: dời ít Không dời nhiều dữ liệu hơn mức cần thiết — tiêu chí loại thẳng hash mod N.
Dọn xong phải đều Không tủ nào còn nặng hơn hẳn tủ khác.
Dọn mà vẫn tra được Người ta vẫn lấy và cất hồ sơ trong lúc bạn đang dọn.
Bê càng ít càng tốt Chỉ bê những ngăn thật sự cần chuyển chỗ.
After: fair Load and data must be shared evenly between nodes once rebalancing finishes.
During: still serving The database must keep accepting reads and writes throughout the move.
Minimal: move little No more data than necessary should move — the criterion that rules out hash mod N.
  1. Why hash mod N is a trap

    Cái bẫy mang tên hash mod NThe trap called hash mod N

    Cách hiển nhiên nhất để chia khóa cho N máy là lấy hash(key) mod N. Nó hỏng đúng vào lúc ta cần nó nhất: N đổi thì gần như mọi khóa phải dời. Kleppmann đưa một ví dụ đếm được: giả sử hash(key) = 123456. Với 10 node, khóa nằm ở node 6, vì 123456 mod 10 = 6. Tăng lên 11 node, nó phải sang node 3 (123456 mod 11 = 3). Tăng tiếp lên 12 node, nó lại phải sang node 0 (123456 mod 12 = 0)1. Việc di chuyển dư thừa ấy vi phạm thẳng yêu cầu thứ ba, và làm cho tái cân bằng đắt tới mức không dùng được.

    Cách dễ nghĩ nhất: đếm số tủ rồi chia. Có 10 tủ thì hồ sơ này vào tủ 6. Mua thêm một tủ nữa thành 11 tủ — phép chia đổi, hồ sơ đó phải sang tủ 3. Mua thêm nữa thành 12 — lại phải sang tủ 0. Mỗi lần mua thêm một cái tủ là gần như cả kho phải bê đi bê lại. Nhớ nhé: cách dễ nghĩ nhất ở đây lại là cách tệ nhất.

    The obvious way to split keys across N machines is hash(key) mod N. It fails exactly when you need it most: if N changes, almost every key has to move. Kleppmann gives a countable example: say hash(key) = 123456. With 10 nodes the key sits on node 6, because 123456 mod 10 = 6. Grow to 11 nodes and it must move to node 3 (123456 mod 11 = 3). Grow to 12 and it must move again, to node 0 (123456 mod 12 = 0)1. That excess movement violates the third requirement outright and makes rebalancing unusably expensive.

    Bài học rút raĐừng để số node xuất hiện trong công thức chọn mảnh. Mọi chiến lược tốt phía dưới đều tách hai việc ra: khóa thuộc mảnh nào là chuyện cố định; mảnh nằm ở node nào là chuyện có thể đổi.
    Bài họcĐừng chọn ngăn bằng cách "đếm xem có mấy tủ". Ngăn cứ giữ nguyên, chỉ đổi chỗ đặt ngăn.
    The lessonNever let the node count appear in the partition-choosing formula. Every good strategy below splits the two: which partition a key belongs to is fixed; which node a partition sits on can change.

    Vì sao lỗi này hay tái diễn

    mod N là cách chuẩn để chọn ô trong một bảng băm cục bộ, nơi việc rehash toàn bộ khi bảng lớn lên là chuyện rẻ và trong bộ nhớ. Sai lầm nằm ở việc mang nguyên phản xạ ấy sang một cụm máy, nơi "rehash" nghĩa là đẩy hàng terabyte qua mạng. Đây là một trường hợp cụ thể của điều Kleppmann đã cảnh báo trong bài viết về hàm băm: cùng một phép toán nhưng hai mục đích khác nhau đòi hỏi hai thiết kế khác nhau6.

    Không phải mọi mod đều xấu

    Bản thân phép chia dư không có tội — vấn đề là chia dư cho số node. Elasticsearch chẳng hạn vẫn băm rồi phân giải tài liệu về một nhóm nhân bản21, nhưng số shard chính là một hằng số của chỉ mục, không phải số máy trong cụm. Chia dư cho một hằng số thì an toàn; chia dư cho một biến thay đổi theo cụm thì không.

  2. Fixed number of partitions

    Số phân mảnh cố định: tạo dư ngay từ đầuA fixed number of partitions: over-provision up front

    Chiến lược đơn giản nhất mà vẫn đúng: tạo nhiều hơn hẳn số mảnh so với số node, rồi giao vài mảnh cho mỗi node. Thêm một node? Nó "ăn bớt" vài mảnh từ mỗi node hiện có cho tới khi cân bằng lại. Chỉ có mảnh di chuyển, còn việc khóa nào thuộc mảnh nào thì không đổi. Riak làm đúng thế: vòng ring được chia sẵn thành các partition, mỗi node chạy khoảng (số partition)/(số node) vnode, và các node đều cố giữ số vnode xấp xỉ bằng nhau17. Couchbase thì nói thẳng ra ràng buộc: số vBucket được ấn định lúc tạo bucket và không đổi sau đó19.

    Mẹo: ngay từ đầu chia tủ thành thật nhiều ngăn nhỏ, nhiều hơn hẳn số tủ bạn có. Mỗi tủ giữ vài chục ngăn. Mua tủ mới? Chỉ việc bê vài ngăn từ mỗi tủ cũ sang tủ mới — không phải mở ngăn ra xếp lại tờ nào cả. Nhớ nhé: chia sẵn nhiều ngăn nhỏ thì sau này dọn rất nhẹ.

    The simplest strategy that still works: create many more partitions than nodes, then assign several to each node. Adding a node? It steals a few partitions from every existing node until things balance again. Only partitions move; which key belongs to which partition never changes. Riak does exactly this: the ring is pre-divided into partitions, each node runs roughly (number of partitions)/(number of nodes) vnodes, and nodes each try to run about the same number17. Couchbase spells out the constraint: the number of vBuckets is fixed when the bucket is created and does not change afterwards19.

    Cái giáCon số chọn lúc đầu là một cái trần. Chọn quá ít thì sau này không chia nhỏ thêm được; chọn quá nhiều thì mỗi mảnh mang chi phí quản lý riêng. Và vì nó cố định, đây là quyết định khó sửa nhất trong cả chương.
    Cái giáSố ngăn chọn lúc đầu là cố định. Chia ít quá thì sau này chật, chia nhiều quá thì rối. Phải đoán trước.
    The priceThe number you pick up front is a ceiling. Too few and you can't subdivide later; too many and each partition carries its own management overhead. And because it's fixed, this is the hardest decision in the chapter to undo.

    Con số thật, hôm nay

    Tài liệu Couchbase hiện thời cho một ví dụ cụ thể về việc con số ấy không phải một hằng số vũ trụ mà là một lựa chọn kỹ thuật: bucket dùng storage engine Couchstore có 1024 vBucket trên Linux và Windows, bucket dùng Magma có thể chọn 128 hoặc 1024, còn trên macOS thì mọi bucket đều có 64 vBucket bất kể storage engine19. Đây là hành vi của bản hiện tại, không phải con số trong khung trình bày của sách năm 2017.

    Vì sao nó chống được mod N

    Vì phép băm chia dư cho số mảnh — một hằng số — chứ không cho số node. Couchbase băm khóa bằng CRC32 để ánh xạ vào vBucket19; Riak băm cặp bucket/key thành 160 bit rồi đặt lên vòng17. Trong cả hai trường hợp, thêm hay bớt máy không hề đụng tới phép tính ấy — chỉ đụng tới bảng "mảnh nào ở máy nào".

  3. Dynamic partitioning

    Phân mảnh động: tách khi phình, gộp khi teoDynamic partitioning: split when big, merge when small

    Với phân mảnh theo dải khóa, một số mảnh cố định là ý tồi: ranh giới đặt sai thì có mảnh rỗng, có mảnh chứa tất cả. Vì thế các hệ theo dải khóa thường tự điều chỉnh: mảnh vượt ngưỡng thì tách đôi, mảnh teo xuống thì gộp lại với hàng xóm. HBase là ví dụ kinh điển, và câu chuyện khởi động của nó rất đáng nhớ: khi một bảng vừa được tạo, HBase mặc định chỉ cấp cho nó một region, nên mọi yêu cầu trong giai đoạn nạp dữ liệu đầu tiên đều dồn về một region server bất kể cụm có bao nhiêu máy15.

    Với tủ chia theo bảng chữ cái thì cố định số ngăn là dở: ngăn "A" luôn đầy còn ngăn "X" luôn trống. Nên người ta để tủ tự chia: ngăn nào đầy quá thì cắt làm đôi, ngăn nào trống quá thì nhập với ngăn bên cạnh. Có điều tủ mới tinh thì chỉ có đúng một ngăn, nên lúc đầu mọi tờ hồ sơ đều đổ vào một chỗ. Nhớ nhé: ngăn tự chia thì tiện, nhưng lúc mới bắt đầu vẫn phải đẩy nó một tay.

    With key-range partitioning, a fixed partition count is a bad idea: get the boundaries wrong and some partitions are empty while one holds everything. So range-partitioned systems usually adjust themselves: a partition over a threshold splits in two, one that shrinks merges with a neighbour. HBase is the classic example, and its start-up story is memorable: when a table is first created HBase by default allocates it just one region, so every request during the initial data-loading phase goes to a single region server regardless of how many machines the cluster has15.

    Cách chữaHBase gọi nó là pre-splitting: bạn tự cung cấp các điểm tách ngay lúc tạo bảng, để lần nạp dữ liệu đầu tiên phân bố đều khắp cụm. Tài liệu nói rõ vì sao hệ không tự làm hộ: nó không thể đoán điểm tách, vì việc đó phụ thuộc nặng vào phân bố khóa trong dữ liệu của bạn15.
    Cách chữaLúc đóng tủ mới thì tự chia sẵn vài ngăn theo kinh nghiệm của mình, đừng để tủ chỉ có một ngăn.
    The fixHBase calls it pre-splitting: you supply the split points at table-creation time so the initial load spreads across the cluster. The docs say plainly why the system can't guess: split points depend heavily on the distribution of keys in your data15.

    Tách theo chính sách cắm được

    Bài viết kỹ thuật của Hortonworks (nay thuộc Cloudera) mô tả rằng bất kể có pre-split hay không, một region vượt quá ngưỡng sẽ tự động tách làm đôi, và thời điểm cũng như cách tính điểm tách được quyết định bởi API RegionSplitPolicy cắm được — bài viết liệt kê ConstantSizeRegionSplitPolicy, IncreasingToUpperBoundRegionSplitPolicy và KeyPrefixRegionSplitPolicy15. Lưu ý phiên bản: bài viết ấy mô tả HBase 0.94 ở thời điểm 2013 và bản thân trang blog cũng ghi rằng nó được đăng trước khi Hortonworks sáp nhập vào Cloudera; ngưỡng và chính sách mặc định hiện thời phải tra ở HBase Reference Guide bản hiện hành16.

    Không chỉ dành cho dải khóa

    Phân mảnh động cũng dùng được cho dữ liệu phân mảnh theo băm — mỗi mảnh khi đó giữ một dải của không gian băm thay vì một dải của không gian khóa, và cơ chế tách/gộp vẫn y nguyên. Ưu điểm chung là số mảnh tự bám theo lượng dữ liệu: kho nhỏ thì ít mảnh nên ít chi phí thừa, kho lớn thì mảnh tự sinh thêm.

  4. Partitioning proportionally to nodes

    Tỉ lệ theo số node: cố định mảnh trên mỗi máyProportional to nodes: fix partitions per machine

    Chiến lược thứ ba đứng giữa hai cái trên: cố định số mảnh trên mỗi node. Số node tăng thì tổng số mảnh tăng theo, còn kích thước mỗi mảnh giữ tương đối ổn định. Cassandra là hiện thân của cách này qua cơ chế vnode: tài liệu Cassandra giải thích rằng một vòng với đúng một token mỗi node vật lý rất khó mở rộng từng bước, vì với ít máy và token cách đều thì không có vị trí token nào cho máy mới mà vẫn giữ được cân bằng — nên Cassandra gán nhiều token cho mỗi node vật lý, cho phép một máy chiếm nhiều vị trí trên vòng13.

    Cách thứ ba: quy định mỗi tủ luôn giữ đúng, ví dụ, 16 ngăn. Mua tủ mới thì tổng số ngăn tăng lên, còn mỗi ngăn vẫn to như cũ. Nhớ nhé: giữ cho mỗi tủ một số ngăn cố định thì kho lớn lên mà không ngăn nào phình.

    The third strategy sits between the two: fix the number of partitions per node. Add nodes and the total partition count rises, while each partition's size stays roughly constant. Cassandra embodies this through vnodes: its documentation explains that a ring with exactly one token per physical node is hard to scale incrementally, because with few machines and evenly spaced tokens there's no token position for a new node that leaves the ring balanced — so Cassandra assigns multiple tokens to each physical node, letting one machine occupy several ring positions13.

    Ràng buộc ngầmVì node mới chọn ranh giới một cách ngẫu nhiên, chiến lược này đòi hỏi phân mảnh phải dựa trên băm — chỉ khi đó các ranh giới mới có thể được rút ra từ không gian số một cách công bằng. Đây cũng là chỗ gần nhất mà thực tiễn CSDL chạm tới thuật toán gốc mang tên "consistent hashing".
    Điều kiệnCách này chỉ chạy được khi ta chia ngăn theo kiểu bốc thăm, chứ không theo bảng chữ cái.
    The hidden constraintBecause a new node picks boundaries at random, this strategy requires hash-based partitioning — only then can boundaries be drawn fairly from the number space. It's also the closest database practice gets to the original algorithm named "consistent hashing".

    Vì sao Dynamo cần vnode

    Tài liệu Cassandra dựng lại lập luận rất rõ: trong một cụm tám node với token cách đều, không có cách nào chèn token thứ chín mà không gây mất cân bằng — muốn cân bằng thì phải chèn thẳng tám token vào giữa các dải hiện có13. Node ảo giải bài toán ấy bằng cách cho một máy giữ nhiều token, để việc thêm một lượng nhỏ năng lực cũng có chỗ đặt vừa vặn.

    Cân bằng token không miễn phí

    Nhiều token mỗi node làm cho việc phân bố dải khóa mượt hơn, nhưng cũng nhân số dải mà mỗi thao tác quản trị phải xử lý và làm tăng chi phí phối hợp. Con số token mỗi node vì thế là một tham số vận hành thật sự, không phải một hằng số — và nó thuộc loại nên tra ở tài liệu phiên bản mình đang chạy chứ không nên nhớ theo trí nhớ.

  5. Automatic or manual rebalancing?

    Tự động hay để người bấm nút?Automatic, or a human pressing the button?

    Câu hỏi cuối của trục này không phải kỹ thuật mà là vận hành. Tái cân bằng hoàn toàn tự động thì tiện, nhưng Kleppmann cảnh báo rằng nó nguy hiểm vì có thể cộng hưởng với cơ chế phát hiện lỗi: một node đang quá tải phản hồi chậm, các node khác tưởng nó chết và bắt đầu dời dữ liệu khỏi nó — việc dời ấy làm mạng và các node còn lại nặng thêm, khiến thêm node bị tưởng là chết. Một vòng xoáy tự khuếch đại. Vì thế ông nghiêng về việc để con người trong vòng lặp: hệ tự đề xuất kế hoạch tái cân bằng, nhưng phải có người duyệt trước khi nó chạy1.

    Câu cuối không phải chuyện máy móc mà là chuyện con người. Nếu tủ tự động dọn mỗi khi thấy một tủ "có vẻ yếu", có khi nó dọn nhầm lúc tủ ấy chỉ đang bận — và việc dọn lại làm cả kho bận hơn, rồi nó lại tưởng tủ khác yếu, cứ thế. Nhớ nhé: máy đề xuất, người bấm nút.

    The last question on this axis isn't technical but operational. Fully automatic rebalancing is convenient, but Kleppmann warns it's dangerous because it can resonate with failure detection: an overloaded node responds slowly, others assume it's dead and start moving data off it — the move loads the network and the remaining nodes further, making more nodes look dead. A self-amplifying spiral. So he leans toward keeping a human in the loop: let the system propose a rebalancing plan, but require someone to approve it before it runs1.

    Nối với Chương 1Đây là lập luận về lỗi-con-người của Chương 1 nhìn từ chiều ngược lại: không phải mọi việc tự động hóa đều giảm rủi ro. Tự động hóa một hành động đắt và khó đảo ngược trong khi tín hiệu kích hoạt nó vốn đã mơ hồ thì là nhân rủi ro lên, không phải chia nó xuống.
    Ý chínhKhông phải cứ tự động là an toàn hơn. Việc càng nặng và càng khó làm lại thì càng nên có người xem trước.
    Back to Chapter 1This is Chapter 1's human-error argument seen from the other side: not every automation reduces risk. Automating an expensive, hard-to-undo action on an already-ambiguous trigger multiplies risk rather than dividing it.

    Vì sao tín hiệu lại mơ hồ

    Vấn đề nằm ở chỗ "chậm" và "chết" trông giống hệt nhau từ phía bên kia của một đường mạng. Đây chính là bài toán mà Chương 8 sẽ đặt tên đầy đủ — không có cách nào phân biệt chắc chắn giữa một node đã chết, một node chậm, và một mạng bị nghẽn. Tái cân bằng tự động là một trong những nơi mà sự bất khả ấy chuyển hóa thành một sự cố có thật.

    Nửa tự động là lựa chọn thật

    Không phải chọn giữa "hoàn toàn tự động" và "hoàn toàn thủ công". Nhiều hệ sinh kế hoạch tự động rồi chờ lệnh xác nhận, hoặc chạy tự động nhưng có hạn mức băng thông và giới hạn số mảnh dời cùng lúc. Điểm chung là đặt một cái van giữa việc phát hiện và việc hành động — đủ để một vòng xoáy không kịp hình thành.

Trục 4 · Request routingAxis 4 · Request routing

Mảnh đã dời — giờ tìm dữ liệu ở đâu?The partition moved — so where is the data now?

Khi client muốn đọc hoặc ghi khóa X, nó phải nối tới máy nào? Vì các phân mảnh liên tục được gán lại sau mỗi lần tái cân bằng, câu trả lời thay đổi theo thời gian. Kleppmann chỉ ra rằng đây chỉ là một trường hợp riêng của bài toán tổng quát hơn — service discovery — và rằng chỉ có ba hình dạng kiến trúc để trả lời, khác nhau ở chỗ đặt tri thức về bản đồ phân mảnh1.

Dọn tủ xong thì hồ sơ đổi chỗ. Người đến tra hỏi: "tờ của tôi giờ ở tủ nào?" Có ba cách trả lời: hỏi đại một tủ rồi nó chỉ đường, hỏi một anh thủ thư đứng giữa, hoặc tự mình giữ một tấm bản đồ. Nhớ nhé: điều khác nhau duy nhất là ai giữ tấm bản đồ.

When a client wants to read or write key X, which machine should it connect to? Because partitions keep getting reassigned after every rebalance, the answer changes over time. Kleppmann points out this is a special case of a more general problem — service discovery — and that there are only three architectural shapes for answering it, differing in where the knowledge of the partition map lives1.

Request routing · who holds the map?

Định tuyến yêu cầu không phải bài toán "chuyển tiếp thế nào" mà là bài toán "ai biết sự thật, và biết lúc nào". Ba kiến trúc khác nhau ở chỗ đặt tri thức ấy — trong node dữ liệu, trong một tầng định tuyến riêng, hay trong chính client.

Câu hỏi thật không phải "chỉ đường kiểu gì" mà là "ai giữ bản đồ": mỗi cái tủ, anh thủ thư đứng giữa, hay chính người đi tra.

Request routing isn't a question of "how do we forward" but of "who knows the truth, and when". The three architectures differ in where that knowledge sits — in the data nodes, in a separate routing tier, or in the client itself.

Chỗ đặt tri thức quyết định luôn chế độ hỏng của hệ. Đặt trong node dữ liệu thì không có thành phần nào thêm để hỏng, nhưng các node phải nói chuyện với nhau liên tục để đồng bộ bản đồ. Đặt trong một tầng định tuyến thì client đơn giản nhất, nhưng tầng ấy trở thành một thành phần phải vận hành, giám sát và nhân bản. Đặt trong client thì độ trễ thấp nhất — bớt được một chặng mạng — nhưng mọi client phải được cập nhật khi bản đồ đổi, và một client cũ mang bản đồ cũ là một nguồn lỗi tinh vi.

Hỏi node nào cũng được Client nối tới một node bất kỳ; node nào giữ mảnh thì trả lời, không thì chuyển tiếp hộ.
Tầng định tuyến Mọi yêu cầu đi qua một proxy biết phân mảnh; nó chỉ làm mỗi việc chuyển tiếp đúng chỗ.
Client tự biết Client giữ bản đồ phân mảnh và nối thẳng tới node đúng, không qua trung gian nào.
Hỏi đại một tủ Tủ nào có thì đưa, không có thì chỉ sang tủ đúng.
Hỏi thủ thư Có một người đứng giữa, ai hỏi gì cũng chỉ đúng chỗ.
Tự cầm bản đồ Người đi tra tự biết đường, đi thẳng tới tủ.
Ask any node The client connects to any node; if it holds the partition it answers, otherwise it forwards.
Routing tier All requests go through a partition-aware proxy whose only job is forwarding to the right place.
Partition-aware client The client holds the partition map and connects directly to the right node, with no middleman.
  1. Three shapes, one question

    Ba hình dạng cho cùng một câu hỏiThree shapes for one question

    Hình dạng thứ nhất: client nối tới bất kỳ node nào — thường qua một bộ cân bằng tải quay vòng; nếu node ấy tình cờ giữ phân mảnh cần tìm thì nó xử lý luôn, còn không thì nó chuyển tiếp yêu cầu tới node đúng rồi trả kết quả về. Hình dạng thứ hai: mọi yêu cầu đi qua một tầng định tuyến — một proxy biết phân mảnh, tự nó không xử lý dữ liệu, chỉ chuyển tiếp. Hình dạng thứ ba: client tự biết bản đồ và nối thẳng1. MongoDB là hiện thân sạch sẽ của hình dạng thứ hai: tài liệu nói mongos là giao diện duy nhất của một cụm phân mảnh từ góc nhìn ứng dụng, và ứng dụng không bao giờ nối hay giao tiếp trực tiếp với các shard12.

    Cách một: cứ hỏi đại một cái tủ. Nếu tủ đó có thì đưa luôn; không có thì nó chạy sang tủ đúng lấy hộ. Cách hai: có một anh thủ thư đứng cửa, ai hỏi gì anh cũng biết nó nằm ở tủ nào. Cách ba: người đi tra được phát sẵn một tấm bản đồ, tự đi thẳng tới tủ. Nhớ nhé: ba cách chỉ khác nhau ở chỗ ai phải nhớ đường.

    Shape one: the client connects to any node — usually via a round-robin load balancer; if that node happens to hold the partition it handles the request, otherwise it forwards it to the right node and passes the answer back. Shape two: every request goes through a routing tier — a partition-aware proxy that handles no data itself and only forwards. Shape three: the client knows the map and connects directly1. MongoDB is a clean instance of shape two: its docs state that mongos is the only interface to a sharded cluster from an application's perspective, and applications never connect to or communicate with the shards directly12.

    Điều thật sự khác nhauKhông phải hiệu năng, mà là chỗ đặt sự phức tạp. Client mỏng thì tầng định tuyến dày; client dày thì phải lo việc cập nhật bản đồ cho tất cả client đang chạy ngoài kia.
    Điều khác nhauAi phải nhớ đường thì người đó phải được cập nhật khi đường thay đổi.
    What actually differsNot performance, but where the complexity lives. A thin client means a thick routing tier; a thick client means keeping every client out there up to date.

    Chuyển tiếp không phải là miễn phí

    Hình dạng thứ nhất trông gọn vì không thêm thành phần nào, nhưng nó trả giá bằng một chặng mạng phụ mỗi khi đoán trượt — và với một cụm nhiều mảnh, xác suất đoán trượt là gần như chắc chắn. Nó cũng đòi hỏi mọi node dữ liệu phải giữ một bản sao của bản đồ phân mảnh và đồng bộ nó liên tục với nhau, thường qua một giao thức gossip.

    Cùng một bài toán ở tầng khác

    Ba hình dạng này lặp lại gần như y hệt ba cách làm khám phá dịch vụ ở tầng ứng dụng: để dịch vụ tự chuyển tiếp, đặt một sidecar/proxy ở giữa, hay để client tự phân giải qua một registry. Không phải trùng hợp — cả hai đều là bài toán "ánh xạ một cái tên tới một địa chỉ đang thay đổi", nên không gian lời giải giống nhau.

  2. Coordination service · who holds the truth

    Ai giữ sự thật về bản đồ?Who holds the truth about the map?

    Dù chọn hình dạng nào, vẫn còn một câu hỏi khó hơn: thành phần ra quyết định định tuyến làm sao biết phân mảnh đã đổi chỗ? Nhiều hệ phân tán giao việc ấy cho một dịch vụ điều phối riêng, điển hình là ZooKeeper — một dịch vụ điều phối phân tán, mã nguồn mở, phơi ra một tập nguyên thủy đơn giản để các ứng dụng phân tán dựng lên các dịch vụ cấp cao hơn như đồng bộ hóa, duy trì cấu hình và đặt tên nhóm; dữ liệu của nó được giữ trong bộ nhớ theo một không gian tên phân cấp gồm các znode, và thiết kế ưu tiên hiệu năng cao, tính sẵn sàng cao cùng truy cập có thứ tự chặt23.

    Dù chọn cách nào thì vẫn có một câu khó: ai là người cập nhật tấm bản đồ khi tủ được dọn lại? Nhiều nơi thuê hẳn một "người giữ sổ cái" — mọi thay đổi phải ghi vào đó, và ai cần đường thì hỏi nó. Nhớ nhé: phải có đúng một chỗ giữ sự thật, không thì mỗi người chỉ một đường.

    Whichever shape you pick, a harder question remains: how does the component making routing decisions learn that a partition moved? Many distributed systems hand that job to a separate coordination service, typically ZooKeeper — a distributed, open-source coordination service that exposes a simple set of primitives distributed applications build higher-level services on, such as synchronization, configuration maintenance, and group naming; its data is kept in memory in a hierarchical namespace of znodes, and the design puts a premium on high performance, high availability and strictly ordered access23.

    Cùng một khuôn, sản phẩm khácMongoDB không dùng ZooKeeper nhưng dựng đúng khuôn ấy: các config server giữ siêu dữ liệu của cụm — gồm danh sách chunk trên mỗi shard và các dải định nghĩa những chunk đó — còn mongos thì cache dữ liệu ấy, dùng nó để định tuyến, và cập nhật cache khi siêu dữ liệu thay đổi1112.
    Ví dụ thậtCó hệ để riêng vài "cuốn sổ cái" ghi tủ nào giữ ngăn nào; anh thủ thư chép lại vào sổ tay của mình và chép lại mỗi khi sổ cái đổi.
    Same mould, different productMongoDB doesn't use ZooKeeper but builds the same shape: config servers store the cluster's metadata — including the list of chunks on every shard and the ranges that define them — while mongos caches that data, uses it to route, and refreshes the cache when the metadata changes1112.

    Cache là con dao hai lưỡi

    Việc mongos cache siêu dữ liệu thay vì hỏi config server cho mỗi yêu cầu là điều khiến định tuyến nhanh — và cũng là điều khiến nó có thể sai trong một khoảng ngắn sau khi chunk vừa dời. Kiến trúc kiểu này vì thế luôn phải có đường phục hồi: node nhận nhầm phải biết nói "tôi không còn giữ dải này nữa" để bên gọi làm mới cache, chứ không được lặng lẽ trả kết quả rỗng.

    Không phải hệ nào cũng cần dịch vụ ngoài

    Có một nhánh thiết kế khác tránh hẳn thành phần điều phối riêng: để các node dữ liệu tự đồn đại trạng thái vòng cho nhau qua giao thức gossip. Tài liệu Cassandra liệt kê đúng gossip là một trong những thứ nó vay từ Dynamo, cùng với phân mảnh bằng băm và nhân bản đa chủ13. Đổi lại sự đơn giản về vận hành, bạn nhận lấy một trạng thái hội tụ dần thay vì một nguồn sự thật duy nhất.

  3. Parallel query execution · MPP

    Truy vấn song song: khi một câu lệnh chạy trên mọi mảnhParallel execution: when one query runs on every partition

    Cho tới đây ta mới chỉ nói về đọc và ghi một khóa — cộng thêm scatter/gather cho chỉ mục phụ. Đó là mức độ phức tạp của phần lớn CSDL NoSQL phân tán. Nhưng các CSDL phân tích song song ồ ạt (massively parallel processing, MPP) đi xa hơn nhiều: trình tối ưu bẻ một truy vấn phức tạp thành nhiều giai đoạn và toán tử, rồi cho chúng chạy song song trên các node giữ các mảnh khác nhau1. Ý tưởng không mới — DeWitt và Gray đã lập luận từ 1992 rằng hệ CSDL song song là tương lai của CSDL hiệu năng cao, chính vì các truy vấn quan hệ vốn rất hợp với thực thi song song24.

    Từ đầu tới giờ ta chỉ nói chuyện lấy một tờ hồ sơ. Nhưng có loại câu hỏi lớn hơn nhiều: "tính tổng tất cả hóa đơn năm ngoái". Lúc đó không ai đi lấy từng tờ — mà giao cho mỗi tủ tự cộng phần của mình, rồi cộng các kết quả lại. Nhớ nhé: câu hỏi lớn thì chia việc ra cho mọi tủ cùng làm một lúc.

    So far we've only discussed reading and writing one key — plus scatter/gather for secondary indexes. That's the level of complexity most distributed NoSQL databases reach. But massively parallel processing (MPP) analytic databases go much further: the query optimizer breaks a complex query into stages and operators and runs them in parallel across the nodes holding different partitions1. The idea is not new — DeWitt and Gray argued in 1992 that parallel database systems were the future of high performance databases, precisely because relational queries suit parallel execution so well24.

    Vì sao nó khép lại chươngVì nó cho thấy phân mảnh không chỉ là chuyện lưu trữ. Một khi dữ liệu đã nằm trên nhiều máy, chính cách tính toán cũng có thể được phân mảnh — và đó là cây cầu dẫn sang phần xử lý theo mẻ và theo luồng ở cuối sách.
    Ý chínhChia dữ liệu ra nhiều chỗ thì cũng chia luôn được việc tính toán ra nhiều chỗ.
    Why it closes the chapterBecause it shows partitioning isn't only about storage. Once data lives on many machines, the computation can be partitioned too — the bridge to the batch and stream processing parts at the end of the book.

    Lập luận gốc năm 1992

    Bài báo của DeWitt và Gray trên Communications of the ACM lập luận rằng các máy CSDL song song dựa trên bộ xử lý phổ thông — chứ không phải phần cứng chuyên dụng đắt tiền — là con đường đi tới hiệu năng cao, vì các toán tử quan hệ hoạt động trên những luồng dữ liệu đồng nhất và có thể ghép nối song song một cách tự nhiên24. Kleppmann đưa bài này vào danh mục tham chiếu chính thức của Chương 63.

    Khoảng cách giữa hai thế giới

    Điều đáng chú ý là khoảng cách năng lực: một kho khóa–giá trị phân tán coi scatter/gather đã là đỉnh cao của độ phức tạp truy vấn, trong khi một hệ MPP coi đó là bước khởi động. Sự chênh lệch ấy không phải vì bên nào kém hơn, mà vì hai bên tối ưu cho hai loại tải khác nhau — đúng như phân biệt OLTP/OLAP đã dựng ở Chương 3.

Ba ngộ nhận thường gặp về phân mảnhThree common misreadings of partitioning

“Consistent hashing là cách tái cân bằng mặc định của CSDL”"Consistent hashing is how databases rebalance by default"

Đây là cái bẫy thuật ngữ lớn nhất của chương. Thuật toán gốc mang tên ấy do Karger và cộng sự đưa ra năm 1997 cho một bài toán khác hẳn: giảm hoặc triệt tiêu điểm nóng trong các mạng caching phân tán trên web, nơi không máy chủ nào có thông tin đầy đủ về trạng thái toàn mạng4. Kleppmann lưu ý rằng trong ngữ cảnh cơ sở dữ liệu, cụm từ này bị dùng rất lỏng so với định nghĩa gốc, rằng cách tiếp cận nguyên bản thực ra hoạt động không tốt cho CSDL nên hiếm khi được dùng, và ông khuyên gọi thẳng là hash partitioning cho đỡ nhầm13. Bằng chứng cho sự trôi nghĩa ấy nằm ngay trong tài liệu sản phẩm: Cassandra đặt tên mục phân mảnh của mình là “Dataset Partitioning: Consistent Hashing”13. Dòng thuật toán gốc thì vẫn sống khỏe ở chỗ khác — libketama cho client memcached25 và thuật toán jump consistent hash của Lamping & Veach5.

This is the chapter's biggest terminological trap. The original algorithm by Karger et al. (1997) addressed a quite different problem: relieving hot spots in distributed web caching networks where no server has complete information about the whole network's state4. Kleppmann notes that in a database context the phrase is used loosely compared with that original definition, that the original approach actually doesn't work well for databases and is rarely used, and he suggests just saying hash partitioning instead13. The evidence of the drift is in product docs: Cassandra titles its partitioning section "Dataset Partitioning: Consistent Hashing"13. The original algorithmic line lives on elsewhere — libketama for memcached clients25 and Lamping & Veach's jump consistent hash5.

“Băm khóa là hết chuyện điểm nóng”"Hashing the key ends hot spots"

Sai, và đây là ngộ nhận tốn kém nhất trong vận hành. Băm rải đều khóa trong không gian số; nó không nói gì về việc bao nhiêu yêu cầu sẽ đổ vào từng khóa. Khi tải lệch tập trung vào một khóa duy nhất, mọi yêu cầu vẫn rơi vào một mảnh, và Kleppmann nói rõ rằng phần lớn hệ dữ liệu hiện chưa tự bù được điều này — cách chữa phải do ứng dụng làm, bằng cách tách khóa nóng ra nhiều khóa con và chịu chi phí gộp lúc đọc1. Nói cách khác, băm giải bài toán lệch dữ liệu, không giải bài toán lệch tải.

Wrong, and operationally it's the costliest misreading. Hashing spreads keys evenly through the number space; it says nothing about how many requests land on each key. When skew concentrates on a single key, every request still hits one partition, and Kleppmann says plainly that most data systems can't compensate for this automatically — the fix has to come from the application, splitting the hot key into sub-keys and paying a merge cost on read1. Hashing solves data skew, not load skew.

“Có phân mảnh rồi thì khỏi cần nhân bản”"Once you partition, you don't need replication"

Sai: hai thứ giải hai bài toán trực giao nhau và luôn được dùng chồng lên nhau. Phân mảnh giải bài toán dung lượng và thông lượng; nhân bản giải bài toán chịu lỗi và đọc gần. Kafka minh họa cấu trúc này gọn gàng: một topic được phân mảnh thành nhiều partition nằm trên các broker khác nhau, một sự kiện mới được nối vào đúng một partition, và các sự kiện cùng khóa luôn vào cùng một partition — trong khi mọi topic đều có thể được nhân bản, và việc nhân bản ấy được thực hiện ở cấp topic-partition26. Đúng một mảnh cho mỗi bản ghi; nhiều bản sao cho mỗi mảnh.

Wrong: they solve orthogonal problems and are always stacked. Partitioning solves capacity and throughput; replication solves fault tolerance and local reads. Kafka illustrates the structure neatly: a topic is partitioned across brokers, a new event is appended to exactly one of the topic's partitions, and events with the same key always go to the same partition — while every topic can be replicated, and that replication is performed at the level of topic-partitions26. Exactly one partition per record; several copies per partition.

Tự đọc mìnhRead yourself

Soi cách phân mảnh của chính hệ bạnTest your own system's partitioning

Chương 6 cho một bộ câu hỏi rất dễ áp dụng, vì phần lớn có thể trả lời bằng cách nhìn vào lược đồ và bảng cấu hình chứ không cần đo đạc gì. Chúng không hỏi bạn dùng CSDL nào, mà hỏi bạn đã biết ranh giới của mình nằm ở đâu và ai đang giữ bản đồ hay chưa.

Thử soi một hệ thật bằng mấy câu dưới. Phần lớn chỉ cần mở cấu hình ra nhìn là trả lời được. Nhớ nhé: không biết mình chia theo cái gì thì cũng không biết chỗ nào sẽ nóng.

Chapter 6 gives questions that are easy to apply, because most can be answered by looking at a schema and a config file rather than by measuring anything. They don't ask which database you use; they ask whether you know where your boundaries are and who holds the map.

Khóa phân mảnh của bạn là gì, và nó có tăng đơn điệu không?Nếu khóa là dấu thời gian hay một ID tăng dần, mọi lần ghi mới đều rơi vào cùng một mảnh — MongoDB mô tả chính xác cơ chế ấy qua chunk có biên trên MaxKey10. Hãy trả lời trước khi tải tăng, chứ không phải sau khi biểu đồ đã lệch.
Bạn chia theo dải khóa hay theo băm — và bạn đã mất cái gì vì lựa chọn đó?Chia theo dải thì được quét theo dải nhưng mua lấy rủi ro điểm nóng; chia theo băm thì đều hơn nhưng mọi truy vấn theo dải trở thành phát tán toàn cụm. Nếu bạn không gọi tên được cái mình đã mất, rất có thể bạn chưa thật sự chọn — mà chỉ đang dùng mặc định của sản phẩm.
Có khóa nào của bạn nóng hơn hẳn phần còn lại không?Không phải mảnh nào to nhất, mà khóa nào bị hỏi nhiều nhất. Đây là hai chỉ số khác nhau và cần hai biểu đồ khác nhau. Nếu có một khóa nóng thật, hãy nhớ rằng băm không cứu được nó — chỉ có việc tách khóa ở tầng ứng dụng, kèm chi phí gộp lúc đọc1.
Chỉ mục phụ của bạn là cục bộ hay toàn cục, và bạn có biết không?Nếu cục bộ, mỗi truy vấn theo chỉ mục phụ là một lần scatter/gather và độ trễ của nó bằng độ trễ của mảnh chậm nhất. Nếu toàn cục, hãy hỏi tiếp: chỉ mục được cập nhật đồng bộ hay bất đồng bộ, và cửa sổ trễ ấy có ai đo không?
Số phân mảnh của bạn cố định hay tự điều chỉnh?Nếu cố định, con số ấy là một cái trần — Couchbase nói thẳng rằng số vBucket không đổi sau khi tạo bucket19. Nếu tự điều chỉnh, hãy kiểm tra trạng thái khởi đầu: một bảng HBase mới chỉ có một region, nên lần nạp dữ liệu đầu tiên dồn về một máy nếu bạn không pre-split15.
Nếu thêm một node, bao nhiêu dữ liệu phải di chuyển?Nếu câu trả lời gần với "gần hết", nghĩa là số node đang nằm trong công thức chọn mảnh — cái bẫy hash mod N1. Một thiết kế lành mạnh phải trả lời được bằng một phân số nhỏ, và phải nói được con số ấy trước khi thực sự thêm máy.
Tái cân bằng của bạn tự động hay có người duyệt?Tự động hoàn toàn có thể cộng hưởng với phát hiện lỗi thành một vòng xoáy: node chậm bị tưởng chết, việc dời dữ liệu làm cụm nặng hơn, thêm node bị tưởng chết1. Nếu hệ bạn tự động, hãy kiểm tra xem có van nào ở giữa không — hạn mức băng thông, giới hạn số mảnh dời đồng thời, hay một bước xác nhận.
Ai đang giữ bản đồ phân mảnh của bạn, và cập nhật nó mất bao lâu?Node dữ liệu qua gossip, một tầng định tuyến, hay chính client? Và khi bản đồ đổi, khoảng thời gian có một bên cầm bản đồ cũ là bao lâu? Với MongoDB, đó là cửa sổ giữa lúc config server đổi và lúc mongos làm mới cache của nó12.
Bạn chia dữ liệu ra các ngăn theo cái gì?Theo thứ tự chữ cái, hay theo bốc thăm? Hai cách cho hai kết quả rất khác nhau.
Có tờ hồ sơ nào ai cũng đòi xem không?Nếu có, cái ngăn chứa nó sẽ kẹt — và chia lại ngăn cũng không cứu được.
Mua thêm một cái tủ thì phải bê bao nhiêu hồ sơ?Nếu câu trả lời là "gần hết kho", cách chia của bạn đang sai chỗ nào đó.
Ai giữ tấm bản đồ chỉ xem hồ sơ nằm ở tủ nào?Phải có đúng một chỗ giữ sự thật, và mọi người phải biết khi nó đổi.
What is your partition key, and does it increase monotonically?If it's a timestamp or an increasing ID, every new write lands on the same partition — MongoDB describes exactly that through the chunk bounded by MaxKey. Answer it before load grows, not after the chart tilts.
Do you partition by key range or by hash — and what did that cost you?Range buys you scans and sells you hot-spot risk; hash buys evenness and sells you range queries. If you can't name what you gave up, you probably haven't chosen — you're on the product default.
Is any one of your keys much hotter than the rest?Not which partition is biggest, but which key is asked for most. Two different metrics, two different charts. Hashing won't save a genuinely hot key; only application-level key splitting will, at a merge cost on read.
Are your secondary indexes local or global — and do you know?If local, every secondary-index query is a scatter/gather whose latency equals the slowest partition's. If global, ask next: is it updated synchronously or asynchronously, and does anyone measure the lag window?
Is your partition count fixed or self-adjusting?If fixed, that number is a ceiling — Couchbase states outright that the vBucket count doesn't change after bucket creation. If self-adjusting, check the starting state: a new HBase table has one region, so the first load lands on one machine unless you pre-split.
If you add one node, how much data has to move?If the answer is near "almost all of it", the node count is inside your partition-choosing formula — the hash mod N trap. A healthy design answers with a small fraction, and can state it before the machine is added.
Is your rebalancing automatic, or does a human approve it?Full automation can resonate with failure detection into a spiral: a slow node is presumed dead, the data movement loads the cluster further, more nodes look dead. If yours is automatic, check for a valve — bandwidth caps, a limit on concurrent partition moves, or a confirmation step.
Who holds your partition map, and how long does an update take to spread?Data nodes via gossip, a routing tier, or the client itself? And when the map changes, how long is the window in which someone holds a stale copy? For MongoDB, that's the gap between a config-server change and mongos refreshing its cache.
Kết luận cô đọngThe conclusion, distilled
Phân mảnh không phải là kỹ thuật làm cho hệ lớn hơn, mà là kỷ luật vẽ ranh giới — và mọi ranh giới đều thu tiền ở một đầu nào đó. Chia theo dải thì rẻ khi quét nhưng dễ nóng; chia theo băm thì đều nhưng mất thứ tự; chỉ mục cục bộ thì ghi gọn mà đọc tán, chỉ mục toàn cục thì ngược lại. Điều chương này thật sự dạy là một phản xạ: khi nghe một khẩu hiệu kiến trúc, hãy hỏi ngay nó thu tiền ở đâu. Và khi ranh giới phải dịch chuyển, đừng để số máy lọt vào công thức — hãy giữ khóa thuộc mảnh nào là cố định, chỉ đổi mảnh nằm ở máy nào.Chia dữ liệu ra nhiều ngăn không phải để "làm cho to hơn", mà là học cách vẽ ranh giới. Cách nào cũng được cái này mất cái kia: chia theo thứ tự thì tra dải nhanh mà dễ kẹt một ngăn; chia theo bốc thăm thì đều mà mất thứ tự. Nhớ nhé: nghe ai khoe một cách chia, hãy hỏi ngay "thế mất gì?" — và khi dọn tủ, hãy bê nguyên cả ngăn chứ đừng xếp lại từng tờ.Partitioning isn't a technique for making systems bigger; it's the discipline of drawing boundaries — and every boundary charges you somewhere. Range is cheap to scan but easy to overheat; hash is even but loses order; local indexes make writes tidy and reads scattered, global indexes the reverse. What the chapter really teaches is a reflex: when you hear an architectural slogan, ask immediately where it charges you. And when boundaries must move, never let the machine count into the formula — keep which partition a key belongs to fixed, and change only which machine holds the partition.