Sans-IO: Bí quyết của Rust để xây dựng dịch vụ mạng hiệu quả
(firezone.dev)- Tại Firezone, Rust được sử dụng để xây dựng truy cập từ xa an toàn, có khả năng mở rộng trên điện thoại Android, máy tính macOS hoặc máy chủ Linux
- Sử dụng thư viện kết nối tên là
connlibđể quản lý kết nối mạng và các đường hầm WireGuard - Sau nhiều lần lặp lại, họ đi đến thiết kế sans-IO, mang lại khả năng kiểm thử nhanh và kỹ lưỡng, mức độ tùy biến sâu và độ tin cậy cao
connlib được viết bằng Rust và tuân theo thiết kế sans-IO
- Nhờ tốc độ và tính an toàn bộ nhớ của Rust, nó rất phù hợp để xây dựng dịch vụ mạng
- Sử dụng
tokioruntime, WebSockettungstenite, triển khai WireGuardboringtun,rustlsđể mã hóa lưu lượng API, v.v. - Thiết kế sans-IO triển khai giao thức như một máy trạng thái thuần túy, thay vì gửi và nhận byte qua socket ở nhiều nơi
Mô hình bất đồng bộ của Rust và tranh luận về "function coloring"
- Hàm bất đồng bộ chỉ có thể được gọi từ hàm bất đồng bộ khác
- Nếu một hàm nằm sâu trong chuỗi gọi là bất đồng bộ, mọi hàm gọi đến nó cũng phải trở thành hàm bất đồng bộ
- Điều này có thể gây vấn đề cho những người muốn viết mã không phụ thuộc vào việc dependency có bất đồng bộ hay không
Giới thiệu sans-IO
- Ý tưởng cốt lõi của sans-IO tương tự nguyên tắc đảo ngược phụ thuộc trong thế giới OOP
- Chính sách (làm gì) không nên phụ thuộc vào chi tiết triển khai (làm như thế nào)
- Thay vì dùng struct
Transmitđể gửi dữ liệu, hệ thống sẽ phát raTransmit
Áp dụng đảo ngược phụ thuộc
- Thay vì dùng struct
Transmitđể gửi dữ liệu, hệ thống sẽ phát raTransmit - Vòng lặp sự kiện triển khai các tác dụng phụ và thực sự gọi
UdpSocket::send
Máy trạng thái
- Sơ đồ máy trạng thái của yêu cầu STUN binding có hai trạng thái:
SentvàReceived - Định nghĩa struct
StunBindingvà các hàm liên quan để triển khai máy trạng thái
Vòng lặp sự kiện
- Vòng lặp sự kiện điều khiển máy trạng thái, xử lý dữ liệu bằng
poll_transmitvàhandle_input
Trừu tượng hóa thời gian
- Xử lý các yêu cầu dựa trên thời gian bằng API
poll_timeoutvàhandle_timeout
Tiền đề của sans-IO
- Thiết kế sans-IO để việc quyết định dependency có bất đồng bộ hay không lại cho ứng dụng
- Thiết kế sans-IO dễ kết hợp, cung cấp API linh hoạt, dễ kiểm thử và rất phù hợp với các đặc tính của Rust
Dễ kết hợp
- API của
StunBindingcó thể áp dụng cho hầu hết các giao thức mạng - Thư viện
snownetcủa Firezone kết hợp ICE và WireGuard để cung cấp một đường hầm IP "ma thuật" hoạt động bất kể cấu hình mạng
API linh hoạt
- Tự viết vòng lặp sự kiện cho phép tinh chỉnh mã và giúp việc bảo trì dễ dàng hơn
Kiểm thử nhanh
- Mã sans-IO không có tác dụng phụ nên cực kỳ dễ kiểm thử
- Tại Firezone, họ triển khai một máy trạng thái tham chiếu để kiểm thử bằng cách so sánh với trạng thái thực tế của
connlib
Edge case và lỗi IO
- Thiết kế sans-IO tách phần triển khai giao thức khỏi các tác dụng phụ IO thực tế, giúp xử lý edge case và lỗi dễ hơn
Rust + sans-IO: Cặp bài trùng?
- Rust mô hình hóa rõ ràng quyền sở hữu và tính khả biến, nên rất phù hợp với thiết kế sans-IO
- Thiết kế sans-IO dùng
&mutmột cách tự do để biểu đạt thay đổi trạng thái, và khác vớiasyncRust ở chỗ chỉ dùng API đồng bộ
Nhược điểm
- Tự viết vòng lặp sự kiện có thể phát sinh các bug tinh vi
- Các workflow tuần tự có thể đòi hỏi nhiều mã hơn
- Trong cộng đồng Rust, thiết kế sans-IO vẫn chưa được sử dụng rộng rãi
Kết luận
- Mã sans-IO lúc đầu có thể lạ lẫm, nhưng khi đã quen thì rất thú vị
- Rust cung cấp những công cụ tuyệt vời để mô hình hóa máy trạng thái
- Thiết kế sans-IO buộc việc xử lý lỗi trở thành một phần của xử lý đầu vào, nên mang lại cảm giác như là cách đúng đắn để viết mã mạng
Ý kiến của GN⁺
- Thiết kế sans-IO rất phù hợp với mô hình sở hữu của Rust, nên đặc biệt thích hợp để triển khai giao thức mạng
- Tự viết vòng lặp sự kiện giúp mã linh hoạt hơn và dễ bảo trì hơn
- Việc dễ kiểm thử là một lợi thế lớn để viết mã ổn định
- Tuy nhiên, vì chưa được sử dụng rộng rãi trong cộng đồng Rust nên có thể thiếu các thư viện liên quan
- Khi áp dụng công nghệ mới, cần cân nhắc đường cong học tập và mức độ hỗ trợ từ cộng đồng
1 bình luận
Ý kiến trên Hacker News
Trước khi Rust đưa vào cú pháp async/await, người ta phải tự triển khai máy trạng thái thủ công
Khi viết thư viện VT100, tác giả nhận ra vấn đề với mẫu đóng gói của Rust
So sánh với thiết kế truyền dữ liệu bằng channel
Trong hệ sinh thái Haskell có ý tưởng tách biệt logic và thực thi
tokio::select!như thế nàoHàm async của Rust được biên dịch thành máy trạng thái
Nếu phơi bày trạng thái thì hàm async có thể trở nên 'thuần' hơn
Firezone là một công cụ ấn tượng
Sẽ rất tốt nếu trình biên dịch có thể tự động chuyển mã async thành sans io
Sau khi đọc bài viết và bình luận, có cảm giác như đang tái phát minh phong cách kiến trúc hexagonal hoặc ports/adapters
Tò mò liệu lưu lượng thực tế có đi qua gateway hay chỉ được dùng cho việc thiết lập kết nối