Skip to content

Replication ​

Replication là giữ nhiều bản copy của cùng một dữ liệu trên nhiều máy. Mỗi máy giữ một bản copy gọi là một replica. Trang này nói phần chung cho mọi database; cách MySQL làm cụ thể (binlog, GTID, semi-sync, Group Replication) nằm ở MySQL chương 9.

Replicate để làm gì ​

  • Không mất dữ liệu, không ngừng phục vụ. Một máy hỏng ổ cứng hay mất điện thì vẫn còn bản copy ở máy khác, và hệ thống chuyển sang dùng máy đó.
  • Tăng throughput đọc. Có ba replica thì có thể chia lượng đọc ra ba máy.
  • Giảm latency theo vùng. Đặt replica ở gần người dùng (Singapore cho người dùng châu Á, Frankfurt cho châu Âu), nên mỗi lần đọc không phải đi nửa vòng trái đất.

Replication không tăng được throughput ghi tổng. Mỗi lệnh ghi cuối cùng vẫn phải được áp dụng trên mọi replica, nên mỗi máy vẫn phải làm toàn bộ lượng ghi. Muốn chia tải ghi thì cần partitioning: mỗi máy chỉ giữ một phần dữ liệu. Thực tế hai kỹ thuật đi cùng nhau, mỗi partition có vài replica.

Sync và async replication ​

Khi một replica nhận lệnh ghi, nó phải gửi lệnh đó sang các replica khác. Câu hỏi là client có phải chờ các replica kia nhận xong không.

SyncAsync
Client nhận OK khiMọi replica (hoặc các replica được chọn) đã nhận lệnh ghiReplica đầu tiên đã ghi xong
Latency ghiCộng thêm thời gian gửi sang replica chậm nhấtThấp
Một replica sậpKhông ghi được nữa nếu chờ mọi replicaKhông ảnh hưởng
Đọc từ replica khácThấy ngay dữ liệu mớiCó thể thấy dữ liệu cũ
Mất dữ liệu khi máy nhận ghi sậpKhôngCó thể mất các lệnh ghi chưa kịp gửi đi

Sync tới mọi replica gần như không ai dùng, vì một replica sập hay chậm là cả hệ thống không ghi được. Cách hay gặp hơn là chờ ít nhất một replica (MySQL gọi là semi-sync), hoặc chờ đa số replica (consensus, xem Đồng thuận). PostgreSQL và MySQL mặc định đều dùng async (MySQL chương 9).

Với async replication, nếu ngừng ghi một lúc thì các replica rồi sẽ giống nhau, nhưng không có cam kết bao lâu. Tính chất này gọi là eventual consistency.

Replicate cái gì ​

Replica nhận lệnh ghi theo một trong ba dạng:

DạngGửi điVấn đềVí dụ
StatementCâu SQL gốcCâu có hàm không tất định (UUID(), RAND(), SYSDATE()) hay UPDATE … LIMIT không có thứ tự rõ ràng sẽ cho kết quả khác nhau trên replicaMySQL binlog_format = STATEMENT
Physical (WAL)Byte thay đổi trên từng page của storage engineGắn chặt với định dạng lưu trữ, nên primary và replica phải cùng major version và cùng kiến trúc máyPostgreSQL streaming replication
Logical (row)Giá trị từng dòng bị thêm, sửa, xoáTốn chỗ hơn statement khi một câu sửa nhiều dòngMySQL binlog_format = ROW (mặc định), PostgreSQL logical replication

Logical log không phụ thuộc storage engine, nên các hệ thống khác đọc được. Đây chính là nền tảng của CDC (MySQL chương 9).

Ba mô hình replication ​

Single leaderMulti leaderLeaderless
Nhận lệnh ghiChỉ leaderMọi leaderMọi replica; client ghi vào nhiều replica cùng lúc
Write conflictKhông cóCó, phải xử lýCó, phải xử lý
Khi máy nhận ghi sậpPhải failover, dễ saiLeader khác vẫn nhận ghiKhông cần failover
Ví dụMySQL, PostgreSQL, MongoDB, DynamoDB (mỗi partition một leader)MySQL Group Replication chế độ multi-primary, ứng dụng offline-firstCassandra, ScyllaDB, Riak

