Hrajeme si s Terracottou

Dnes budu psát o svých pokusech s Terracottou. Předem chci upozornit, že o ní mám jen velmi chabé znalosti, takže to prosím berte jenom jako popis experimentů, které jsem s ní prováděl. Navíc jsem i pár věcí pro přehlednost zanedbal, takže zájemce o přesnou a úplnou informaci odkáži na stránky Terracoty.

Terracotta je nástroj pro clusterování na úrovni virtuálního stroje. Funguje pomocí AOP na úrovni bytecode. Tzn. při startu aplikace upraví bytocode tříd tak, že dostane notifikaci při zápisu a čtení primitivních vlastností (field) objektů. Prostě a jednoduše, když mám třídu, která obsahuje vlastnost typu int, Terracota ví, že ji někdo změnil nebo že se ji někdo pokouší číst. Poté už je snadné distribuovat změny na více JVM. Dokonce tím i dostáváme možnost změny distribuovat až podle potřeby. Tzn. když se pokouším číst hodnotu proměnné změněnou na jiném JVM, Terracotta může změnu promítnout na mém JVM, až když zjistí, že chci tuto proměnnou číst. Princip je to jednoduchý, efektivní a elegantní. Navíc nám to umožňuje používat cosi, co se chová jako distribuovaná heap atp.

Na únorovém CZJUGu o Terracottě mluvil jeden z autorů Jonas Bonér. Tenkrát mě to dost zaujalo, ale k nějakému experimentování s ní jsem se dostal až nedávno. Co pro experimentování potřebujeme? Pokud si chcete hrát přímo s kódem, je dobré stáhnout si Eclipse plugin. Ten obsahuje jak nástroje pro práci s konfigurací v Eclipse, tak i Terracotta server, který celé clusterování řídí. Navíc se nám postará o snadné spouštění aplikací, tak aby byly clusterované.

Je důležité si uvědomit, že Terracotta samozřejmě nedistribuuje celou paměť. Sdílená je jenom část pod tzv. kořenem (rootem). Jak to funguje si ukážeme na příkladu. Mějme následující třídu.



01 public class Store {
02   private Map<String, Object> data = new ConcurrentHashMap<String, Object>();
03   public void put(String key, Object value)
04   {
05     data.put(key, value);
06   }
07   
08   public Object get(String key)
09   {
10     return data.get(key);
11   }
12 }

Poté mohu Terracotě říci, že proměnná data je kořenem a ona mi začne distribuovat všechny změny provedené v této proměnné a všech objektech na které má tato proměnná reference. Tzn. v clusteru se mi bude sdílet všechno, co umístím do této mapy. Ač to podle kódu nevypadá, kořen se chová jako singleton. A to dokonce singleton v rámci clusteru! To je to, po čem uživatelé EJB marně touží.

Konfigurace kořene je pomocí Eclipse plugin jednoduchá, prostě na proměnnou kliknu a řeknu toto je kořen. Plugin vygeneruje příslušnou XML konfiguraci.

Funkčnost můžeme vyzkoušet následovně:



01 public class AdditionTest {
02   public static void main(String[] args) {
03     Store store = new Store();
04     Integer number = (Integerstore.get("number");
05     if (number==null)
06     {
07       number = 0;
08     }
09     store.put("number", ++number);
10     System.out.println("Putting "+number+" to the store.");
11   }
12 }

Kód je jednoduchý, vyrobím novou instanci Store (skladiště), vezmu si z ní číslo, přičtu k němu jedničku a uložím ho zpět. Když tento příklad spustím jako normální aplikaci, vždy začínám s prázdnou instancí skladiště a vždy tam tedy budu ukládat jedničku. Když ale aplikaci spustím jako distribuovanou pomocí Terracotty, bude se to takto chovat jen při prvním spuštění. Při každém dalším už bude skladiště obsahovat hodnoty do něj uložené z předchozích spuštění! To je hodně proti intuici. Na jednom řádku vytvořím novou instanci a na druhém už jsou v ní nějaké hodnoty o kterých netuším jak se tam dostaly. To je ale u distribuovaných aplikací normální, můžeme si představovat, že mi je tam zapsal někdo z jiného virtuálního stroje. Navíc to, že mi data přežila konec virtuálního stroje je přeci jeden z důvodů, proč chci používat cluster. Zvídavější jistě zajímá, kde ta data přežila. Odpověď je jednoduchá, abych mohl Terracotu používat, musím mít spuštěný Terracotta server. Ten se stará o koordinaci clusteru a v našem případě i o uložení dat. Pokud vypnu i tento server (a všechny jeho případné kopie), o data přijdu definitivně.
Každá sranda samozřejmě něco stojí. Když chci clusterovat program, musím ho napsat tak, aby počítal s tím, že v něm poběží více vláken. Musím tedy používat sychnronizace, zámky atp. Terracotta se pak už postará o to, aby se aplikace chovala jako vícevláknová na jednom JVM. Když se podíváte na výše uvedený příklad, uvidíte, že se změnami z více vláken nepočítá. Než budete číst dál, zkuste schválně přijít na to, co tam je špatně. Tak co přišli jste na to? Mapa ve skladišti je typu ConcurrentHashMap, je na běh více vláken připravená. Zkuste to jinde. Pořád nic? Dobře nebudu vás napínat, určitě jste na to přišli. Chyba je v použití skladiště. Já si z něj vytáhnu Integer a o pár řádků dál do něj vložím Integer o jedničku větší. Tím ale mohu přepsat hodnotu, kterou tam mezi tím vložilo jiné vlákno! Nejprve nás napadne vyřešit to synchronizaci. V našem případě by byl problém s tím, jaký objekt použít jako zámek. Navíc existuje elegantnější řešení. Použijeme AtomicInteger, který se nám postará o bezpečné zvyšování hodnoty.



01 public class AdditionTest2 {
02   public static void main(String[] args) {
03     Store store = new Store();
04     AtomicInteger counter = (AtomicIntegerstore.get("counter");
05     if (counter==null)
06     {
07       counter = new AtomicInteger();
08       store.put("counter", counter);
09     }
10     counter.incrementAndGet();
11     System.out.println("Putting "+counter.intValue()+" to the store.");
12   }
13 }

Další cenou za clusterování je razantní snížení výkonu při manipulaci se sdílenými objekty. Terracotta musí na konci každé transakce (opuštění synchronizovaného bloku) přesunout data buď na server nebo na jiného člena clusteru pro případ, že by aktuální virtuální stroj bídně zhynul. Takže pokud dělám hodně změn ve sdílených objektech má s tím hodně práce. Pokud předchozí příklad spustím normálně ve 100 000 iteracích, proběhne za 20ms (na mém obstarožním notebooku). Pokud ho pustím v Terracottě, proběhne přibližně za 86 sekund! Což je sice pořád ještě dost slušných 1000 operací za sekundu, ale už to není ono. Pokud pustím tyto testy dva současně tak, že se mi perou o procesor, doběhnou přibližně za tři minuty. Nicméně si dovoluji tvrdit, že to na sdílení HTTPSession, cache nebo konfigurace musí stačit. Pokud se vám moje mikrobenchmarky nezdají dost vypovídající (oprávněně) podívejte se na tento dokument.

Pro dnešek už toho bylo dost, více možná napíši příště. Zdrojové kódy jsou ke stažení pomocí SVN na adrese https://java-crumbs.svn.sourceforge.net/svnroot/java-crumbs/terracotta-playground.