Python 비디오 스트리밍으로 포르노를 20배 더 효율적으로 만든 방법
게시 됨: 2022-03-11소개
포르노는 큰 산업입니다. 인터넷에서 가장 큰 플레이어의 트래픽과 경쟁할 수 있는 사이트는 많지 않습니다.
그리고 이 엄청난 트래픽을 저글링하는 것은 어렵습니다. 상황을 더욱 어렵게 만들기 위해 포르노 사이트에서 제공되는 콘텐츠의 대부분은 단순한 정적 비디오 콘텐츠가 아닌 대기 시간이 짧은 라이브 비디오 스트림으로 구성됩니다. 그러나 관련된 모든 문제에 대해 나는 그것을 맡는 파이썬 개발자에 대해 거의 읽은 적이 없습니다. 그래서 나는 직장에서 내 자신의 경험을 쓰기로 결정했습니다.
뭐가 문제 야?
몇 년 전 나는 포르노 산업뿐만 아니라 세계에서 가장 많이 방문한 26번째(당시) 웹사이트에서 일하고 있었습니다.
당시 이 사이트는 RTMP(실시간 메시징 프로토콜)를 사용하여 포르노 비디오 스트리밍 요청을 제공했습니다. 보다 구체적으로 말하면 Adobe에서 구축한 FMS(Flash Media Server) 솔루션을 사용하여 사용자에게 라이브 스트림을 제공했습니다. 기본 과정은 다음과 같았습니다.
- 사용자가 일부 라이브 스트림에 대한 액세스를 요청합니다.
- 서버는 원하는 영상을 재생하는 RTMP 세션으로 응답합니다.
몇 가지 이유로 FMS는 우리에게 좋은 선택이 아니었습니다. 비용부터 시작하여 다음 두 가지를 모두 구매해야 했습니다.
- FMS를 실행한 모든 컴퓨터에 대한 Windows 라이선스.
- ~$4,000 FMS 관련 라이선스. 그 중 규모로 인해 수백 개(매일 더 많이)를 구매해야 했습니다.
이 모든 수수료가 쌓이기 시작했습니다. 그리고 비용을 제쳐두고 FMS는 특히 기능면에서 부족한 제품이었습니다(좀 더 자세히 설명). 그래서 나는 FMS를 폐기하고 내 자신의 Python RTMP 파서를 처음부터 작성하기로 결정했습니다.
결국 나는 우리 서비스를 약 20배 더 효율적으로 만들 수 있었습니다.
시작하기
관련된 두 가지 핵심 문제가 있었습니다. 첫째, RTMP 및 기타 Adobe 프로토콜 및 형식이 공개되지 않아(즉, 공개적으로 사용 가능) 작업하기 어려웠습니다. 당신이 전혀 알지 못하는 형식의 파일을 어떻게 되돌리거나 구문 분석할 수 있습니까? 운 좋게도 우리가 작업의 기반이 된 공개 영역(Adobe에서 제작하지 않고 OS Flash라는 그룹에서 현재는 사용되지 않음)에서 사용할 수 있는 반전 노력이 있었습니다.
참고: Adobe는 나중에 Adobe에서 생산하지 않은 역방향 위키 및 문서에 이미 공개된 것보다 더 많은 정보가 포함되지 않은 "사양"을 발표했습니다. 그들의 (Adobe의) 사양은 터무니 없이 낮은 품질이었고 그들의 라이브러리를 실제로 사용하는 것을 거의 불가능하게 만들었습니다. 게다가 프로토콜 자체가 때때로 의도적으로 오해의 소지가 있는 것처럼 보였습니다. 예를 들어:
- 그들은 29비트 정수를 사용했습니다.
- 여기에는 리틀 엔디안인 특정(아직 표시되지 않은) 필드를 제외하고 모든 곳에서 빅 엔디안 형식의 프로토콜 헤더가 포함되었습니다.
- 그들은 9k 비디오 프레임을 전송할 때 계산 능력을 희생하면서 더 적은 공간에 데이터를 압축했습니다. 이는 한 번에 비트 또는 바이트를 다시 얻었기 때문에 의미가 거의 또는 전혀 의미가 없었습니다. 이러한 파일 크기에 대해 유의미한 이득이 없었습니다.
그리고 두 번째로 RTMP는 세션 지향성이 높기 때문에 들어오는 스트림을 멀티캐스트하는 것이 사실상 불가능합니다. 이상적으로는 여러 사용자가 동일한 라이브 스트림을 시청하기를 원할 경우 해당 스트림이 방송되는 단일 세션에 대한 포인터를 다시 전달할 수 있습니다(멀티캐스트 비디오 스트리밍). 그러나 RTMP를 사용하면 액세스를 원하는 모든 사용자를 위해 완전히 새로운 스트림 인스턴스를 만들어야 했습니다. 이것은 완전한 낭비였습니다.
내 멀티캐스트 비디오 스트리밍 솔루션
이를 염두에 두고 일반적인 응답 스트림을 FLV '태그'('태그'는 일부 비디오, 오디오 또는 메타 데이터임)로 다시 패키징/분석하기로 결정했습니다. 이러한 FLV 태그는 거의 문제 없이 RTMP 내에서 이동할 수 있습니다.
이러한 접근 방식의 이점:
- 스트림을 한 번만 재패키징하면 됩니다(위에 설명된 사양 및 프로토콜 문제가 없기 때문에 재패키징하는 것은 악몽이었습니다).
- FLV 태그에 대한 내부 포인터(스트림에서 위치를 나타내는 일종의 오프셋과 함께)는 내용.
나는 당시 내가 가장 잘 알고 있던 언어로 개발을 시작했습니다. C. 시간이 지남에 따라 이 선택은 번거로워졌습니다. 그래서 C 코드를 포팅하면서 파이썬의 기초를 배우기 시작했습니다. 개발 프로세스는 빨라졌지만 몇 번의 데모 후에 리소스를 고갈시키는 문제에 빠르게 직면했습니다. Python의 소켓 처리는 이러한 유형의 상황을 처리하기 위한 것이 아닙니다. 특히 Python에서는 작업당 여러 시스템 호출과 컨텍스트 전환을 수행하여 엄청난 양의 오버헤드를 추가하는 것을 발견했습니다.
비디오 스트리밍 성능 개선: Python, RTMP 및 C 혼합
코드를 프로파일링한 후 성능에 중요한 기능을 완전히 C로 작성된 Python 모듈로 옮기기로 결정했습니다. 이것은 상당히 낮은 수준의 내용이었습니다. 특히 커널의 epoll 메커니즘을 사용하여 로그 성장 순서를 제공했습니다. .
비동기 소켓 프로그래밍에는 주어진 소켓이 읽기/쓰기 가능/오류로 채워져 있는지 여부에 대한 정보를 제공할 수 있는 기능이 있습니다. 과거에 개발자는 이 정보를 얻기 위해 select() 시스템 호출을 사용했는데, 이는 확장성이 좋지 않습니다. Poll()은 선택의 더 나은 버전이지만 모든 호출에서 많은 소켓 설명자를 전달해야 하므로 여전히 좋지 않습니다.