Phần lớn hệ thống dùng single leader. Hai mô hình còn lại giải quyết bài toán ghi ở nhiều nơi, và đổi lại phải xử lý write conflict.

Single leader ​

Mọi lệnh ghi đi vào leader. Leader ghi vào replication log rồi gửi log sang các follower. Client đọc được từ leader hoặc từ follower.

Follower sập ​

Follower nhớ vị trí cuối cùng mình đã áp dụng trong replication log (MySQL dùng GTID). Ví dụ follower áp dụng tới lệnh ghi thứ 50 thì mất điện. Lúc nó chạy lại, leader đã ghi tới 70, nên follower xin leader gửi tiếp từ 51 tới 70 rồi đuổi kịp. Đây là trường hợp dễ.

Leader sập: failover ​

Phải chọn một follower lên làm leader mới, rồi chuyển client và các follower khác sang leader mới. Bước này gọi là failover, và có ba chỗ dễ sai:

  1. Khó biết leader sập thật hay chỉ chậm. Mạng không cam kết thời gian gửi tin, nên chỉ có cách đoán bằng timeout. Timeout ngắn thì failover nhầm khi leader chỉ đang bận; timeout dài thì hệ thống không ghi được lâu hơn.
  2. Mất lệnh ghi. Với async replication, leader cũ có thể đã nhận lệnh ghi 71 tới 80 mà follower mới chỉ có tới 70. Khi follower đó lên làm leader, các lệnh ghi 71 tới 80 biến mất dù client đã nhận OK.
  3. Split brain. Leader cũ chạy lại mà vẫn tưởng mình là leader, nên có hai leader cùng nhận ghi. Phải chặn leader cũ, ví dụ bằng fencing token (Distributed lock) hoặc cắt hẳn nó khỏi mạng.

Muốn mọi node cùng đồng ý ai là leader thì cần consensus. Vì vậy các công cụ failover tự động thường dựa vào một hệ consensus: Patroni (PostgreSQL) dùng etcd, Consul hoặc ZooKeeper; MySQL Group Replication tự chạy một biến thể của Paxos.

Ưu điểm: đơn giản, không có write conflict, thêm follower là tăng được throughput đọc. Nhược điểm: throughput ghi giới hạn ở một máy; failover khó làm đúng.

Replication lag: ba kiểu đọc sai ​

Với async replication, follower luôn chậm hơn leader một chút. Khoảng chậm này gọi là replication lag, bình thường vài mili giây nhưng có lúc lên tới vài phút (nguyên nhân trên MySQL). Không bỏ được lag hoàn toàn, nhưng có ba kiểu đọc sai làm người dùng khó chịu nhất, và cả ba đều có cách chặn.

Kiểu đọc saiVí dụCách chặn
Không đọc được thứ mình vừa ghi (cần read-your-writes)Đổi trạng thái thành “đang hẹn hò”, tải lại trang vẫn thấy “độc thân” vì đọc từ follower chưa nhận lệnh ghiĐọc từ leader trong vài giây sau khi ghi; hoặc nhớ vị trí log (GTID) của lệnh ghi và chỉ đọc follower đã áp dụng tới đó
Đọc lùi về quá khứ (cần monotonic reads)Bấm làm mới lần đầu thấy 3 tin nhắn, lần sau thấy 2, vì lần sau đọc phải follower chậm hơnMỗi người dùng luôn đọc từ cùng một follower, ví dụ chọn theo hash(user_id) % số follower
Thấy câu trả lời trước câu hỏi (cần consistent prefix reads)Thấy “Ừ, đi” trước “Trưa nay đi ăn không?” vì hai tin nằm ở hai partition, partition chứa câu trả lời replicate nhanh hơnGhi các dữ liệu phụ thuộc nhau (câu hỏi và câu trả lời) vào cùng một partition, để chúng đi qua cùng một replication log theo đúng thứ tự

