Mi a gond ezzel a kóddal? Illetve cache alternatívák

Fórumok

Van egy egyszerű cache kódom a KittyCache alapján készült. A performanciája jobb volt mint a többié (EhCache, Guava ...), viszont a napokban fedeztem fel egy furcsaságot, ami gondot is okozhat. Estére, amikor eléri a kb. 900 ezer / 1 milliós lekérésszámot, elkezd nőni a load. Az addig 0.04-es load felmegy kb. 0.4-0.7-es loadra. A cache max elemszáma: 10 ezer.

CentOS alatt fut, java 8 update 20-al. Virtualizációs környezet: OpenVZ 4G RAM. Most így visszagondolva java 7-el is hasonlót produkált, de akkor még nem tűnt fel, hogy ez hiba lehet. 100M szabad ilyenkor még a heapből.

Ebből kb. 1G-át eszik futás közben a java. Van amikor 2 példány is fut egymás mellett, ilyenkor kb. 2G-át eszik, de ez nem az esti időszak.

Kipróbáltam, ha ilyenkor magas loadnál törlöm a cache-t, akkor visszamegy a CPU load 0.04 környékére.

Az is jó, ha tudtok ajánlani valami egyszerű memória cache-t (régen whirlycache-t használtam, de valami a mostani kódjában nem stimmel). De első sorban a hibalehetőségek izgatnak.
Tehát a kód:


import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class YSCache {

private final Map cache;
private final Queue queue;
private final int maxSize;
private final AtomicInteger cacheSize = new AtomicInteger();
private int gets;
private int hits;

public YSCache(int maxSize) {
this.maxSize = maxSize;
cache = new ConcurrentHashMap<>(maxSize, 0.8f, 16);
queue = new ConcurrentLinkedQueue<>();
}

public V get(K key) {
gets++;
V v = cache.get(key);
if (v != null) {
hits++;
}
return v;
}

public void put(K key, V value) {
if (cache.get(key) == null) {
cacheSize.incrementAndGet();
while (cacheSize.get() > maxSize) {
cacheSize.decrementAndGet();
cache.remove(queue.poll());
}
}
cache.put(key, value);
queue.add(key);
}

public int getGets() {
return gets;
}

public int getHits() {
return hits;
}
}

Hozzászólások

0.4-0.7es load. lol. eddig olvastam. :)

majd ha 10 felett lesz, szolj:D

Köszi Zoli a hozzászólást.

A load valami miatt több mint 10x-esére nő. Pedig a cache már az első 20 ezer kérésnél eléri a mérethatárt, mégis csak 1 millió körül történik a load ilyen mértékű növekedése. Mivel naponta indítom újra az alkalmazást, így nem nagy gond, de mi lenne, ha mondjuk fél év alatt 2 millió / nap lenne a forgalmam (ennek van realitása is). Azt vajon kibírja-e az alkalmazás, ha ilyenkor a load 10 lesz, ahogy írod? Igaz lehet ilyenkor még egy szervert is beizzítani, de nagy presztízs veszteség lenne, ha emiatt állna akár egy fél napot is.

Tudom, készítsek load tesztet. Azt már készítettem JMeterrel, access logokból, de nem jutottam előrébb, nem látok semmi rendelleneset, csak a load növekedést / átvitel csökkenést. Igaz 2 millióig nem vittem el a kéréseket.

Tehát itt van valami amiből lehetne tanulni, vagy valamit esetleg rosszul csinálok és arra rájönni: mit.

Néztem a verbose GC logot, de nem látok változást a cache törlése előtt / után.
Arra is gondoltam, hogy esetleg az OpenVZ löki ki a memóriából az adatokat swapre és pl. egy elem törlésénél, a swapről kell visszabányásznia a java-nak törlés előtt. De gyanús, hogy simán el tudok indítani ilyenkor egy másik 1G-ás java példányt is.
Az is lehet, hogy a java heap fragmentálódik és a sok kicsi lyuk miatt nehéz elhelyezni az objektumokat.

Köszi! :-)

