GN⁺: Sans-IO: 네트워크 서비스에 효과적인 Rust의 비밀
(firezone.dev)- Firezone에서는 Rust를 사용하여 Android 폰, MacOS 컴퓨터 또는 Linux 서버에서 확장 가능한 안전한 원격 액세스를 구축함
-
connlib
라는 연결 라이브러리를 사용하여 네트워크 연결과 WireGuard 터널을 관리함 - 여러 번의 반복 끝에 sans-IO라는 설계에 도달하여 빠르고 철저한 테스트, 깊은 커스터마이징, 높은 신뢰성을 제공함
connlib
는 Rust로 작성되었으며 sans-IO 설계를 따름
- Rust의 속도와 메모리 안전성 덕분에 네트워크 서비스 구축에 적합함
-
tokio
런타임,tungstenite
WebSockets,boringtun
WireGuard 구현,rustls
API 트래픽 암호화 등 사용 - sans-IO 설계는 여러 곳에서 소켓을 통해 바이트를 보내고 받는 대신 순수 상태 기계로 프로토콜을 구현함
Rust의 비동기 모델과 "함수 색칠" 논쟁
- 비동기 함수는 다른 비동기 함수에서만 호출될 수 있음
- 비동기 함수 깊숙이 있는 함수는 호출하는 모든 함수도 비동기 함수로 만들어야 함
- 이로 인해 종속성의 비동기 여부에 대해 무관심한 코드를 작성하고자 하는 사람들에게 문제가 될 수 있음
sans-IO 소개
- sans-IO의 핵심 아이디어는 OOP 세계의 의존성 역전 원칙과 유사함
- 정책(무엇을 할지)은 구현 세부 사항(어떻게 할지)에 의존하지 않아야 함
-
Transmit
구조체를 사용하여 데이터를 전송하는 대신Transmit
을 방출함
의존성 역전 적용
-
Transmit
구조체를 사용하여 데이터를 전송하는 대신Transmit
을 방출함 - 이벤트 루프는 부작용을 구현하고 실제로
UdpSocket::send
를 호출함
상태 기계
- STUN 바인딩 요청의 상태 기계 다이어그램은
Sent
와Received
두 가지 상태를 가짐 -
StunBinding
구조체와 관련 함수들을 정의하여 상태 기계를 구현함
이벤트 루프
- 이벤트 루프는 상태 기계를 구동하며,
poll_transmit
과handle_input
을 사용하여 데이터를 처리함
시간 추상화
-
poll_timeout
과handle_timeout
API를 사용하여 시간 기반 요구 사항을 처리함
sans-IO의 전제
- sans-IO 설계는 종속성의 비동기 여부에 대한 결정을 애플리케이션으로 미룸
- sans-IO 설계는 조합이 쉽고, 유연한 API를 제공하며, 테스트가 용이하고, Rust의 기능과 잘 맞음
쉬운 조합
-
StunBinding
의 API는 대부분의 네트워크 프로토콜에 적용 가능함 - Firezone의
snownet
라이브러리는 ICE와 WireGuard를 결합하여 네트워크 설정에 관계없이 작동하는 "마법" IP 터널을 제공함
유연한 API
- 이벤트 루프를 직접 작성하면 코드 튜닝이 가능하고 유지 관리가 쉬움
빠른 테스트
- sans-IO 코드는 부작용이 없으므로 테스트가 매우 용이함
- Firezone에서는 참조 상태 기계를 구현하여
connlib
의 실제 상태와 비교하는 테스트를 수행함
엣지 케이스와 IO 실패
- sans-IO 설계는 프로토콜 구현을 실제 IO 부작용과 분리하여 엣지 케이스와 오류 처리를 쉽게 만듦
Rust + sans-IO: 천생연분?
- Rust는 소유권과 가변성을 명시적으로 모델링하여 sans-IO 설계와 잘 맞음
- sans-IO 설계는
&mut
를 자유롭게 사용하여 상태 변경을 표현하고,async
Rust와는 달리 동기 API만 사용함
단점
- 이벤트 루프를 직접 작성하면 미묘한 버그가 발생할 수 있음
- 순차적 워크플로우는 더 많은 코드를 요구할 수 있음
- Rust 커뮤니티에서 sans-IO 설계는 아직 널리 사용되지 않음
마무리
- sans-IO 코드는 처음에는 생소하지만 익숙해지면 매우 즐거움
- Rust는 상태 기계를 모델링하는 데 훌륭한 도구를 제공함
- sans-IO 설계는 오류 처리를 입력 처리의 일부로 강제하여 네트워킹 코드를 작성하는 올바른 방식처럼 느껴짐
GN⁺의 의견
- sans-IO 설계는 Rust의 소유권 모델과 잘 맞아 네트워크 프로토콜 구현에 매우 적합함
- 이벤트 루프를 직접 작성하면 코드의 유연성과 유지 관리가 용이해짐
- 테스트가 용이하여 안정적인 코드를 작성하는 데 큰 도움이 됨
- 그러나 Rust 커뮤니티에서 널리 사용되지 않아 관련 라이브러리가 부족할 수 있음
- 새로운 기술을 도입할 때는 학습 곡선과 커뮤니티 지원을 고려해야 함
Hacker News 의견
-
Rust의 async/await 문법 도입 전에는 수동으로 상태 기계를 구현했었음
- Rust의 async/await 문법 덕분에 생산성이 크게 향상되었음
- Rust의 async는 자동 상태 기계로 변환되어 I/O 지점에서 값을 저장해줌
-
VT100 라이브러리를 작성하면서 Rust의 캡슐화 패턴 문제를 깨달았음
- 캡슐화에 집착하는 것이 문제를 일으킴
- 컴퓨터는 입력, 데이터 변환, 출력을 수행하는 기계임을 상기시킴
-
채널을 사용하여 데이터를 전송하는 디자인과 비교
- 코드가 복잡해짐
- 메시지 타입을 수동으로 구현해야 함
- 송신기를 명시적으로 제공해야 함
- 네트워크 전송 실패 시 결과를 얻지 못함
- 그러나 편리한 점도 있음
-
Haskell 생태계에서 논리와 실행을 분리하는 아이디어가 있음
-
tokio::select!
호출을 어떻게 캡슐화했는지 언급되지 않음 - sans-IO 스타일로 캡슐화된 함수 구현에 관심이 있었음
-
-
Rust의 async 함수는 상태 기계로 컴파일됨
- sans-io와 async를 결합하려는 시도가 있었는지 궁금함
- 주요 문제는 사용성 및 Pin 처리임
-
상태를 노출하면 async 함수가 '순수'해질 수 있음
- OpenSSL을 async Rust에 바인딩하려고 시도했음
-
Firezone이 놀라운 도구임
- Rust-libp2p와 유사한 패턴을 발견했음
-
컴파일러가 async 코드를 sans io로 자동 변환할 수 있으면 좋겠음
- 수동 변환은 오류가 발생하기 쉬움
-
기사와 댓글을 읽고 hexagonal 또는 ports/adapters 아키텍처 스타일을 재발명한 것 같음
-
실제 트래픽이 게이트웨이를 통해 지나가는지, 아니면 연결 설정에만 사용되는지 궁금함