Chi tiết cách làm read-your-writes trên MySQL: Đọc lại thứ vừa ghi.

Multi leader ​

Nhiều leader cùng nhận ghi. Mỗi leader gửi lệnh ghi của mình sang các leader khác, thường bằng async replication.

Khi nào dùng:

  • Nhiều data center. Mỗi data center có một leader, nên người dùng ghi vào data center gần mình, và một data center sập thì data center khác vẫn nhận ghi.
  • Ứng dụng chạy offline. Ứng dụng ghi chú hay lịch trên điện thoại cho sửa khi mất mạng; mỗi thiết bị thực chất là một leader, có mạng lại thì đồng bộ.
  • Sửa chung tài liệu theo thời gian thực: mỗi người sửa trên bản của mình rồi gộp lại.

Trong một data center thì gần như không đáng dùng multi leader: lợi ích nhỏ mà phải xử lý write conflict.

Topology ​

Các leader gửi lệnh ghi cho nhau theo một sơ đồ:

TopologyCách gửiVấn đề
Vòng (circle)Mỗi leader gửi cho leader kế tiếpMột node sập là cả vòng tắc
Sao (star)Mọi leader gửi về một node trung tâm, node này phát lại cho các node khácNode trung tâm sập là tắc
All-to-allMọi leader gửi cho mọi leaderLệnh ghi có thể đến sai thứ tự: một node nhận lệnh B (sửa dòng X) trước lệnh A (tạo dòng X)

Để một lệnh ghi không chạy vòng mãi giữa các leader, mỗi lệnh ghi mang danh sách các node đã nhận nó; node nào thấy tên mình trong danh sách thì không gửi tiếp.

Write conflict ​

Hai người cùng sửa một bản ghi trên hai leader khác nhau, mà không ai biết lệnh ghi của người kia. Hai lệnh ghi như vậy gọi là concurrent, dù thời điểm có thể lệch nhau vài giây. Khi hai leader gửi lệnh ghi cho nhau, phải chọn giá trị cuối cùng. Có bốn cách:

  1. Tránh conflict. Mọi lệnh ghi cho cùng một bản ghi đi vào cùng một leader, ví dụ mọi thay đổi trên tài khoản của một user đi vào leader ở vùng của user đó. Đơn giản nhất, nhưng khi leader đó sập hay user đổi vùng thì vẫn có conflict.
  2. Last write wins (LWW). Lệnh ghi có timestamp lớn hơn thắng. Dễ làm nhưng mất dữ liệu, vì lệnh ghi thua bị bỏ đi mà không ai biết. Timestamp lại không đáng tin:
    • Nếu dùng giờ của client thì client sửa được. Một người chỉnh giờ điện thoại sang năm 2100 là lệnh ghi của họ luôn thắng.
    • Nếu dùng giờ của server thì đồng hồ các máy lệch nhau (clock skew), vì thạch anh trong mỗi máy chạy nhanh chậm khác nhau. NTP định kỳ chỉnh lại giờ, nhưng một máy đang chạy nhanh bị NTP kéo lùi 1 giây thì lệnh ghi sau lại mang timestamp nhỏ hơn lệnh ghi trước, và lệnh ghi sau thua.
  3. Giữ cả hai giá trị (siblings) rồi để ứng dụng hoặc người dùng chọn. Ví dụ hai thành viên cùng đổi tên nhóm chat thành “Team Backend” và “Team API” trên hai leader. Database giữ cả hai tên, lần đọc sau ứng dụng hỏi người dùng chọn tên nào rồi ghi lại một giá trị. Muốn biết hai lệnh ghi có concurrent hay không thì dùng version vector, ở mục dưới.
  4. Để database tự gộp bằng CRDT, ở mục dưới.

Version vector: nhận ra hai lệnh ghi concurrent ​

Mỗi replica giữ một vector, mỗi phần tử là số lệnh ghi nó từng nhận từ một replica. Ví dụ có hai leader L1, L2: vector [3, 5] nghĩa là đã nhận 3 lệnh ghi từ L1 và 5 lệnh ghi từ L2.

