SFU에서 Actor패턴을 적용한 이유
· 2 min read
Rust로 WebRTC SFU 서버를 만들면서 HTTP 서버와 미디어 처리 루프가 같은 Rooms 상태를 사용해야 했다. 처음에는 Arc<Mutex<Rooms>>로 상태를 공유했다. 여러 스레드에서 안전하게 접근할 수 있는 익숙한 방법이었다.
HTTP 서버는 Tokio 런타임에서 요청을 비동기로 처리했고, SFU는 str0m을 사용하는 별도의 동기 루프에서 동작했다. HTTP API는 Room을 만들거나 상태를 바꾸고, 미디어 루프는 Rooms를 순회하며 이벤트 루프와 미디어 라우팅을 처리했다. 두 실행 흐름이 같은 Room에 대해서 접근해야 했다.
처음에는 아래처럼 Rooms를 공유했다.
Arc<Mutex<Rooms>>문제
이 방식은 데이터 접근을 한 번에 하나로 제한해 안전하게 만들지만, 락을 얻기 위해 기다리는 시간은 없애주지 않는다. 특히 SFU 루프는 주기적으로 모든 방과 클라이언트를 순회하며 작업한다. 루프가 Rooms를 사용하는 동안 HTTP 요청도 같은 락을 얻어야 했고, 요청 처리가 밀리기 시작했다. 반대로 HTTP 쪽이 락을 오래 잡으면 미디어 루프도 작동 중 그만큼 기다려야 했다.
RwLock도 검토했지만, SFU 루프는 상태를 읽기만 하는 게 아니라 클라이언트와 방 정보를 계속 갱신하기 때문에, 읽기 락을 늘리는 것만으로 경합을 해결하기 어려웠다.
해결 방법
해결 방향은 간단하게 Rooms를 공유하지 않는 것이었다. SFU 루프가 Rooms를 단독으로 소유하고, HTTP 핸들러는 직접 상태를 바꾸는 대신 처리할 작업을 메시지로 보낸다.
HTTP 핸들러 ── SfuMessage ──▶ SFU 루프 ──▶ Rooms 변경 또는 조회
HTTP 핸들러 ◀── reply 채널 ── SFU 루프
실제 구현에서는 std::sync::mpsc::sync_channel을 사용했다. 예를 들어 방 조회 요청은 GetRoom 메시지와 응답용 채널을 함께 보내고, SFU 루프는 메시지를 받아 자신이 소유한 Rooms에서 조회한 결과를 응답 채널에 전달한다. 생성·삭제·상태 변경·클라이언트 등록도 같은 방식으로 처리한다.
이제 Rooms를 읽고 쓰는 곳은 SFU 루프 하나뿐이다. 따라서 Arc<Mutex<>>가 필요 없어졌고, 락 경합도 사라졌다.
HTTP 요청의 처리 시간
SFU 루프는 매 순회 시작 때 채널에 쌓인 메시지를 최대 500개까지 처리한 뒤 Rooms를 순회한다. 큐가 밀리지 않는 일반적인 상황에서는 요청이 다음 처리 기회를 기다리는 시간이 짧다. 이 정도 지연보다 Arc<Mutex<>>를 제거해 락 경합을 없애서 얻게 되는 이점이 더 크다고 판단했다.
베이스라인에서는 4개 미디어 루프의 순회 속도가 800명 부하에서 루프당 초당 약 600회, 1600명 부하에서 약 130회였다. 순회 간격은 각각 약 1.7ms와 7.7ms다. 다만 이 테스트는 웹 요청 메시지 처리량을 측정한 것이 아니므로, 이 수치를 실제 요청 처리량으로 볼 수는 없다.