Category Archives: Uncategorized

L2 cache v Javě

Nedávno jsem si pořídil knihu o Terracottě a v ní jsem narazil na myšlenku, které úplně převrátila můj pohled na cachování dat v Javě. Řekl jsem si, že se s vámi o ni podělím. Jde o srovnání využití vyrovnávacích pamětí v moderních procesorech a v Javě.

Cache v CPU

Začneme tím procesorem. Procesor zpracovává instrukce, které provádějí operace nad daty. Tato data se načítají z paměti. Paměť je ale od procesoru vzdálena neuvěřitelně dlouhých několik centimetrů a mezi ní a procesorem jsou všelijaké řadiče, a jiná hejblátka. Tím pádem je přístup do paměti relativně pomalý. To donutilo autory procesorů navrhnout L1 cache (vyrovnávací paměť první úrovně). Ta je hned u jádra procesoru takže je přístup do ní rychlý. Je s ní ale jedna potíž, je dost malá. Proto se do procesorů přidává ještě L2 cache, která je podstatně větší. Jelikož je ale složitější, je od jádra trochu dál, a proto je přístup do ní pomalejší. Někdy je dokonce i sdílena mezi jádry, což přináší výhody například, když se mi vlákno dostane na jiné jádro. Občas je dokonce použita i L3 cache, která je zase o něco větší a pomalejší.

Celé to pak funguje jednoduše, pokud data nejsou v L1 paměti, kouknu se do L2, případně L3 a když to ani v jedné není, jdu až do hlavní paměti. Samozřejmě se musí nějak řešit i invalidace a uložení změněných dat, ale to už bych se pouštěl na tenký led. Už tak jsem se pustil do vod, kterým nerozumím.

Takže se radši vrátím do Javy. Paralela je nasnadě. Z Javy často potřebuji přistupovat k pomalým nebo přetíženým zdrojům a tak si pomáhám vyrovnávací pamětí. Použijme pro příklad relační databázi. Ta bývá přístupná přes pomalou síť a často špatně škáluje, tak si některá data ukládám na heap. V přirovnání k CPU tu databáze odpovídá relativně pomalé RAM a heap nám tu slouží jako L1 cache. To přirovnání docela sedí. Heap je to nejrychlejší co v Javě máme. Samo sebou se pohybujeme v úplně jiných řádech než u CPU jak do velikosti tak i rychlosti.

Na ale každá sranda něco stojí, tak i cache není bez problémů. Největší problém s tímto řešením je v tom, že já obvykle dopředu nevím, která data s vyplatí držet v paměti, a která ne. To poznám až při opakovaných dotazech. Celou věc ještě mnohonásobně komplikuje nasazení aplikace na více serverech. Každý node má pak svoji cache. Abych si zjednodušil argumentaci, budu předpokládat, že data jenom čteme a neměníme. Uznávám, to se nám v praxi moc často nestává, ale i tak se dostáváme se do zajímavých problémů. Aby vyrovnávací cache k něčemu byla, musíme z ní data číst opakovaně. Čím více máme ale serverů, tím je menší šance, že k tomu dojde. Řekněme, že nám na jeden server přijde dotaz, který způsobí načtení dat do vyrovnávací paměti. Čím více máme serverů, tím menší pravděpodobnost je, že příští obdobný dotaz přijde na ten samý server a data v paměti využije. Větší šanci má, že tento dotaz přijde na jiný server, který ta data zase musí odněkud stáhnout.

V praxi se to řeší například použitím chytrým load balancerem, který nám zajistí, že dotazy od jednoho uživatele chodí na ten samý server. Toto řešení může fungovat, například u aplikace typu Gmail, ale vůbec to nepomůže například u zpravodajského serveru.

Dalším populárním řešením je sdílení vyrovnávací paměti mezi servery. Tzn. pokud jeden server načte data do cache, pošle je všem svým kamarádům. Toto řešení obvykle špatně škáluje. Navíc si představme situaci, kdy jsou daná data potřeba jen jednou. Nejen, že je zbytečně budu držet v paměti na jednom serveru, já je i pošlu na několik dalších strojů. Tam nebudou přečtena ani jednou. Zato však budou nafukovat paměť, zpomalovat garbage collector, zatěžovat síť a dělat jinou neplechu.