Lehet, hogy igazad van a hashel kapcsolatban. Elvileg az elején, mivel a maxSize-vel inicializálom a Map-et. Ettől függetlenül nem tudom mi történik, ennek utána nézek a JDK forráskódban. Eddig úgy tudtam, hogy ez alapján készít "rekeszeket", viszont lehet, hogy a túl sok elem ki-be pakolgatása megviseli a "rekeszek" eloszlását.

volatile: Korrekt észrevétel. Direkt nem raktam volatile-re, mert nyugodtan gyorsítótárazza a java, nekem csak egy megközelítő érték kell, ritkán nézem meg az értékét.

en nem nagyon aggodnek a load miatt. ertem, hogy megno, de szerintem addig nincs mit fixalni, amig nem romlik el.

esetleg integralj gyorsan bele egy metrics-et, es told ki az adatokat valami grafikon rajzoloba, hogy megnezzuk hogy alakulnak a JVM belso ertekei (ezt akkor is erdemes megtenni, ha epp minden jol mukodik :))

Megnézegettem ezt a Metrics-et, meg a srácot is aki fejleszti és úgy tűnik, hogy érti amit csinál...

Lehet nekünk is jól jönne egy ilyen tool, csak az nem világos még, hogy ha van egy enterprise software, akkor hogy fogsz neki egy ilyen integrálásának.
Nekem egy kicsit furcsa, hogy a prod kódba elkezd az ember mindenhova ilyen metereket, meg markerek berakosgatni.
De mint mondtam, ettől függetlenül a srác nagyon jó dolgokat mond a videóban is és tetszik a tool is.

Neked van vele tapasztalatod nagyobb software esetén? Esetleg meg tudsz osztani valami érdekesebb statisztikát amit ez a tool adott ki?

en nem felnek a production hasznalattol - miben mas ez, mint a konteneredben megbizni, hogy majd jol kezeli az annotaciot? :)

raadasul ha valaki fel ettol, a JVM ertekeket JMX-en ki lehet tolni, es akkor metrics nelkul is van sok adatod.

van nalunk fejlesztett, nagy termek ahol hasznalatban van, nem volt vele semmi gond, viszont nem tudok grafikonokat megosztani.

+1
Ez teljesen normális, futtasd le az ls-t 20x egymás után és ugyan ezt fogod látni.
0.4-0.7 az nem load, 4 környékén már beszélhetünk a szervert érintő terhelésről, de míg reszponzív a konzol és nem dadog, ne foglalkozz vele

// Happy debugging, suckers
#define true (rand() > 10)

Azt nézted, hogy a map hogyan reagál arra, ha egyre több elem van benne?

Az OpenJDK7 óta az OpenJDK nem valami kínai másolat hanem "A Referencia Implementáció". Ezt tolja ki az Oracle is csak rácsap még pár plusz proprietary dolgot (fontok, browser plugin, etc).
De elég ha megnézzük az importokat, semmi extra.

--
arch,debian,windows,android

dev: http://goo.gl/7Us0GN
BCI news: http://goo.gl/fvFM9C

Lehet a 7 óta így van, nem tudom, csak gondoltam rákérdezek, mert nekünk most volt ebből egy elég nagy problémánk.

Laborkörnyezetben minden ment rendesen 1.6-al és azt tudtuk, hogy az éles rendszeren is 1.6 van, de valami mégis máshogy működött (konkrétan a GC).
Én bár megemlítettem egy hete a kollégáknak, hogy nem lehetséges-e egész véletlen, hogy Oracle helyett OpenJDK fut az ügyfélnél... De kinevettek, hogy az nem létezik.

Nem ismerem a YSCache implementációval szemben támasztott követelményeket, és a metódusok hívásának környezetét se részletezted, de a field-ek típusa alapján arra következtetek, hogy egy thread-safe cache-re van szükséged. A fenti kód attól még nem lesz thread-safe, hogy thread-safe objektumok metódushívásait tartalmazza. Először szerintem újra kellene gondolni ezt a részt, pl. cache esetén remekül használható a ReentrantReadWriteLock. Ez csak egy észrevétel, nem biztos, hogy a load probléma erre vezethető vissza.

