RTMP(Real-Time Messaging Protocol) 레퍼런스 — 핸드셰이크·청크·Nginx·FFmpeg·OBS
이 글의 핵심
RTMP의 바이트 단위 핸드셰이크·청크 스트림·명령(AMF0)·오디오/비디오 메시지를 스펙 관점에서 정리하고, Nginx-RTMP·FFmpeg·OBS·Node(node-media-server)로 송수신·인증·멀티비트레이트까지 이어집니다. RTMPS/RTMPT·버퍼·지연·방화벽·성능 튜닝은 운영 체크리스트로 수렴합니다.
RTMP(Real-Time Messaging Protocol)는 원래 Flash 미디어 서버와 Flash 플레이어 사이의 프로토콜로 만들어졌고, Flash가 사라진 지금도 방송 송출(ingest) 쪽에서는 사실상 표준으로 쓰입니다. TCP 연결 하나 위에서 핸드셰이크 → 청크 스트림 다중화 → AMF(주로 AMF0) 명령으로 채널을 열고, 그 위에 오디오·비디오 프레임을 싣습니다. 설정 화면의 버튼만 아는 것보다 패킷이 어떻게 쌓이는지를 알면 장애 원인을 훨씬 빨리 좁힐 수 있기 때문에, 이 글은 스펙, 실무 설정, 튜닝, 트러블슈팅을 한데 묶어 정리합니다.
브라우저에서 RTMP를 직접 재생하는 길은 Flash 제거와 함께 사실상 사라졌습니다. 그래서 요즘은 RTMP로 받고(ingest) HLS·DASH·LL-HLS 같은 HTTP 방식으로 배포하는 구성이 일반적이며, 이 글도 ingest와 미디어 서버 쪽을 중심으로 다룹니다.
RTMP가 TCP에서 하는 일 (큰 그림)
RTMP는 UDP처럼 손실을 허용하는 전송이 아니라 TCP의 신뢰성(순서 보장, 재전송)에 기대는 바이트 스트림 프로토콜입니다. 그래서 네트워크 지터, 늦은 ACK, 재전송이 그대로 체감 지연으로 이어질 수 있습니다. 이것은 RTMP 자체의 결함이라기보다 혼잡 제어와 버퍼가 TCP 계층에 있기 때문에 생기는 성질입니다. TCP 혼잡 제어와 버퍼의 기본 틀은 TCP 가이드를 함께 보면 이해가 빨라집니다.
3-way handshake는 OS가, RTMP 핸드셰이크는 앱이
클라이언트와 서버가 connect()/accept()로 TCP 3-way handshake를 마친 뒤, RTMP는 애플리케이션 수준의 핸드셰이크를 한 번 더 합니다. 이 단계는 OS 소켓 API가 해 주는 것이 아니라 nginx-rtmp, node-media-server 같은 RTMP 구현이 직접 바이트를 읽고 씁니다.
RTMP 앱 수준 핸드셰이크(요지)
Adobe가 공개한 RTMP 스펙(버전 3)의 핸드셰이크는 C0/C1, S0/S1, S2, C2를 주고받는 흐름입니다. 클라이언트는 보통 C0과 C1을 한 번에 보내고, 서버는 S0, S1, S2를 연달아 보냅니다.
| 단계 | 보내는 측 | 내용(요지) |
|---|---|---|
| 1a | Client → Server | C0: 1바이트 버전(0x03) |
| 1b | Client → Server | C1: 1536바이트(4바이트 시각 + 4바이트 0 + 1528바이트 임의 데이터) |
| 2a | Server → Client | S0/S1: 서버의 버전과 1536바이트 블록 |
| 2b | Server → Client | S2: 클라이언트 C1을 되돌려 보내는 에코 |
| 3 | Client → Server | C2: 서버 S1을 되돌려 보내는 에코 |
- 목적: 버전을 맞추고, 양쪽이 상대가 보낸 블록을 정확히 되돌려 보내는지로 연결이 정상인지 확인합니다. 보안 기능은 아닙니다. 참고로 Flash 시절 일부 서버·클라이언트는 C1/S1 안에 HMAC 다이제스트를 넣는 “complex handshake”를 요구했는데, 공개 스펙에는 없는 방식이라 구현 간 호환 문제가 생기는 지점입니다.
- 운영 관점:
tcpdump나 Wireshark로 1935 포트를 봤을 때 TCP 연결은 됐는데 몇 초 만에 끊긴다면 RTMP 핸드셰이크 실패를 먼저 의심합니다. 흔한 원인은 중간 프록시가 첫 바이트를 건드리는 경우, RTMPS 서버에 평문 RTMP로 붙거나 그 반대인 경우, HTTPS 포트에 RTMP를 붙인 경우입니다.
청크(Chunk)와 메시지(메시지 포맷)
RTMP는 한 TCP 연결에서 여러 논리 채널을 섞어 보내기 위해 메시지를 청크로 잘라 보내고, 각 청크 앞에 가변 길이 헤더를 붙입니다. 큰 비디오 프레임 하나가 연결을 독점하지 않고 오디오나 제어 메시지가 사이사이 끼어들 수 있게 하는 것, 그리고 반복되는 헤더를 줄이는 것이 목적입니다.
-
기본 흐름
- 상위의 논리 메시지는 타입, 길이, 메시지 스트림 ID, 타임스탬프를 가집니다.
- 이 메시지를 청크 크기 단위(기본 128바이트,
Set Chunk Size로 변경 가능)로 자릅니다. - 각 청크 앞의 Basic Header에는 fmt(2비트)와 CSID(Chunk Stream ID)가 들어가며, CSID 값에 따라 1~3바이트입니다. 이어지는 Message Header는 fmt에 따라 11바이트(fmt 0: 전체), 7바이트(fmt 1: 스트림 ID 생략), 3바이트(fmt 2: 타임스탬프 델타만), 0바이트(fmt 3: 이전과 동일)로 줄어듭니다. 타임스탬프가 0xFFFFFF 이상이면 4바이트 확장 타임스탬프가 붙습니다.
-
CSID(Chunk Stream ID): 이 청크가 어느 청크 스트림에 속하는지를 나타냅니다. 2는 프로토콜 제어 메시지용으로 예약되어 있고, 2
63은 1바이트, 64 이상은 23바이트로 표현합니다. -
메시지 스트림 ID:
NetConnection(0)과,createStream명령으로 만든NetStream의 정수 ID를 구분합니다. 실무에서 가장 자주 마주치는 식별자입니다. -
타임스탬프(절대값과 델타): 오디오·비디오 정렬의 기준입니다. B-frame, 오디오 인터리빙과 맞물리므로, 서버가 송출자의 타임베이스를 그대로 믿을지 재먹싱하면서 보정할지 정책을 정해야 합니다.
실무 팁: 청크 구조 이해, Wireshark(필터
tcp.port==1935, RTMPT 해석기 사용), 서버 로그(nginxdebug/access) 세 가지를 조합하는 것이 가장 싸고 빠른 디버깅 방법입니다.
메시지 타입(요지)
자주 보는 Message Type ID는 다음과 같습니다. 서버와 클라이언트에 따라 일부는 생략하거나 무시합니다.
| Type (예시) | 설명(실무) |
|---|---|
| 1(Set Chunk Size), 2(Abort) | 청크 크기 변경, 전송 중인 메시지 중단 등 프로토콜 제어 |
| 3(Ack), 5(Window Ack Size), 6(Set Peer Bandwidth) | 수신 바이트 확인과 윈도 크기, 상대 송신 대역 제한 신호 |
| 4(User Control) | Stream Begin, Ping Request/Response 등 이벤트 |
| 8(Audio), 9(Video) | 오디오·비디오 페이로드(FLV 태그 형식의 코덱 헤더 + 프레임). 대역 대부분을 차지 |
| 18(Data: AMF0) | @setDataFrame/onMetaData 등 메타데이터 |
| 20(Command: AMF0) | connect/createStream/publish/play/deleteStream 등 명령, FCPublish/FCUnpublish 같은 비공식 확장 |
| 15/17(AMF3 Data/Command) | 같은 용도의 AMF3 인코딩 버전(Flex 계열 클라이언트) |
RTMP의 세션 제어는 AMF(주로 AMF0)로 직렬화한 객체를 주고받습니다. 그래서 publish나 play의 초기 단계에는 명령 메시지와 함께 코덱, 해상도, 프레임레이트 같은 메타데이터(@setDataFrame)가 오가며, 인증 실패나 스트림 이름 오류도 이 명령의 응답(_error, onStatus)으로 나타납니다.
스트림 ID·타임스탬프 운용(엔지니어 체크리스트)
- 메시지 스트림 ID:
createStream응답으로 받은 ID로publish나play를 보냅니다. 한 연결에서 여러 스트림을 다루거나 재발행할 때 이 매핑이 꼬이면 스트림을 찾지 못하는 오류가 납니다. - 타임스탬프: 립싱크와 A/V 인터리빙에 직접 영향을 줍니다. 긴 GOP, B-frame, 가변 프레임레이트가 겹치면 “RTMP 플레이어로는 괜찮은데 HLS로 변환한 쪽만 끊기는” 증상이 나기도 합니다. 이럴 때는 서버에서 트랜스코딩이나 재먹싱으로 타임스탬프를 정리하는 편이 안전합니다.
- Set Peer Bandwidth: 상대에게 출력 대역 상한을 알리는 신호일 뿐 TCP 혼잡 제어를 바꾸지 않으며, 구현에 따라 무시되기도 합니다. 실제 효과는 측정으로 확인해야 합니다.
Nginx-RTMP 모듈(대표) 템플릿
주의(보안):
record, 공개 HLS 경로,exec로 실행하는 셸 명령, 콜백 HTTP 엔드포인트와 시크릿, CORS·도메인 제한은 배포 전에 반드시 보안 정책과 함께 점검하십시오. 아래는 기능 설명 위주입니다.
# /etc/nginx/nginx.conf (또는 conf.d/ 아래 rtmp { ... } 블록)
worker_processes auto;
# 데몬/로그는 배포에 맞게
rtmp {
server {
listen 1935;
# chunk_size: 청크 전송(미디어) 단위 — 너무 작으면 오버헤드, 너무 크면 지연(체감)↑
chunk_size 4096; # 128~4K+ 범위에서 샘플링(환경·코덱·CPU에 따라)
# buflen: 내부 임시 버퍼(구현/버전/문서 스코프: 운영자가 "어느 지점에서 막힌다" 를 캡쳐로 대조)
buflen 1s;
application live {
live on;
# --- 녹화(옵션) ---
# record all;
# record_path /data/rec;
# record_unique on;
# record_suffix .flv;
# --- HLS(배포 쪽 흔한 패턴) ---
hls on;
hls_path /var/www/hls;
hls_fragment 2s; # 지연 vs 안정(세그 짧을수록 지연↓, 스위치 비용/오버헤드↑)
hls_playlist_length 6s;
# 푸시(옵션: CDN/다른 rtmp)
# push rtmp://cdn.example.com/app1;
# rtmp://host/live/$name 로 들어온 퍼블리시/플레이를 콜백으로 사전승인
on_play http://127.0.0.1:9000/auth;
on_publish http://127.0.0.1:9000/auth;
}
}
}
http {
server {
listen 80;
server_name _;
# HLS 정적 서빙(HTTPS는 별도 server 블록/인증서)
location /hls {
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /var/www;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *; # 잠깐: 실서비스는 엄격히(도메인 제한)
}
}
}
chunk_size: 작게 잡으면 헤더 비중이 늘고, 크게 잡으면 큰 비디오 청크가 오디오·제어 메시지를 오래 막을 수 있습니다. nginx-rtmp 기본값은 4096이며, 정답은 없으므로 대역·CPU·지연을 A/B로 측정해 정합니다.hls_fragment/hls_playlist_length: 플레이리스트 길이가 조각 길이의 3배보다 짧으면 플레이어가 재생 시작 위치를 잡기 어렵고, 네트워크가 잠깐 느려지면 이미 삭제된 조각을 요청할 수 있습니다. 위 예의 6s/2s는 최소한의 값입니다.on_publish/on_play: nginx-rtmp는 콜백 URL에 POST를 보내고, 응답이 2xx면 허용, 3xx면 리다이렉트, 그 밖의 코드면 연결을 끊습니다. 응답 본문은 보지 않습니다. 아래 Node 예시로 이어집니다.
간이 인증(Express 예시): 실서비스라면 HMAC 서명, IP 제한, 토큰 만료, 레이트 리밋, 감사 로그를 더해야 합니다.
// auth-server.js (node 18+)
// npm i express
import express from "express";
const app = express();
// nginx-rtmp의 http 콜백은 application/x-www-form-urlencoded POST
app.post("/auth", express.urlencoded({ extended: true }), (req, res) => {
const name = String(req.body.name || ""); // 스트림 이름(키)
const call = String(req.body.call || ""); // "publish" 또는 "play"
const ip = String(req.body.addr || ""); // 클라이언트 IP
const ok = /^\w+-\d+$/.test(name); // 예: live-2026042312 형태만 허용(샘플)
if (ok) return res.status(200).end(); // nginx-rtmp는 2xx면 허용(본문은 보지 않음)
return res.status(403).end();
});
app.listen(9000, "127.0.0.1", () => {
console.log("rtmp http auth: http://127.0.0.1:9000/auth");
});
운영할 때는 nginx와 인증 서버 사이의 연결, 타임아웃, 그리고 nginx-rtmp가 보내는 필드 이름(
call,name,addr,app,tcurl및 URL 쿼리 인자)을 버전별 문서와 대조하십시오. “403만 찍힌다”는 문의의 상당수는 인증 서버가 존재하지 않는 필드를 읽고 있는 경우입니다.
FFmpeg로 RTMP 퍼블리시(핵심 옵션)
기본(재인코딩):
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -tune zerolatency -g 50 -keyint_min 50 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 44100 -ac 2 \
-f flv "rtmp://rtmp.example.com/live/STREAMKEY"
copy(재인코딩 없이 전달 — 코덱이 FLV/RTMP에 맞을 때):
ffmpeg -re -i srt://source:2000?mode=caller \
-c copy -f flv "rtmp://rtmp.example.com/live/KEY"
-re: 입력을 실시간 속도로 읽게 합니다. 파일을 라이브처럼 송출할 때 쓰는 옵션이라, SRT나 RTMP처럼 이미 실시간으로 들어오는 소스에 붙이면 필요 없거나 오히려 지연이 쌓일 수 있습니다. 위copy예시처럼 라이브 소스라면 빼는 편이 맞습니다.-g/키프레임 간격: 짧을수록 HLS 시작·탐색·오류 복구가 빨라지지만 같은 화질에서 대역과 CPU가 늘어납니다. 위 예시의-g 50은 25fps 기준 2초이며, 플랫폼 권고(대개 2초)와 콘텐츠에 맞춰 정합니다.- 코덱 제약: 전통적인 RTMP/FLV는 H.264와 AAC(또는 MP3)만 담을 수 있어, HEVC나 Opus 소스를
-c copy로 보내면 실패합니다. HEVC·AV1·VP9는 2023년에 나온 Enhanced RTMP 확장으로 전송할 수 있지만, 서버와 수신 플랫폼이 이를 지원해야 합니다. - B-frame·프로파일·레벨: 일부 구형 단말이나 트랜스코딩을 거치지 않는 경로에서 재생 문제를 일으킬 수 있으므로, 디코딩·CPU·지연·화질을 비교 측정해 정합니다.
OBS Studio (송출) — 실제로 설정하는 항목
- 설정 → 방송
- 서비스: 사용자 지정
- 서버:
rtmp://HOST/app(애플리케이션 경로까지만 적습니다) - 스트림 키: 서버에서 발급한 키를 따로 입력합니다. OBS는 서버 URL 뒤에
/스트림키를 붙여 접속합니다. 키 회전·만료 정책은 서버 정책을 따릅니다.
- 출력(인코딩):
x264(CPU)와NVENC/QuickSync/AMF(하드웨어) 중에서 CPU 부하, 화질, 지연의 균형을 보고 고릅니다. - 키프레임 간격: 플랫폼 권고에 따라 보통 2초로 둡니다. 너무 긴 GOP는 끊김 복구와 HLS 채널 전환 시간 면에서 불리합니다.
- 고급(네트워크): 동적 비트레이트, B-frame, CBR/VBR 설정. 서버에서 ABR 래더로 트랜스코딩한다면 송출은 안정적인 CBR로 두고 화질 단계는 서버에 맡기는 구성이 흔합니다.
주의(보안):
rtmp://user:pass@host처럼 URL에 자격 증명을 넣으면 패킷 캡처, 로그, 셸 히스토리, 저장소에 그대로 남기 쉽습니다. 스트림 키와 HTTPS 콜백 인증을 조합하고, 전송 구간 암호화가 필요하면 RTMPS나 SRT, stunnel 같은 TLS 래퍼를 쓰는 편이 안전합니다(TLS/HTTPS 참고).
Node.js: RTMP 서버(검증된 모듈 사용)
raw 소켓으로 RTMP 전체를 직접 구현하려면 청크 프레이밍, AMF0, A/V 디먹싱까지 한꺼번에 다뤄야 합니다. 운영 환경에서는 Nginx-RTMP, SRS, OvenMediaEngine, node-media-server 같은 검증된 구현을 쓰는 것이 비용 대비 합리적이고, 학습이나 소규모 용도라면 node-media-server로 빠르게 시작할 수 있습니다. 아래 예시는 node-media-server 2.x의 설정 형식이며, 4.x에서는 설정과 이벤트 API가 바뀌었으니 사용하는 버전의 문서를 확인하십시오.
// nms.mjs
// npm i node-media-server
import NodeMediaServer from "node-media-server";
const nms = new NodeMediaServer({
rtmp: {
port: 1935,
chunk_size: 4096,
gop_cache: true,
ping: 30,
ping_timeout: 60,
},
http: {
port: 8000,
mediaroot: "./media",
allow_origin: "*", // dev 전용(실서비스는 CORS/도메인 제한)
},
trans: {
ffmpeg: process.platform === "win32" ? "ffmpeg.exe" : "/usr/bin/ffmpeg",
tasks: [
{
app: "live",
hls: true,
hlsFlags: "[hls_time=2:hls_list_size=3:hls_flags=delete_segments]",
dash: true,
dashFlags: "[f=dash:window_size=3:extra_window_size=1]",
mp4: false,
mp4Flags: "[movflags=frag_keyframe+empty_moov]",
},
],
},
});
// hook 예시(권한/로그)
nms.on("prePublish", (id, path, args) => {
console.log("prePublish", { id, path, args });
// 거절: nms.getSession(id).reject() (2.x 기준, 버전별 시그니처는 패키지 문서 확인)
});
nms.run();
gop_cache: 마지막 GOP를 메모리에 들고 있다가 새 시청자에게 먼저 보내 줍니다. 첫 화면은 빨리 뜨지만, 시청자가 최대 GOP 길이만큼 과거부터 보게 되므로 지연이 늘고 스트림당 메모리를 씁니다. 저지연이 목표라면 끄는 것을 검토하십시오.trans+ FFmpeg: 다중 화질(ABR)을 만들려면 결국ffmpeg의 스케일·인코딩·매핑 구성이 필요하고, 그때부터 CPU·메모리·큐 관리가 핵심 과제가 됩니다.
다중 화질(ABR) — 개념과 FFmpeg(예시)
서버에서 입력 하나를 세 가지 비트레이트로 나눠 HLS로 내보내는 기본 구조입니다. 경로, 코덱, 프로파일은 실제 환경에 맞게 바꾸십시오.
# 개념 예시: 한 입력을 3개 비디오 + 공유 오디오 그룹 -> HLS 마스터
ffmpeg -i rtmp://in/app/key \
-filter_complex \
"[0:v]split=3[v1][v2][v3]; \
[v1]scale=1280:-2[v1o]; [v2]scale=960:-2[v2o]; [v3]scale=640:-2[v3o]" \
-map "[v1o]" -c:v:0 libx264 -b:v:0 3500k -maxrate:v:0 4000k -bufsize:v:0 8000k \
-map "[v2o]" -c:v:1 libx264 -b:v:1 2000k -maxrate:v:1 2500k -bufsize:v:1 5000k \
-map "[v3o]" -c:v:2 libx264 -b:v:2 800k -maxrate:v:2 900k -bufsize:v:2 2000k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-map 0:a:0? -c:a aac -b:a 128k -ac 2 \
-f hls -hls_time 2 -hls_list_size 6 -hls_flags delete_segments+independent_segments \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,agroup:aud v:1,agroup:aud v:2,agroup:aud a:0,agroup:aud" \
/out/hls/stream_%v.m3u8
-var_stream_map에 v:0 v:1 v:2 a:0처럼 오디오를 독립 variant로만 두면, 마스터 플레이리스트에 오디오 없는 비디오 variant와 오디오 전용 variant가 따로 생겨 플레이어가 소리 없는 비디오를 고를 수 있습니다. 위처럼 agroup으로 오디오를 공유 그룹(EXT-X-MEDIA)으로 묶어야 합니다. 세 비디오에 같은 -g와 -sc_threshold 0을 주는 것도 variant 간 세그먼트 정렬에 필수입니다.
운영에서는 (1) GPU·노드 수, (2) 트랜스코딩 큐와 백프레셔, (3) 키프레임 정렬을 대시보드(큐 길이, GOP 누락, 초기 버퍼)와 함께 봅니다.
RTMP vs RTMPS vs RTMPT
- RTMP(일반): 보통 1935/TCP를 쓰는 평문 프로토콜이라 스트림 키와 미디어가 경로상에서 노출되고 변조될 수 있습니다. 그래서 ingest 구간을 사설망이나 VPN으로 보호하는 경우가 많습니다.
- RTMPS: RTMP를 TLS로 감싼 것으로, 흔히 443 포트를 씁니다. 인증서 체인, SNI, TLS 1.2/1.3, 암호 스위트 같은 고민은 일반 TLS와 같고, 추가로 핸드셰이크 RTT만큼의 연결 지연과 서버의 TLS 처리 부하(세션 재사용, 오프로딩)를 고려해야 합니다. 주요 라이브 플랫폼은 RTMPS ingest를 지원하거나 요구합니다.
- RTMPT(HTTP 터널): RTMP를 HTTP POST 요청들로 감싸 80/443만 열린 방화벽을 통과하려던 레거시 방식입니다. HTTP 오버헤드와 폴링 때문에 효율이 낮고 지원하는 서버도 줄었으므로, 지금은 RTMPS, SRT, WebRTC(WHIP) 같은 대안을 먼저 비교하는 편이 낫습니다.
”청크/버퍼/대역” 최적화(현실)
- RTMP 청크 크기, TCP MSS, 애플리케이션
chunk_size설정은 서로 다른 층의 이야기입니다. 지연이나 CPU 문제가 인코더, 서버, 리버스 프록시, 플레이어 중 어디에서 재현되는지 분리해서 측정하십시오. - RTMP의 Window Ack/Set Peer Bandwidth와 TCP의
rwnd/cwnd는 따로 움직입니다. 대역 초과, 끊김, 재전송 타임아웃 문제는ss,tc qdisc, 혼잡 제어 알고리즘(BBR 등), 인코딩 설정, GOP·버퍼를 번갈아 보면서 원인을 좁힙니다. - 버퍼는 안정성을 주는 대신 지연을 비용으로 받습니다. 인코더, 서버(HLS 조각 길이,
gop_cache), 플레이어의 초기 버퍼, CDN 캐시가 모두 한 체인으로 연결되어 있다는 점을 기억하십시오.
트러블슈팅(현장)
연결 실패(빠른 점검표)
- (a) L4:
tcp/1935가 LISTEN 상태인지, 방화벽·보안 그룹이 열려 있는지 확인합니다. - (b) TLS·프록시: RTMPS 서버에 평문으로 붙거나 그 반대인지, 인증서·SNI가 맞는지, 로드밸런서(ALB는 HTTP 전용이므로 RTMP에는 NLB 같은 L4 LB 필요)와 프록시의 유휴 타임아웃을 봅니다.
- (c) 애플리케이션: TCP 연결 직후 1~2초 안에 RST가 온다면 핸드셰이크나 앱 설정(잘못된 애플리케이션 이름 등)을 의심합니다.
- (d) 인증: TCP는 되는데 publish만 거절된다면
on_publish콜백의 응답 코드와 필드 이름을 확인합니다.
지연(latency) 최적화(순서)
- 인코더: 키프레임 간격, B-frame, 프로파일, 가변 프레임레이트
- 첫 프레임과 버퍼: 플레이어 버퍼, HLS
hls_fragment/hls_time - 서버: 큐,
gop_cache, A/V 정렬, 트랜스코딩 대기 - 경로: 혼잡, 재전송, RTO — Wi-Fi, 이동통신망, 리전 간 구간
- OS(TCP):
nodelay, 버퍼, 큐 — 근거 없이sysctl부터 바꾸지 말고 마지막에 봅니다
패킷 손실(체감)
TCP 위에서는 손실이 곧 재전송이고 재전송은 지연과 지터로 나타납니다. 특히 I-frame을 싣던 구간에서 재전송이 길어지면 플레이어나 디코더에 따라 화면이 멈추거나 깨지거나 건너뜁니다. 회선 품질이 나쁜 구간이라면 ARQ와 지연 상한을 직접 조절할 수 있는 SRT로 바꾸거나, 재전송·RTT를 관측하면서 송출 비트레이트를 낮추는 것이 현실적인 대응입니다.
방화벽(정책)
- 송출 측(인코더 → 인터넷): 1935(또는 RTMPS 443) 아웃바운드가 열려 있어야 합니다.
- 시청 측: HLS(HTTP/443)와 CDN 엣지로 받으므로, 송출 포트와 시청 포트를 혼동하지 마십시오.
- 기업 HTTP 프록시: HTTP/HTTPS는 되는데 1935만 막힌 환경이라면 RTMPT보다 RTMPS(443)나 SRT, 별도 터널을 쓰는 편이 보안과 성능 면에서 대개 낫습니다.
성능(스케일링) 점검
- CPU: 트랜스코딩, HLS 세그먼트 생성, FFmpeg 프로세스 수가 가장 흔한 병목입니다(특히 4K나 고프레임레이트). GPU 인코딩이나 전용 트랜스코딩 노드를 검토합니다.
- 메모리·디스크 I/O: HLS 세그먼트와 녹화 파일 쓰기·삭제가 계속 일어나므로, 작은 파일이 많은 NFS보다 로컬 SSD가 유리합니다.
- 네트워크:
chunk_size만 바꾸기보다 BDP, RTO, 혼잡 제어 알고리즘, 로드밸런서 유휴 타임아웃, TLS 세션 재사용을 함께 봅니다. - 다중 프로세스·샤딩: 스트림 키 해시 기반 분산, 애플리케이션별 인스턴스, 엣지 ingest + 코어 트랜스코딩 같은 구조를 단일 장애점과 장애 범위까지 고려해 설계합니다.
- 커널 TCP 버퍼: BDP(대역×RTT)의 2배 정도는 출발점일 뿐이고,
ss -ti로 재전송·드롭·페이싱을 관측하지 않는 튜닝은 위험합니다.
# (예시) 서버 BDP 100Mbps × 40ms = ~0.5MB급(대략) — "환경마다" 다름(측정 필수)
# Linux: /proc/sys/net/core/rmem_max, wmem_max, tcp_rmem, tcp_wmem, netdev_max_backlog
# 변경 전/후 iperf+실제 RTMP A/B(동시 n스트림)를 같이
권고:
sysctl변경은 지연, 큐, 드롭, RTO 그래프를 보면서 하고, 언제든 되돌릴 수 있게 기록해 두십시오.
필드 단위로 확인해야 할 때
이 글은 운영·구현 관점에서 썼기 때문에 바이트 단위 레이아웃은 다루지 않았습니다. RTMP는 Adobe가 공개한 초기 스펙 이후 구현마다 세부 차이가 있어서, 필드가 필요하면 1935 포트 트래픽을 캡처해 Wireshark의 RTMP·AMF 해석기로 열어 보는 것이 문서보다 정확합니다. 팀에서 실제로 쓰는 인코더와 서버 조합으로 캡처를 한 번 떠서 공용 스냅샷으로 남겨 두면, 핸드셰이크나 connect 명령의 필드 문제를 디버깅할 때 비교 기준이 됩니다.