So hai vector A và B:

  • Mọi phần tử của A lớn hơn hoặc bằng phần tử tương ứng của B: A xảy ra sau B, A thay thế B được.
  • Có phần tử A lớn hơn và có phần tử B lớn hơn, ví dụ [2, 1] và [1, 2]: hai lệnh ghi concurrent, không ai biết người kia, nên phải xử lý conflict.

Ví dụ: [2, 1, 2] so với [1, 1, 1] thì mọi phần tử đều lớn hơn hoặc bằng, nên replica giữ [2, 1, 2] đã có đủ mọi thứ mà [1, 1, 1] mang tới và bỏ qua nó được. Còn [5, 2] so với [4, 3] thì bên đầu hơn ở L1 nhưng thua ở L2, nên đây là hai lệnh ghi concurrent.

Ví dụ counter lượt thích ở hai leader. L1 nhận 3 lượt thích, L2 nhận 5 lượt. Hai leader gửi giá trị cho nhau:

Cách lưuL1L2Sau khi gộpĐúng không
Một con số, gộp bằng max355Sai, mất 3 lượt của L1
Một con số, gộp bằng tổng358, nhưng lần gửi sau lại cộng dồn thành 16Sai
Vector, gộp bằng max từng ô[3, 0][0, 5][3, 5], tổng 8Đúng

Sau đó L1 nhận thêm 1 lượt thành [4, 5], tổng 9, rồi gửi cho L2. L2 gộp [4, 5] với [3, 5] bằng max từng ô, ra [4, 5], tổng 9. Không có chuyện cộng 9 với 8 thành 17, vì vector cho biết hai bên đã trùng nhau ở phần [3, 5]. Đây chính là G-Counter ở mục CRDT bên dưới.

Riak dùng version vector rồi trả về mọi sibling cho ứng dụng tự gộp. Ứng dụng giỏ hàng có thể gộp bằng cách lấy hợp các món hàng.

CRDT ​

CRDT (conflict-free replicated data type) là các kiểu dữ liệu được thiết kế sao cho các replica luôn gộp được với nhau mà không có conflict, và cuối cùng ra cùng một kết quả.

KiểuCách lưuGộp
G-Counter (counter chỉ tăng)Mỗi replica một ô đếm số lần tăng của chính nóLấy max từng ô; giá trị là tổng các ô
PN-Counter (tăng và giảm)Hai G-Counter: một cho tăng, một cho giảmGộp từng G-Counter; giá trị là tổng tăng trừ tổng giảm
2P-SetMột tập phần tử đã thêm, một tập phần tử đã xoáHợp từng tập. Đã xoá thì không thêm lại được
OR-SetMỗi lần thêm gắn một tag ngẫu nhiên; xoá là xoá đúng các tag mình đã thấyThêm lại được sau khi xoá, và lệnh thêm concurrent với lệnh xoá sẽ thắng
Sequence CRDTDanh sách có thứ tự, mỗi phần tử có định danhDùng cho sửa chung văn bản (thư viện Yjs, Automerge)

Ví dụ với danh sách đi chợ dùng chung. Một người thêm “trứng, sữa” trên L1, người kia thêm “bánh mì, cà phê” trên L2. Gộp hai set ra cả bốn món, ở replica nào cũng vậy.

  • Với 2P-Set, nếu ai đó xoá “sữa” thì “sữa” nằm mãi trong tập đã xoá, nên lần sau thêm “sữa” lại cũng không hiện.
  • Với OR-Set, lần thêm đầu là sữa#a1, xoá là xoá đúng sữa#a1. Lần thêm sau là sữa#b7, một tag mới chưa bị xoá, nên “sữa” hiện lại.