A tady vstupuje do kdy L2 cache. Místo toho, abych se snažil distribuovat všechna data na všechny ostatní JVM, uložím je někam mimo heap. Toto bych chtěl zdůraznit. Já ta data opravdu dám někam mimo JVM. Můžu buď do Terracotty nebo pokud si chci trochu zaprogramovat, třeba do NoSQL databáze. Tato data jsou mimo heap, takže přístup k nim je pomalejší. Ale tato L2 cache je trapně jednoduchá a může běžet na stejném serveru jako moje JVM takže přístup do ní může být stále řádově rychlejší než do relační databáze. Jen pro představu, autor Terracotty Ari Zilka tvrdí, že přístup na heap trvá kolem 1 mikrosekundy, přístup do Terracoty 4 milisekundy.

L2 cache navíc může být schopna využít mnohem více paměti než Java. Lidé z Terracotty se chlubí, že umí držet v paměti stovky gigabajtů dat. Není se co divit, nemusí řešit garbage collection, v podstatě se jen starají o takovou trochu větší hash mapu.

Toto řešení má mnohem větší šanci na to, aby škálovalo. Samozřejmě se mi komplikuje změna dat. Musím je změnit nebo smazat nejen v L1 cache ale i v L2 cache. Ale to je podle mě malá cena za to, že můžu svoji aplikaci pouštět na víc než čtyřech serverech. Navíc mohu aplikaci zmodularizovat, pouštět ji ve více JVM a i tak využít toho, že mám data někde v cache. Jediné co musím udělat je uvědomit si, že vytažení dat na půl cesty mezi jejich zdroj a místo spotřeby je docela dobrý nápad. Něco, co si výrobci procesorů uvědomili už dávno.

Opusťme ideální svět

Oko a mozek mají mezi sebou dohodu. Mozek souhlasí s tím, že bude věřit tomu co oko vidí, ale na oplátku oko souhlasí s tím, že bude koukat jen na to, co chce mozek vidět.
Daniel Gilbert, Stumbling on Happiness

V poslední době často narážím na ideální svět. Lidé v tomto ideálním světe čtou dokumentaci, programátoři používají API tak jak mají, prohlížeče dodržují specifikace, lidé nedělají chyby a chovají se racionálně, zadání zůstávají konstantní, holky vám samy skáčou kolem krku a pečení holuby lítají sami do huby. Krásná představa, skoro jako Utopie kolegy Platóna.

Problém je, že se jedná pouze o představu. Nic z toho, co se děje v ideálním světe, se neděje v realitě. Opravdu nic. To samozřejmě všichni víme, ale ani to nám nebrání v tom, chovat se tak, jako by ideál a realita byli jedno a totéž.

Občas někoho vidím divit se, jak to, že jeho kolegové něco nevědí. Vždyť to přeci napsal do dokumentace, kterou po něm sami chtěli. Jak je tedy možné, že ji nečetli?

Protože lidi nečtou! Bez ohledu na to, že to po vás chtěli sepsat, že jste s tím strávili spoustu času a práce. Prostě nečtou. Tečka. Vykřičník. Je je fakt, s kterým se nedá nic dělat. Slovy klasika, můžeme o tom diskutovat, můžeme o tom vést spory, můžeme s tím nesouhlasit, ale to je tak všechno, co se proti tomu dá dělat.

“PLEASE READ THIS OWNER’S MANUAL
BEFORE UNPACKING THE DEVICE.
You’ve already unpacked it, haven’t you? You’ve unpacked it and plugged it in and turned it on and fiddled with the knobs, and now your four-year old child, the same child who once shoved a Polish sausage into your new VCR and pressed fast forward, this child is also fiddling with the knobs, right? We might as well just break these devices right at the factory before we ship them out, you know that?”

Samozřejmě toto není jediný příklad. Například s dodržováním standardů a kontraktů API je to podobné. Když programátoři chtějí použít nějakou knihovnu, tak to prostě zkouší, dokud to nezačne fungovat. Jakmile to začne fungovat, tak toho nechají. Nezajímá je, co je v dokumentaci nebo nedejbože ve standardech. Tak to prostě je. Chovám se tak já, chovají se tak vaši kolegové, vaši zákazníci, prostě všichni, až na pár podivných výjimek.

A tím se dostávám k tomu, proč to vlastně všechno píšu. Je to takový můj první krok k pokusu o akceptaci reality. Z vlastní zkušenosti vím, jak je to těžké nežít v ideálním světě. Často se sám přistihnu při tom, jak se divím, že se lidé (mě nevyjímaje) chovají iracionálně. Nebo něco ťukají do počítače, když na schůzi mluvím. Nebo naprosto, ale naprosto špatně chápou mé skvěle vyargumentované články na blogu a naprosto, ale naprosto falešně je vykládají. Nemluvě o tom, že se uživatelé mých skvělých knihoven ptají na něco, co mají naprosto jasně napsané v dokumentaci. Hrůza. Skoro jako bych žil v reálném světě.

