Cách triển khai khóa phân tán (2016)
(martin.kleppmann.com)- Redlock dựa trên Redis nhắm tới khóa phân tán chịu lỗi, nhưng thiếu an toàn cho các tác vụ đòi hỏi tính đúng đắn và lại quá phức tạp nếu chỉ dùng để tối ưu hiệu quả
- Trước hết cần phân biệt mục đích của khóa phân tán là hiệu quả để giảm công việc trùng lặp hay tính đúng đắn để bảo vệ trạng thái dùng chung; tiêu chí đánh giá là khi thất bại thì chỉ tăng chi phí hay gây hỏng dữ liệu
- Ngay cả khi có một dịch vụ khóa hoàn hảo, việc GC dừng lâu, tiến trình bị tạm dừng, hay độ trễ mạng vẫn có thể khiến ghi cũ được thực thi sau khi lease hết hạn, nên cần fencing token
- Redlock không thể tạo token tăng đơn điệu cho mỗi lần lấy khóa, và việc hết hạn khóa Redis phụ thuộc vào đồng hồ hệ thống dựa trên
gettimeofday, nên bước nhảy đồng hồ hoặc độ trễ có thể phá vỡ tính an toàn - Với các khóa cần tính đúng đắn, nên dùng hệ thống đồng thuận như ZooKeeper cùng với kiểm tra fencing token; còn khóa Redis một nút chỉ nên giới hạn cho các mục đích gần đúng, không cốt lõi
Điểm xuất phát khi xem xét Redlock
- Redlock là một thuật toán triển khai khóa phân tán chịu lỗi, chính xác hơn là lease, trên Redis
- Đã có hơn 10 bản triển khai độc lập, và không rõ ai đang phụ thuộc vào thuật toán này, nên việc xem xét công khai là có giá trị
- Bản thân Redis rất phù hợp để chia sẻ dữ liệu tạm thời, gần đúng và thay đổi nhanh giữa các máy chủ
- Ví dụ: bộ đếm số yêu cầu theo địa chỉ IP, tập hợp IP duy nhất theo ID người dùng
- Điểm đáng lo là Redis ngày càng được dùng để quản lý dữ liệu nơi người ta kỳ vọng tính nhất quán và độ bền vững mạnh hơn, và khóa phân tán là một trong những lĩnh vực đó
Mục đích của khóa: hiệu quả hay tính đúng đắn
- Trong ứng dụng phân tán, khóa là cơ chế để đảm bảo rằng khi nhiều nút cùng cố thực hiện một công việc thì chỉ một nút làm tại một thời điểm
- Có thể chia lý do dùng khóa thành hai nhóm lớn
- Hiệu quả: một tối ưu hóa nhằm tránh chạy cùng một phép tính tốn kém hai lần; nếu thất bại thì cùng lắm chỉ tăng một chút chi phí AWS hoặc gửi trùng email thông báo
- Tính đúng đắn: một cơ chế ngăn các tiến trình đồng thời làm hỏng cùng một trạng thái; nếu thất bại có thể dẫn đến hỏng file, mất dữ liệu, sai lệch vĩnh viễn, hoặc thậm chí cấp thuốc sai
- Với khóa phục vụ hiệu quả, chi phí và độ phức tạp của Redlock với 5 máy chủ Redis và kiểm tra đa số là không cần thiết
- Dùng một Redis instance đơn lẻ và nếu cần thì sao chép bất đồng bộ sẽ phù hợp hơn
- Trong trường hợp này, có thể mất một số khóa do sự cố nguồn điện hay lỗi nút Redis, nhưng nếu chỉ là tối ưu không cốt lõi thì đó là thất bại có thể chấp nhận
- Vì có 5 bản sao và cơ chế đa số, Redlock trông có vẻ phù hợp cho các khóa quan trọng về tính đúng đắn, nhưng thực tế lại không thích hợp cho mục đích đó
Chỉ lease thôi không thể bảo vệ tài nguyên an toàn
- Khóa trong hệ thống phân tán khác với mutex trong ứng dụng đa luồng, vì nút và mạng có thể hỏng độc lập với nhau nên phức tạp hơn
- Một luồng điển hình khi cập nhật file trên kho lưu trữ dùng chung là: lấy khóa, đọc file, sửa đổi, ghi lại, rồi nhả khóa
- Khóa nhằm ngăn hai client cùng thực hiện read-modify-write đồng thời và làm mất cập nhật
- Nếu client dừng quá lâu khi đang giữ khóa thì lease có thể hết hạn
- GC có thể can thiệp và làm client dừng trong thời gian dài
- Lease là thiết kế tốt để tránh việc client bị crash giữ khóa mãi mãi, nhưng nếu thời gian dừng dài hơn thời gian hết hạn thì client có thể không biết lease đã hết mà vẫn thực hiện một ghi nguy hiểm
- Đây không phải trường hợp lý thuyết; HBase trước đây cũng từng gặp vấn đề tương tự
- Đã từng có các lần GC “stop-the-world” kéo dài nhiều phút
- Ngay cả GC “concurrent” như CMS của HotSpot JVM đôi khi cũng phải dừng ứng dụng
- Kiểm tra xem khóa đã hết hạn ngay trước khi ghi cũng không giải quyết được
- GC có thể dừng thread đang chạy ở bất kỳ điểm nào, kể cả giữa lần kiểm tra cuối cùng và thao tác ghi
Tạm dừng tiến trình và độ trễ mạng là mô hình đe dọa rất bình thường
- Ngay cả khi dùng runtime không có các lần GC dài, tiến trình vẫn có thể bị dừng vì nhiều lý do
- Có thể đọc vào một địa chỉ không nằm trong bộ nhớ và gây page fault
- Nếu đĩa là EBS thì việc đọc một biến có thể biến thành yêu cầu đồng bộ qua mạng Amazon
- Tranh chấp CPU, trễ scheduler, hay một
SIGSTOPgửi nhầm cũng có thể làm tiến trình dừng lại
- Độ trễ mạng cũng tạo ra cùng một vấn đề
- Ứng dụng gửi yêu cầu ghi nhưng gói tin bị trì hoãn và chỉ đến máy chủ lưu trữ sau khi lease đã hết hạn
- Trong một sự cố của GitHub, gói tin mạng đã bị trễ tới khoảng 90 giây
- Các mạng gói như Ethernet và IP có thể trì hoãn gói tin một cách tùy ý, và điều đó thực sự xảy ra
- Vì vậy ngay cả trong mạng được quản lý tốt cũng không thể giả định về timing, và mã chỉ dựa trên lease về bản chất là không an toàn dù dùng dịch vụ khóa nào đi nữa
Cần chặn ghi cũ bằng fencing token
- Giải pháp là đưa fencing token vào mọi yêu cầu ghi tới hệ thống lưu trữ
- Fencing token là một con số tăng lên mỗi khi client lấy được khóa
- Ví dụ: client 1 lấy lease với token 33 rồi dừng rất lâu khiến lease hết hạn
- Client 2 lấy lease mới với token 34 và gửi yêu cầu ghi tới storage
- Sau đó client 1 tỉnh lại và gửi ghi với token 33; storage sẽ từ chối vì nó đã xử lý token 34 cao hơn
- Máy chủ lưu trữ phải chủ động kiểm tra token và từ chối các ghi có token đi lùi thì mới an toàn
- Nếu dịch vụ khóa có thể sinh token tăng đơn điệu nghiêm ngặt thì khóa có thể an toàn
- Nếu dùng ZooKeeper làm dịch vụ khóa thì có thể dùng
zxidhoặc số phiên bản znode làm fencing token
- Nếu dùng ZooKeeper làm dịch vụ khóa thì có thể dùng
- Vấn đề lớn của Redlock là không có khả năng sinh fencing token
- Giá trị ngẫu nhiên duy nhất của Redlock không cung cấp tính tăng đơn điệu cần thiết
- Bộ đếm trên một nút Redis đơn lẻ là không đủ vì nút đó có thể hỏng
- Bộ đếm trên nhiều nút có thể lệch nhau
- Ngay cả để sinh fencing token cũng có thể cần một thuật toán đồng thuận
Redlock phụ thuộc tính an toàn vào các giả định về thời gian
- Trong thuật toán phân tán, mô hình thực tế là mô hình bất đồng bộ với bộ phát hiện lỗi không đáng tin cậy
- Tiến trình có thể dừng trong khoảng thời gian tùy ý
- Gói tin có thể bị trì hoãn tùy ý trong mạng
- Đồng hồ có thể sai lệch tùy ý
- Dù vậy thuật toán vẫn phải đưa ra quyết định đúng
- Đồng hồ chỉ nên được dùng để tạo timeout, nhằm tránh chờ mãi khi một nút bị down
- Timeout không cần chính xác, và việc một yêu cầu bị timeout không có nghĩa nút bên kia chắc chắn đã down
- Đó cũng có thể là do trễ mạng hoặc lỗi đồng hồ cục bộ
- Redis dùng
gettimeofdaychứ không phải monotonic clock để quyết định lúc khóa hết hạngettimeofdaycho phép thời gian hệ thống nhảy không liên tục- Nếu NTP điều chỉnh đồng hồ hoặc quản trị viên đổi giờ thủ công, khóa Redis có thể hết hạn sớm hơn hoặc muộn hơn rất nhiều so với dự kiến
- Các thuật toán trong mô hình bất đồng bộ thường giữ tính an toàn mà không cần giả định về timing, còn các bộ phát hiện lỗi như timeout chỉ ảnh hưởng đến tính sống
- Nếu timing hỗn loạn thì hiệu năng có thể tệ đi, nhưng không được phép dẫn đến quyết định sai
- Redlock thì khác: tính an toàn của nó phụ thuộc vào nhiều giả định về timing
- Mọi nút Redis đều phải giữ khóa trong khoảng thời gian gần đúng
- Độ trễ mạng phải nhỏ hơn đáng kể so với thời gian hết hạn
- Thời gian tiến trình bị dừng phải ngắn hơn rất nhiều so với thời gian hết hạn
Ví dụ Redlock hỏng khi timing xấu
- Với 5 nút Redis A, B, C, D, E và hai client 1, 2, nếu đồng hồ của một nút nhảy về phía trước thì cả hai client đều có thể tin rằng mình đang giữ khóa
- Client 1 lấy khóa ở A, B, C nhưng không chạm được D, E do sự cố mạng
- Đồng hồ của C nhảy lên trước khiến khóa hết hạn
- Client 2 lấy khóa ở C, D, E nhưng không chạm được A, B do sự cố mạng
- Kết quả là cả client 1 và client 2 đều kết luận mình là bên giữ khóa
- Tình huống tương tự cũng có thể xảy ra nếu C bị crash và khởi động lại ngay trước khi ghi bền khóa xuống đĩa
- Tài liệu Redlock khuyến nghị trì hoãn khởi động lại một nút bị crash ít nhất lâu bằng TTL khóa dài nhất
- Việc trì hoãn khởi động lại này cũng phụ thuộc vào đo thời gian chính xác ở mức hợp lý, nên vẫn có thể thất bại nếu đồng hồ nhảy
- Việc client bị dừng tiến trình cũng có thể làm Redlock hỏng
- Client 1 gửi yêu cầu khóa tới A, B, C, D, E
- Khi phản hồi đang trên đường về, client 1 rơi vào một đợt GC stop-the-world
- Tất cả khóa trên các nút Redis đều hết hạn
- Client 2 lấy được khóa ở A, B, C, D, E
- Client 1 kết thúc GC và nhận được các phản hồi thành công vẫn còn nằm trong kernel network buffer
- Cả hai client đều tin rằng mình đang giữ khóa
- Việc Redis được viết bằng C và không có GC không giúp ích gì
- Vấn đề xảy ra trong hệ thống nơi client có thể gặp GC pause
- Để an toàn, cần có cơ chế như fencing token để chặn công việc của client 1 sau khi client 2 đã lấy khóa
- Độ trễ mạng dài cũng có thể tạo ra hiệu ứng tương tự như việc tiến trình bị dừng
- Nếu đặt TCP user timeout ngắn hơn nhiều so với Redis TTL thì các gói bị trì hoãn có thể bị bỏ qua, nhưng phải xem triển khai TCP cụ thể mới chắc chắn được
- Dù vậy, cuối cùng vẫn quay lại bài toán độ chính xác của đo thời gian
Giả định hệ thống đồng bộ mà Redlock yêu cầu
- Redlock chỉ hoạt động đúng trong một mô hình hệ thống đồng bộ có các tính chất sau
- Có giới hạn trên được đảm bảo cho độ trễ mạng
- Thời gian tiến trình bị dừng bị giới hạn
- Sai số đồng hồ bị giới hạn
- Mô hình đồng bộ không có nghĩa là đồng hồ được đồng bộ chính xác, mà là tồn tại các giới hạn cố định đã biết cho độ trễ mạng, thời gian dừng và clock drift
- Redlock giả định rằng độ trễ, thời gian dừng và drift đều nhỏ so với TTL của khóa
- Nếu vấn đề timing lớn đến mức xấp xỉ TTL thì thuật toán sẽ thất bại
- Trong môi trường data center thông thường, các giả định timing kiểu này có thể đúng trong phần lớn thời gian; đó được gọi là hệ thống đồng bộ từng phần
- Nếu tính đúng đắn phụ thuộc vào khóa thì “phần lớn thời gian” là không đủ
- Ngay khi các giả định timing bị phá vỡ, Redlock có thể vi phạm tính an toàn, chẳng hạn cấp lease cho client khác trước khi lease của client đầu tiên hết hạn
- Trường hợp gói tin bị trễ 90 giây ở GitHub là bằng chứng cho thấy khó có thể giả định mô hình hệ thống đồng bộ trong môi trường thực
- Raft, Viewstamped Replication, Zab và Paxos thuộc nhóm thuật toán đồng thuận được thiết kế cho mô hình hệ thống đồng bộ từng phần hoặc mô hình bất đồng bộ có bộ phát hiện lỗi
- Với các thuật toán này, cần từ bỏ các giả định timing và cẩn thận không coi mạng, tiến trình hay đồng hồ trong hệ thống phân tán là đáng tin hơn thực tế
Kết luận và lựa chọn khuyến nghị
- Redlock quá nặng và tốn kém nếu chỉ dùng cho khóa tối ưu hiệu quả, nhưng lại không đủ an toàn cho các khóa liên quan đến tính đúng đắn
- Đặc biệt, nó trên thực tế giả định một hệ thống đồng bộ nơi độ trễ mạng và thời gian thực thi có giới hạn trên; khi giả định đó bị phá vỡ thì có thể vi phạm tính an toàn
- Nó cũng không có khả năng sinh fencing token để bảo vệ hệ thống khỏi độ trễ mạng dài hay các tiến trình bị treo
- Nếu cần một khóa tối ưu hiệu quả theo kiểu best-effort thì dùng thuật toán khóa Redis một nút sẽ hợp lý hơn
- Lấy khóa bằng conditional set-if-not-exists
- Chỉ xóa nguyên tử để nhả khóa khi giá trị khớp
- Cần ghi rõ trong mã rằng khóa này là gần đúng và đôi khi có thể thất bại
- Không cần dựng cụm 5 nút Redis
- Với các khóa cần tính đúng đắn, không nên dùng Redlock mà nên dùng hệ thống đồng thuận như ZooKeeper
- Nếu có thể, hãy dùng Curator recipes đã triển khai sẵn cơ chế khóa
- Tối thiểu, có thể dùng cơ sở dữ liệu như PostgreSQL với các đảm bảo giao dịch hợp lý
- Cần bắt buộc kiểm tra fencing token cho mọi truy cập tài nguyên nằm dưới khóa
- Redis là một công cụ hữu ích nếu được dùng đúng với mục đích ban đầu; cũng như mọi công cụ khác, nó có giới hạn và cần được hiểu rõ để lên kế hoạch phù hợp
- Trong bản cập nhật ngày 9/2/2016, tác giả gốc của Redlock là Salvatore đã đăng bài phản biện, nhưng kết luận vẫn được giữ nguyên
1 bình luận
Các ý kiến trên Hacker News
Ở chỗ làm, chúng tôi đang dùng Temporal, và đã triển khai khóa phân tán bằng workflow và signal chuyên dụng
Cho đến nay nó hoạt động tốt, và vì giao phần xử lý phân tán của khóa cho các tính năng của Temporal nên phần triển khai cũng khá đơn giản
Tôi tò mò liệu Temporal có độc tôn trong lĩnh vực này không, hay có những lựa chọn thay thế ở mức tương tự
Nghe nói nó được tách ra từ Uber và được các vendor lớn sử dụng, nên có vẻ đã được kiểm chứng thực chiến khá đầy đủ
Với khóa phân tán, tôi thường dùng PostgreSQL advisory lock
Ngay cả khi công việc không liên quan đến cơ sở dữ liệu, nếu bắt đầu một transaction và lấy advisory lock, khóa sẽ được giữ cho đến khi ứng dụng tự giải phóng hoặc transaction kết thúc do crash, v.v.
Đến giờ tôi vẫn cảm thấy cách này khá an toàn, nhưng vừa nhận ra rằng mình chưa từng kiểm tra xem kết nối cơ sở dữ liệu còn bình thường hay không
Nếu là công việc liên quan đến cơ sở dữ liệu thì query sẽ thất bại và công việc cũng thất bại theo, nhưng nếu không thì có thể đã mất khóa mà vẫn không biết
Nếu muốn độ chính xác tuyệt đối mà không có fencing token hay thao tác nguyên tử, tôi tự hỏi cuối cùng có phải mọi công việc đều cần 2-phase commit không
Có lẽ để làm đúng điều bạn định làm, bạn cần dùng “EXCLUSIVE” hoặc “ACCESS EXCLUSIVE”, hoặc đảm bảo 2-phase commit hay tính idempotent cho công việc
[0] https://www.postgresql.org/docs/current/explicit-locking.htm...
Hầu hết thư viện thường dùng connection pool, nên bạn phải dành riêng một kết nối cho khóa và các lần kiểm tra khóa định kỳ cũng nhất thiết phải thực hiện qua chính kết nối đó
Bạn nên đọc bình luận tôi từng để lại trên blog này trước đây, cũng như bài phản hồi tôi viết trên blog của mình
Nói theo thứ tự ngẫu nhiên, tác giả đã bỏ lỡ điểm cốt lõi về cách thuật toán hoạt động, rồi sau đó bác bỏ thuật toán dựa trên những lập luận yếu hơn còn lại
Nói rằng trên máy tính và API hiện đại không thể chờ một khoảng thời gian xấp xỉ đúng cũng không đúng. GC pause là bounded và monotonic clock cũng hoạt động, nên đó là một giả định có thể chấp nhận
Phê bình vì bản thân cơ chế tự động giải phóng bộc lộ race condition tiềm ẩn là một chuyện, còn phê bình trong phạm vi mục tiêu của thuật toán và mô hình hệ thống lại là chuyện khác
Redlock đã được dùng thành công trong nhiều use case suốt nhiều năm, và nếu đặt timeout lớn hơn nhiều so với thời gian hoàn tất công việc cũng như các lần tạm dừng tùy ý có thể xảy ra trên hệ điều hành thông thường, thì rất khó gây ra race condition
Tất nhiên, nếu đặt timeout tự động giải phóng quá nhỏ và công việc dễ dàng kéo dài đến mức đó, thì đó là lỗi thiết kế, nhưng không phải vấn đề của bản thân Redlock
Bạn có dùng RedLock trong tình huống timeout khá ngắn (ví dụ 1–2 giây), công việc thường dùng khoảng 90% timeout đó, và công việc thực hiện trong lúc giữ khóa RedLock tuyệt đối không được chạy đồng thời với một holder khóa khác không?
Tôi cho rằng câu trả lời đúng ở đây luôn là “không”. Vì nguy cơ lease hết hạn trước khi client hoàn tất công việc là rất lớn
RedLock không thể đảm bảo mutual exclusion trong mọi tình huống, nên cần biến công việc thành idempotent; loại bài toán này tốt hơn nên được triển khai bằng optimistic lock
Tôi đang củng cố lại kiến thức low-level và thuật toán, có cuốn sách nào hay cho chủ đề này không? Tôi có cuốn sách của tác giả rồi
Tôi muốn làm thử thứ gì đó cho vui, nhưng tài liệu thì hoặc chỉ ở mức đồ chơi, hoặc quá phức tạp
Chọn một chủ đề rồi thực sự triển khai thử là được
Trước đây tôi đã viết một bài blog về khóa phân tán dựa trên tài liệu này: https://medium.com/sahibinden-technology/an-easy-integration...
Cách giải thích “khóa có timeout (tức là lease)” nghe có vẻ lạ
Thứ nhất, nếu client crash thì ngay từ đầu, dù không có timed lease, OS hoặc supervisor cũng phải giải phóng khóa; nếu cả hai đều chết thì kết nối cuối cùng cũng bị ngắt, và hệ thống mạng phải phát hiện điều đó qua reset, timeout, thiếu heartbeat, v.v., rồi vô hiệu hóa kết nối và nhả khóa
Thứ hai, nếu vấn đề là client do bug mà không crash nhưng giữ khóa quá lâu, chẳng phải một supervisor nào đó nên phát hiện việc này và kill client trước khi nhả khóa cho những bên khác sao?
Thứ ba, nếu đặt khóa có timeout để xử lý các corner case như vậy, chẳng phải nên thông báo cho chương trình thực tế bằng exception, signal, shutdown hay cách tương tự sao? Và trước khi nhả khóa, chẳng phải nên chờ xác minh rằng chương trình đã nhận được thông báo sao?
Chính ý tưởng cho phép chương trình tiếp tục chạy luồng điều khiển bình thường dù timeout đã xảy ra trông như nguyên nhân gốc rễ của vấn đề, nhưng tôi không hiểu vì sao mọi người lại bỏ qua. Tôi có đang bỏ lỡ một lý do hiển nhiên nào không?
Chủ thể vô hiệu hóa khóa ở phía mình là dịch vụ lưu trữ, và nếu không có các đảm bảo bổ sung mà Redlock không cung cấp, client không thể tự phát hiện vấn đề của chính nó
Trong một số trường hợp, các gói này bị drop, client trên máy từ xa đã chết nhưng trên server vẫn còn kết nối mở
Nhân tiện, tôi không phải người đã bấm downvote
Đã triển khai khóa phân tán bằng Deno KV do Deno và Deno Deploy lưu trữ
Bên trong dùng FoundationDB, một cơ sở dữ liệu phân tán, và các instance Deno chạy trên thiết bị cục bộ kết nối tới cùng Deno KV để giành khóa
Nếu dùng PostgreSQL thì cũng hoạt động với
SELECT FOR UPDATE, nhưng bản thân cơ sở dữ liệu không phải dạng phân tánNăm 2018, chúng tôi đã xem xét Redis cho use case của mình, nhưng cuối cùng chọn một giải pháp kém hào nhoáng hơn và thực sự chưa từng thất bại lần nào
Use case là phát từng vé có định danh từ một tập vé hữu hạn của chiến dịch, tương tự cách Ticketmaster phân bổ ghế trong địa điểm biểu diễn
Khi có yêu cầu đến, cần cung cấp một vé khả dụng, gắn metadata của yêu cầu vào vé đã được cấp, rồi loại vé đó khỏi các yêu cầu tiếp theo
Trước đây từng có các chiến dịch thất bại như phân bổ quá mức, phân bổ thiếu, phân bổ trùng, nên tính đúng đắn là yếu tố cốt lõi
Chúng tôi cũng đã thử một triển khai đơn giản với Redis: lấy khóa, kiểm tra khóa, thực hiện công việc, rồi nhả khóa; nhưng khi đó gánh nặng vận hành với chúng tôi là lớn, và may là đã không đi theo hướng đó
Lựa chọn cuối cùng là Postgres. “Khóa phân tán” của chúng tôi gần giống một câu lệnh
UPDATEphức hợp dùng tính năng riêng của Postgres; chúng tôi biến yêu cầu thành một dạng phép toán trên tập hợp để cơ sở dữ liệu trả về bản ghi thành công hoặc dấu hiệu thất bại. Giao dịch ACID đã thắngSau khi giải quyết tính đúng đắn, chúng tôi xem xét quy mô và hiệu năng; tuy không cần hàng triệu request mỗi giây, nhưng vẫn có tiêu chí cho các đợt tăng vọt tức thời
Chúng tôi tối ưu các instance cơ sở dữ liệu đọc/ghi trong cụm, bố trí chiến lược các chiến dịch lớn hơn hoặc có nhu cầu cao hơn vào các hệ thống được chỉ định, và tiếp tục tối ưu trong 2 năm, nhưng chưa từng có chiến dịch phát vé nào thất bại
Tôi không phải chuyên gia về công nghệ khóa phân tán; chỉ là tập trung vào vấn đề cần giải quyết, thử vài cách rồi tìm được lời giải phù hợp
Vì giao dịch
UPDATEchỉ kéo dài vài micro giây, có thể tập trung hóa vấn đề, và như vậy đơn giản hơn, nhanh hơn, an toàn hơnNhưng như bài viết giải thích, đây không phải là một vấn đề phân tán
Khóa trong hệ thống phân tán khác với mutex trong ứng dụng đa luồng, và phức tạp hơn vì nhiều node và mạng có thể độc lập thất bại theo nhiều cách khác nhau
Bạn cần khóa phân tán khi giao dịch có thể kéo dài từ vài giây đến vài giờ, và máy liên quan có thể thất bại trong lúc đang giữ khóa
Trong trường hợp này, ràng buộc là “không bán nhiều hơn N vé”, và phần lớn quy mô lưu lượng thực tế của những bài toán như vậy có thể được giải quyết bằng hành vi giao dịch của cơ sở dữ liệu quan hệ truyền thống; cứ để cơ sở dữ liệu lo việc quản lý khóa nội bộ
Mong các developer đừng nhảy quá nhanh vào hướng “tôi sẽ xây khóa phân tán”. Gần như luôn có câu trả lời tốt hơn, nhưng câu trả lời đó khác nhau tùy từng ứng dụng
Có thể cũng là việc phù hợp với những thứ mới như SQLite của Cloudflare
Nơi đầu tiên tôi đọc được nội dung này là ở đây: https://code.flickr.net/2010/02/08/ticket-servers-distribute...
Nhiều kỹ sư không thật sự quan tâm đến vấn đề tính đúng đắn cho đến khi đã quá muộn. Nó giống bảo mật
Ngay cả khi có quan tâm, nhiều người cũng không kiểm chứng xem việc mình làm có đúng hay không
Ví dụ trong lĩnh vực của tôi, các microservice, actor, process trao đổi message qua mạng, nhưng hơn 95% triển khai tôi thấy đều có edge case khiến message có thể bị mất hoặc được xử lý sai thứ tự
Nhưng incentive không được căn chỉnh đủ để sửa vấn đề này. Cấu trúc đãi ngộ của lãnh đạo và kỹ sư không khớp với kết quả tốt nhất cho khách hàng và cổ đông
Người ta muốn đặt ranh giới mạng giữa các lời gọi hàm mà chẳng có lý do đặc biệt, rồi sau đó cứ liên tục tạo HTTP server và client, serialize/deserialize JSON cho lời gọi hàm đó; nếu may mắn thì dùng gRPC, và lại cố triển khai lại những thứ như giao dịch phân tán qua ranh giới mạng đó
Cuối cùng chỉ sinh ra công việc bận rộn để xử lý những “tương tác rùng rợn ở xa” không thể tránh khỏi
Đội product và đội engineering phải đồng thuận về điều này, và khi vi phạm SLO thì cần chuyển trọng tâm sang độ ổn định của hệ thống
Cần lãnh đạo tốt vì rất khó thuyết phục tất cả mọi người
Khi bug liên tục xuất hiện, tính năng mới chậm hoặc gần như không có, và khách hàng bắt đầu rời đi, lập luận rằng phải biến chất lượng thành một phần của quy trình sẽ trở nên rất dễ dàng
Nhà lãnh đạo trưởng thành sẽ đi trước giai đoạn đó sớm nhất có thể
[0] https://en.wikipedia.org/wiki/British_Post_Office_scandal
Nhưng tôi không thấy có cách nào rõ ràng để thuyết phục các quản lý của ngày hôm qua cho thời gian để làm cho đúng
Điều này làm mọi thứ trở nên quá phức tạp
Nếu có thứ như fencing token mà bài viết nói đến thì không cần lock
Token cũng không cần tăng đơn điệu; chỉ cần là một giá trị duy nhất mang tính thụ động mà cả client và storage cùng giữ
Nếu gọi là version token, nó có thể là giá trị tăng đơn điệu, và UUID thường dễ tạo hơn cũng hoạt động được. Về mặt kỹ thuật, hash của toàn bộ dữ liệu trong storage cũng có thể dùng, nhưng không thực tế
Luồng xử lý như sau. Client lấy đồng thời version token hiện tại và dữ liệu cần sửa từ storage, còn storage đọc dữ liệu và token một cách nguyên tử để bảo đảm token đó thuộc về phiên bản dữ liệu tương ứng
Sau đó client gửi lại version token cùng với các thay đổi, và storage chỉ chấp nhận thay đổi khi token hiện tại khớp với token được gửi đến, đồng thời tạo version token mới một cách nguyên tử
Có thể đưa lock vào vì những lý do khác, nhưng trong hệ thống phân tán, nó nên độc lập với tính toàn vẹn của storage
Tôi cũng không thích lắm thuật ngữ “lock”. Vì nó tạm thời và không được bảo đảm, lease hoặc reservation có thể truyền tải ý nghĩa tốt hơn
Đây là cách đẩy độ phức tạp sang phía cơ sở dữ liệu, nhưng ở đây cần nhớ rằng vấn đề là lock phân tán
Nếu chỉ là một cơ sở dữ liệu đơn lẻ thì mọi thứ đơn giản cho đến khi cơ sở dữ liệu crash và ta không biết thao tác ghi CAS nào đã thực sự được phản ánh
Trong các hệ thống lớn cần tính sẵn sàng cao và sao lưu đa trung tâm dữ liệu, cách này cũng có thể hỏng do các kịch bản xoay quanh lỗi node, nên trở nên khá phức tạp
Thường sẽ dùng transaction log dạng Paxos. Đừng giả định rằng có giải pháp dễ dàng cho hệ thống phân tán. Lúc nào cũng đau đầu
Từ góc nhìn hiệu quả, giữ lock có thể tránh phải làm cùng một công việc hai lần một cách không cần thiết. Ví dụ như một phép tính tốn kém
Nếu lock thất bại và hai node cùng làm một việc, thì kết quả có thể chỉ là tăng chi phí đôi chút hoặc gửi trùng cùng một email thông báo, điều đó có thể không đáng kể
Nhưng tôi cho rằng việc nhiều node cùng làm một công việc tệ hơn nhiều so với ví dụ đã nêu, vì nó có thể cản trở chính việc xử lý phân tán có khả năng mở rộng
Giả sử hệ thống storage có hai node, và có hai quy trình đọc-sửa-ghi đang chạy. Cả quy trình 1 và 2 đều nhận token đầu tiên là
abcQuy trình 1 commit khiến token đổi thành
cde, và thay đổi được stream sang node 2, nhưng do độ trễ mạng nên đến node 2 muộnTrong lúc đó, nếu quy trình 2 commit lên node 2 với token
abc, node 2 chưa nhận được thông điệp từ node 1 nên sẽ chấp nhận thay đổi, và hệ thống rơi vào trạng thái không nhất quánNếu có fencing token tăng đơn điệu thì chuyện này sẽ không xảy ra. Vì yêu cầu đó buộc các node phải đồng thuận về thứ tự toàn cục của các thao tác trước khi cung cấp token