Tech: Na pozadí komunálních voleb

V samizdatím nepravidelníku Tech se budu příležitostně rozepisovat o zajímavých technických řešeních, která obstála (nebo naopak velkolepě selhala) v nově vznikajícím odvětví novinářského programování. První díl se bude věnovat technickému řešení realtimové volební aplikace, kterou jsme tvořili pro webové zpravodajství Českého rozhlasu.

Právě proběhlé komunální a senátní volby pro mě byly druhou příležitostí, jak národu co nejrychleji sdělit výsledky demokracie – minule to byly podstatně sledovanější parlametní volby, kde se nám obevila vcelku slušná řada chyb, neočekávaných situací a všeobecného chaosu. Je mi potěšením psát, že tentokrát byl průběh znatelně klidnější.

Zatímco minule byly kromě backendového “processoru” výsledků již jen generické “frontendy” bez bližšího zaměření, tentokrát byla architektura pestřejší. Backend přepočítávající výsledky z ČSÚ byl jediný, který víceméně zůstal, pouze se pochopitelně upravily algoritmy pro komunální a senátní volby. Oddělil se z něj však Redis master server, na který jsme tentokrát použili dedikovanou Redis Cache v rámci Azure portfolia. Původní frontend se pak rozdělil na tři díly – hlavní server samizdat.cz sloužil jako poskytovatel statického obsahu a jako reverzní proxy + SSL terminátor pro server realtime updatů, dále běžel samotný server realtime updatů a jako poskytovatele všech výsledkových souborů jsme zvolili Azure Blob Storage.

Backend vytvářel po jednom souboru pro každou obec (přes 6000 unikátních, každou minutu se měnících necachovatelných souborů) a dále jeden s celkovými výsledky senátních a jeden komunálních voleb (příklady JSONů: obec, výsledky komunálek, senátu). Ty pak uploadoval do Storage, a po úspěšném uploadu dal přes Redis pub/sub vědět RT updateru, že se daný resultset změnil. A zatímco backend i nějakou verzi CDNky s výsledky měla snad všechna média (snad jen s rozdílem přepočítávání průběžných výsledků na mandáty), u nás se k tomu celému přidal právě realtime update server, o kterém bude většina následujícího povídání. Díky němu jsme mohli na připomínky redakce “na iDnesu to vychází jinak” odpovídat pyšným “protože jsou dvě minuty za námi“.

Socket.io: Dobrý, ale…

Zásadní změnou oproti minule bylo opuštění technologie WebSockets, zvláště pak knihovny Socket.io. Sockety jsou skvělé pro obousměrnou komunikaci, umí přenášet binárku a zvláště Socket.io je se svými fallbacky výtečná technologie např. pro hry, ale pro volby se příliš nehodí. Sockety v jejich režii totiž mají vcelku majestátní overhead a rozumné maximum bylo kolem 1000 spojení na server (dedikované jádro a 1.5GB RAM, žádné ořezávátko) přičemž o trablích s horizontálním škálováním jsem psal už před rokem. Navíc minule jsem udělal tu chybu, že jsem sockety prakticky nahradil HTTP, které tehdy sloužilo pouze k dodání statických assetů – všechny dynamické výsledky se tahaly až v nadstavbových spojeních.

Letos jsme tedy místo WS použili Server Sent Events (SSE), které jsou jednodušší, nevyžadují složitý handshake, dají se snáze škálovat a fungují na čístém HTTP protokolu. Zjednodušeně řečeno se vlastně jedná o svérázně formátovaný chunked transfer. I díky tomu mají přes párkilobajtové polyfilly podporu i u prohlížečů, které je nenabízí nativně (jedinou významnou výjimkou jsou staré Androidí browsery). Nadto umí nativně gzip kompresi, reconnect včetně advertisingu poslední přijaté zprávy a hlavně jejich serverovou implementaci lze pochopit a upravovat za pět minut. Samozřejmě, není to zadarmo – narozdíl od WS umí komunikovat pouze jednosměrně, od serveru ke klientovi, upstream (z pohledu klienta) je třeba řešit out-of-band. Pro volební aplikaci, kde uživatel neposílá příkazy, které by měnily stav serveru, jsou však ideální.

K dalšímu snížení zátěže na realtime server přispělo rozložení odpovědnosti na několik serverů a na klienta. Minule se klienti po připojení k serveru přihlašovali ke konkrétním informačním kanálům (celkové výsledky, výsledky za konkrétní kraj, okres, obec) a server při updatu některého kanálu ze svého výkonu vybral přihlášené adresáty a jim pak poslal všechna požadovaná data. To znamenalo, že si server musel držet v paměti všechny kanály (a obcí máme přes 6000) a jejich adresáty, případně při každém updatu projít tisíce příhlášených klientů a vyfiltrovat pouze relevantní, což opět přispělo k velké náročnosti loňské aplikace.