Có hai cách gửi CRDT giữa các replica:

  • Gửi thao tác (operation-based), ví dụ “tăng ô của L1 thêm 1”. Gọn, nhưng mỗi thao tác phải đến đúng một lần và đúng thứ tự nhân quả, nên cần hệ thống gửi tin tin cậy. Ví dụ L1 thêm “trứng” rồi xoá “trứng”. Nếu thao tác “thêm” bị mất trên đường tới L2 thì L2 nhận lệnh xoá một món không có. Nếu thao tác “thêm” đến hai lần thì có thể thành hai quả trứng.
  • Gửi toàn bộ trạng thái (state-based) rồi gộp. Hàm gộp phải giao hoán, kết hợp và idempotent (nhận lại cùng một trạng thái nhiều lần cũng không sai), nên tin đến trùng hay sai thứ tự đều không sao. Vì vậy nó hợp với gossip protocol: mỗi node gửi ngẫu nhiên cho vài node khác, lan dần ra cả cluster. Đổi lại, trạng thái lớn thì gửi tốn băng thông.

Redis Enterprise dùng CRDT cho chế độ Active-Active (nhiều vùng cùng nhận ghi). Riak có sẵn counter, set và map dạng CRDT.

Leaderless ​

Không có leader. Client gửi lệnh ghi tới nhiều replica cùng lúc, và đọc cũng từ nhiều replica cùng lúc. Mô hình này xuất phát từ bài báo Dynamo của Amazon (2007), và được Cassandra, ScyllaDB, Riak dùng. DynamoDB thì không: mỗi partition của DynamoDB có một leader bầu bằng Multi-Paxos, và chỉ leader nhận ghi.

Mỗi giá trị có một version. Khi đọc từ nhiều replica, client lấy giá trị có version lớn nhất. Replica nào trượt lệnh ghi (đang sập, mạng chập chờn) sẽ đuổi kịp bằng hai cơ chế:

  • Read repair. Lúc đọc, client thấy một replica trả version cũ thì ghi giá trị mới vào replica đó. Cách này chỉ sửa được dữ liệu có người đọc.
  • Anti-entropy. Một tiến trình chạy nền so dữ liệu giữa các replica rồi gửi phần khác nhau. Để không phải so từng dòng, mỗi replica dựng một Merkle tree: lá là hash của từng khoảng dữ liệu, mỗi node cha là hash của các node con. Hai replica so hash từ gốc xuống, nhánh nào hash bằng nhau thì bỏ qua, nên chỉ phải đi vào đúng nhánh khác nhau. Cassandra làm việc này khi chạy nodetool repair.

Anti-entropy không gửi lại cả replication log như single leader, vì hai replica thường đã có phần lớn lệnh ghi giống nhau. Ví dụ replica 1 đã nhận các lệnh ghi a, c, e, f, g, còn replica 2 có b, c, d, f, g. Replica 1 chỉ cần gửi a và e, replica 2 chỉ cần gửi b và d. Merkle tree giúp tìm ra đúng các phần thiếu đó mà không phải duyệt cả bảng.

Thử: Merkle tree tìm dòng khác nhau giữa hai replica
Bấm để sửa dòng ở replica B:
85152493677899795364916904468337260845907693a=10b=45c=63d=7e=21f=88g=5h=34
1hash phải so
0dòng cần gửi
8dòng nếu so từng dòng
hash khác nhau: đi xuống hai node conhash bằng nhau: bỏ qua cả nhánh bên dướikhông phải so

Mỗi lá là hash của một dòng, mỗi node cha là hash của hai node con. Hash ở đây chỉ có 3 chữ số cho dễ nhìn; hệ thống thật dùng hash dài hơn nhiều và mỗi lá thường là một khoảng nhiều dòng. Với 8 dòng thì chưa thấy lợi nhiều, nhưng bảng có hàng triệu dòng mà chỉ lệch vài dòng thì số hash phải so chỉ tăng theo log của số dòng.

Quorum ​

Gọi N là số replica của một key. Client coi lệnh ghi thành công khi W replica xác nhận, và mỗi lần đọc hỏi R replica. Nếu W + R > N thì tập replica được đọc luôn có ít nhất một replica nằm trong tập vừa ghi, nên lần đọc thấy được giá trị mới nhất. Cấu hình hay gặp là N = 3, W = 2, R = 2. Cassandra gọi mức này là QUORUM.

