Nejnovější:
- Sailfish OS and memory
- Hillshade tile server
- Statistics of OSM Scout for Sailfish OS
- Nebezpečný router
- Tree model with Silica components
Podle data:
- listopad 2021
- leden 2019
- prosinec 2017
- prosinec 2016
- květen 2014
- duben 2014
- listopad 2013
- duben 2013
- duben 2011
- únor 2011
- leden 2011
- srpen 2010
- květen 2010
- březen 2010
- leden 2010
- říjen 2009
- duben 2009
- únor 2009
- říjen 2008
- září 2008
- srpen 2008
- duben 2008
- březen 2008
- únor 2008
Téma:
Systém pro push distribuci malých balíků dat mnoha uživatelům
Jak co nejrychleji rozeslat jeden balík velkému počtu uživatelů na internetu? Tento zápisek se pokusí shrnout ty najzajmavější problémy které jsme museli řešit při <strike>vývoji</strike> optimalizaci aplikace pro proudové aktualizace Avast! antiviru.
Pokud je nalezena nová hrozba v podobě počítačového viru, červa, trojského koně či jiné nebezpečné havěti, je zapotřebí aby se potřebné aktualizace virové databáze dostali k uživatelům co nejrychleji. Nový vir umístěný na kompromitovaný web s vysokou návštěvností může každou minutu napadnout tisíce počítačů. Klasické aktualizace kdy se antivir jednou za čas dotazuje serveru vytvářejí zbytečnou prodlevu kdy je počítač proti nové hrozbě bezbranný. Proto je Avast! antivirus neustále připojen na server který mu pošle aktualizaci v okamžiku jejího vydání. Celosvětová komunita uživatelů Avast! antiviru ale čítá zhruba 160 milionů uživatelů z nichž bývají v jeden okamžik online až 32 miliony. Tento počet přináší na servery streamující aktualizace značné nároky.
Koncept push aktualizací je v Avastu již něco přes rok, na serverové straně ale nebyl ideální. Dostal jsem za úkol části stávající aplikace přepsat a pokusit se odstranit neduhy kterými systém trpěl. Výsledek je o poznání stabilnější, rychlejší a lépe škáluje.
Základní informace:- Servery mají instalováno 48 GiB RAM, 6 procesorových hyper-thread jader, běží na Linuxu CentOS.
- Serverová aplikace je napsána v Javě, síťová vrstva je založna na frameworku JBoss Netty.
Optimalizace
Starší verze aplikace přistupovala z mnoha vláken k některým neefektivně synchronizovaným sdíleným kolekcím, které byly úzkým hrdlem při rozesílání updatů. Takové kolekce lze většinou jednoduše najít a optimalizovat pro daný druh přístupu. Například pro paralelní iterování velkých kolekcí jsem vytvořil BlockParallelIterator.
V některých místech byla vlákna uspávána na empiricky zjištěnou dobu či byla jinak omezována aby data k odeslání nebyla produkována rychleji než jsou skutečně odesílána. Těchto omezení jsem se chtěl zbavit nebo regulaci založit na skutečném stavu serveru.
Java heap
Komunikační protokol je klasický HTTP (funguje i pro většinu uživatelů za proxy) s dlouhou prodlevou odpovědi (long-pool technika). Data jsou „zabalena“ do binárního formátu Google Protobuffer. Každá odeslaná zpráva se skládá ze dvou částí – hlavičky a „příkazu“. Krom speciálních případů jsou jako příkaz odesílány podepsané balíky s aktualizacemi.
Průměrná velikost jedné aktualizace je 8 KiB a frekvence vydávání
bývá 7 za hodinu. Při 32 M klientů ve špičkách se jedná o datový tok
480 MiB/s na všechna datacentra
((8 Ki * 32 M * 7) / 3600) / 1 Mi = 480 MiB/s
.
Pro každý z dvacítky serverů připadá zhruba 24 MiB/s, což v dnešní
době není nijak významný tok.
Problém ale nastane když se jeden nebohý server rozhodne co nejrychleji
rozeslat tento balík dat svým až 2 M klientů (1,6 M průměrně). Pro
každého klienta je potřeba alokovat buffer obsahující http hlavičku,
informace o příkazu a data samotného příkazu. I když aplikace má
k dispozici 20 GiB heap, při větších update balících není z čeho
alokovat. I kdyby byl celý heap použit na buffery k odesílání, kritická
velikost balíku je 10 KiB
(20 GiB / 2 M = 10 KiB
).
Protokol jsem měnit nemohl, naštěstí šlo ale lehce oddělit malou
hlavičku která je pro každého klienta jiná od vlastního balíku
s aktualizacemi který lze sdílet pro všechny klienty. Odpověď jsem
namísto jediné http odpovědi rozdělil do http chunků. Sdílený http chunk
musí být alokovaný jako přímo mapovaný (direct memory)
ByteBuffer
. Pokud by byl použit Netty ChannelBuffer
alokovaný na heapu, jeho obsah by byl překopírován na nejnižší úrovni
Netty kde probíhá předávání dat do TCP stacku pomocí „java.nio“,
takže data by reálně sdílena nebyla a prostorem v heapu by bylo plýtváno
stejně. Za tímto účelem sdílení http chunků jsem vytvořil HttpChunkCache.
Kernel TCP memory
V okamžiku kdy jsem vyřešil problémy s heapem, odstranil umělé
zabržďování vláken a optimalizoval některé části aplikace, objevil se
jiný problém. V okamžiku vydání nového updatu přestal server na několik
minut odpovídat na všech portech. Poté co se situace uklidnila jsme v logu
jádra našli následující zprávu: Out of socket memory
.
Příčina byla zjevná. Aplikace produkuje data rychleji než je kernel
stíhá odesílat. Druhou možnou příčinou této zprávy je mnoho osiřelých
(orphan) socketů, to ale nebyl náš případ. Ve všech diskuzních fórech a
blozích je jednoduchá rada – zvětšit paměť pro TCP stack. My ale
už měli nastaveno 6 GiB a moc výše jsme nemohli. Do Netty pipeline jsem
tedy přidal regulační vrstvu která na základě využití TCP paměti může
pozdržet odchozí data. Nastavené limity jednoduše čte ze souboru
/proc/sys/net/ipv4/tcp_mem
a aktuální stav z
/proc/net/sockstat
. Pak už bylo jen otázkou experimentů
nastavit správné limity a účinný „brzdící“ algoritmus.
Výsledek
Optimalizací bylo samozřejmě více, toto ale byly základní myšlenky které nám umožnili odstranit limit na maximální velikost updatu, snížit paměťovou náročnost a zkrátit dobu distribuce jednotlivých updatů pod pět minut (průměrně minuta pro 8 KiB velký update).
Několik drobných problémů zůstává stále otevřeno. Především se zdá že aplikace leakuje. Využití heapu je po několika týdnech o poznání větší. Zatím se žádné problémy nestihly objevit, aktualizace spojená s restartem vždy přišla dříve. Najít leakující místo ale není vůbec jednoduché, zvláště když velikost heapu v produkčním prostředí je 20 GiB.