4 điểm bởi GN⁺ 2024-10-21 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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độ 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 SIGSTOP gử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 zxid hoặc số phiên bản znode làm fencing token
  • 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 gettimeofday chứ không phải monotonic clock để quyết định lúc khóa hết hạn
    • gettimeofday cho 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

 
GN⁺ 2024-10-21
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 vừa mới biết đến Temporal, và nó trông giống như chén thánh của workflow, cung cấp khả năng quản lý tác vụ cấp cao rất rõ ràng trên một hạ tầng phức tạp
      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 đủ
    • Thú vị đấy. Bạn có thể giải thích chi tiết hơn một chút không? Tôi muốn thử làm một thứ tương tự bằng Temporal
    • Tôi muốn thử dùng Temporal nhưng nghe nói nó có thể không ổn định. Thực tế nó có hoạt động tốt không?
  • 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

    • Advisory lock có nhiều cạm bẫy. Xem [0]
      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...
    • Điểm cần chú ý với khóa là, theo tôi biết, khóa là theo từng kết nối
      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

    • Thành thật mà nói, từ trước đến nay tôi vẫn không hiểu lắm bài blog phản hồi đó. Có lẽ câu hỏi này sẽ giúp tìm được điểm chung
      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
    • Bạn có thể cho link không?
  • 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

    • Tôi khuyên đọc System Design Interview I, II của Alex Xu
      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?

    • Đây không phải mutex, mà là phiên bản hệ thống phân tán của nó
      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ó
    • Giả định rằng server luôn nhận được RST hoặc FIN từ client là sai
      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án

  • Nă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 UPDATE phứ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ắng
    Sau 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

    • Nói rằng tối đa 50.000 giao dịch nguyên tử ngắn mỗi giây thì chỉ cần Postgres là đúng
      Vì giao dịch UPDATE chỉ 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ơn
      Như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
    • Điều quan trọng mà ví dụ này cho thấy là thứ cần thiết không phải khóa, mà là ràng buộc
      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
    • Rốt cuộc câu trả lời là, trái với suy nghĩ ban đầu, trong đa số trường hợp không cần khóa phân tán :)
    • Đây có vẻ là một bài toán dễ song song hóa đến mức đáng ngại, vì có thể shard theo từng concert sang các instance khác nhau
      Có thể cũng là việc phù hợp với những thứ mới như SQLite của Cloudflare
    • Đây là cách tốt nhất, và thực tế là cách hợp lý duy nhất để tiếp cận vấn đề
      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

    • Bản thân microservice thường là một triệu chứng của vấn đề này
      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
    • Để sửa, trước tiên phải đo lường và monitoring, rồi đặt ra mục tiêu mức dịch vụ thể hiện trải nghiệm khách hàng
      Độ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ể
    • Những loại vấn đề như vậy có thể khiến người vô tội bị bỏ tù hoặc thiệt mạng
      [0] https://en.wikipedia.org/wiki/British_Post_Office_scandal
    • Tôi nghĩ vẫn có một mức độ căn chỉnh incentive nào đó. Những edge case này rất rắc rối, nên developer có khả năng phải xử lý nhiều ticket hỗ trợ, và điều đó chẳng tốt cho ai
      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

    • Thứ đang được mô tả là compare-and-swap (CAS), và đó là một giải pháp tốt
      Đâ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
    • Bạn đã hiểu sai vấn đề và đang đề xuất giải pháp cho một vấn đề khác
    • Điều này được biết đến với tên optimistic locking. Nhưng tôi sẽ không gọi nó là một cơ chế lock phân tán
    • Phần giải thích này bỏ sót lý do đầu tiên trong bài viết về việc dùng lock
      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
    • Nếu không dùng token tăng đơn điệu thì chẳng phải sẽ dẫn đến trạng thái không nhất quán sao?
      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à abc
      Quy 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ộn
      Trong 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án
      Nế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