RTCP 완전정복 시리즈 #4
1편에서 PLI(Picture Loss Indication)를 설명하면서 한 줄을 남겨뒀다. "PLI는 공짜가 아니다."
PLI는 "화면이 깨졌으니 키프레임을 보내달라"는 요청이다. 받는 쪽 입장에서는 응급 처치다. 키프레임이 오면 깨진 화면이 즉시 복구된다. 그래서 문제가 생기면 PLI를 보내는 게 자연스럽다.
문제는, 이 응급 처치가 비용이 매우 크다는 것이다. 키프레임은 일반 프레임(P-frame)보다 5~10배 크다. 일반 프레임이 5KB라면 키프레임은 30~50KB다. 갑자기 대역폭 사용량이 치솟는다.
P2P 통화에서는 이게 큰 문제가 아니다. PLI를 보내는 사람이 1명이니까, 키프레임도 1번 생성하면 끝이다. 그런데 SFU에서는 이야기가 완전히 달라진다.
10명이 화상회의를 하고 있다고 하자. A가 영상을 보내고, 나머지 9명이 A의 영상을 받는다.
B의 네트워크가 잠깐 나빠져서 A의 영상이 깨졌다. B가 PLI를 보낸다. SFU가 이 PLI를 A에게 전달하면, A의 인코더가 키프레임을 생성한다.
여기까지는 괜찮다. 문제는 그 다음이다.
A의 인코더가 키프레임을 생성하면, 이 키프레임은 B에게만 가는 게 아니다. SFU가 9명 전원에게 복사 전달한다. 키프레임은 50KB다. 9명에게 보내면 450KB가 한꺼번에 나간다. 평소에 9명 × 5KB = 45KB였던 트래픽이 순간적으로 10배가 되는 것이다.
B 한 명의 PLI가 만드는 파급 효과
평소: A → SFU → [B,C,D,E,F,G,H,I,J] 각 5KB = 45KB
PLI 후: A → SFU → [B,C,D,E,F,G,H,I,J] 각 50KB = 450KB
↑
순간 10배 트래픽
450KB 한 방이면, 다른 사람들의 패킷이 밀린다. C나 D도 패킷을 잃어버릴 수 있다. C가 패킷을 잃으면? C도 PLI를 보낸다. A가 또 키프레임을 만든다. 또 9명에게 50KB씩 나간다.
이걸 키프레임 폭풍(keyframe storm)이라고 부른다. 1명의 PLI가 전원의 품질을 떨어뜨리고, 떨어진 품질 때문에 더 많은 PLI가 발생하고, 더 많은 키프레임이 생기는 악순환이다.
키프레임 폭풍 — 악순환
B 손실 → PLI → 키프레임 → 대역폭 치솟음
↓
C, D도 패킷 손실
↓
C, D도 PLI 발송
↓
또 키프레임 → 또 치솟음
↓
... 반복
이 문제를 모르는 SFU는 없다. 각자 나름의 방법으로 PLI를 제어한다.
고정 시간 스로틀(throttle) — 가장 흔한 방법이다. "PLI는 최소 1초 간격으로만 보내겠다." LiveKit은 레이어별로 500ms/1000ms 간격을 둔다. mediasoup은 전역으로 1초 스로틀을 건다. Janus는 3초마다 주기적으로 FIR(키프레임 강제 요청)을 보낸다.
이 방식은 간단하고 폭풍은 막을 수 있다. 그런데 한계가 명확하다.
상황을 모른다. PLI를 보내고 1초를 기다리는 동안, 키프레임이 이미 도착했을 수 있다. 이미 복구된 상황인데 1초 후에 또 PLI를 보내면 — 불필요한 키프레임이 생긴다. 반대로, 키프레임이 아직 안 왔는데 1초를 기다리고 있을 수도 있다. 정말 급한데 시간이 안 됐다고 PLI를 막으면 — 화면이 깨진 채로 1초를 버티는 것이다.
비유하면, 교차로에서 신호등 없이 일정 시간마다 차를 한 대씩 보내는 것과 같다. 차가 없어도 기다리고, 차가 밀려도 기다린다. 상황에 반응하지 않는다.
우리가 택한 방법은 다르다. 시간이 아니라 결과를 본다.
PLI를 "1초 간격으로 보내겠다"가 아니라, "PLI를 보냈으면 — 키프레임이 도착했는가? 도착했으면 subscriber에게 전달됐는가?"를 추적한다. 이 인과 체인의 상태에 따라 다음 PLI를 보낼지 말지 결정한다.
비유하면, 교차로에 카메라를 달아서 차가 실제로 지나갔는지 확인하고, 지나갔으면 다음 차를 보내는 것이다. 차가 안 지나갔으면 좀 더 기다리고, 지나갔으면 바로 보낸다.
구체적으로 어떤 사건들을 추적하는지 보자.
PLI 인과 체인
1) SFU가 publisher에게 PLI를 보냈다
→ "PLI 보냈음" 상태로 전환
2) publisher가 키프레임을 생성해서 보냈다
→ SFU가 RTP 패킷을 보고 키프레임 도착을 감지
→ "키프레임 도착" 상태로 전환
3) SFU가 이 키프레임을 subscriber에게 전달했다
→ "전달 완료" 상태로 전환 → 초기 상태로 복귀
이 체인이 완료되면 — 문제 해결이 확인된 것이므로, 다음 PLI 요청을 받을 준비가 된다. 체인이 아직 진행 중이면 — "이미 처리 중이니 기다려라"가 된다.
시간 기반과의 차이가 여기서 드러난다.
키프레임이 50ms 만에 도착하면? 고정 스로틀은 나머지 950ms를 무의미하게 기다린다. 인과관계 방식은 키프레임 도착을 감지하는 즉시 다음 요청을 처리할 수 있다. 반대로, 네트워크가 매우 나빠서 키프레임이 2초 걸려 도착하면? 고정 1초 스로틀은 1초 후에 또 PLI를 보내서 불필요한 키프레임을 만든다. 인과관계 방식은 "아직 전 키프레임이 안 왔으니 기다린다."
Simulcast 환경에서는 고화질(h) 레이어와 저화질(l) 레이어의 키프레임이 크기가 다르다. h 키프레임은 30~50KB지만, l 키프레임은 5~10KB다. 같은 PLI인데 비용이 5배 차이 난다.
고정 스로틀은 이 차이를 모른다. h든 l이든 1초 간격으로 똑같이 제한한다. 하지만 실제로는 l 레이어의 PLI는 좀 더 관대하게 허용해도 된다. 비용이 작으니까. h 레이어의 PLI는 더 신중해야 한다. 키프레임 한 방에 대역폭이 크게 요동치니까.
인과관계 기반 방식에서는 레이어별로 독립적인 상태를 관리한다. h 레이어의 PLI 인과 체인과 l 레이어의 PLI 인과 체인이 따로 돌아간다. h 키프레임이 아직 도착 안 했더라도, l 키프레임은 이미 전달 완료되었을 수 있다. 각자의 상태에 따라 독립적으로 판단한다.
SFU 내부에서 키프레임이 필요한 상황은 subscriber의 PLI 요청만 있는 게 아니다. 새 참여자가 입장하거나, Simulcast 레이어가 전환되거나, 화면이 잠시 멈췄다가 재개될 때도 키프레임이 필요하다.
이런 경우, PLI를 한 번 보내고 끝내면 불안하다. 네트워크에서 PLI 자체가 유실될 수 있고, publisher 인코더가 즉시 반응 못할 수도 있다. 그래서 PLI를 여러 번 연달아 보내는 기법을 쓴다. 이걸 PLI burst라고 부른다. 예를 들어, 첫 PLI 직후 + 500ms 후 + 1500ms 후 — 3번에 걸쳐 보낸다.
문제는 이거다. 첫 번째 PLI에 publisher가 바로 반응해서 키프레임을 보내면, 2번째와 3번째 PLI는 쓸데없는 키프레임을 추가로 만들어낸다. 한 번이면 충분한데 세 번 만드는 거다. 앞서 말한 키프레임 폭풍의 또 다른 원인이 된다.
인과관계 기반에서는 이걸 자연스럽게 해결한다. 2번째 PLI를 보내기 전에 "전 PLI의 키프레임이 이미 도착했는가?" 확인한다. 도착했으면 — 남은 burst를 취소한다. 도착 안 했으면 — 예정대로 보낸다.
PLI burst + 자동 취소
[t=0ms] PLI #1 발사
[t=50ms] 키프레임 도착! → "이미 해결됐네"
[t=500ms] PLI #2 예정 → 취소 ✓ (키프레임 이미 옴)
[t=1500ms] PLI #3 예정 → 취소 ✓
고정 스로틀에서는 이런 취소가 불가능하다. 시간만 보니까, 키프레임이 왔는지 안 왔는지 모른다. 스케줄대로 3발 다 보낸다.
한 가지 주의할 점이 있다. 모든 PLI를 Governor로 제어하면 안 된다.
새 참여자가 입장했을 때 보내는 첫 키프레임 요청, 레이어 전환 후 보내는 키프레임 요청 — 이런 건 "인프라 PLI"다. 이건 품질 저하 때문이 아니라, 시스템이 정상 동작하기 위해 반드시 필요한 키프레임 요청이다.
인프라 PLI를 스로틀 걸면, 새로 들어온 사람이 1초 동안 검은 화면을 보거나, 레이어 전환 후 화면이 한참 멈춰있는 상황이 생긴다. 이건 최적화가 아니라 고장이다.
그래서 인프라 PLI는 Governor를 우회한다. 즉시 보내고, 즉시 처리한다. Governor가 제어하는 건 subscriber가 네트워크 손실로 인해 보내는 "일반 PLI"만이다.
| 고정 시간 스로틀 | 인과관계 기반 | |
|---|---|---|
| 판단 기준 | 마지막 PLI 이후 경과 시간 | 키프레임 도착 여부 |
| 반응 속도 | 타이머 만료까지 대기 | 키프레임 도착 즉시 해소 |
| 레이어 차등 | 없음 (전역 1개 타이머) | 레이어별 독립 상태 |
| burst 제어 | 스케줄대로 전부 발사 | 키프레임 도착 시 자동 취소 |
| 인프라 PLI | 구분 없음 | bypass 처리 |
| 구현 난이도 | 낮음 | 높음 |
고정 시간 스로틀이 나쁜 방법은 아니다. 구현이 간단하고 폭풍은 막을 수 있다. 소규모에서는 충분하다. 하지만 참여자가 늘어나고 Simulcast 레이어 전환이 잦아지면, "상황을 모르는 제어"의 한계가 드러난다.
인과관계 기반은 구현이 복잡하지만, "일어난 일을 관측하고 판단한다"는 원칙 자체가 단순하다. 시간이 아니라 사실을 본다.
PLI 제어의 핵심은 "키프레임이 필요한 상황과 불필요한 상황을 구분하는 것"이다.
고정 시간 스로틀은 시간으로 구분한다 — "1초 안 됐으니 필요 없겠지." 인과관계 기반은 결과로 구분한다 — "키프레임이 아직 안 왔으니 필요하다" 또는 "이미 왔으니 필요 없다."
다음 편에서는 RTCP의 마지막 주제, TWCC(Transport-Wide Congestion Control)를 다룬다. SR/RR은 5초 간격으로 대략적인 네트워크 상태를 보고하지만, TWCC는 패킷 한 개 단위로 도착 시간을 추적해서 혼잡이 시작되는 순간을 감지한다. 손실이 발생한 뒤에 보고하는 게 아니라, 손실이 발생하기 전에 비트레이트를 낮추는 것이 목적이다.
다음 편: [RTCP 완전정복 #5] TWCC/BWE — 대역폭 추정의 현실
이 글은 OxLens — Rust로 만드는 경량 SFU 서버 프로젝트를 개발하면서 배운 것들을 정리한 시리즈입니다.