Köszönöm ezt az észrevételt is.
Elvileg átgondoltam, milyen gondok keletkezhetnek, amikor (egy jó éve?) írtam ezt a kódot. A szálbiztosság nem volt követelmény, inkább a gyorsaság.

Viszont most utánagondolva lehet olyankor is gond, ha pl. az egyik szál rájön, hogy a cache mérete túl nagy és elkezdi törölni az elemeket, majd a többi szál meg folyamatosan tolja be az új értékeket. Ez nem egy túl valószínű eset és valószínűleg elég hamar "letekeri" a kívánt méret alá a map és queue méretét.

Esetleg most így megnézve még egy olyan eset is lehet, hogy pl. az egyik szál kivesz egy értéket a mapből, míg a másik belerakja a mapbe, és a queue-ból pedig kiveszi az értéket az egyik, miután már berakta a másik. Ezzel lehetnek gondok, hogy folyamatosan nő a map mérete olyanokkal, amik nincsenek a queue-ban.

Köszi, ez jó ötlet volt. :-) Ezt még megnézem, hogy a cache egyes elemeinek mi a mérete / értéke.

Közben találtam egy whirlycache-hoz hasonló újabb cache-t:
http://cache2k.org/
Ezt is megnézem.

Ha több szál használja egyszerre a cache-t, akkor valahogy szinkronizálni kell - legalábbis amennyiben konzisztens kell legyen a queue és a map tartalma. Ezért a put és a get metódusod atomi kell legyen kívülről nézve. Ezt több módon is elérheted, synchronized metódusok vagy synchronized blokkok használatával, vagy a már említett ReentrantReadWriteLock segítségével.

Így elsőre nem látom be miért kellene konzisztensnek lennie.
A queue csak azt a célt szolgálja, hogy ki tudjuk találni, melyik elemet lehet törölni, ha túl sok a cache tartalma.
Ha egyszer-kétszer becsúszik, hogy több elem lesz a Map-ben, mint a megengedett, az még nem gond. Mondjuk nap végére 10 ezer helyett lesz benne 15 ezer elem. Még ez sem gond.
De ezt igyekszem megnézni holnap estére (ma frissítek, holnapra deployolodik, ezért tudok csak holnap nézelődni), hogy mennyire térnek el így szinkronizálatlanul.

Mégiscsak megléptem a kódcserét. Végül is zero downtime-al tudok új alkalmazást feltenni.
Eddig minden rendben, pedig már volt 30 ezer kérés. Egy másik cache-t is nézek (ott ezer a max.). Ott is minden rendben:

GZIP cache: 5157 / 32800 = 15.722561
Search cache: 125 / 12040 = 1.038206

GZIP cache size: 10000
GZIP queue size: 10000
GZIP Integer value: 10000
GZIP max size: 10000

Search cache size: 1000
Search queue size: 1000
Search Integer value: 1000
Search max size: 1000

Közben:
20:34:41 up 97 days, 30 min, 1 user, load average: 0.02, 0.03, 0.16

Úgy néz ki találtam egy hibás newgen beállítást. Túl alacsonyra volt téve. Így lehet, hogy az idő teltével fragmentálódott.
Estére kiderül ez volt-e az oka.

Ettől függetlenül köszönöm az ötleteket. Sokat tanultam belőle. Valamint át tudtam gondolni közben a dolgokat.
Nézem a cache belső változóit is. Tegnap 170 ezernél volt egy olyan állapot, hogy a Map és Queue mérete 10001-re beállt. Az AtomicInteger értéke meg 10000 volt. Ez nem okozhatott gondot. De azért megköszönném, ha jeleznétek, ha valami gond van a logikámban:
Ha a Map értékét kivesszük és null-al tér vissza, akkor nincs cache hit, ha valami értékkel, akkor azzal már nem lehet gond, csak akkor lehetne, ha közben az értéke megváltozna és ez gondot okozhatna - nálam nincs ilyen helyzet: egy számítás ugyanazt az értéket adja vissza egészen a napi újraindításig.

Most 200 ezer fölött járunk, de nincs eltérés a belső változókban.