Nix đã viết giúp tôi một nửa trình gỡ lỗi

Công nghệ09 tháng 10, 2026·8 phút đọc

Một lập trình viên đã xây dựng Rewind VM — máy ảo xác định (deterministic) cho các bản build Nix, nơi mỗi lần chạy là một hàm thuần túy của đầu vào. Nhờ tận dụng triệt để hệ sinh thái Nix, anh đã có sẵn mã nguồn, ký hiệu gỡ lỗi và khả năng tái lập mà không tốn công sức, biến việc truy tìm các lỗi race condition thành một siêu năng lực.

Nix đã viết giúp tôi một nửa trình gỡ lỗi

Nix đã viết giúp tôi một nửa trình gỡ lỗi

Có một lập trình viên vừa chia sẻ câu chuyện thú vị về Rewind VM — một máy ảo xác định (deterministic) nơi mỗi lần chạy bản build Nix là một hàm thuần túy của các đầu vào, kể cả lịch trình luồng (thread schedule). Anh dùng công cụ này để tìm, tái hiện và xử lý vô số lỗi race condition trong các bản build Nix — nhưng điều khiến anh cảm thấy như có siêu năng lực là ở chỗ: phần khó nhất của mỗi tính năng mới thì Nix đã làm sẵn từ lâu.

Siêu năng lực gỡ lỗiSiêu năng lực gỡ lỗi

Từ một máy ảo nhỏ đến trình gỡ lỗi đầy đủ

Công cụ này nhanh chóng được mở rộng với bảng mã nguồn, khung ngăn xếp (stack frames), bookmark, tab "Compare", hỗ trợ gdb nhiều hơn, các làn luồng (thread lanes) hiển thị ai đang giữ CPU ở mỗi bước, và tính năng "Check from here" giúp tìm ra chính xác bước mà race condition xảy ra.

Mỗi lần thêm tính năng, anh đều nghĩ đây sẽ là một khối代码 lớn. Nhưng lần nào cũng vậy: phần khó đã được Nix giải quyết. Một trình gỡ lỗi cần đầu vào chính xác của chương trình, ký hiệu gỡ lỗi, mã nguồn, mã nguồn của mọi thư viện bên dưới, và cách để người khác có đủ những thứ đó trên máy của họ. Đó chính xác là những gì một derivation cung cấp — và Nix làm điều đó thật dễ dàng.

Hai giao dịch viên, một tài khoản

Ví dụ kinh điển về race condition là hai luồng cùng gửi tiền vào một tài khoản ngân hàng. Mỗi lần gửi sẽ đọc số dư, ghi một dòng vào sổ cái, rồi lưu số dư cộng với khoản tiền gửi. Nếu một giao dịch viên chạy xen vào giữa lúc người kia đọc và lưu, nó sẽ ghi đè số dư cũ lên khoản tiền gửi kia.

Trên chiếc laptop 16 nhân của tác giả, chương trình mất tiền trong 396/1000 lần chạy. Khi ghim vào một nhân duy nhất bằng taskset, không mất lần nào: một nhân đơn hiếm khi chuyển luồng giữa chừng một giao dịch — đó là lý do Rewind phải nhiễu loạn lịch trình.

Sổ cái ngân hàng — mã nguồnSổ cái ngân hàng — mã nguồn

Máy ảo của Rewind chỉ có một CPU, nên lần chạy đầu tiên cũng thành công. Lệnh rewind check chạy lại bản build dưới nhiều lịch trình bị nhiễu loạn, mỗi lần yêu cầu kernel khách lập lịch lại ở các bước khác nhau, rồi thu hẹp thất bại đầu tiên xuống còn một bước duy nhất.

Chạy kiểm tra này trên laptop chỉ mất 11 giây. Hai lần chạy giống hệt nhau từng bước cho đến bước 3237, nơi chỉ lần thất bại mới bị lập lịch lại.

Miễn phí thứ nhất: đầu vào

Đối số của check là một derivation và có thể là tham chiếu flake. Điểm hay của Nix là nó biết đầu vào của derivation, nên chỉ cần thế là đủ để đảm bảo lần chạy có thể tái lập. Đầu vào của derivation gồm mã nguồn, trình biên dịch, thư viện, kernel và cấu hình máy ảo.

Lệnh rewind nix hiện thực hóa các đầu vào của derivation, đóng gói closure vào một ảnh erofs chỉ đọc, rồi khởi động máy ảo trên đó. ID của lần chạy là băm của các đầu vào — giống như đường dẫn trong store — và rewind show in ra lệnh tạo lại nó.

Khi bản build thành công, khách báo cáo mã băm NAR của từng đầu ra, và rewind nix đối chiếu với bản sao trên máy chủ cũng như mọi binary cache mà Nix dùng, chỉ bằng cách tải file .narinfo. Điều này giúp xác thực rằng bản build trong máy ảo giống hệt bản build trên laptop, và lần chạy trong máy ảo có thể tái lập bằng Nix.

Miễn phí thứ hai: mọi ký hiệu và mọi mã nguồn

Gỡ lỗi đơn giản hơn nhiều khi bạn nhìn thấy mã nguồn. Một bảng mới trong ứng dụng Rewind hoặc lệnh rewind where trong terminal hiển thị mã nguồn của chương trình tại vị trí playhead, cùng các khung ngăn xếp đã gọi nó.

