Mọi người đều muốn cập nhật thời gian thực cho đến khi họ nhận ra rằng "nhanh" và "chính xác" không phải là một. Trong một hệ thống phân tán, các sự kiện có thể di chuyển với tốc độ ánh sáng nhưng vẫn có thể đến sai thứ tự. WebSockets bị ngắt và kết nối lại. Các message broker gửi lại các gói tin. Các background worker chạy đua với các lệnh hủy do hết thời gian (timeout). Kết quả là gì? Một client có thể thấy sự kiện 42, sau đó là sự kiện 40, rồi một snapshot tuyên bố rằng hệ thống đã ở sự kiện 45. Nếu bạn đang xây dựng các workflow agent chạy lâu dài, sự hỗn loạn đó không phải là một edge case. Nó là trạng thái cơ bản. Hãy xử lý thứ tự của các sự kiện trước khi bạn lo lắng về việc cắt giảm từng mili giây trong việc truyền tải.
Thực tế hỗn loạn của "Thời gian thực"
Thời gian thực là một thuộc tính truyền tải. Nó mô tả tốc độ một gói tin di chuyển qua đường truyền, chứ không phải liệu câu chuyện mà nó kể có hợp lý hay không. Các tác vụ chạy lâu dài sẽ khuếch đại mọi sự không nhất quán vì chúng kéo dài theo thời gian. Một công việc huấn luyện mô hình, một quy trình phê duyệt nhiều bước, hoặc một pipeline render video có thể phát ra hàng chục sự kiện trong vài phút hoặc vài giờ. Trong khoảng thời gian đó, bất cứ điều gì cũng có thể xảy ra sai sót.
Một broker có thể thử lại một tin nhắn vì một xác nhận (acknowledgement) bị thất lạc. Một load balancer có thể định tuyến hai sự kiện qua các đường truyền mạng khác nhau, khiến sự kiện mới hơn đến trước. Một worker process có thể bị chết sau khi ghi vào cơ sở dữ liệu nhưng trước khi phát đi sự kiện thành công, để rồi một worker thứ hai tiếp nhận tác vụ và phát ra tiến trình riêng của nó. Nếu frontend của bạn giả định rằng tin nhắn mới nhất là tin nhắn chính xác nhất, nó sẽ hiển thị một trạng thái chưa từng tồn tại. Người dùng sẽ thấy huy hiệu "đã hoàn thành" nhấp nháy quay lại trạng thái "đang xử lý", hoặc tệ hơn, một tác vụ đã bị hủy đột nhiên tự hồi sinh. Tốc độ mà không có thứ tự chỉ là sự hỗn loạn với tốc độ khung hình cao hơn.
Số thứ tự (Sequence Numbers) mới là chiếc đồng hồ thực sự
Giải pháp là các số thứ tự tăng dần (monotonic sequence numbers) nghiêm ngặt được tạo ra bởi producer. Mọi thao tác thay đổi trạng thái đều nhận được một con số tăng chính xác thêm một đơn vị, không có khoảng trống và không có sự quay lui (rollback). Con số đó phải được lưu trữ trong cùng một transaction với chính sự kiện đó. Nếu hàng trong cơ sở dữ liệu được cập nhật nhưng việc commit số thứ tự thất bại, bạn phải rollback cả hai. Điều này giữ cho dòng thời gian logic mang tính nguyên tử (atomic) với sự thay đổi trạng thái.
Event ID vẫn hữu ích, nhưng chúng giải quyết một vấn đề khác. Một event ID xác định một payload cụ thể để bạn có thể loại bỏ trùng lặp (deduplicate) khi broker gửi cùng một tin nhắn hai lần. Ngược lại, một số thứ tự cho bạn biết payload đó thuộc về đâu trong chuỗi nhân quả. Nó làm lộ ra các khoảng trống. Nó làm lộ ra thứ tự. Một timestamp không làm được cả hai điều này. Đồng hồ bị lệch (drift), NTP nhảy ngược lại, và các máy ảo bị tạm dừng. Chỉ sử dụng timestamp cho mục đích hiển thị, chẳng hạn như "Đã bắt đầu 3 phút trước", và đừng bao giờ dùng nó làm khóa sắp xếp (sorting key) cho logic nghiệp vụ.
Cách Client nên xử lý luồng dữ liệu (Stream)
Một khi producer đảm bảo được một chuỗi tăng dần, consumer sẽ có các quy tắc đơn giản và cứng rắn. Nếu một số thứ tự đến nhỏ hơn hoặc bằng số đã áp dụng gần nhất, hãy loại bỏ nó. Đó có thể là một bản trùng lặp hoặc một dữ liệu cũ đến muộn. Nếu số thứ tự lớn hơn chính xác một đơn vị so với số đã áp dụng gần nhất, hãy áp dụng nó ngay lập tức. Đó là kịch bản lý tưởng (happy path). Nếu số thứ tự nhảy vọt, chẳng hạn bạn mong đợi số 12 nhưng lại nhận được số 15, nghĩa là có thứ gì đó bị thiếu. Hãy lưu tạm (buffer) sự kiện mới và yêu cầu máy chủ phát lại (replay) bắt đầu từ số thứ tự tiếp theo được mong đợi. Đừng đoán mò. Đừng nhảy cóc với hy vọng rằng khoảng trống đó không quan trọng.
Các trạng thái kết thúc (terminal states) phải được coi là không thể đảo ngược. Một khi tác vụ đã được đánh dấu là hoàn thành, thất bại hoặc bị hủy, client nên từ chối bất kỳ thay đổi trạng thái tiếp theo nào cho thao tác đó. Điều này nghe có vẻ hiển nhiên cho đến khi bạn đối mặt với
