- ที่ Firezone ใช้ Rust เพื่อสร้างระบบการเข้าถึงระยะไกลที่ปลอดภัยและขยายขนาดได้บนโทรศัพท์ Android, คอมพิวเตอร์ MacOS หรือเซิร์ฟเวอร์ Linux
- ใช้ไลบรารีการเชื่อมต่อชื่อ
connlibเพื่อจัดการการเชื่อมต่อเครือข่ายและอุโมงค์ WireGuard - หลังจากวนปรับปรุงหลายรอบ จึงได้มาถึงแนวทางออกแบบแบบ sans-IO ซึ่งให้การทดสอบที่รวดเร็วและละเอียด การปรับแต่งเชิงลึก และความน่าเชื่อถือสูง
connlib เขียนด้วย Rust และใช้การออกแบบแบบ sans-IO
- เหมาะกับการสร้างบริการเครือข่ายด้วยความเร็วและความปลอดภัยด้านหน่วยความจำของ Rust
- ใช้งาน
tokioruntime,tungsteniteWebSockets, อิมพลีเมนเทชัน 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ได้อย่างอิสระเพื่อแสดงการเปลี่ยนสถานะ และต่างจากasyncRust ตรงที่ใช้เฉพาะ 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 ความคิดเห็น
ความเห็นจาก Hacker News
ก่อนที่ Rust จะเพิ่มไวยากรณ์ async/await เราต้องเขียน state machine เองด้วยมือ
ระหว่างเขียนไลบรารี VT100 ทำให้ตระหนักถึงปัญหาในแพตเทิร์นการห่อหุ้มของ Rust
เมื่อเทียบกับดีไซน์ที่ใช้ channel ในการส่งข้อมูล
ใน ecosystem ของ Haskell มีแนวคิดเรื่องการแยกตรรกะออกจากการทำงานจริง
tokio::select!ไว้อย่างไรฟังก์ชัน async ของ Rust จะถูกคอมไพล์เป็น state machine
ถ้าเปิดเผย state ออกมา ฟังก์ชัน async ก็อาจกลายเป็น "pure" ได้
Firezone เป็นเครื่องมือที่น่าทึ่ง
คงจะดีถ้าคอมไพเลอร์สามารถแปลงโค้ด async เป็น sans io ได้อัตโนมัติ
หลังจากอ่านบทความและคอมเมนต์แล้ว รู้สึกเหมือนกำลังคิดค้นสไตล์สถาปัตยกรรมแบบ hexagonal หรือ ports/adapters ขึ้นมาใหม่
สงสัยว่าทราฟฟิกจริงวิ่งผ่าน gateway หรือใช้แค่ตอนตั้งค่าการเชื่อมต่อเท่านั้น