Trình gỡ lỗi cần biết mã nguồn nằm ở đâu — và Nix cũng cho không điều đó. nixpkgs xây dựng các gói với separateDebugInfo, và thông tin gỡ lỗi được lưu trên cache.nixos.org dưới dạng một đầu ra debug. Ta có thể dùng debuginfod để tải thông tin gỡ lỗi và mã nguồn theo build ID, nhờ đó xem được mã nguồn của bất kỳ binary nào trong máy ảo — kể cả kernel Linux.

gdb, trên một nhánh fork ở bất kỳ bước nào

Đôi khi mã nguồn là chưa đủ, ta cần thấy trạng thái của chương trình. Lệnh rewind gdb mở gdb trên một nhánh fork của lần chạy tại playhead, với mọi luồng của tiến trình. Trình gỡ lỗi có thể đặt breakpoint, watchpoint và kiểm tra bộ nhớ, thanh ghi, biến.

Điều đáng chú ý: gdb không làm thay đổi bản ghi. Ta có thể tua lại cùng bước và fork lần nữa, hoặc fork ở bất kỳ bước nào khác, và gdb sẽ thấy đúng trạng thái như vậy.

Nếu gdb vẫn chưa đủ, rewind shell --with nixpkgs#strace mở một shell trong máy ảo tại một bước với bất kỳ gói nào từ nixpkgs trên PATH — chỉ là thêm một closure được đóng gói vào một ảnh nữa.

So sánh hai lần chạy

Rewind giúp so sánh hai lần chạy rất dễ dàng. Tab Compare trong ứng dụng, hoặc lệnh rewind compare trong terminal, hiển thị những sự kiện chung cuối cùng của cả hai lần chạy và sự kiện đầu tiên khác biệt.

So sánh hai lần chạySo sánh hai lần chạy

Tab Compare đặt sự kiện của cả hai lần chạy cạnh nhau từ ngay trước điểm rẽ nhánh, với phần khác biệt được đánh dấu. Giao dịch viên 2 bắt đầu từ 150 trong lần chạy thất bại và từ 200 trong lần chạy thành công.

Ai đã giữ CPU

Máy ảo của Rewind chỉ có một CPU, nên ở mỗi bước chỉ có đúng một luồng đang chạy. Race condition là câu hỏi về thứ tự: luồng nào chạy khi nào.

Vết (trace) không trả lời trực tiếp điều đó. Nó ghi lại các luồng đã làm gì — ghi, mở, fork, thoát, tín hiệu — nhưng không ghi ai đang chạy trong khoảng trống giữa các sự kiện. Rewind có thể lấp đầy khoảng trống mà không cần ghi thêm gì: mỗi lần chạy phát lại chính xác, nên nó có thể bước từng bước và hỏi kernel khách xem luồng nào đang trên CPU.

Trong lần chạy thất bại, giao dịch viên 1 (Thread 140) bị ngắt ở bước 3238, giữa chừng một giao dịch: nó đã đọc số dư nhưng chưa lưu. Giao dịch viên 2 (Thread 141) nhận CPU đúng một bước, 3239, đủ lâu để đọc số dư, rồi giao dịch viên 1 lấy lại và hoàn tất.

"Check from here"

Tìm ra một xen kẽ xấu lại dấy lên câu hỏi tiếp theo: nó khả thi đến mức nào? Lần chạy thành công là bình thường hay chỉ gặp may?

Lệnh rewind check thường trả lời bằng cách xây dựng lại toàn bộ derivation dưới nhiều lịch trình. Với --run, nó bắt đầu từ một lần chạy đã có, tại bất kỳ bước nào bạn chọn. Nó fork lần chạy ở đó một lần cho mỗi lịch trình, mỗi nhánh lấy thứ tự luồng khác nhau kể từ bước đó, và đếm xem bao nhiêu nhánh kết thúc khác đi. Mọi thứ trước bước đó giữ nguyên.

Áp dụng cho ví dụ ngân hàng, bắt đầu từ bước 3221 nơi hai lần chạy rẽ nhánh: chạy 16 lịch trình từ đó, cả 16 đều mất tiền. Ta có thể làm điều tương tự trong ứng dụng Rewind với "Check from here" trong menu ngữ cảnh của playhead.

Bookmark

Phím b đánh dấu bước tại playhead kèm ghi chú. Bookmark được lưu cùng lần chạy và đi kèm trong file xuất .rwd, nên người nhận được mở lên sẽ thấy ghi chú của bạn trên dòng thời gian.

Các ví dụ khác để thử

Mỗi ví dụ là một derivation trong flake, với một lỗi và một cách sửa:

  • philosophers: deadlock từ bài viết trước.
  • bank: lỗi lost update ở trên.
  • waiter: tín hiệu SIGCHLD đến giữa lúc kiểm tra cờ và pause, khiến tiến trình cha ngủ cho đến khi timeout 10 giây của make check giết nó.
  • config-reload: một tiến trình ghi lại file cấu hình tại chỗ trong khi tiến trình khác đọc lại. Trên nhiều nhân gần như luôn thất bại; trên một CPU chỉ một số lịch trình đặt được bên đọc vào giữa lúc truncate và lần ghi cuối.

Nix là siêu năng lực

Phần lớn công việc khó của một trình gỡ lỗi đã được Nix làm sẵn. Rewind chỉ thêm một chút, còn lại đều đã có: đầu vào, mã nguồn, ký hiệu gỡ lỗi, và cách để người khác có đủ tất cả trên máy họ.

Khi bạn bắt đầu với một thứ kín kẽ như derivation Nix, bạn có được một trình gỡ lỗi miễn phí.

$ nix run github:fzakaria/rewindvm -- check github:fzakaria/rewindvm#bank
Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