Thử: quorum trong leaderless replication
W + R = 4 > N = 3. Tập node đọc luôn giao với tập node vừa ghi ít nhất 1 node.
node 1Av1
node 2Av1
node 3Av1
có giá trị mới nhất đã ghi thành cônggiá trị của lệnh ghi báo lỗivừa ghivừa đọc

Gợi ý: để N = 3, W = 2, R = 2, bấm “Ghi lỗi giữa chừng” rồi “Đọc 20 lần”, sẽ thấy lúc đọc ra giá trị mới, lúc ra giá trị cũ. Đổi R = 1 rồi ghi bình thường và đọc 20 lần để thấy đọc trượt khi W + R ≤ N. Mỗi lần ghi và đọc chọn node ngẫu nhiên.

W và R là chỗ đánh đổi. W nhỏ thì ghi nhanh nhưng đọc phải hỏi nhiều replica; W = N thì một replica sập là không ghi được.

Quorum vẫn không đảm bảo đọc đúng mọi lúc ​

Dù W + R > N, vẫn có những trường hợp hai client đọc cùng lúc mà thấy hai giá trị khác nhau:

  • Nhiều lệnh ghi concurrent đến các replica theo thứ tự khác nhau. Ví dụ ba client cùng ghi x = 1, x = 2, x = 3 vào cả ba replica. Replica 1 nhận theo thứ tự 1, 2, 3 nên giữ 3; replica 2 nhận 3, 1, 2 nên giữ 2; replica 3 nhận 2, 3, 1 nên giữ 1. Hai client cùng đọc quorum 2 replica sẽ thấy hai cặp giá trị khác nhau, và không replica nào biết giá trị nào mới nhất. Nếu dùng LWW để chọn thì hai trong ba lệnh ghi bị mất.
  • Lệnh ghi lỗi giữa chừng. Ghi thành công ở 1 replica, thiếu replica thứ 2 nên client nhận lỗi. Nhưng replica kia không rollback, nên có lần đọc thấy giá trị mới, có lần không. Read repair hay anti-entropy còn có thể lan giá trị đó ra cả cluster. Bấm “Ghi lỗi giữa chừng” trong mô phỏng để thấy.
  • Sloppy quorum. Khi không liên lạc được đủ W replica “chính chủ” của một key, Dynamo và Riak cho ghi tạm vào replica khác trong cluster để không từ chối lệnh ghi. Lúc đó tập ghi và tập đọc có thể không giao nhau. Khi replica chính chủ sống lại, replica tạm gửi trả dữ liệu, gọi là hinted handoff. Cassandra cũng có hinted handoff (mặc định giữ hint trong 3 giờ), nhưng hint không được tính vào W.
  • Một replica chứa giá trị mới bị hỏng và được khôi phục từ replica cũ hơn, nên số replica có giá trị mới tụt xuống dưới W.

Vì vậy quorum chỉ cho eventual consistency mạnh hơn một chút, không phải linearizable.

Replication và linearizability ​

Linearizable nghĩa là hệ thống cư xử như thể dữ liệu chỉ có một bản: một khi có client đọc được giá trị mới, mọi lần đọc sau đó (của bất kỳ client nào) cũng phải thấy giá trị đó hoặc mới hơn. Distributed lock, bầu leader, kiểm tra trùng username đều cần tính chất này (Distributed lock).

Mô hìnhLinearizable?
Single leader, đọc từ follower, asyncKhông, vì follower có thể chậm
Single leader, chỉ đọc từ leaderGần như có, nhưng failover với async replication làm mất lệnh ghi, và leader cũ chưa biết mình bị thay vẫn có thể trả lời
Multi leaderKhông, vì có write conflict
Leaderless với quorumKhông, vì các trường hợp ở mục trên
Consensus (Raft, Paxos)Có

