Kiểm thử mô phỏng tất định trong celld: Truy vết và tái hiện lỗi trong hệ phân tán
celld — runtime chạy ứng dụng Cloudflare Workers và Durable Objects trên máy chủ riêng — đang áp dụng kỹ thuật kiểm thử mô phỏng tất định (DST) để kiểm soát thứ tự thực thi, tái hiện lỗi và phát hiện các lỗi tiềm ẩn trong hệ phân tán. Bài viết phân tích cách DST hoạt động và một lỗi race condition liên quan đến alarm đã được tìm ra, sửa chữa và chuyển thành bài kiểm thử hồi quy.

Kiểm thử mô phỏng tất định trong celld: Truy vết và tái hiện lỗi trong hệ phân tán
Khi xây dựng các hệ phân tán, việc đảm bảo độ tin cậy là một trong những thách thức khó nhằn nhất. Một lỗi có thể chỉ xuất hiện khi các sự kiện như gửi tin nhắn chậm, ghi dữ liệu thất bại hay khởi động lại node xảy ra theo một thứ tự rất cụ thể. Vấn đề là ở lần chạy thử tiếp theo, thứ tự ấy có thể khác đi, khiến việc tái hiện lỗi trở nên cực kỳ khó khăn.
celld — runtime cho phép chạy ứng dụng Cloudflare Workers và Durable Objects trên máy chủ riêng, với S3 tương thích làm dịch vụ ngoài duy nhất — đã giải quyết bài toán này bằng kỹ thuật kiểm thử mô phỏng tất định (Deterministic Simulation Testing – DST). Dù công cụ mô phỏng vẫn đang trong quá trình phát triển và chưa được công bố trong kho mã nguồn mở, nó đã giúp nhóm phát triển tìm ra nhiều lỗi chưa từng được biết đến.
Thách thức của hệ phân tán và tám ngộ nhận
Peter Deutsch từng đúc kết Tám ngộ nhận của điện toán phân tán (The Eight Fallacies of Distributed Computing), trong đó có những giả định sai lầm như "mạng lưới luôn đáng tin cậy" hay "độ trễ bằng không". Những ngộ nhận này chính là nguồn gốc của nhiều lỗi khó tái hiện.
Vấn đề cốt lõi nằm ở chỗ: lỗi có thể phụ thuộc vào một chuỗi sự kiện rất cụ thể, nhưng thứ tự ấy lại thay đổi ngẫu nhiên giữa các lần chạy. Nếu không tái hiện được lần chạy thất bại, ta không thể điều tra nguyên nhân, cũng không thể xác nhận liệu bản sửa lỗi có thực sự giải quyết vấn đề hay không.
Cách DST hoạt động trong celld
Điểm mấu chốt của DST là chạy mã production thực tế của celld trong một môi trường do trình mô phỏng kiểm soát. Mỗi cell trong celld chạy mã ứng dụng với cơ sở dữ liệu SQLite riêng, đồng thời xử lý các sự kiện như yêu cầu đến, thao tác lưu trữ hoàn tất và timer kích hoạt.
Trong kiến trúc của celld, phần mã chọn sự kiện tiếp theo được tách biệt hoàn toàn khỏi phần mã xử lý sự kiện. Nhờ vậy, trình mô phỏng có thể can thiệp vào thứ tự sự kiện mà vẫn đảm bảo mã xử lý giống hệt môi trường production.
Trình mô phỏng còn kiểm soát:
- Thời điểm các tác vụ bất đồng bộ được thực thi
- Cách dịch vụ lưu trữ đối tượng phản hồi, bao gồm cả việc trì hoãn hoặc làm thất bại thao tác ghi
- Khả năng tua nhanh thời gian mô phỏng mà không cần chờ thời gian thực trôi qua
Mỗi lần chạy được điều khiển bởi một seed (giá trị khởi tạo cho bộ sinh số ngẫu nhiên). Với cùng mã nguồn, cùng cấu hình và cùng seed, ta sẽ nhận được chuỗi sự kiện, trạng thái trung gian và kết quả giống hệt nhau. Đây chính là chìa khóa để tái hiện lỗi.
Trình kiểm tra (checker) sẽ xác minh các bất biến (invariant) — những điều kiện mà hệ thống bắt buộc phải duy trì xuyên suốt quá trình chạy — sau mỗi hành động, dựa trên phản hồi quan sát được và dữ liệu đã lưu.
Lỗi race condition mà trình mô phỏng phát hiện
Một trong những lỗi thú vị nhất mà DST tìm ra liên quan đến cơ chế alarm trong celld. Alarm cho phép lên lịch để một cell chạy mã ứng dụng vào thời điểm xác định. Vì cell có thể bị giải phóng khỏi bộ nhớ khi rảnh rỗi, celld cần một cách hiệu quả để đánh thức nó trở lại.
Cơ chế hoạt động như sau: thời điểm alarm được lưu trong SQLite, nhưng để tránh phải mở lại cơ sở dữ liệu của từng cell, celld quét các wake entry trong kho lưu trữ đối tượng. Khi cell đã hoạt động trở lại, celld đọc thời điểm từ SQLite để quyết định thời điểm chạy alarm.
Để giảm số lần ghi vào kho lưu trữ, celld tái sử dụng wake entry. Chẳng hạn, khi ứng dụng xóa alarm lúc 10:00 và đặt alarm mới lúc 10:05, celld có thể dùng lại wake entry 10:00 để đánh thức cell sớm, rồi chờ đến 10:05 mới thực thi.
Lỗi xuất hiện ở đây: khi xóa alarm 10:00, một tác vụ dọn dẹp (cleanup) riêng biệt sẽ xóa wake entry tương ứng — nhưng tác vụ này chạy bất đồng bộ. Nếu client đặt alarm 10:05 và nhận phản hồi thành công trước khi cleanup cũ hoàn tất, thì cleanup đó vẫn tiếp tục xóa wake entry 10:00 — vốn đang được alarm 10:05 tái sử dụng.
Kết quả là alarm 10:05 vẫn còn trong SQLite, nhưng không còn wake entry nào để đánh thức cell. Nếu node khởi động lại trước 10:05, alarm sẽ không bao giờ chạy, bất chấp việc client đã nhận phản hồi thành công.
Trình kiểm tra đã phát hiện vi phạm bất biến này ngay trong lúc chạy, chứ không chỉ ở trạng thái cuối cùng — một điểm cực kỳ quan trọng.
Tái hiện và sửa lỗi
Nhờ seed, nhóm phát triển có thể chạy lại chính xác lần thất bại đó, quan sát thời điểm cleanup cũ xóa wake entry cần thiết mà không phải mò tìm thứ tự lỗi từ đầu.
Giải pháp được đưa ra là thay đổi thiết kế wake entry: mỗi lần đặt alarm mới sẽ có entry riêng trong kho lưu trữ. Một tác vụ xóa trễ cho alarm cũ chỉ có thể xóa entry của chính nó, không ảnh hưởng đến entry mới. Các entry cũ chỉ bị xóa khi celld xác nhận chúng không còn cần thiết.
Nhóm cũng đã chuyển lần chạy thất bại thành bài kiểm thử hồi quy, tái hiện đúng thứ tự yêu cầu và cleanup để đảm bảo alarm mới luôn giữ được wake entry khả dụng. Điểm đáng chú ý: bài kiểm thử này xác minh bất biến xuyên suốt quá trình chạy, chứ không chỉ kiểm tra trạng thái cuối — bởi nếu wake entry biến mất rồi được khôi phục, việc chỉ nhìn vào kết quả cuối sẽ bỏ sót lỗi.
Hướng phát triển tiếp theo
Dù còn đang phát triển, trình mô phỏng đã chứng minh giá trị thực tiễn khi tìm ra và sửa chữa những lỗi chưa từng được biết đến. Nhóm phát triển dự kiến sẽ mở rộng phạm vi kiểm thử với nhiều tổ hợp yêu cầu, lỗi lưu trữ và khởi động lại node hơn — chẳng hạn kịch bản node khởi động lại giữa lúc kho lưu trữ gặp sự cố rồi nhận yêu cầu mới trước khi dịch vụ hồi phục.
Họ cũng sẽ bổ sung kiểm tra cho nhiều đảm bảo khác của celld, ví dụ như một thao tác ghi được báo thành công phải tồn tại qua lần khởi động lại node. Mỗi khi phát hiện lỗi, họ có thể tái hiện, sửa và lưu lại chuỗi sự kiện làm bài kiểm thử hồi quy.
Góc nhìn cho cộng đồng phát triển Việt Nam
Đối với các nhóm phát triển phần mềm tại Việt Nam đang xây dựng các hệ thống phân tán — từ backend microservices đến nền tảng edge computing — kỹ thuật DST đáng để nghiên cứu. Cách tiếp cận này giải quyết một trong những nỗi đau lớn nhất của kiểm thử phân tán: tính tái hiện. Thay vì dựa vào may mắn để gặp lại lỗi, bạn chủ động kiểm soát và ghi lại mọi thứ tự thực thi.
Ngay cả khi chưa xây dựng được trình mô phỏng đầy đủ, nguyên tắc cốt lõi — tách biệt phần chọn sự kiện khỏi phần xử lý sự kiện, kiểm tra bất biến xuyên suốt quá trình chạy, và dùng seed để tái hiện — đều có thể áp dụng cho các hệ thống tự xây dựng.


