Lập trình 22/09/2026 8 phút

Mạng không đáng tin: đo thật bằng tc netem giữa hai container — mất gói 30% vẫn 137/200 thành công nhưng p99 lên 420 ms, và 10 đơn hàng được tạo mà client tưởng là lỗi

Mở đầu sê-ri hệ thống phân tán bằng cách làm hỏng mạng có chủ đích: tc netem trên một container Go gọi sang container khác. Trễ 50 ms thành 107 ms mỗi request vì bắt tay TCP; mất gói 5% làm p99 nhảy từ 0,3 lên 209 ms vì TCP truyền lại theo bậc 200/400/800 ms; mất 30% vẫn 137/200 thành công nhưng tối đa 829 ms. Phép đo quan trọng nhất: dưới mất gói 30%, client thấy 125 thành công và 75 lỗi, server đếm 135 đơn đã tạo — 10 request client tưởng lỗi nhưng server đã làm. Tải 1 MB ở mất gói 15% không lỗi lần nào, chỉ chậm gấp 160 lần. Phân vùng 3 giây được phát hiện sau đúng một timeout.

Lập trình 22/09/2026 9 phút

Đồng hồ và thứ tự: đo thật trên ba tiến trình — lệch đồng hồ 1 ms đã đủ để đồng hồ tường xếp 300/900 cặp gửi→nhận ngược nhân quả, Lamport và vector thì 0

Ba tiến trình Go gửi 900 thông điệp cho nhau qua HTTP, mỗi sự kiện ghi cả đồng hồ tường, Lamport và vector clock. Cặp gửi→nhận là nhân quả chắc chắn, vậy mà chỉ cần một node lệch đồng hồ 1 ms — nhỏ hơn mọi lời hứa của NTP — đồng hồ tường đã xếp 300/900 cặp ngược: nhận trước gửi. Lamport và vector sai 0 lần ở mọi độ lệch. Trong 270.000 cặp sự kiện ở hai node khác nhau, vector nhận ra 1.269 cặp đồng thời, còn Lamport vẫn gán trước sau cho 1.109 trong số đó. Và LWW theo đồng hồ tường: node có đồng hồ nhanh 20 ms thắng 200/200 dù ghi trước, bản cũ đè bản mới.

Lập trình 22/09/2026 8 phút

Khoá phân tán: đo thật SET NX kèm TTL — tiến trình treo quá TTL làm mất 100/100 cập nhật im lặng, DEL trần xoá khoá của người khác 100/100 lần, và fencing token chặn đủ 100 bản ghi cũ

Khoá bằng Redis SET NX PX với TTL 300 ms, vùng găng là đọc-cộng-ghi số dư trong PostgreSQL. Tiến trình A lấy khoá rồi treo 500 ms, khoá hết hạn, B lấy khoá và ghi; A tỉnh dậy ghi đè: mất 100/100 cập nhật của B, không lỗi, không log. Thêm fencing token tăng dần cấp cùng lúc với khoá và kiểm ở tầng lưu trữ: 100 bản ghi cũ bị từ chối, A biết mà làm lại, số dư đúng 200. Mở khoá bằng DEL trần: A xoá khoá của B, C vào khi B đang làm, 100/100 vòng; so chủ trước khi xoá: 0. Redlock 3 node: khi một Redis chết, một Redis đơn 0/200, Redlock 200/200 với giá một timeout mỗi lần.

Lập trình 22/09/2026 7 phút

Bầu chủ với etcd lease: đo thật trên cụm 3 node — leader chết đột ngột thì chờ đúng TTL (10,01 s với TTL 10 s), từ nhiệm thì 1 ms, và leader cũ tỉnh dậy ghi thẳng 50/50 lần trừ khi dùng Txn

Ba ứng viên dùng etcd concurrency.Election trên lease. Giết leader bằng cách đóng kết nối không revoke: thời gian có leader mới bằng đúng TTL — 1,98 s, 2,98 s, 10,01 s cho TTL 1, 3, 10 giây (etcd đẩy TTL 1 s lên sàn 2 s). Từ nhiệm có chủ ý: 1 ms. Kịch bản đáng sợ hơn: leader A bị treo 4 s với TTL 2 s, B lên thay lúc 2,30 s, A tỉnh dậy lúc 4,07 s vẫn tưởng mình là leader và ghi 50 bản ghi — Put thẳng thành công 50/50, hai leader cùng ghi; Txn kèm điều kiện khoá leader còn tồn tại bị etcd từ chối 50/50. Chi phí: TTL 1 s là 1 keepalive/giây mỗi leader.

Lập trình 22/09/2026 8 phút

Raft trong etcd ba node: đo thật — mất một follower không chậm đi, giết leader là 1,07 giây không ghi được, mất hai node là 0/20 lần ghi nhưng đọc serializable vẫn 20/20

Cụm etcd 3.5.16 ba node, bench Go dùng cả ba endpoint, dừng và giết node bằng docker từ shell. Ba node sống: ghi p50 0,38 ms, đọc linearizable 0,22 ms, serializable 0,08 ms. Dừng một follower: ghi 300/300, p50 0,29 ms, không chậm đi vì đa số vẫn còn. Giết leader giữa lúc ghi liên tục: 1,07 giây không ghi được rồi tự hồi phục với leader mới. Dừng hai node: ghi 0/20, đọc linearizable 0/20, nhưng đọc serializable 20/20 — node sót lại vẫn trả bản cuối nó có. Khởi động lại: ghi được sau 0,45 giây. Cái giá của đồng thuận: 3 node 0,37 ms so với 1 node 0,15 ms, và fsync trên đĩa ảo chỉ 0,06 ms.

Lập trình 22/09/2026 7 phút

Sao chép PostgreSQL primary–replica: đo thật — lag 0,32 ms khi rảnh và 52,67 ms p99 khi bận, đọc ngay từ replica không thấy thứ vừa ghi 500/500 lần, và hai cách chữa với giá của từng cách

Một primary PostgreSQL 16 và một streaming replica dựng bằng pg_basebackup. Lag đo bằng cách ghi rồi poll replica: p50 0,32 ms khi primary rảnh, 3,26 ms và p99 52,67 ms khi hai luồng chèn lô nền. Đọc ngay từ replica sau khi ghi: 500/500 lần không thấy, kể cả khi rảnh. Chữa phía đọc bằng cách chờ replica replay tới LSN của lần ghi: 0/500 — với điều kiện LSN lấy sau commit, lấy trong cùng câu INSERT thì vẫn 187/500 sai. Chữa phía ghi bằng synchronous_commit: remote_apply cho 0/300 với giá 0,24 so với 0,07 ms mỗi commit, và khi replica đồng bộ dừng, primary treo cho tới khi đổi tham số.