3 điểm bởi GN⁺ 2024-07-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 tokio runtime, WebSocket tungstenite, triển khai WireGuard boringtun, 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 ra Transmit

Á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 ra Transmit
  • 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: SentReceived
  • Định nghĩa struct StunBinding và 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_transmithandle_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_timeouthandle_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 StunBinding có thể áp dụng cho hầu hết các giao thức mạng
  • Thư viện snownet củ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 &mut một cách tự do để biểu đạt thay đổi trạng thái, và khác với async Rust ở 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

 
GN⁺ 2024-07-05
Ý 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

    • Nhờ cú pháp async/await của Rust mà năng suất đã được cải thiện đáng kể
    • Async của Rust được chuyển đổi thành máy trạng thái tự động và lưu giá trị tại các điểm I/O
  • 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

    • Việc quá ám ảnh với đóng gói gây ra vấn đề
    • Nhắc lại rằng máy tính là cỗ máy thực hiện đầu vào, biến đổi dữ liệu và đầu ra
  • So sánh với thiết kế truyền dữ liệu bằng channel

    • Mã trở nên phức tạp hơn
    • Phải tự triển khai kiểu message thủ công
    • Phải cung cấp sender một cách tường minh
    • Khi truyền tải mạng thất bại thì không nhận được kết quả
    • Tuy nhiên cũng có những điểm tiện lợi
  • Trong hệ sinh thái Haskell có ý tưởng tách biệt logic và thực thi

    • Không đề cập đã đóng gói lời gọi tokio::select! như thế nào
    • Có quan tâm đến việc triển khai hàm được đóng gói theo phong cách sans-IO
  • Hàm async của Rust được biên dịch thành máy trạng thái

    • Tò mò liệu đã có ai thử kết hợp sans-io với async hay chưa
    • Vấn đề chính là tính dễ dùng và cách xử lý Pin
  • Nếu phơi bày trạng thái thì hàm async có thể trở nên 'thuần' hơn

    • Đã thử bind OpenSSL vào async Rust
  • Firezone là một công cụ ấn tượng

    • Đã phát hiện mẫu tương tự trong Rust-libp2p
  • 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

    • Việc chuyển đổi thủ công rất dễ phát sinh lỗi
  • 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