3 คะแนน โดย GN⁺ 2024-07-05 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ที่ Firezone ใช้ Rust เพื่อสร้างระบบการเข้าถึงระยะไกลที่ปลอดภัยและขยายขนาดได้บนโทรศัพท์ Android, คอมพิวเตอร์ MacOS หรือเซิร์ฟเวอร์ Linux
  • ใช้ไลบรารีการเชื่อมต่อชื่อ connlib เพื่อจัดการการเชื่อมต่อเครือข่ายและอุโมงค์ WireGuard
  • หลังจากวนปรับปรุงหลายรอบ จึงได้มาถึงแนวทางออกแบบแบบ sans-IO ซึ่งให้การทดสอบที่รวดเร็วและละเอียด การปรับแต่งเชิงลึก และความน่าเชื่อถือสูง

connlib เขียนด้วย Rust และใช้การออกแบบแบบ sans-IO

  • เหมาะกับการสร้างบริการเครือข่ายด้วยความเร็วและความปลอดภัยด้านหน่วยความจำของ Rust
  • ใช้งาน tokio runtime, tungstenite WebSockets, อิมพลีเมนเทชัน WireGuard อย่าง boringtun, การเข้ารหัสทราฟฟิก API ด้วย rustls เป็นต้น
  • การออกแบบแบบ sans-IO คือการอิมพลีเมนต์โปรโตคอลเป็น state machine แบบล้วน แทนที่จะส่งและรับไบต์ผ่านซ็อกเก็ตจากหลายจุด

โมเดล async ของ Rust และข้อถกเถียงเรื่อง "function coloring"

  • ฟังก์ชัน async สามารถถูกเรียกได้จากฟังก์ชัน async อื่นเท่านั้น
  • หากมีฟังก์ชันที่อยู่ลึกลงไปเป็น async ฟังก์ชันทั้งหมดที่เรียกมันก็ต้องกลายเป็น async ไปด้วย
  • สิ่งนี้อาจเป็นปัญหาสำหรับคนที่ต้องการเขียนโค้ดที่ไม่ต้องสนใจว่าดีเพนเดนซีจะเป็น async หรือไม่

แนะนำ sans-IO

  • แนวคิดหลักของ sans-IO คล้ายกับหลัก dependency inversion ในโลก OOP
  • นโยบาย (จะทำอะไร) ไม่ควรขึ้นอยู่กับรายละเอียดการอิมพลีเมนต์ (ทำอย่างไร)
  • แทนที่จะส่งข้อมูลด้วย struct Transmit จะเป็นการปล่อย Transmit ออกมา

การนำ dependency inversion มาใช้

  • แทนที่จะส่งข้อมูลด้วย struct Transmit จะเป็นการปล่อย Transmit ออกมา
  • event loop จะอิมพลีเมนต์ side effect และเรียก UdpSocket::send จริง

State machine

  • ไดอะแกรม state machine ของคำขอ STUN binding มีสองสถานะคือ Sent และ Received
  • อิมพลีเมนต์ state machine โดยกำหนด struct StunBinding และฟังก์ชันที่เกี่ยวข้อง

Event loop

  • event loop ทำหน้าที่ขับเคลื่อน state machine และประมวลผลข้อมูลด้วย poll_transmit และ handle_input

การนามธรรมเรื่องเวลา

  • ใช้ API poll_timeout และ handle_timeout เพื่อจัดการข้อกำหนดที่อิงเวลา

สมมติฐานของ sans-IO

  • การออกแบบแบบ sans-IO โยนการตัดสินใจเรื่องความเป็น async ของดีเพนเดนซีไปให้แอปพลิเคชัน
  • การออกแบบแบบ sans-IO ประกอบรวมกันได้ง่าย ให้ API ที่ยืดหยุ่น ทดสอบง่าย และเข้ากับความสามารถของ Rust ได้ดี

ประกอบรวมกันได้ง่าย

  • API ของ StunBinding สามารถนำไปใช้ได้กับโปรโตคอลเครือข่ายส่วนใหญ่
  • ไลบรารี snownet ของ Firezone รวม ICE และ WireGuard เข้าด้วยกัน เพื่อมอบอุโมงค์ IP แบบ "มหัศจรรย์" ที่ทำงานได้ไม่ว่าสภาพแวดล้อมเครือข่ายจะเป็นอย่างไร

API ที่ยืดหยุ่น

  • การเขียน event loop เองทำให้สามารถปรับจูนโค้ดได้ และดูแลรักษาได้ง่าย

การทดสอบที่รวดเร็ว

  • โค้ดแบบ sans-IO ไม่มี side effect จึงทดสอบได้ง่ายมาก
  • ที่ Firezone มีการอิมพลีเมนต์ reference state machine แล้วทดสอบโดยเปรียบเทียบกับสถานะจริงของ connlib

Edge case และความล้มเหลวของ IO

  • การออกแบบแบบ sans-IO แยกการอิมพลีเมนต์โปรโตคอลออกจาก side effect ของ IO จริง ทำให้จัดการ edge case และข้อผิดพลาดได้ง่าย

Rust + sans-IO: คู่ที่เกิดมาสำหรับกันและกัน?

  • Rust โมเดลเรื่อง ownership และ mutability อย่างชัดเจน จึงเข้ากับการออกแบบแบบ sans-IO ได้ดี
  • การออกแบบแบบ sans-IO ใช้ &mut ได้อย่างอิสระเพื่อแสดงการเปลี่ยนสถานะ และต่างจาก async Rust ตรงที่ใช้เฉพาะ API แบบ synchronous

