Ez most egy kocka bejegyzés lesz, akit az ilyen nem érdekel, nyugodtan hagyja ki.
Mint egy-kétszer utaltam rá, nem volt szerencsénk az elmúlt hónapokban. Több szerverhibába botlottunk, mint az előtte lévő 3 évben. Persze a 9-esek száma úgy működik, hogy egyrészt ahogy növekszik, az ár hatványozottan emelkedik, másrészt az egész "csak" valószínűség, vagyis a nagyobb biztonság csak hosszú távon és várható értékben jön össze. Garancia nincs rá, hogy nem esik egy tégla a fejemre, csak annyit mondhatok, ha mégis megtörténik, hogy nagyon-nagyon-nagyon nagy pechem volt. Ami viszont nem boldogít a másvilágon.
Van még egy harmadik tényező is: nem csak abban van bizonytalanság, hogy mikor következik be hiba és milyen jellegű. Hanem abban is, hogy ha már bekövetkezett, mi a tényleges ok. Ritkán mondja meg a gép egyértelműen, hogy ez és ez az alkatrész hibás, tessék cserélni. Inkább csak sejteni lehet, hogy inkább a memória, a vinyó vagy a táp, mint mondjuk a proci vagy az alaplap (legalábbis bizonyos esetekben, bizonyos jelek alapján). Ez azt jelenti, hogy ha az ember úgy dönt, lecseréli az X alkatrészt, akkor nem elég, hogy kifizet érte nem kevés pénzt (mert azért egy bizonyos 9-es számot muszáj alapból elérni, annak meg ára van), de még az is könnyen lehet, hogy nem ott volt a hiba.
Ezt így leírva (és gondolom olvasva) nem igazán látszik a fény az alagút végén. Pedig van. És pedig ott van, hogy a valódi cél nem feltétlenül az, hogy soha ne történjen hiba, hanem inkább az, hogy ha történik is, minél kisebb legyen az okozott kár (vö. R=W·K). Ami a játék esetén a kiesett idő, és a "visszatekerés" mértéke.
Annó, lassan 4 éve, amikor belevágtam a Zandagortba, hoztam két technológiai döntést:
- a játék PHP+MySQL+AJAX alapon megy
- az adatbázis nagyrészt MEMORY táblákat használ
Bár mindegyikbe bele lehetne kötni, igazából nem bántam meg egyiket sem. De most csak a másodikról essék szó, mivel az érinti a mostani témát.
A játékban két adatbázis van: az egyik tulajdonképpen egy hatalmas állapotleíró, ami megadja, hogy melyik bolygón milyen gyárak, erőforrások vannak, hol találhatók az egyes flották és mi az összetételük, egyszóval mindent, ami a játékbeli világot jellemzi. Ez persze folyamatosan változik, hiszen a gyárak termelnek, a flották mozognak, vagyis maga a világ is változik. A másik adatbázis egyfajta log vagyis napló, amiben eltárolódnak csaták, kémjelentések, toplisták, flottamozgások. Itt változás nincs, csak kiegészülés: vagyis csak újabb és újabb sorok íródnak hozzá, ami már le van írva, nem változik.
Egy ilyen logjellegű adatbázis tökéletesen jól működik MyISAM táblákkal. Minden, ami történik, folyamatosan és főleg sorfolytonosan kiíródik lemezre, így egyrészt nem vész el (szinte) semmi, másrészt az I/O szekvenciális vagyis gyors.
Az állapotleíró adatbázishoz viszont a MEMORY táblák tűntek és tűnnek a legjobbnak. Ezek legnagyobb előnye, hogy kizárólag memóriában vannak az adatok, így a tipikus szűk keresztmetszet, az I/O-műveletek kimaradnak. Hátránya kettő is van, bár elsőre csak az egyik nyilvánvaló. Ha elszáll a gép (akár csak a MySQL-szerver is, akár csak egy másodpercre is), elveszik az összes adat (a táblák maguk megmaradnak, csak kiürülnek).
A kevésbé nyilvánvaló hátrány, nem a MyISAM-mal, hanem az InnoDB-vel szemben, hogy nincs soronkénti lock, csak egész táblás. Ez főleg akkor lehet probléma, ha se az INSERT/UPDATE-ek, se a SELECT-ek aránya nem kiemelkedően magas, vagyis alapvetően vegyesen történik írás és olvasás. A játék, szemben mondjuk egy tipikus weboldallal, ahol szinte alig van írás, pontosan ilyen (s7-re konkrétan: Com_select/sum(Com_%)=49%). Hiszen ha más nem, a bolygók gazdaságai percenként frissülnek. Ennek ellenére az a tapasztalat, hogy ez itt nem jelent gondot. Valószínűleg azért, mert az I/O-műveletek hiánya annyira meggyorsítja az írást/olvasást, hogy még a táblaszintű lock sem okoz annyi fennakadást, mint mondjuk egy sorszintű lock I/O-val együtt.
Visszatérve a nyilvánvaló hátrányra. Jön egy keményebb villám, a szervertermen végigfut az áramingadozás (na jó, ahogy Móricka elképzeli), a szerver újraindul. Vagy valami hülye rejtett bug a MySQL-ben, amitől ritkán ugyan, de behal az adatbázis, még ha utána egyből restartol is. Egy szimpla honlapon ezt észre sem venni, max 1-2 percig nem elérhető, aztán helyreáll a rend. Ez a helyzet akár a zandagort honlappal, akár a fórummal. (Nyilván más a helyzet, ha totál meghal a gép, de ez az egész cikk most nem erről szól.)
Ezzel szemben a játéknak elszáll a MEMORY táblákban tárolt állapotleírója, vagyis se kép, se hang, még a galaxis sem marad meg. Egészen addig, amíg valaki helyre nem állítja egy mentésből. És még utána is csak az az állapot jön vissza, ami a mentés idején volt.
Egészen mostanáig ez úgy nézett ki, hogy minden hajnalban készült egy teljes mentés, ezen kívül be volt kapcsolva a binary log, ami egy speciális log, ami rögzít minden egyes változást minden egyes táblában. Ez teljesen más, mint a fent említett log adatbázis, egyrészt mert tényleg minden változást rögzít (pl hogy X bolygón a kő mennyisége D időpontban Y-ra változott) (még ha nem is pont ebben a formában), másrészt mert a MySQL magától elkészíti.
Felmerülhet a kérdés, hogy ha van binary log, ami persze lemezre íródik, akkor mi van az I/O-sebességével. A válasz az, hogy a binary log szekvenciálisan íródik, ami gyors. Olyannyira gyors, hogy nem nyer vele sokat az ember, ha kikapcsolja.
Vagyis egy elszállás utáni helyreállítás kétféle lehet:
- Betöltöm az utolsó mentést, és kész. Ez viszonylag gyors. Már persze onnan számolva, hogy észreveszem (hogy szólt valaki), géphez kerülök, és elindítom a visszatöltést.
- Betöltöm az utolsó mentést, és a binary logból leszimulálom az onnantól az elszállásig történteket. Ez jóval lassabb, hiszen ha senki hozzá nem nyúlt a játékhoz a köztes időben, akkor is van a percenkénti bolygó/flotta/satöbbi léptetés. Percenként kb 10 percet lehet előretekerni, vagyis átlag esetben, amikor 12 órát kell újraszimulálni, olyan 70 perc alatt fut le a helyreállításnak ez a fázisa. Ami már nem olyan gyors. És sok esetben nem is megy gond nélkül, mert nem nehéz véletlenül elérni, hogy olyan is íródjon a binary log-ba, aminek nem kéne, és ami az újrajátszásnál duplikált bejegyzést eredményez.
Felmerült persze már korábban is, hogy jó lenne ezt az egész helyreállítást valahogy feljavítani: elsősorban automatizálni, másodsorban felgyorsítani. De ha az embernek épp ezer dolga van, amik látszólag mind fontosabbak, mint egy ilyen (jó esetben) ritkán használt komponens, akkor könnyű sosem belevágni. Az elmúlt időszakban megszaporodott hibák arra voltak jók, hogy átrendezzék a prioritásokat, így most végre belevágtam, és elkészült a GKR vagyis a Gebasz Kezelő Rendszer.
Ez 3 shell szkript, amiből az egyik percenként indul cron-ból, a másik kettőt pedig ez hívja meg.
gkr
Az elején néhány apróság (ezeket nem kommentelem, mert érthetőek, és nem befolyásolják az egész szkript megértését):
#!/bin/sh server_prefix="$1" mysql_password="$2" cd "/home/web2/mmog_$server_prefix" date=$(date +"%Y-%m-%d_%H%M%S") min=$(date +"%M")
Egy fájl, ami a rendszer aktuális állapotát tartja nyilván:
server_status=$(cat www/up/index.html)
Ez a http://s8.zandagort.hu/up/ helyen bárki számára elérhető. Ami nem csak arra jó, hogy bármikor bárhonnan látni lehessen, mi a helyzet, hanem hogy a másik szerveren futó Zandagort honlap is le tudja kérni, és ez alapján kiírni valami bocsánatkérő hibaüzenetet.
A szkript kimenete egy fájlba íródik, hogy utólag meg lehessen nézni, mi történt:
echo "$date gkr: $server_prefix $server_status"
Ha a helyreállítás rendben zajlik, akkor a szkript kilép, nehogy mondjuk újra elindítsa a helyreállítást (vagy esetleg elkezdje lementeni a félig helyreállított állapotot). Ha viszont túl sokáig "fut", akkor valószínű, hogy elszállt közben, és jobb újraindítani:
if [ "$server_status" = "RESTORING....." ]; then echo "RESTART gkr_restore" ./gkr_restore "$1" "$2" exit fi if [ $(echo "$server_status" | cut -c-9) = "RESTORING" ]; then echo "$server_prefix $server_status" echo -n "." >> www/up/index.html exit fi
A várakozás azt jelzi, hogy a helyreállító (gkr_restore) már megpróbált elindulni, de még a MySQL szerver sem fut. Ekkor a rendszer várakozik és újrapróbálkozik. Illetve még 15 percenként küld egy (sürgető) emailt, hogy valami komolyabb baj van. Mondjuk ezen még érdemes lehet finomítani. Egyrészt, hogy esetleg megpróbálja újraindítani a MySQL-t, másrészt nem biztos, hogy annyira hasznos ennyi emailt küldeni. De első körben (nekem) az is elég, hogy a rendszer egy sima, zökkenőmentes restart után helyreállítja az adatbázist.
if [ "$server_status" = "WAITING" ]; then echo "$server_prefix WAITING" if [ "$((min%15))" -eq 0 ]; then echo "Server is totally down, still waiting." | mail -s "Zandagort $server_prefix is totally down" "admin@email.com" fi echo "START gkr_restore" ./gkr_restore "$1" "$2" exit fi
Ezután nézi meg, hogy mi a helyzet az adatbázissal. Ez szintén egy kívülről is elérhető rész (http://s8.zandagort.hu/up/up.php), ami egyszerűen az egyik egysoros MEMORY táblából kér le egy sort. Ez kb a létező leggyorsabb művelet (az egysorosság miatt), főleg, hogy ezt a táblát csak ritkán használja a játék. Viszont ha elszállt az adatbázis, akkor ez is kiürül.
is_it_up=$(wget -q -O - "http://$server_prefix.zandagort.com/up/up.php")
Ha bármi gond van, akkor indul a helyreállító szkript.
if [ "$is_it_up" != "UP" ]; then echo "$server_prefix DOWN" echo "START gkr_restore" ./gkr_restore "$1" "$2" exit fi
És végül, ha a rendszerrel minden rendben, akkor 15 percenként készül egy mentés:
if [ "$((min%15))" -eq 0 ]; then echo "START gkr_backup" ./gkr_backup "$1" "$2" fi
Ez az egyik legfontosabb újítás az eddigiekhez képest, amikor csak naponta egyszer készült mentés. Így a maximum aktív játékidő, ami kieshet, az 15 perc. Ez egyrészt elég kevés ahhoz, hogy ne nagyon fájjon, másrészt szükségtelenné teszi a binary logból való újraszimulálást, így a helyreállítás nagyon gyorsan megvan (kb 1 perc). Maga a mentés olyan 5-10 másodpercig tart, ami alatt ugyan lockolva vannak a táblák (hogy konzisztens legyen a dump), de ez 15 percenként számolva nem sok idő (kb 1%).
gkr_backup
A mentést készítő szkript hasonló apróságokkal kezdődik:
#!/bin/sh server_prefix="$1" mysql_password="$2" cd "/home/web2/mmog_$server_prefix" server_database="mmog""$server_prefix" server_admin="mmog""$server_prefix""admin" date=$(date +"%Y-%m-%d_%H%M%S") echo "$date gkr_backup: $server_prefix $date"
Utána jön maga a mentés:
mysqldump -u "$server_admin" --password="$mysql_password" --net_buffer_length=4096 "$server_database" | gzip --fast > dump/mmog_teljes_dump_$date.sql.gz
A net_buffer_length-et azért kell beállítani, mert a mysqldump alapból ún. extended insert-eket használ, vagyis egy INSERT nem egy, hanem egy csomó sort beszúr. Ami azért jó, mert a dump kisebb lesz (ami nem csak helyet, hanem I/O-t is spórol), a visszatöltése pedig gyorsabb. A probléma csak az, hogy alapból irgalmatlan hosszú sorokat generál, konkrétan annyira hosszúakat, hogy a MySQL szerver simán nem bírja beolvasni (persze lehetne azon is állítani).
A tömörítés a méret miatt fontos. Egy dump olyan 100-200 mega, ami egy nap alatt már 10-20 giga. De itt sem annyira a tárhely a probléma, inkább az I/O. A gzip a fast paraméterrel gyorsabb, mintha a tömörítetlen dumpot írnánk ki lemezre.
Mentés után leellenőrizzük, hogy megvan-e az adatbázis:
is_it_up=$(wget -q -O - "http://$server_prefix.zandagort.com/up/up.php")
Itt is elég csak egy táblába beleolvasni, mert ha végig rendben futott a MySQL, akkor minden oké, ha meg elszállt, akkor az összes tábla tartalma elszállt.
Ha pont mentés közben történt valami, akkor a dumpot töröljük, mert hibás mentéseket ne őrizgessünk. Így lehet legbiztosabban elkerülni, hogy a helyreállítás ne hibás mentésből történjen:
if [ "$is_it_up" != "UP" ]; then echo "$server_prefix backup mmog_teljes_dump_$date.sql.gz deleted" rm dump/mmog_teljes_dump_$date.sql.gz else echo "$server_prefix backup mmog_teljes_dump_$date.sql.gz stored" fi
gkr_restore
Nem meglepő módon a helyreállítás is a szokásos apróságokkal indul:
#!/bin/sh server_prefix="$1" mysql_password="$2" cd "/home/web2/mmog_$server_prefix" server_database="mmog""$server_prefix" server_admin="mmog""$server_prefix""admin" last_intact_dump=$(ls -t dump | head -1) date=$(date +"%Y-%m-%d_%H%M%S") echo "$date gkr_restore: $server_prefix $last_intact_dump"
Egyetlen extra van benne, a last_intact_dump, ami a dump fájlok közül a legutóbbi. Ezért fontos, hogy a hibás mentés törlődjön.
Bebillentjük a közös flag fájlt helyreállítás üzemmódba (innen tudja pl a percenként induló gkr szkript, hogy ne csináljon semmit), és küldünk egy emailt:
echo -n "RESTORING" > www/up/index.html echo "Server is down, trying to restore." | mail -s "Zandagort $server_prefix is down" "admin@email.com"
Ezután jön maga a helyreállítás:
gunzip -c "dump/$last_intact_dump" | mysql -u "$server_admin" --password="$mysql_password" --default-character-set=utf8 "$server_database"
Ha nem sikerült, akkor átváltunk várakozó üzemmódba (vagyis az alap gkr szkript percenként elindítja a helyreállíót):
is_it_up=$(wget -q -O - "http://$server_prefix.zandagort.com/up/up.php") if [ "$is_it_up" != "UP" ]; then echo "gkr_restore: unsuccessful" echo -n "WAITING" > www/up/index.html echo "Server is totally down, waiting." | mail -s "Zandagort $server_prefix is totally down" "admin@email.com" exit fi
Ha sikerült, visszabillentjük a flag-et UP-ba:
echo "gkr_restore: successful" echo "Server successfully restored." | mail -s "Zandagort $server_prefix is up again" "admin@email.com" echo -n "UP" > www/up/index.html
Ennyi. Teszt körülmények között jól szuperál, hogy éles helyzetben milyen, az majd elválik. Jelenleg csak az s8 alá raktam be, bár a 15 percenkénti mentés már az s7-re is megy.
Utolsó kommentek