2009.01.02.
clapf turbo
Azon tűnödtem – így az év első napjaiban, meg már az előző utolsóiban is – hogyan lehetne gyorsítani a clapf statisztikai elemzésén? A szűk keresztmetszet a MySQL token adatbázis lekérdezése, azaz az I/O-műveletek.
Mert én ugyan a széles nyílvánosság számára nem publikált “mydb” fantázianevű token adatbázist használom, amivel egy átlag levél n * 10 msec alatt kategorizálható, de akik a MySQL backendet használják, azoknál ez tovább tart.
Szóval magamban hangosan gondolkodva azt találtam ki, hogy a clapf démon induláskor betölti a memóriába, pontosabban egy hash szerkezetbe a tokeneket, és a child processzek, amelyek a leveleket kategorizálják, már nem a MySQL démontól kérdezik le az egyes tokenekhez tartozó számlálókat, hogy abból még kiszámolják a spam valószínűségeket, hanem ebből a hash-ből olvasnák ki közvetlenül a tokenekhez tartozó valószínűségértékeket. 1 tokenhez 8+4(+4) byte tartozna, azaz 1 MB-on 64k rekordot lehetne tárolni.
Nekem jelenleg ~60k token van az adatbázisomban. Ha ennek a duplájával számolunk, akkor az 10 child processz esetén ~22 MB memóriaigényt jelent, ami nudli. Ha pedig enterprise-level alkalmazásról van szó, akkor ott alaposan ki szokták bélelni memóriával a gépeket, így ott sem gond a memóriahasználat.
Az első időkben persze ennél sokkal több token is lehet a MySQL adatbáisban, amíg le nem tisztul, hogy melyek a hasznos tokenek, és ki nem hullanak az érdektelenek, így lesz majd egy változó, ahol be lehet állítani, hogy max. hány token legyen a hash-ben. Így nem szalad el a ló a memóriafoglalással, mert a fork() miatt minden child processznek szüksége van az x MB memóriára.
Ezért az alábbi algoritmust találtam ki. Addig folytatom a keresést, amíg a tokenek száma a beállított érték (max_tokens) alá esik.
1. select count(*) from t_token where uid=0
2. select count(*) from t_token where uid=0 and NOT (nham=0 and nspam=1)
3. select count(*) from t_token where uid=0 and (nham + nspam)=1
4. select count(*) from t_token where uid=0 and (nham + nspam) <= 2
5. select count(*) from t_token where uid=0 and (nham + nspam) <= 2 and erdekesseg > 0.2
6. select count(*) from t_token where uid=0 and (nham + nspam) <= 2 and erdekesseg > 0.2 ORDER BY timestamp DESC LIMIT max_tokens
Az egyelőre fejben/blogban levő megoldás egyetlen hátránya, hogy induláskor kell egy full table scan, ami pár másodpercig azért eltarthat. Minden frissítéskor szintén kell az előbb említett full table scan, de ha már fut-robog a kicsi kocsi, akkor ezt észre sem lehet venni.
A tanítás továbbra is közvetlenül a MySQL adatbázisba történne, így 5 perces frissítési időközökkel számolva a token adatbázis módosítása 5 perces késleltetéssel terjed az éles/használt tokenek közé, ami általában elfogadható.
A MySQL adatbázisban az egyes tokenekhez egy timestamp-et is tárolok, amit a child processzek kilépéskor egy nagy UPDATE utasítással frissítenek az SQL táblába. Hogy a nem használt tokeneket ki tudjam hagyni ebből, ehhez kell még egy byte (kb. a dirty bit mintájára), amit beállítok, ha az adott tokent lekérdeztem.
A megoldásnak van/lesz még egy hátránya: közös (shared) token adatbázis kell hozzá, azaz mindenki ugyanazon a token halmazon osztozik. Ahol ez nem megfelelő, vagy megbízhatatlanoknak is meg kell adni a tanítás lehetőségét, ott továbbra is a jól bevált, közvetlen MySQL backend használata a járható út.
Hamarosan lekódolom, aztán majd jön egy benchmark, hogy mit gyorsít ez a levéláteresztő-képességen.
Igazság szerint van még egy ötletem a gyorsításra: ugyanezt egy threaded démonon keresztül megoldani, mert abban az esetben csak 1 példányban szerepel a hash tábla a memóriában, így ez sokkal gazdaságosabb megoldás, viszont a hálózati kommunikáció is időbe telik, mire egy nagy halom tokent át tudok vinni. Ennek további erénye, hogy nem nagyon kell azzal foglalkozni, hogy csak x db tokent olvashassak be. Majd ezt is kipróbálom még.




