Trên trang này
Trong một bài nói do AI Engineer công bố ngày 3 tháng 10 năm 2026, một developer advocate (chuyên viên quan hệ nhà phát triển) của Crusoe khởi chạy một tác vụ huấn luyện PyTorch trên hai node A100. Sau đó người trình bày báo cho chính hệ thống giám sát của bản demo rằng một GPU đã gặp lỗi XID 79. Node bị đánh dấu ngừng hoạt động và tác vụ bị hủy. Tiến trình nhận SIGTERM và có “tối đa hai phút để xử lý SIGTERM đó, để lưu checkpoint hoặc đẩy hết log ra”. Một node dự phòng được đưa vào, và “tác vụ slurm sẽ khởi động lại. Nó sẽ nạp checkpoint và tiếp tục huấn luyện từ chỗ đã dừng”. Từ đầu đến cuối mất chưa đến mười lăm phút một chút. Không ai bị gọi dậy.
Một lượt đồng bộ là con vật nhỏ hơn nhiều so với một lượt huấn luyện. Câu hỏi thì vẫn vậy. Một lượt chạy cần gì để việc giết worker chỉ tốn vài phút và không sinh bản trùng?
Đây là câu trả lời cho trình chạy đồng bộ của chính Tajo, đọc từ mã nguồn chứ không phải từ tờ quảng cáo. Reverse ETL mô tả hình dạng của công việc. Bài này là những gì xảy ra khi công việc đó chết.
Theo dõi một liên hệ
Một lượt chạy đọc nguồn từng trang một, bắt đầu từ một con trỏ đã lưu. Gọi con trỏ trang là c3 và liên hệ của chúng ta là bản ghi thứ 212 trên trang đó. Có ba điều đúng về liên hệ này trước khi bất cứ thứ gì được ghi.
Thứ nhất, nó có một khóa gửi. Khóa là giá trị băm của id đồng bộ, nguồn, con trỏ trang, vị trí trên trang và chính bản ghi. Con trỏ và bản ghi đi vào hàm băm và không bao giờ được nhúng vào khóa, nên khóa không chứa địa chỉ email nào.
Thứ hai, lượt ghi đi qua một sổ cái (ledger), không đi thẳng đến đích. Trước khi gọi đến đích, khóa được nhận là đang chờ. Khi ghi thành công, dòng đó được đánh dấu hoàn tất và giữ trong bảy ngày.
Thứ ba, con trỏ chưa di chuyển. Nó chỉ dời đi ở cuối một lô, chỉ khi mọi bản ghi trong lô đó đã ghi xong, và chỉ sau khi lượt chạy đã chứng minh nó vẫn sở hữu đồng bộ.
Giờ hãy giết worker giữa lượt ghi và checkpoint.
Khi nhận SIGTERM, mọi worker hàng đợi xả việc dưới một hạn chót chung mười lăm giây, và tiến trình thoát sau hai mươi lăm giây dù việc gì vẫn còn đang chạy; systemd cho nó bốn mươi lăm giây trước khi giết. Lượt chạy nào không xong trong khoảng đó vẫn được đánh dấu đang thực hiện, và worker tiếp theo nhận lại nó qua cơ chế khôi phục tác vụ bị treo (stalled-job recovery) của hàng đợi. Với một lần giết cứng (hard kill), không có bước xả nào cả. Dù theo cách nào, hai sự thật vẫn giữ nguyên. Đích đã có liên hệ của chúng ta. Con trỏ của chúng ta vẫn ghi c3.
Phát lại
Lượt chạy tiếp theo bắt đầu ở c3, đọc cùng trang, suy ra cùng khóa cho bản ghi thứ 212 và hỏi sổ cái. Sổ cái trả lời đã hoàn tất. Đích không được gọi; kết quả đã lưu được phát lại và lượt chạy đi tiếp. Trên đường xử lý hàng loạt, nơi endpoint nhập dữ liệu của Brevo nhận cả một trang trong một lệnh gọi, cùng phép kiểm tra đó chạy theo từng khóa trước khi yêu cầu được dựng. Một trang mà mọi khóa đều đã hoàn tất sẽ không gửi gì.
Đó là toàn bộ mẹo, và đáng nói vì sao nó hiệu quả. Khóa được suy ra từ con trỏ trang ở đầu lô, không phải từ lượt chạy tình cờ đang thực thi. Một sự cố, một lần thử lại sáu mươi giây sau và một lượt chạy theo lịch ngày mai đều suy ra cùng một khóa cho cùng một bản ghi. Sổ cái không quan tâm ai trong số đó đang hỏi.
Giờ đến cái giá. Sổ cái quên sau bảy ngày. Chính chú thích trong mã nói rằng bảy ngày đủ cho một lần thử lại bị trễ và một lần gián đoạn ngắn, và nó chỉ khẳng định có vậy. Phát lại từ một con trỏ cũ hơn thế sẽ ghi lại. Và một lượt chạy bị ngắt cứng có thể phải chờ. Quyền giữ (lease) của lượt chạy đã chết có thể kéo dài tới mười lăm phút, và với đồng bộ theo lịch, một lượt chạy có quyền giữ đang trống chỉ được quét dọn và lên lịch lại khi nó đã bắt đầu hơn một giờ trước. Bản demo của Crusoe quay lại trong chưa đến mười lăm phút. Của chúng tôi thì không phải lúc nào cũng vậy.
Ai sở hữu đồng bộ trong lúc khởi động lại
Mỗi lúc chỉ một lượt chạy sở hữu một đồng bộ. Lượt chạy lấy một quyền giữ trong Redis với token riêng, thời hạn mười lăm phút và gia hạn mỗi năm phút. Trước mỗi checkpoint, nó xác nhận mình vẫn giữ quyền đó. Nếu không, nó dừng. Lượt chạy được ghi lại bằng câu đơn giản mà người dùng sẽ đọc: “The run lost its slot part-way through and stopped to avoid writing twice.” (Lượt chạy mất quyền giữa chừng và đã dừng để tránh ghi hai lần.) Lượt chạy thứ hai va vào một chủ sở hữu còn sống sẽ không được thử lại. Nó kết thúc với “Another run of this sync was already in progress” (Một lượt chạy khác của đồng bộ này đang diễn ra), và lượt chạy theo lịch tiếp theo bắt đầu bình thường.
Trách nhiệm sửa lỗi cũng được ghi rõ. Mỗi mã lỗi ứng với một câu nói điều gì đã xảy ra, một câu nói cần làm gì tiếp theo, và một người chịu trách nhiệm: bạn hoặc Tajo. Một đích từ chối mọi bản ghi vì ánh xạ sai là việc của bạn. Một quyền giữ bị mất là việc của chúng tôi. Trang của lượt chạy cho biết bước tiếp theo; hướng dẫn khôi phục đồng bộ liệt kê là bên nào.
Đến đây là nửa dễ. Một sự cố tốn một lần phát lại và không gì bị ghi hai lần. Hai sự cố đã dạy nửa còn lại.
Thất bại nối tiếp thất bại
Một đồng bộ thất bại hết lần này đến lần khác. Không phải trong một vòng lặp dày đặc: mỗi chu kỳ bộ lập lịch một lần, lượt này đến lượt khác, với cùng một thông báo mỗi lần, rằng custom id không được chứa dấu hai chấm. Lượt chạy không bao giờ đến được đích. Nó chết khi đang được đưa vào hàng đợi, trước khi bất kỳ trình chạy nào thấy nó, nên một cảnh báo gắn với trình chạy chẳng bao giờ có gì để gắn vào.
Không có gì theo dõi chuỗi thất bại đó. Đó là toàn bộ phát hiện.
Cách sửa là một cầu dao. Sau mỗi lần thất bại được ghi nhận, nó đọc năm lượt chạy đã kết thúc gần nhất của đồng bộ kể từ lần thay đổi gần nhất của đồng bộ. Nếu cả năm đều lỗi, nó tạm dừng đồng bộ, ghi một dòng audit với lý do circuit_breaker_tripped và phát một cảnh báo nghiêm trọng. Chuỗi thất bại được suy ra từ các lượt chạy đã lưu, không phải từ bộ đếm trong worker. Thất bại được ghi nhận qua bất kỳ đường nào cũng được tính, kể cả thất bại khi xếp hàng đợi đã gây ra sự cố. Chú thích trong mã nói rằng nó sẽ chặn sự cố đó ở lượt chạy thứ năm. Một lần thành công đặt lại chuỗi. Bỏ tạm dừng sẽ đặt lại cầu dao thay vì làm nó ngắt lại dựa trên các dòng cũ. Hướng dẫn trình kết nối Shopify nói cùng điều đó trong một dòng: sau năm lượt chạy thất bại liên tiếp, đồng bộ tự tạm dừng và trang của nó hiển thị lỗi gần nhất.
Điều cầu dao không làm là sửa bất cứ thứ gì. Crusoe thay node và khởi động lại tác vụ mà không hỏi. Tajo dừng lại và hỏi: một đồng bộ bị tạm dừng sẽ đứng yên cho đến khi một người sửa nguyên nhân và xuất bản lại. Mặc định thận trọng này là có chủ ý: một lần thử lại tự động của một đồng bộ ghi vào hồ sơ khách hàng tự nó đã là một lượt ghi.
Sự im lặng không ai thấy
Lời phản bác là hợp lý, và nó như sau: “Một cầu dao tạm dừng sau năm lần thất bại lẽ ra cũng đã bắt được sự cố thứ hai. Vì sao phải xây thêm một bộ canh thứ hai?” Vì sự cố thứ hai trông không giống thất bại. Trong một thời gian dài, một workspace có một đồng bộ đang hoạt động, một đống dòng lỗi ngày càng lớn và không một lần thành công nào, chưa từng, và không có gì cảnh báo. Mọi tín hiệu hiện có đều gắn với hoạt động: một lượt chạy thất bại, một webhook đến, một ngân sách bị vượt. Không có lần thành công nào thì không phải là một sự kiện. Đó là sự vắng mặt của một sự kiện, và cầu dao chỉ chạy khi có thất bại được ghi nhận. Một bộ lập lịch đã chết, một hàng đợi bị kẹt, một worker không bao giờ khởi động: những thứ này không ghi lại gì, và cầu dao không bao giờ thức dậy.
Tay cầm dead-man trên đầu máy xe lửa hoạt động theo chiều ngược lại. Người lái phải liên tục ấn giữ nó, và sự im lặng sẽ dừng đoàn tàu. Phiên bản cho đồng bộ chạy mỗi giờ một lần, vào phút thứ mười ba. Với mọi workspace có đồng bộ đang hoạt động, nó hỏi một câu: có lượt chạy nào thành công trong hai mươi bốn giờ qua không? Workspace có đồng bộ đang hoạt động lâu đời nhất được bật chưa đầy một ngày thì được để yên; nó chưa có đủ một ngày để thành công. Workspace cũ hơn mà im lặng sẽ nhận một cảnh báo nghiêm trọng. Cảnh báo nói rõ đó là loại im lặng nào: có lượt chạy được ghi nhận nhưng không lượt nào thành công, hoặc không có lượt chạy nào được ghi nhận cả. Nó kích hoạt một lần cho mỗi sự cố. Một dấu đánh trong Redis giữ nó đóng cho đến lượt chạy thành công đầu tiên, rồi được xóa, để sự cố tiếp theo lại được cảnh báo.
Cả hai bộ canh được đưa vào trong cùng một commit, sau khi sự việc đã xảy ra. Chú thích của cầu dao nói rằng nó sẽ chặn sự cố đầu tiên ở lượt chạy thứ năm. Chú thích của dead-man nói rằng một đồng bộ được bật từ lâu mà không có lượt chạy nào phải cảnh báo. Cả hai đều chưa tồn tại khi cần đến. Chúng tôi viết chúng sau đó, ghi cả hai sự cố vào phần đầu tệp để không ai quên lý do.
Những gì đã xây, và những gì chưa
Đã xây, tính đến lúc viết bài:
- Con trỏ được lưu ở cuối mỗi lô trọn vẹn, và lưu lại lần nữa khi có sự cố.
- Khóa gửi của mỗi bản ghi được suy ra từ con trỏ trang và vị trí của nó, và được đối chiếu với sổ cái bảy ngày trước mỗi lệnh gọi đến đích.
- Mỗi đồng bộ một quyền giữ, thời hạn mười lăm phút.
- Năm lần thử cho mỗi lượt chạy, giãn cách tăng dần bắt đầu từ sáu mươi giây.
- Một cầu dao tạm dừng đồng bộ sau năm lần thất bại liên tiếp và ghi lại lý do.
- Một cảnh báo mỗi giờ khi hai mươi bốn giờ trôi qua không có lần thành công nào.
Chưa xây, và nên biết trước khi bạn dựa vào nó:
- Dead-man theo dõi workspace, không theo dõi từng đồng bộ. Một đồng bộ khỏe mạnh che khuất một đồng bộ anh em đang im lặng trong cùng workspace.
- Cảnh báo của nó đến tay đội vận hành của Tajo: luôn có một dòng log, và Sentry, webhook hoặc email nếu đã cấu hình. Nó không xuất hiện trên trang của đồng bộ. Việc tạm dừng của cầu dao thì có.
- Không có gì khởi động lại một đồng bộ đã tạm dừng. Một người làm việc đó.
- Khôi phục sau một lần giết cứng không nhanh. Quyền giữ có thể kéo dài mười lăm phút và lượt quét dọn chờ một giờ.
- Cả hai ngưỡng, năm lần thất bại và hai mươi bốn giờ, là thiết lập của bản triển khai. Bạn không thể đặt chúng cho từng đồng bộ.
Một chuỗi dài các bước gần như hoàn hảo vẫn thất bại; lối ra là xác định trạng thái cuối và kiểm tra nó. Bài này là nửa vận hành của điều đó. Hãy xác định một lượt chạy chết để lại gì, và kiểm tra cả sự trống rỗng mà im lặng để lại.
Câu kết của Crusoe là “thất bại là điều không thể tránh khỏi”. Câu của chúng tôi tiến thêm một bước. Sự cố sập là nửa dễ, vì nó để lại các dòng dữ liệu. Sự im lặng là nửa chúng tôi phải tự đi tìm.
Câu hỏi Thường gặp
Điều gì xảy ra với một lượt đồng bộ đang chạy khi worker chết giữa chừng một lô?
Con trỏ vẫn trỏ vào đầu lô bị gián đoạn, vì Tajo chỉ dời con trỏ sau khi một lô đã ghi xong trọn vẹn. Lượt chạy tiếp theo đọc lại đúng trang đó. Mỗi bản ghi mang một khóa gửi (delivery key) được suy ra từ con trỏ trang và vị trí của nó, và một sổ cái (ledger) giữ mọi lượt ghi đã hoàn tất trong bảy ngày, nên các bản ghi đã ghi rồi được phát lại từ sổ cái thay vì gửi đến đích lần thứ hai.
Vì sao một lượt đồng bộ được phát lại không ghi cùng một liên hệ hai lần?
Trước mỗi lệnh gọi đến đích, lượt chạy đối chiếu khóa gửi của bản ghi với sổ cái. Khóa là giá trị băm của đồng bộ, nguồn, con trỏ trang, vị trí trên trang và chính bản ghi, nên một sự cố, một lần thử lại một phút sau và một lượt chạy theo lịch ngày hôm sau đều suy ra cùng một khóa cho cùng một bản ghi. Khóa đã được đánh dấu hoàn tất trong khung bảy ngày sẽ không được gửi lại.
Cảnh báo dead-man cho đồng bộ là gì?
Một phép kiểm tra kích hoạt khi thiếu vắng điều gì đó, chứ không phải khi có lỗi. Mỗi giờ một lần, Tajo xem mọi workspace có đồng bộ đang hoạt động và cảnh báo khi không có lượt chạy nào thành công trong hai mươi bốn giờ qua, kể cả trường hợp không có lượt chạy nào được ghi nhận. Cảnh báo kích hoạt một lần cho mỗi sự cố và được kích hoạt lại sau lượt chạy thành công đầu tiên. Nó tồn tại vì cảnh báo lỗi chỉ thấy được những lỗi đã được ghi nhận.