A aby toho nebylo málo, akceptace reality není všechno, je to jen první krok. Správně bychom podle toho měli také konat. Například bychom neměli rozbíjet klientům kód, pod hloupou záminkou, že nedodržují standardy. Ano, ve standardu je napsáno, že můžeme vracet také nějaké jiné návratové kódy než zrovna vracíme. Ale to nás neopravňuje k tomu najednou změnit chování a tím všechny naštvat. Prostě žijeme v realitě a klienti s tím nepočítají.

Nebo jiný příklad. Když něco přednášíme, měli bychom se snažit to udělat co nejzajímavější. V ideálním světe by samozřejmě všichni chtěli poslouchat co jim chceme zdělit, v realitě ale mají za sebou těžký den a sami dobře víte jak je snadné při přednášce se začít dloubat se v nose a přemýšlet o vaší krásné sousedce.

Takže tímto vyhlašuji válku ideálnímu světu a chtěl bych vás, ale hlavně sebe poprosit, přestaňme snít a začněme přijímat realitu.

Jak je to s tím technologickým dluhem?

Už dlouho jsem tu nevystavil takový ten klasický plkací článek, kde jen o něčem filozofuji. Kdysi jsem měl takové články v oblibě, tak jsem se rozhodl, že to s nimi zase zkusím.

Bude to reakce nebo spíš doplnění Dagiho článku Technologický dluh vs. Overengineering. Chci se k tomu vyjádřit, protože jsem asi jedním z důvodů, proč nad tímto tématem Dagi tak hloubá.

Samozřejmě nehloubá nad tím, jestli máme psát špagety kód nebo ne. Tam není co řešit. Špagety do produkčního kódu prostě nepatří. Tečka. Ale máme hromadu případu, kdy situace není tak černobílá.

Ať se nám to lépe představuje, předveďme si to na příkladu dvou naprosto imaginárních programátorů, kteří mají k programování trochu odlišný přístup. Programátor R. nad věcmi víc uvažuje, snaží se je dělat co nejlépe a nejčistěji. Programátor L. je více pragmatický a hlavní je pro něj jednoduchost a co nejméně práce. Je možná tak trochu líný.

Takže L. předvádí svůj úžasně napsaný minimalistický kód a R. mu říká: “Hmm, tady ten krásný kousek kódu by bylo pěkné vytáhnout do znovupoužitelného modulu, ať si i ostatní programátoři mohou užít výplodu tvé geniality“. A na to mu L., hrozící se další dřiny, odpovídá: „Chápu tvůj neskonalý obdiv k mému veledílu, ale bojím se, že tento modul stejně nikdo nebude potřebovat. Byla by to jen zbytečná práce navíc.“

Tak a teď bych vás chtěl poprosit, ať se zamyslíte kdo z nich má pravdu. Je lepší strávit nějaký čas vytahováním kódu do separátního modulu a tím zvyšovat komplexitu nebo je lepší držet kód méně čistý, ale jednodušší? Je lepší jednoduchý nebo znovupoužitelný kód? Nějaké nápady? Řešení?

Odpověď je samozřejmě klasická. Zní: „Přijde na to.“ Překvapivě totiž záleží hlavně na tom, jestli se ten modul opravdu použije ještě někde jinde nebo ne. Pokud ano, pak se ta práce vyplatila a vyhnuli jsme se duplikování kódu. Takže ten modul snížil technologický dluh.

Pokud ale ten modul bude opravdu použit jenom z toho jednoho místa, tak jsme to evidentně překombinovali a máme zbytečně složitý kód.

Fór je v tom, že dopředu často nevíme, o který případ se jedná. Jak zní můj oblíbený citát: “Je obtížné cokoliv předvídat, obzvláště pak budoucnost“. A to je zdroj té neshody. Jedna strana tvrdí, že ten modul můžeme vytvořit, až to bude potřeba. Druhá strana zase tuší, že když se to neudělá teď, tak už se k tomu nedostaneme nikdy. Jednou mají pravdu jedni, jindy zase ti druzí.

Neplatí to samozřejmě jen o modulech, ale i o většině ostatních programátorských fíglech. Takže vrstvení aplikace je dobré, ale jen pokud je opravdu potřeba. Webové služby jsou dobré, ale jen pokud jsou opravdu potřeba. ORM je dobré, ale jen pokud je opravdu potřeba … Je mi skoro trapné skončit takovou samozřejmostí, ale co se dá dělat.