Tentokrát jsme místo toho z RT serveru distribuovali všem klientům pouze identifikátor změněného zdroje. Pokud se tedy updatovaly výsledky obce Adamov, rozeslala se tato informace všem přihlášeným klientům a na nich bylo zvážit, zda je vůbec Adamov zajímá (tj. uživatel má obec otevřenou v detailním pohledu) a pokud ano, tak si z Storage serveru stáhnout aktualizované výsledky. Tím jsme sice přidali cca 2-3 RTT latence, to se ale na většině připojení přetaví do méně než 1s zpoždění, což je u volebních výsledků akceptovatelné.

Binárka v UTF

Datové požadavky na toto řešení byly minimální díky formátu přenášených dat – nepřenášeli jsme IDčka v plaintextu (např. 581291 pro Adamov), ale místo toho jsme využili faktu, že uživatel má kvůli našeptávači a detekci nejbližších obcí v detailu již stažený seznam všech obcí. Mohli jsme se tedy udkazovat na index pole (0 pro první Adamov, 1 pro následující Adršpach apod.) a tento pak přenášet svým způsobem binárně. JavaScript a UTF-8 totiž poskytují univerzální built-in podporu pro de facto binární transfery nezáporných celých čísel mezi serverem a klientem – stačí na serveru vytvořit character přes String.fromCharCode(ord) a na klientovi ho číst přes string.charCodeAt(index). Bez jediné řádky programování navíc tedy je možné přenášet hodnoty menší než 127 v 1 Bajtu, 128-1023 v 2B a větší než 1024 v 3B.

Na serveru ještě fungovala sekundová agregace, pokud se tedy během vteřiny updatovaly obce Adamov i Adršpach, posílaly se ID 0 a 1 v jedné zprávě, které opět díky UTF “binárce” mohly být encodovány bez oddělovače – klientský JS nás od variabilní délky prefixované binárky odstíní a druhou obec vždy najdeme na charCodeAt(1).

Přes tyto optimalizace jsem na rozdíl od minule architekturu navrhoval tak, aby realtime updater byl zcela separátním modulem, v případě jehož selhání aplikace spadla do režimu pravidelného refreshování požadovaných zdrojů. To by sice snížilo celkovou efektivitu služby (refreshovaly by se většinou ještě stále aktuální obce), Azure Blob Storage má ale SLA kapacitu 20 000 requestů/s, my jsme ve špičce využili cca 100. Zdroje tedy byly, jen by to stálo trochu víc.

V praxi k překvapení mému i kolegů tato architektura opravdu funovala, jak měla. Realtime updaty klientům docházely, na jejich základě si stahovali nová data, celé se to krásně hýbalo. Backend tedy proběhl neobvykle klidně, o to veselejší byla práce na frontendu. Jak se kolegové několikrát vyjádřili, komunálky jsou volby s nejhorším poměrem cena/výkon. Parlamentní i prezidentské volby se volí do jedné instituce, s jedněmi výsledky, které navíc jdou vcelku slušně agregovat. Komunálky se ale děli na obecní, dále na volby do zastupitelstev městských částí a obvodů a nad to se ještě volí do senátu, to však pouze v některých okrscích. Toto jsem tentokrát o to hruběji podcenil, výsledkem bylo, že aplikace byla ke spuštění připravena v 13:53 (sčítat se začínalo v 14:00), nejnutnější úpravy probíhaly do 14:49 a drobnější detaily se ladily do noci. Postupující frustraci dobře ilustruje následující výpis změn z verzovacího systému Git (čas postupoval odspodu nahoru):

git log z těsně předvolebního času

git log z těsně předvolebního času

Nakonec však vše dobře dopadlo a vyšel i můj vedlejší experiment s tím, že jsme byli jediný kryptograficky důvěryhodný zdroj volebních zpráv (jako jediní jsme všechny informace posílali přes zabezpečené https připojení). A právě o tom, proč je důležité, aby zabezpečené připojení použivaly i jiné instituce než vaše banka (a konkrétně proč by zrovna média měla tento trend vést) bude přístí příspěvek.

A v duchu datového novinářství se rozloučím pár grafy z provozu:

Vytížení procesoru na SSL terminátoru a statickém hostingu (fialově), realtime updateru pro cca 1200 klientů (zeleně) a procesor XMLek (modře).

Vytížení procesoru na SSL terminátoru a statickém hostingu (fialově), realtime updateru pro cca 1200 klientů (zeleně) a procesor XMLek (modře).

Redis jsme použili i jako cache průběžných výsledků, agregátor po obcích (sloučení výsledků obcí/magistrátů a městských částí) a detektor, zda se v obci stala změna. Šlo na něj tedy cca 100 requestů za sekundu…

Redis jsme použili i jako cache průběžných výsledků, agregátor po obcích (sloučení výsledků obcí/magistrátů a městských částí) a detektor, zda se v obci stala změna. Šlo na něj tedy cca 100 requestů za sekundu…

…což ho moc nevytížilo

…což ho moc nevytížilo

Vytížení Blob Storage

Vytížení Blob Storage. Před polednem je vidět plnění “prázdnými” daty pro každou obec