Hai ví dụ cụ thể:

  • Single leader, chỉ đọc từ leader. Leader nhận x = 5 rồi x = 10, nhưng mới gửi x = 5 sang follower. Client đọc leader thấy 5, đọc lần nữa thấy 10. Leader sập, follower lên thay, client đọc tiếp thấy 5. Giá trị đã lùi về quá khứ dù client chưa từng đọc từ follower.
  • Multi leader. L1 nhận x = 1, L2 nhận x = 5 sau đó. Trước khi hai leader kịp gửi cho nhau, một client đọc L1 thấy 1, đọc L2 thấy 5, đọc lại L1 lại thấy 1.

Muốn xếp mọi lệnh ghi theo một thứ tự chung thì có thể dùng Lamport clock. Mỗi node và mỗi client giữ một bộ đếm. Mỗi lần client ghi vào node, cả hai đặt bộ đếm bằng max của hai bên cộng 1, và lệnh ghi mang số đó kèm tên node. Hai lệnh ghi có cùng số thì so tên node. Ví dụ hai client C1, C2 và hai node A, B, mọi bộ đếm bắt đầu từ 0:

BướcLệnh ghiTínhLamport timestamp
1C1 ghi vào Amax(0, 0) + 1(1, A)
2C2 ghi vào Bmax(0, 0) + 1(1, B)
3C2 ghi vào Bmax(1, 1) + 1(2, B)
4C2 ghi vào Amax(C2 = 2, A = 1) + 1(3, A)
5C1 ghi vào Amax(C1 = 1, A = 3) + 1(4, A)

Thứ tự toàn phần là (1, A), (1, B), (2, B), (3, A), (4, A). Ở bước 4, A nhảy từ 1 lên 3 vì C2 mang theo số 2 từ B, nhờ vậy lệnh ghi C2 làm sau luôn đứng sau lệnh ghi C2 làm trước. Lamport clock gọn hơn version vector vì chỉ gồm một số và một tên node, dù cluster có bao nhiêu node. Nhưng thứ tự này chỉ biết sau khi đã gom đủ mọi lệnh ghi, nên lúc đang ghi một node không biết có lệnh ghi nào khác “đứng trước” mình hay không. Vì vậy Lamport clock không đủ để làm lock hay kiểm tra trùng. Muốn mọi node thống nhất thứ tự ngay lúc ghi thì cần total order broadcast, tức là consensus (Đồng thuận).

Chọn mô hình nào ​

Tình huốngChọn
Ứng dụng web thông thường với MySQL/PostgreSQLSingle leader, async replication, read replica để tăng đọc
Không được mất lệnh ghi đã báo OKSingle leader với semi-sync, hoặc cluster consensus (MySQL Group Replication, etcd)
Người dùng ở nhiều châu lục, cần ghi nhanh ở mọi nơiMulti leader theo vùng, kèm cách xử lý conflict; hoặc partition theo vùng để mỗi user chỉ ghi vào một leader
Ghi rất nhiều, chấp nhận eventual consistency, không muốn failoverLeaderless (Cassandra, ScyllaDB)
Ứng dụng offline-first, sửa chung tài liệuMulti leader với CRDT

Ghi nhớ

  1. Replication tăng độ bền, throughput đọc và giảm latency theo vùng. Nó không chia được tải ghi; việc đó là của partitioning.
  2. Async replication nhanh nhưng có replication lag, và failover có thể làm mất lệnh ghi đã báo OK. Ba kiểu đọc sai cần chặn: không đọc được thứ mình vừa ghi, đọc lùi về quá khứ, thấy câu trả lời trước câu hỏi.
  3. Single leader không có write conflict, nhưng failover khó: khó biết leader sập thật, dễ mất lệnh ghi, dễ split brain.
  4. Multi leader và leaderless phải xử lý write conflict. LWW làm mất dữ liệu; version vector phát hiện được lệnh ghi concurrent; CRDT gộp tự động.
  5. Quorum W + R > N giúp đọc thấy giá trị mới trong đa số trường hợp, nhưng không linearizable. Muốn linearizable thì cần consensus.

Nguồn tham khảo ​

Video gốc (playlist Systems Design 2.0 của kênh Jordan has no life; đọc qua bản chép lời tự động):

