Rádi bychom se podělili o tom, jak jsme postavili CDN pro jeden český video projekt, který narazil na výkonostní limity svých storage serverů.
Zákazník má několik serverů a na nich stovky TB souborů v MP4 formátu. Servery jsou optimalizované na kapacitu, takže obsahují běžné vysokokapacitní SAS disky s omezeným výkonem.
Výkon jednoho takového serverů končí někde kolem 3-5 Gbps. Konkrétní hodnota záleží na velikosti množiny videí, které uživatelé v daný moment sledují. Tím tedy i na efektivitě využití kernel cache. Jsou i hodně specifické případy, kdy ze serveru teče celých 10Gbps. Pro nás to znamená, že nutně nemusíme cachovat agresivně, ale máme prostor pro optimalizace.
Do konfigurace serverů a do aplikace jsme původně zasahovat nechtěli. Drobné úpravy jsme přecijen ale nakonec udělat museli.
V prvním kroku jsme zjistili jak vypadá množina sledovaných souborů a výsledky nás velmi potěšily. Distribuce sledovanosti videí byla rozdělena do 3 kategorií.
Velmi sledovaná videa mají vyšší desítky sledujících a jedná se o jednotky nebo menší desítky video souborů.
Středně sledovaných videí jsou už nizké stovky, ale zároveň každé z nich má malé desítky sledujících.
Málo sledovaná videa mají po jednotkách sledujících, ale je jich velmi velké množství.
Velikostí souborů se videa pohybují od 50MB do 2GB (jsou větší, ale těch je jen několik kusů). Není zde možnost použít HLS nebo DASH, pomocí kterých bychom si situaci ještě více zjednodušili.
Většina uživatelů je z ČR a Slovenska, takže se veškerý provoz dá odservírovat z českých datacenter.
Veřejné CDN mají nelehký úkol v tom, že musí obsloužit provoz, který jim není předem znám a může být jakýkoliv. Není jim známo ani množství dat, se kterým budou pracovat. Nemají možnost zasahovat do aplikací a neznají uživatele, kterým budou obsah servírovat.
Na základě sesbíraných dat vzniklo několik zásadních omezujících podmínek, které nám pomohly postavit CDN výrazně efektivněji.
Žádné video nemůže přetížit jeden server: I ta nejsledovanější videa měla vyšší desítky sledujících, což není nic, co by nezvládl jeden server. Na toto omezení jsme požději nalezli řešení, ale nebylo potřeba jej implementovat.
SSD mají neomezený čtecí výkon: Díky této podmínce jsme si mohli dovolit udělat distribuci na cachovací SSD výrazně jednodušší. Později se sice ukázalo, že SSD nemají neomezený výkon, ale s tím jsme si také poradili.
Na nová videa nemusíme reagovat ihned: Storage servery mají nějaký výkon, na který se můžeme spolehnout. Zároveň nenastává situace, kdy nově vydané přitáhne větší množství sledujících, ale zájem uživatelů je více pozvolný.
Řešení jsme postavili na sadě předřazených HTTP proxy serverů. Všechen provoz prochází skrz naše servery a my se rozhodujeme co cachovat budeme a co pošleme dál na originální storage servery.
Od začátku jsme měli jeden velmi důležitý požadavek - všechny servery musí fungovat 100% autonomně, nesmí o sobě navzájem vědět a nesmí sdílet žádná data mezi sebou. Tímto požadavkem jsme si zjednodušili celou aplikaci, která se tím stala slušně škálovatelná - stačí mít pouze dobrý balancing.
V první verzi jsme zkoušeli jít cestou cachování v nginxu, která sice fungovala, ale nebyli jsme s tím spokojeni. Potýkali jsme se s problémy s přetěžováním SSD při zápisech a častou invalidací souborů.
V druhé verzi jsme si napsali malou aplikaci, která řidí celé cachování.
Jako webserver stále zůstal nginx. Databázi pro uchování mapování souborů na jednotlivá SSD používáme redis. Nginx z redisu získává data pomocí kódu napsaného v LUA.
Pro samotné cachování používáme PHP, příčemž aplikaci jsme rozdělili na 2 části.
Sběr dat o provozu
Kolektor dat sbírá statistiky o vytížení jednotlivých souborů. Jako zdroj dat může použít několik metrik. Např. počet hitů na soubor, množství přenesených dat, počet současných sledování jednotlivých videí a zcela jistě by se daly vymyslet i další metriky. S ohledem na velikost souborů jsme zvolili jako metriku, podle které budeme vybírat soubory ke cachování, množství přenesených dat. To sice přináší jistá omezení, protože data sbíráme z běžného access logu (čili až v momentě, kdy je požadavek dokončen), ale v tomto případě to funguje dobře.
Veřejné CDN většinou cachují podle počtu hitů, protože jejich důležité KPI často bývá hitrate. V našem případě ale sledovaných indikátorů máme víc. A je mezi nimi i životnost SSD disků. Metriku jsme tedy vybírali tak, abychom cachovali to, co reálně zatěžuje storage servery.
Kolektor data agreguje a posílá do InfluxDB tak, aby pod každým klíčem (url) byl za daný časový úsek počet bajtů, které daný soubor vygeneroval.
Cachovací proces
V periodických intervalech se dělají pohledy do InfluxDB, ze které se získá seznam URL adres a počet přenesených bajtů. Tento seznam je seřazený sestupně podle datových přenosů tak, abychom primárně cachovali soubory, které generují nejvíce provozu.
Každý disk má svůj jeden řídící proces, který se stará o data na něm. Proces se vždy stará jen o jeden disk a téměř neví o dalších discích a procesech.
V prvním kroku si proces získá aktuální seznam URL adres s metrikami z pohledu, který jsme popsali výše. Postupně tento seznam prochází a hledá soubor, který na serveru ještě není uložen. Jestli je soubor na serveru nebo ne kontroluje proti redisu. Pokud nalezne soubor, který ještě na žádném disku uložen není a zároveň na něj má volné místo, tak jej začne stahovat. Stahování probíhá se zámkem, aby se zamezilo tomu, že více procesů bude stahovat jeden soubor.
Pokud na disku volné místo není, tak se podívá, jestli na disku není soubor, který má menší počet přenesených dat. Pokud takový najde, tak se jej pokusí invalidovat. To je ale podrobněji rozepsané v kapitole níže.
Omezení jednoho procesu na jeden disk nám také pomohlo s výkonem SSD disků. Při současném zápisu a čtení z SSD disku dochází k výraznému poklesu jeho výkonu. Tímto jsme zápisy omezili a ještě jsme k tomu přidali optimalizaci, aby se zápisy SSD disky zatěžovaly co nejméně.
Stejně jako se snažíme mít autonomní servery, tak k tomu přistupujeme i v aplikační části. Každý proces se snažíme mít co nejjednodušší tak, aby dělal jednu věc, byl lehce debugovatelný a extrémně jednoduchý.
Rozdělení zátěže mezi servery vyžadovalo drobnou úpravu aplikace. Rozhodli jsme se použít konzistentní hashování, které z originální URL vygeneruje URL ve zjednodušeném tvaru: [a-z].cdn.projekt.tld. Velikost hashovací tabulky je 26. Jsme tím sice omezeni pouze na 26 serverů, ale toto se v případě potřeby dá v budoucnu upravit. Na každý z fyzických serverů tak připadá část provozu a vše jsme schopni ovládat pohodlně pomocí DNS záznamů.
Když potřebujeme jeden server odstavit, tak stačí upravit DNS, nějakou dobu počkat, než se dokončí navázaná spojení, provést údržbu a následně jej vrátit do provozu. Když potřebujeme jednomu serveru odlehčit, tak stačí snížit počet bucketů, které obsluhuje a nechat nacachovat data na jiných serverech. Stejným způsobem můžeme přidat i nový server do farmy, kdy na něj přidáváme provoz postupně, abychom jej nezahltili.
Jako u všech našich projektů děláme hromadu měření a zvolený hardware průběžně obměňujeme podle potřeby.
V tomto případě to vyžadovalo 3 iterace a finální konfigurace, na které jsme službu postavili byla následující.
SSD disky nám nejlépe fungují od Intelu a Samsungu. V obou případech se jedná o disky s vyšším počtem zapisů. Kapacitu disků jsme zvolili 240GB. Ačkoliv se kapacita může zdát malá, tak při použití vyšší kapacity se nám stávalo, že se potkalo více žádaných souborů na jednom SSD a došlo k jeho přetížení. Omezená kapacita SSD disků tento problém eliminuje. SSD od obou výrobců se nám osvědčily a líbí se nám dobrá predikovatelnost životnosti, podle které plánujeme výměny. K náhodným a neplánovaným úmrtím dochází minimálně. V serverech, vyjma systémových disků, není použit žádný RAID.
Skříně používáme 2U Supermicro na 24x 2,5" bez expandéru. Každé 4 disky obsluhuje jeden SAS kontektor. 8 disků jsme zapojili do SATA portů na desce, zbytek obsloužily 2 kusy LSI 9300-8i v HBA režimu.
Jako základní desky jsou použité X10SRL-F. Použitím jednoprocesorových základních desek jsme se vyhnuli problému s NUMA - situace, kdy jeden procesor potřebuje přistoupit do paměti druhého procesoru. Zároveň tím značně snižujeme spotřebu celé sestavy.
Desky jsou osazeny procesory E5-2640v4, které mají 10 jader/20 vláken, zároveň pouze 90W TDP a stále slušnou frekvenci 2.4GHz/3.4GHz.
Paměť RAM je použita 8x16GB DDR4 moduly a nepozorovali jsme viditelné rozdíly při použití rychlých pamětí proti pomalejším.
Síťové karty používáme X520-DA2, celkem ve 2 kusech na každý server. Každý server tak disponuje 40Gbps konektivitou.
Z jednoho serveru k uživatelům servírujeme 20-25 Gbps provozu a obsluhujeme kolem 10 000 spojení. Na storage servery posílále přibližně 1-2Gbps provozu z každého serveru, které dokáží bez problémů obsloužit.
V extrémních případech máme vyzkoušeno, že jsou servery schopny obsloužit až 40Gbps, ale to už je hraniční a vyžaduje to 100% hit rate a přístup k velmi malé množině videí.
Z těch zajímavějších metrik je zátěž procesoru. Většinu procesorového času spotřebovala obsluha IRQ od síťových karet. Druhou významnou část dělal SSL offloading.
Na long tail problém jsme narážíme u třetí množiny souborů - tedy u těch, kterých je zároveň hodně a mají malou sledovanost. Takové soubory velmi často mají stejné nebo velmi podobné množství přenesených bajtů, takže dochází k tomu, že v jednom cyklu je soubor odstraněn, aby následně o pár hodin později byl znovu nahrán.
To je situace, která se nám úplně nelíbila, protože se takto výrazně snižovala životnost SSD. Řešením bylo dát na nově cachované soubory 24 hodinovou ochrannou lhůtu, než může být odstraněn. Zároveň máme implementovanou podmínku, která tuto ochrannou lhůtu odstraní v případě, že nově cachovaný soubor má 2x větší množství přenesených dat. Tím jsme snížili počet zápisů na SSD disky a ty mají více výkonu na čtení. Zároveň ale máme ochranu proti videím, o která je náhlý zájem.
Při kopírování na SSD disk se také kontroluje volné místo. Máme definované 2 hraniční hodnoty. První hranice je, kdy dojde k odstranění souboru. Druhá hranice je, kdy se už na disk nebudou kopírovat nová data. Toto omezení vzniklo záměrně kvůli odstranění právě sledovaného souboru. Servírovaný soubor drží otevřený file descriptor a filesystém tedy soubor nesmaže a neuvolní místo, dokud file descriptor není uzavřený. Takto máme vytvořený manévrovací prostor, kdy můžeme zároveň cachovat nové soubory a nechat doběhnout aktuálně stahované, u kterých se místo uvolní později.
V případě, že máme soubory, o které aktuálně není zájem, ale zároveň nejsou žádné jiné, které by v cache měly být, tak je na SSD discích necháváme. K jejich odstranění dojde, až když je místo potřeba. Cache disky proto necháváme neustále zaplněné.
Stavba celé CDN byla hodně specifická pro tento projekt a narozdíl od veřejných postkytovalů jsme mohli využít několik triků, které nám usnadnily život. Oproti veřejným CDN máme výhodu v tom, že jsme aplikaci mohli optimalizovat pro konkrétní provoz, nemusíme řešit škálování napříč kontinenty a můžeme využít 100% dostupného výkonu bez nutnosti držet velkou rezervu pro případné špičky nebo nové zákazníky.
Zákazník je plně seznámen s limity daného řešení a naopak oceňuje predikovatelné náklady. Může si sám určovat, kolik provozu se bude cachovat a kolik necháme na storage serverech. Při porovnání s ostatními CDN jsme se v konečných nákladech dokázali dostat na zlomek částky, než by zákazník bez slev s tímto provozem zaplatil jinde.
Na proxy serverech jsme také museli udělat další optimalizace, ale jejich podrobnější rozbor a výsledky našich měření si necháme na příště.