Epoll은 소켓을 등록하기만 하면 시스템이 그 고유한 소켓을 기억하여 내부적으로 모든 세부 사항을 처리하기 때문에 놀랍습니다. 따라서 각 호출에 인수 전달 오버헤드가 없습니다. 또한 훨씬 더 잘 확장되고 관심 있는 소켓만 반환합니다. 이는 100k 소켓 설명자 목록을 실행하여 비트 마스크가 있는 이벤트가 있는지 확인하는 것보다 훨씬 낫습니다. 다른 솔루션을 사용하는 경우 수행해야 합니다.
그러나 성능 향상을 위해 우리는 대가를 치렀습니다. 이 접근 방식은 이전과 완전히 다른 디자인 패턴을 따랐습니다. 사이트의 이전 접근 방식은 (제 기억이 맞다면) 수신 및 송신을 차단하는 하나의 모놀리식 프로세스였습니다. 저는 이벤트 기반 솔루션을 개발 중이었기 때문에 이 새 모델에 맞게 나머지 코드도 리팩토링해야 했습니다.
특히, 우리의 새로운 접근 방식에는 다음과 같이 수신 및 전송을 처리하는 메인 루프가 있습니다.
- 수신된 데이터는 RTMP 계층까지 (메시지로) 전달되었습니다.
- RTMP를 해부하고 FLV 태그를 추출했습니다.
- FLV 데이터는 버퍼링 및 멀티캐스팅 계층으로 전송되어 스트림을 구성하고 발신자의 하위 수준 버퍼를 채웠습니다.
- 발신자는 모든 클라이언트에 대한 구조체를 마지막으로 보낸 색인과 함께 보관하고 가능한 한 많은 데이터를 클라이언트에 보내려고 했습니다.
이것은 데이터의 롤링 윈도우였으며 클라이언트가 수신하기에 너무 느릴 때 프레임을 삭제하는 몇 가지 경험적 방법을 포함했습니다. 일이 잘 풀렸습니다.
시스템 수준, 아키텍처 및 하드웨어 문제
그러나 우리는 또 다른 문제에 봉착했습니다. 커널의 컨텍스트 전환이 부담이 되고 있다는 것입니다. 결과적으로 우리는 즉시 쓰기가 아니라 100밀리초마다 쓰기를 선택했습니다. 이것은 더 작은 패킷을 집계하고 컨텍스트 스위치의 버스트를 방지했습니다.
아마도 더 큰 문제는 서버 아키텍처 영역에 있었습니다. 로드 밸런싱 및 장애 조치가 가능한 클러스터가 필요했습니다. 서버 오작동으로 인해 사용자를 잃는 것은 재미가 없습니다. 처음에는 지정된 '디렉터'가 수요를 예측하여 방송사 피드를 생성하고 파괴하려고 시도하는 분리형 디렉터 접근 방식을 사용했습니다. 이것은 훌륭하게 실패했습니다. 사실, 우리가 시도한 모든 것이 상당히 실패했습니다. 결국 우리는 클러스터 노드 간에 브로드캐스터를 무작위로 공유하여 트래픽을 균등하게 하는 비교적 무차별적인 접근 방식을 선택했습니다.
이것은 효과가 있었지만 한 가지 단점이 있었습니다. 일반적인 경우는 꽤 잘 처리되었지만 사이트의 모든 사람(또는 불균형한 수의 사용자)이 단일 방송인을 시청했을 때 끔찍한 성능을 보았습니다. 좋은 소식은 마케팅 캠페인 밖에서는 절대 일어나지 않는다는 것입니다. 우리는 이 시나리오를 처리하기 위해 별도의 클러스터를 구현했지만 실제로 마케팅 노력을 위해 유료 사용자 경험을 위험에 빠뜨리는 것은 무의미하다고 추론했습니다. 사례).
결론
최종 결과의 일부 통계: 클러스터의 일일 트래픽은 피크 시 약 100,000명의 사용자(60% 로드), 평균 ~50,000명이었습니다. 두 개의 클러스터(HUN 및 US)를 관리했습니다. 그들 각각은 부하를 분담하기 위해 약 40대의 기계를 처리했습니다. 클러스터의 집계된 대역폭은 약 50Gbps였으며 최대 부하일 때 약 10Gbps를 사용했습니다. 결국 나는 10Gbps/머신을 쉽게 밀어낼 수 있었습니다. 이론적으로 1 이면 이 수치는 시스템당 30Gbps까지 올라갈 수 있으며, 이는 약 300,000명의 사용자가 한 서버에서 동시에 스트림을 시청한다는 의미입니다.
기존 FMS 클러스터에는 200개 이상의 시스템이 포함되어 있었는데 이 시스템을 15대로 교체할 수 있었습니다. 이것은 우리에게 대략 200/10 = 20x 개선을 제공했습니다.
아마도 Python 비디오 스트리밍 프로젝트에서 내가 얻은 가장 큰 교훈은 새로운 기술을 배워야 한다는 전망 때문에 스스로를 멈추게 해서는 안 된다는 것이었습니다. 특히 파이썬, 트랜스코딩, 객체 지향 프로그래밍은 모두 이 멀티캐스트 비디오 프로젝트를 시작하기 전에 매우 하위 수준의 경험을 가진 개념이었습니다.
즉, 자체 솔루션을 롤링하는 것은 큰 비용을 지불할 수 있습니다.
1 나중에 우리가 코드를 프로덕션에 넣을 때 낮은 PCI 대역폭 때문에 10기가비트 이더넷 카드를 처리할 수 없는 구형 sr2500 Intel 서버를 사용하면서 하드웨어 문제에 부딪쳤습니다. 대신 1-4x1 Gbit 이더넷 본드에서 사용했습니다(여러 네트워크 인터페이스 카드의 성능을 가상 카드로 집계). 결국 우리는 성능 꼬임 없이 광학을 통해 10Gbps를 제공하는 최신 sr2600 i7 Intel을 얻었습니다. 모든 예상 계산은 이 하드웨어를 참조합니다.