VideoDùng cho phần
Intro to ReplicationReplicate để làm gì, sync và async, ba dạng replication log
Dealing with Stale ReadsBa kiểu đọc sai do replication lag
Single Leader ReplicationFollower đuổi kịp, ba vấn đề khi failover
Multi Leader ReplicationTopology, chặn lệnh ghi chạy vòng, tránh conflict, LWW và clock skew
Dealing with Write ConflictsVersion vector, siblings
CRDTsOperation-based và state-based, các kiểu CRDT, gossip
Leaderless Replication IntroductionRead repair, anti-entropy, Merkle tree, DynamoDB không phải leaderless
QuorumsW + R > N, các trường hợp quorum vẫn đọc sai
Replication SummarizedBảng so sánh ba mô hình
Linearizable DatabasesLinearizability, Lamport clock, vì sao cần consensus

Những chỗ trang này không theo video:

  • Video nói replicate dạng statement hỏng vì hàm lấy thời gian hiện tại. Thực ra MySQL coi NOW() là an toàn khi replicate dạng statement; các hàm bị coi là không an toàn là UUID(), RAND(), SYSDATE(), USER()…, cùng UPDATE … LIMIT.
  • Video lấy ví dụ WAL không dùng được khi replicate MySQL sang PostgreSQL. Vấn đề thường gặp hơn là WAL buộc primary và replica phải cùng major version, nên khó nâng cấp từng máy.
  • Video nói multi leader chỉ đọc từ follower. Thực tế đọc từ leader cũng được.
  • Video tổng kết rằng multi leader và leaderless có throughput ghi cao hơn single leader. Mỗi lệnh ghi vẫn phải áp dụng trên mọi replica, nên replication không tăng throughput ghi tổng; cái tăng là latency ghi theo vùng và khả năng ghi khi một máy sập. Chia tải ghi là việc của partitioning.
  • Video mô tả sloppy quorum là chuyển lệnh ghi sang một cluster ở vùng khác. Trong Dynamo, sloppy quorum là ghi tạm vào node khác trong cùng cluster khi node chính chủ không liên lạc được.
  • Video nói sequence CRDT là nền tảng của Google Docs. Google Docs dùng operational transformation (OT), một kỹ thuật khác; CRDT có trong các thư viện như Yjs, Automerge.
  • Video định nghĩa linearizable là các lệnh ghi có thứ tự và đọc không lùi về quá khứ. Định nghĩa đó gần với monotonic reads; linearizable mạnh hơn, đòi hỏi mọi client thấy cùng một bản dữ liệu mới nhất.

Đã đối chiếu (đọc lại nguồn ngày 2026-10-15):

NguồnDùng cho phần
MySQL 8.4 Reference Manual, Safe and Unsafe Statements in Binary LoggingNOW() an toàn; UUID(), RAND(), SYSDATE(), USER(), UPDATE … LIMIT không an toàn
PostgreSQL docs, Log-Shipping Standby ServersStreaming replication mặc định async; không ship WAL giữa các major version khác nhau; phải cùng kiến trúc máy
Cassandra docs, Hints, Repair, Read repairHinted handoff (max_hint_window 3 giờ), repair dùng Merkle tree, read repair chạy khi đọc ở mức QUORUM
Elhemali và cộng sự, Amazon DynamoDB (USENIX ATC 2022)Mỗi partition có leader bầu bằng Multi-Paxos, chỉ leader nhận ghi

Từ kiến thức chung, chưa đối chiếu lần này:

  • Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017), chương 5 (replication) và chương 9 (linearizability, Lamport clock, total order broadcast). Playlist đi theo thứ tự của hai chương này.
  • Cassandra không tính hint vào consistency level (trừ mức ANY); tài liệu Hints chỉ gợi ý chứ không nói thẳng.
  • Patroni dùng etcd, Consul hoặc ZooKeeper; MySQL Group Replication dùng một biến thể Paxos (chương 9 đã nhắc).
  • Redis Enterprise Active-Active dùng CRDT; Riak có kiểu dữ liệu CRDT; Google Docs dùng OT.
  • OR-Set: lệnh thêm concurrent với lệnh xoá thì thêm thắng.