ข้อเสีย

  • การเขียน event loop เองอาจทำให้เกิดบั๊กที่ละเอียดอ่อนได้
  • เวิร์กโฟลว์แบบลำดับอาจต้องใช้โค้ดมากขึ้น
  • ในชุมชน Rust การออกแบบแบบ sans-IO ยังไม่ได้ถูกใช้อย่างแพร่หลาย

บทสรุป

  • โค้ดแบบ sans-IO อาจดูแปลกในตอนแรก แต่เมื่อคุ้นเคยแล้วจะสนุกมาก
  • Rust มอบเครื่องมือที่ยอดเยี่ยมสำหรับการจำลอง state machine
  • การออกแบบแบบ sans-IO บังคับให้การจัดการข้อผิดพลาดเป็นส่วนหนึ่งของการประมวลผลอินพุต จนให้ความรู้สึกว่าเป็นวิธีที่ถูกต้องในการเขียนโค้ดเครือข่าย

ความเห็นของ GN⁺

  • การออกแบบแบบ sans-IO เข้ากับโมเดล ownership ของ Rust ได้ดี จึงเหมาะมากกับการอิมพลีเมนต์โปรโตคอลเครือข่าย
  • การเขียน event loop เองช่วยเพิ่มความยืดหยุ่นของโค้ดและทำให้ดูแลรักษาง่ายขึ้น
  • การทดสอบทำได้ง่าย จึงช่วยอย่างมากในการเขียนโค้ดที่เชื่อถือได้
  • อย่างไรก็ตาม เนื่องจากยังไม่ถูกใช้อย่างแพร่หลายในชุมชน Rust จึงอาจมีไลบรารีที่เกี่ยวข้องไม่มาก
  • เมื่อนำเทคโนโลยีใหม่มาใช้ ควรคำนึงถึง learning curve และการสนับสนุนจากชุมชนด้วย

1 ความคิดเห็น

 
GN⁺ 2024-07-05
ความเห็นจาก Hacker News
  • ก่อนที่ Rust จะเพิ่มไวยากรณ์ async/await เราต้องเขียน state machine เองด้วยมือ

    • ด้วยไวยากรณ์ async/await ของ Rust ทำให้ประสิทธิภาพในการพัฒนาดีขึ้นมาก
    • async ของ Rust จะถูกแปลงเป็น state machine อัตโนมัติ และเก็บค่าต่าง ๆ ไว้ที่จุด I/O
  • ระหว่างเขียนไลบรารี VT100 ทำให้ตระหนักถึงปัญหาในแพตเทิร์นการห่อหุ้มของ Rust

    • การยึดติดกับการห่อหุ้มมากเกินไปเป็นต้นเหตุของปัญหา
    • มันทำให้นึกขึ้นได้ว่าคอมพิวเตอร์คือเครื่องจักรที่รับอินพุต แปลงข้อมูล และส่งเอาต์พุต
  • เมื่อเทียบกับดีไซน์ที่ใช้ channel ในการส่งข้อมูล

    • โค้ดจะซับซ้อนขึ้น
    • ต้องกำหนดชนิดของข้อความเองด้วยมือ
    • ต้องส่งตัวส่งข้อมูลเข้าไปอย่างชัดเจน
    • ถ้าการส่งผ่านเครือข่ายล้มเหลว ก็จะไม่ได้ผลลัพธ์กลับมา
    • แต่ก็มีจุดที่สะดวกอยู่เช่นกัน
  • ใน ecosystem ของ Haskell มีแนวคิดเรื่องการแยกตรรกะออกจากการทำงานจริง

    • ไม่มีการกล่าวถึงว่า encapsulate การเรียก tokio::select! ไว้อย่างไร
    • เคยสนใจการทำฟังก์ชันที่ encapsulate ในสไตล์ sans-IO
  • ฟังก์ชัน async ของ Rust จะถูกคอมไพล์เป็น state machine

    • เลยสงสัยว่าเคยมีความพยายามจะรวม sans-io กับ async เข้าด้วยกันหรือไม่
    • ปัญหาหลักคือเรื่องการใช้งานและการจัดการ Pin
  • ถ้าเปิดเผย state ออกมา ฟังก์ชัน async ก็อาจกลายเป็น "pure" ได้

    • เคยพยายาม bind OpenSSL เข้ากับ async Rust
  • Firezone เป็นเครื่องมือที่น่าทึ่ง

    • พบแพตเทิร์นที่คล้ายกับ Rust-libp2p
  • คงจะดีถ้าคอมไพเลอร์สามารถแปลงโค้ด async เป็น sans io ได้อัตโนมัติ

    • การแปลงด้วยมือมีโอกาสผิดพลาดได้ง่าย
  • หลังจากอ่านบทความและคอมเมนต์แล้ว รู้สึกเหมือนกำลังคิดค้นสไตล์สถาปัตยกรรมแบบ hexagonal หรือ ports/adapters ขึ้นมาใหม่

  • สงสัยว่าทราฟฟิกจริงวิ่งผ่าน gateway หรือใช้แค่ตอนตั้งค่าการเชื่อมต่อเท่านั้น