Transmisja na żywo na AWS: sygnał, DRM i test na dwa i pół miliona widzów
Dzielimy się doświadczeniem z projektu, w którym obok istniejącego toru transmisji na żywo stanął drugi, zbudowany na AWS. Sygnał, szyfrowanie treści, player na klastrze i test obciążeniowy. Opisujemy, co w tym łańcuchu jest trudne naprawdę, a co tylko wygląda groźnie na slajdzie.
Transmisja na żywo jest jedną z niewielu rzeczy w IT, których nie da się poprawić po fakcie. Jeśli obraz stanie w trakcie meczu, nie ma znaczenia, że pół godziny później wszystko wróciło. Dlatego działającego toru transmisji nie wymienia się pochopnie i słusznie.
Nasi konsultanci pracowali przy projekcie, w którym pytanie postawiono inaczej. Nie „czy zamienić działający tor”, tylko „czy da się zbudować drugi, w chmurze, uruchomić go równolegle i przełączać ruch, kiedy zechcemy”. To jest pytanie, na które proof of concept potrafi odpowiedzieć uczciwie.
Poniżej dzielimy się tym, czego się przy tym nauczyliśmy: co ten łańcuch faktycznie robi, gdzie się potyka i co wyszło z testu obciążeniowego. Nie piszemy, dla kogo ani gdzie.
Co trzeba było sprawdzić
Cztery rzeczy, w tej kolejności:
- Czy sygnał ze studia dojdzie do chmury i wyjdzie z niej jako strumień adaptacyjny bez utraty jakości nadawczej.
- Czy da się nałożyć na niego DRM, którego wymaga dział praw autorskich, i czy player to odtworzy.
- Czy player wytrzyma wejście widzów liczone w milionach w kilkanaście minut.
- Czy ruch da się przełączyć między torem chmurowym a istniejącym na żądanie, o zadanej godzinie.
Punkty pierwszy i czwarty poszły gładko. Punkt drugi zjadł najwięcej czasu. Punkt trzeci nauczył nas czegoś o samych testach, nie o systemie.
Sygnał: MediaConnect, MediaLive, MediaPackage
Łańcuch AWS Elemental wygląda znajomo dla każdego, kto pracował z enkoderami sprzętowymi, tylko poszczególne pudełka stoją w chmurze.
MediaConnect przyjmuje sygnał ze studia. W naszym przypadku po SRT, bo to protokół, który radzi sobie z publicznym internetem: nadaje po UDP, ale sam odtwarza zgubione pakiety i szyfruje transport. Dostęp jest oparty na uprawnieniach, więc strumienia nie odbierze ktoś, kto zna adres.
MediaLive to enkoder. Tu zapada decyzja, która potem ciągnie się przez cały projekt: klasa kanału. Kanał w klasie STANDARD to dwa niezależne pipeline’y w dwóch strefach dostępności, przełączane automatycznie. Kanał SINGLE_PIPELINE to jeden. Różnica w cenie jest mniej więcej dwukrotna i nie da się jej zmienić po utworzeniu kanału, trzeba go odtworzyć. W torze podstawowym bierzemy STANDARD, w zapasowym SINGLE_PIPELINE, chyba że wydarzenie jest premium.
Drugą decyzją jest drabina ABR, czyli zestaw wariantów jakości. U nas cztery: 1080p przy 6 Mb/s, 720p przy 3 Mb/s, 480p przy 1,5 Mb/s i 240p przy 700 kb/s. Ta drabina jest kosztem stałym. MediaLive liczy sobie za godzinę pracy kanału, nie za obejrzane minuty, więc kanał włączony na noc kosztuje tyle samo przy zerze widzów co przy komplecie.
MediaPackage pakuje wyjście enkodera na żądanie, do HLS i DASH naraz, i to on jest źródłem dla CDN. Pakowanie just in time oznacza, że nie trzymamy dwóch kompletów segmentów, tylko jeden zestaw CMAF, z którego manifesty generują się na bieżąco. To samo miejsce odpowiada za szyfrowanie treści, o czym za chwilę.
Rzecz, o której warto wiedzieć zawczasu: między MediaLive a MediaPackage wysyłamy CMAF, a nie klasyczny HLS z segmentami TS. Wtedy segmenty w obu torach mają te same nazwy i te same granice, co jest warunkiem sensownego przełączania.
Architektura
DRM, czyli miejsce, w którym PoC najczęściej staje
Szyfrowanie treści jest tą częścią, która na diagramie zajmuje jeden prostokąt, a w kalendarzu dwa tygodnie.
Standard, który to spina, nazywa się SPEKE. To jest umowa o tym, jak pakowacz rozmawia z dostawcą kluczy. MediaPackage pyta serwer kluczy o materiał do zaszyfrowania konkretnej treści, dostaje klucze, dostaje gotowy blok PSSH do wstawienia w manifest i adres serwera licencji. Potem player prosi ten sam serwer o licencję i dopiero wtedy obraz rusza.
W wersji drugiej standardu jedno żądanie obsługuje wiele kluczy naraz, co ma znaczenie, kiedy chcecie osobno szyfrować obraz i dźwięk albo dawać różne klucze różnym poziomom jakości. My szyfrowaliśmy Widevine na DASH w kontenerze CMAF, jednym kluczem współdzielonym przez całą drabinę.
Cztery rzeczy, które zabierają czas, a nie są opisane jako trudne:
- Uwierzytelnienie do serwera kluczy. Dostawca dał nam OAuth2 na poświadczeniach klienta. Te poświadczenia muszą gdzieś mieszkać, a jedynym rozsądnym miejscem jest Secrets Manager, nie zmienna środowiskowa w definicji kanału.
- Player musi mieć prawdziwy moduł deszyfrujący. Widevine działa w przeglądarce przez CDM, a CDM wymaga bezpiecznego kontekstu. Na testowej domenie bez certyfikatu nie zobaczycie nic i przez pierwszą godzinę będziecie szukać błędu w konfiguracji kluczy.
- Trzeba widzieć, co się dzieje. Ustawiliśmy player Shaka z włączonym trybem diagnostycznym, bo bez logów z warstwy szyfrowania różnica między „nie ma klucza”, „klucz jest zły” i „PSSH nie trafił do manifestu” wygląda identycznie: czarny obraz.
- Rotacja kluczy dotyka przełączania. Jeśli klucze zmieniają się w czasie, oba tory muszą zmieniać je na tej samej granicy segmentu. Dlatego równe segmentowanie z poprzedniej sekcji nie jest kosmetyką.
Do pełnej obsługi wszystkich urządzeń potrzebne są trzy systemy naraz: Widevine dla Androida i Chrome, PlayReady dla Windows i wielu telewizorów, FairPlay dla Apple. Każdy chce innego formatu, dlatego dostawcy sprzedają to jako jedną usługę multi DRM. W PoC świadomie zrobiliśmy jeden, żeby nie mylić problemu integracji z problemem pokrycia urządzeń.
Player na klastrze, a nie na S3
Player jest plikiem statycznym. Naturalny odruch to wrzucić go na S3 i podać przez CloudFront, i w wielu przypadkach to jest właściwa odpowiedź.
My uruchomiliśmy go w kontenerze na Amazon EKS w trybie Auto Mode, za load balancerem aplikacyjnym z WAF. Powód nie jest taki, że plik statyczny wymaga Kubernetesa. Powód jest taki, że wokół pliku statycznego było wszystko inne: podawanie adresów strumieni i serwera licencji zależnie od środowiska, własne nagłówki, reguły dostępu, miejsce na logikę, którą zespół chciał dopisać później. Kiedy aplikacja przestaje być jednym plikiem, a zaczyna być usługą, klaster przestaje być przesadą.
Auto Mode zdejmuje z tego większość pracy administracyjnej. Nie zarządzamy grupami węzłów ani wersjami obrazów systemowych, klaster sam dobiera i wymienia maszyny pod obciążenie. Za skalowanie w poziomie odpowiada standardowy mechanizm Kubernetesa, a ruch wchodzi przez ALB, na którym siedzi WAF z limitami żądań i regułami OWASP.
Jeżeli player naprawdę jest jednym plikiem i nikt nie planuje wokół niego logiki, S3 z CloudFrontem jest tańszy i prostszy. Warto tę decyzję podjąć świadomie, a nie odziedziczyć.
Test na dwa i pół miliona widzów
Cel brzmiał: sprawdzić, czy wejście widzów liczone w milionach w kilkanaście minut położy aplikację.
Zbudowaliśmy generator obciążenia na Locuście w trybie rozproszonym, 189 procesów roboczych. Scenariusz mieszał kilka wzorców zachowania, bo prawdziwy widz nie odświeża strony w pętli: gwałtowne wejście, ruch tła, pobranie strony playera, pobranie zasobów pobocznych.
Docelowe dwa i pół miliona wirtualnych użytkowników zostało osiągnięte w całości. W trakcie testu poszło ponad 2,4 miliona żądań, z czego nieudanych było 12 285, czyli pół procenta. Szczytowo notowaliśmy powyżej trzech tysięcy żądań na sekundę.
I teraz najciekawsza część, czyli to, czego się nie spodziewaliśmy.
Najszybsze odpowiedzi schodziły do 6 milisekund, mediana przez cały test trzymała się nisko, a 95. percentyl wchodził w setki sekund. Odpowiedzi trwające ponad pięć minut. Aplikacja, która naprawdę potrzebuje pięciu minut na oddanie strony, byłaby martwa, tymczasem połowa żądań wracała natychmiast.
Wniosek jest taki, że w tym momencie wąskim gardłem przestała być aplikacja, a stał się nim generator. Przy tej liczbie wirtualnych użytkowników na proces roboczy Locust nie zdąża obsłużyć własnych korutyn i czas oczekiwania na wykonanie żądania wlicza sobie w czas odpowiedzi. Mierzyliśmy wtedy własne narzędzie.
Stąd trzy zasady, które od tego projektu stosujemy zawsze:
- Prawdę mówi strona serwerowa. Liczba żądań do origin, liczba trafień w cache, kody odpowiedzi i opóźnienia z CloudFronta, a nie percentyle z generatora. Generator mówi, ile ruchu wypuścił, nie jak zachował się system.
- Generator też trzeba wyskalować i zmierzyć. Jeżeli obciążenie procesora na maszynach generujących ruch dobija do sufitu, wyniki od tego momentu są opowieścią o nim.
- Test bez wideo nie testuje wideo. Nasz scenariusz świadomie nie pobierał segmentów, żeby nie przepalić transferu. To był test warstwy aplikacyjnej i tak trzeba go opisać, bo CDN przy realnej konsumpcji zachowuje się inaczej.
Do tego jedna sprawa proceduralna: testy obciążeniowe dużej skali zgłasza się wcześniej do AWS. To formalność, ale zrobiona w dniu testu przestaje nią być.
Co dokłada wariant międzyregionowy
Opisywany tor był jednoregionowy, z odpornością wewnątrz regionu, bo tak brzmiało pytanie. Dla usług premium AWS opisuje wariant idący dalej, na dwa regiony naraz, i warto wiedzieć, na czym on stoi, bo to nie jest zwykłe zduplikowanie zasobów.
Kluczem jest zgranie obu regionów w czasie. Enkoder w studiu wstawia znacznik czasu w te same klatki w obu torach, a kanały MediaLive pracują z blokadą wyjścia opartą na czasie uniwersalnym. Dzięki temu oba regiony produkują segmenty o tych samych granicach i tych samych nazwach.
Przełączanie robi CloudFront przez grupę origin. MediaPackage w torze podstawowym potrafi sam zgłosić, że jest w złym stanie, i zaczyna odpowiadać kodem 404, kiedy manifest się zestarzał, ma dziury, brakuje kluczy DRM albo w sygnale poszła plansza zastępcza. CloudFront widzi te odpowiedzi i przechodzi na origin zapasowy, a potem sam wraca, gdy podstawowy przestaje zgłaszać błąd.
Warunek jest jeden i jest twardy: bez zgrania sygnału w czasie u samego źródła widz przy przełączeniu zobaczy skok obrazu w tył albo w przód. Sama redundancja usług tego nie załatwia.
AWS uczciwie dodaje, że dla większości zastosowań redundancja wewnątrz jednego regionu wystarcza. Zgadzamy się. Wariant dwuregionowy to decyzja o kosztach, nie domyślny poziom bezpieczeństwa.
Czego nauczył nas ten projekt
| Rzecz | Co okazało się prawdą |
|---|---|
| Sygnał do chmury | Najłatwiejsza część. SRT do MediaConnect po publicznym łączu działa. |
| Klasa kanału MediaLive | Decyzja nieodwracalna i kosztowa. Podejmowana zanim ktokolwiek zobaczy obraz. |
| DRM | Największy pożeracz czasu. Trzy czwarte problemów było po stronie playera, nie kluczy. |
| Player na klastrze | Uzasadniony logiką wokół pliku, nie samym plikiem. |
| Test obciążeniowy | Sam wymaga zaprojektowania i pomiaru. Inaczej mierzy generator. |
| Przełączanie ruchu | Zadziałało. Trudność leży w zgraniu segmentów, nie w przekierowaniu. |
Największa lekcja jest jednak nietechniczna. PoC był wart tyle, ile był, dlatego że wolno w nim było czegoś nie umieć. Gdyby ten sam zakres wjechał od razu jako wdrożenie produkcyjne, dwa tygodnie spędzone na czarnym obrazie w playerze byłyby porażką projektu. W PoC były po prostu odpowiedzią na pytanie, po co się PoC robi.
Nasi konsultanci mają doświadczenie w projektach tego typu, od odbioru sygnału, przez szyfrowanie treści, po testy skali. Chętnie porozmawiamy, jeśli macie przed sobą podobną decyzję.
Źródła
- Build a resilient cross-region live streaming architecture on AWS, blog AWS for M&E
- What is Secure Packager and Encoder Key Exchange, dokumentacja SPEKE
- Plan the channel class, dokumentacja AWS Elemental MediaLive
- What is AWS Elemental MediaPackage, dokumentacja AWS
- Amazon EKS Auto Mode, dokumentacja Amazon EKS
- Locust, narzędzie użyte do testów obciążeniowych