Megoldás párhuzamos futás ellen

 ( kumgabor | 2019. március 21., csütörtök - 17:06 )

Sziasztok!

Egy kis logikai segítséget kérnék:

Adott egy adatbázis tábla, amiben a sorokat egy PHP scripttel dolgozom fel, majd a sor egyik mezőjét feldolgozottra állítom. A feldolgozó script bármikor futhat, az is előfordulhat, hogy egyszerre 5 példányban indul el, így meg kell tudom oldani, hogy egy-egy sort párhuzamosan ne dolgozzak fel kétszer.
Eddig erre a megoldás az volt, hogy a script futásának elején lockfájlt hozok létre, majd a végén törlöm, de sajnos ha pontosan ugyanakkor indult el két folyamat, akkor ez nem segített.

Ti mit csinálnátok?

Egy kis pontosítás a hozzászólások alapján:
A script egy bonyolultabb, 4-5 másodperces (nem csak adatbázis) műveletet végez el a táblában lévő adatok alapján. A tábla nem zárolható, mert beszúrni kell tudni ez idő alatt is.
A párhuzamos indítás nem igazán kerülhető el, mert különböző rendszerek is meghívhatják ezt a scriptet és cronból is fut rendszeresen.

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

man flock(1)

[...]
EXAMPLES
       shell1> flock /tmp -c cat
       shell2> flock -w .007 /tmp -c echo; /bin/echo $?
              Set exclusive lock to directory /tmp and the second command will fail.

Van, hogy PHP include-ként hívjuk meg, és olyankor ez macerás lenne.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Ilyenkor kell áttekinteni a folyamatokat, az eljárásokat, aztán magát a kódot, és újragondolni plusz újraírni azt, ami a galibát okozza. Lehet, hogy most nagyobb szívásnak tűnik, de az ilyen tákoljunk még bele dolgokkal csak a féceszkupac tologatása (és növesztése) történik.

+1

Könnyen több idő lesz itt olvasgatni, molyolni, mint átépíteni a jó felépítésre.

Mit értesz jó felépítésre?

A szempontok jelenleg:
- Bizonyos események után azonnal le kell futnia
- Ritkán előfordulhat olyan, hogy bekerül a táblába feldolgozandó elem, de nem a fenti, "bizonyos események" következtében, ezért van 10 percenkénti CRON

Nem írhatom le pontosan miről van szó, de olyasmi ez, mint amikor a táblában fizetett rendelések vannak. Fizetett rendelést sok mindenki létrehozhat, a közös pont ez a tábla. Ha ide bekerül, akkor vannak fix teendők, amiket el kell végezni, és a végén bejelölni a rendelésnél, hogy elvégeztük a teendőket.
A gond akkor van, ha a teendőket kétszer is elvégzi a script.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Tehát van egy queue, amibe besehetnek feldolgozandó dolgok, amikre egy adott tevékenységsorozatnak egyszer és csak egyszer kell lefutnia.
ha DB-ben kell megoldani, akkor a "rendelések" egyedi aznosítójához rendelnék néhány állapotkódot: új, feldolgozás_alatt_1, feldolgozás_alatt_2, ... feldolgozás_alatt_n, kész.
Ha bekerül egy rendelés tetszőleges úton, akkor az állapot mező defaultja az "új" lesz.

Ha valamelyik ágon (a tál spagettiből az egyik tészta) elindul a feldolgozás, akkor a legrégebbi/legfirssebb/megadott azonosítójú/szabadon választott "új" állapotú id-nál bebillenti, hogy feldolgozás_alatt_1, és elkezd vele foglalkozni, aztán ahogy halad előre, a feldolgozás lépéseit _2 _3 ... _n állapotokra billegtetéssel jelzi, illetve amikor végzett, akkor "kész" állapotot rak be az id - állapot táblába. Ezzel a feldolgozás egyes lépéseit is szét lehet szedni, minden explicit lépést elvégző kódnak azt kell tudnia, hogy melyik állapotból indul, és milyen szabályok alapján választja ki a matatandó adatokat.

Nagyjából így is működik. A gond akkor van, ha pont ugyanabban a pillanatban fut rá két folyamat. Kéthavonta egyszer fordul elő, de akkor komoly gondot okoz.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Az a baj, hogy csak nagyjából. Az update-ek egymás után fognak lefutni, ha jól csinálod. Annyi kell, hogy az update során ne csak a statusz kerüljön frissítésre, hanem egy task_id mező is, ami az adott feldolgozófolyamatra egyedi (folyamatazonosító+időbélyeg pl., mert az update nem tud visszaadni a frissített sorból vett értéket...), utána a rendeles_id simán kivehető egy következő select-tel a táblicskuból, és lehet maszírozni az adatokat.

Jó, ez tetszik. Köszönöm!

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

egyszerre, atomi műveletként történjen ez, akkor jó.
--
Gábriel Ákos

Mi a cel? Hogy az adatbais rekord csak 1x frissuljon, vagy az, hogy mondjuk egy koltseges feldolgozas csak 1x tortenjen meg? Elobbire egyszeruen bevezetnek egy version mezot a tablaban, es amikor frisstenem, akkor beletennem a feltetelbe, hogy update ... where version = $version. Amelyik feldolgozo nyer az updatel, a tobbiek munkaja meg megy a levesbe. Az utobbi mar kenyesebb szitu. En valami olyasmi iranyba indulnek:
select ... where processor is nul
update ... set processor = $UUID
select ... where processor = $UUID
ha van rekord mehet a feldolgozas
ha nincs a masik feldolgozo nyert
nyertes feldolgozo a vegen update ... set processor = null

-
First impressions of the new Cloud Native programming language Ballerina

Elfelejtettem irni, hogy ez csak akkor mukodik, ha strong consistent az adatbazis.

-
First impressions of the new Cloud Native programming language Ballerina

sejtésem szerint bármilyen mezőt akarsz erre, azzal nem lehet garantálni teljesen, hogy ne kezdjenek párhuzamosan dolgozni rajta kliensek. Atomi műveletként kellene ahhoz megoldani a kiválasztást

Pár random találat
https://en.wikipedia.org/wiki/Record_locking
https://www.brainbell.com/tutors/php/php_mysql/When_and_how_to_lock_tables.html
https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html

De, ha túl nagy lassulással jár ez, akkor lehet maga a koncepció hibás és máshogy kell kezelni az adatokat. De mondjuk ha a lockolás csak a feldolgozás flag beállítása közben él, akkor az elég lehet.

Ha lehet egyáltalán párhuzamosan feldolgozni két sort (ha nem, akkor meg minek a több példány), akkor a példányoknak már az elején diszjunkt halmazokat kell választani (valamilyen stratégia alapján).

Igazad van, mert az update sorba kell egy feltetel, hogy where processor is null. Mert ebben az esetben aki nyer az updatel, a masik mar nem tudja updatelni, es selectkor is mindannyian a helyes erteket latjak es csak egy fog rafutni.

Igazan eleeg keves dolog ismertt a feladattal kapcsolatban, igy meg eleg nehez mondani valamit. Bedobok egy masik otletet, kell egy HA distributed locking service :D

-
First impressions of the new Cloud Native programming language Ballerina

A cél a párhuzamos, és ezáltal többszöri feldolgozás elkerülése. Nem költséges, és nem is csak a sor frissítése a lényeg, hanem az, hogy a script által végzett feladatok is csak egyszer fussanak le.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Akkor az utobbi modszert tudod alkalmazni (egy hibat vetettem a peldaban, de ket kommenttel lejebb javitottam), ha DIY megoldast keresel.

-
First impressions of the new Cloud Native programming language Ballerina

a szkript amit irtal mi alapjan indul akar tobbszor is es akar parhuzamosan is?

Egyebkent nezz ra erre: http://zguide.zeromq.org/php:all#Getting-the-Message-Out

---------------------------------------------
Support Slackware: https://paypal.me/volkerdi

+1 Én sem értem. Most az a cél hogy ne induljon el többször (lock fájl) vagy az hogy elindulhasson csak ne dolgozzanak ugyanazon a soron a példányok?
Milyen adatbázisról van szó (mert ez sem mindegy)?

valoszinu, hogy cron-bol hivja es a foldolgozas tovabb tarthat mint a cron intervallum

Más PHP-k is behívják include-ként és CRON-ból is fut.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

define párhuzamos

Mert hogy az, ha percenként fut a cron-ból, de még az előző fut, ezért akkor ne induljon el másodpéldány az más probléma, mint az, ha 5 helyről egyszerre include-olgatják, nem nagyon, de kicsit más. Azt is definiálni kéne, hogy egy include esetén mi legyen, ha épp fut a program, az az include-olt futás hagyja ki, vagy várja meg, hogy végez, vagy mi?

Megoldások lehetnek még: időbélyeg mentése, hogy mikor futott utoljára, és ha ez x másodpercnél nem régebbi, akkor nem fut, de az "egyszerre" include-olás esetén ez se véd. Még mindig a totál átépítést, egyszálúsítást javaslom, egy cron, az flock-olva, include-olt változatok meg cache-elt eredményt kapnak (utóbbi persze nem biztos hogy jó a usecase-ednek, szerintem fejtsd ki).

Rendszeresen fut cronból és különböző PHP-k is includeolják és futtatják az érintett függvényt, tőlünk független események hatására. Fentebb írtam pár perce egy példát, ami kb. megegyezik azzal, amit mi csinálunk:

Olyasmi ez, mint amikor a táblában fizetett rendelések vannak. Fizetett rendelést sok mindenki létrehozhat, a közös pont ez a tábla. Ha ide bekerül, akkor vannak fix teendők, amiket el kell végezni, és a végén bejelölni a rendelésnél, hogy elvégeztük a teendőket.
A gond akkor van, ha a teendőket kétszer is elvégzi a script.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

select for update where ... limit n;

és akkor n sorod van a kurzorban amit egyszerre feldolgozhatsz és a végén commit-olsz.
ha akarod akkor egyesével csinálod

--
Gábriel Ákos

+1, ha sql az adatbázis, ezt csakis db szinten kell megoldani.

Az, de nem kizárólag adatbázis műveletek történnek.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Hibasan parameterezed a lock filet letrehozasat. Probald ugy krealni, hogy ne hozza letre a pointert/hibat toljon/exception-t adjon ha mar letezik. Mivel a file letrehozasa atomikus muvelet 2 vagy tobb process nem fogja tudni letrehozni a filet egy idoben, igy csak az fut tovabb akinek sikerul.

http://php.net/manual/en/function.fopen.php

'x' Create and open for writing only; place the file pointer at the beginning of the file. If the file already exists, the fopen() call will fail by returning FALSE and generating an error of level E_WARNING.


...
$fp = @fopen($lockfile, 'x');
if(!$fp) exit();
fclose($fp);
... // az elvegzendo muveletek
unlink($lockfile);
...

ezek azért "jó" megoldások mert ha bármi okból a php futása megszakad akkor sose törli ki a lockfile-t, sose indul el új feldolgozás.
nyilván lehet cleanup-olni, változatos eredménnyel...

--
Gábriel Ákos

termeszetesen igy van de nem errol szolt a kerdes es nem szandekom mas hazi feladatat megoldani, inkabb iranyt mutatni. Egyszeru logikaval tovabfejlesztheto ez u.h. a true branch-et tovabb gyurja es sima exit helyett megnezi a file tartalmat, amibe beirja a jelenlegi timestampet lentebb (fclose elott). Igy ha pl. a fileba szereplo timestamp regebbi mint "X" akor torli a filet es nekifut ujra a locknak.

Ha elofordulhat, hogy a php script sosem timeoutol (X legalabb legyen egyenlo a php max execution time-al) akor meg lehet gondolkodni touch tipusu megoldassal (idonkent a futo script updateli a mentett timestampet, igy jelezve, hogy bizony meg mindig fut, de garantalni kell, hogy a touch mindig megtortenik X idon belul, ezert jobb a max execution time megoldas amennyiben nem okoz korrupt/felkesz eredmenyt ami kesob gond lehet), file helyett lehetne mysql tablat unique id indexel hogy a foldolgozas vizszintesen skalazhato legyen, vagy flock() ami sajnos blockolja a tobbi scriptet es meg fojtathatnam reggelig.

Ha az a cel, hogy tobb feldolgozas fusson parhuzamosan, de ne kopjenek egymas levesebe akor a foreign key-t kell hasznalni lock filenevnek es ha a select azt az id-t teriti visza ami mar lock alatt van akor lehet ujra futtatni a selectet order by random-al es kizarni az id-t amit jelenleg kinyertunk es lockolva van...

Szoval ha a kerdezonek ez nem eleg a megoldashoz akor ki kell bovitenie a kerdest es reszletesebben bemutatni a problemat, hogy megtalaljuk az optimalis megoldast a tobb tucatnyi lehetsegesbol

Ez vállalható.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Köszönöm.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Stored procedure az SQL oldalán?
--
https://naszta.hu

Ennek utána kell néznem, köszönöm.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Tábla vagy sor szintű lock? Ha mysql:
LOCK TABLES tbl1 WRITE;

vagy a fentebb említett FOR UPDATE

A sor szintű lock-ot megnézem, a táblát nem zárolhatom ennyi időre. Köszönöm.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Szerintem nézz utána a lockolás körüli témának sql adatbázis esetén kicsit részletesebben.
a "write lock" mellett nyugodtan lehet "olvasni" az adatbázisból.
itt a "transaction isolation" dolognak érdemes utánanézni hogy tudd mit szeretnél / mit tud az adatbázisod (implementáció függő)

a "row level lock" esetén nyugodtan lehet írni akár a tábla többi sorát is, nem beszélve a többi tábláról.

--
Gábriel Ákos

Rendben, köszönöm.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Külön lock tábla csak a lockolás céljára.

De azzal nem oldod meg a rekord szintu lokkolast, fixme, szoval ugyanugy csak az egesz tabalt tudod lockolni (ami nem cel)??

-
First impressions of the new Cloud Native programming language Ballerina

Erről nem szólt a felvetés.

Arról szólt a felvetés, hogy miközben a script fut, és tartja a lockot, közben másoknak az adatokat kéne tudni túrnia, azaz _azt_ a táblát nem lehet lockolni.

A kommentem pillanataban pont egy sorral van a tied felett, hogy kovetelmeny. De ha jobban belegondolunk, mivel van egy szekvencialis feldolgozas, ami feldolgozza a sorokat, gyakorlatilag megall az elet az alkalmazas oldalon, amig a feldolgozas tart, hiszem amint feloldodik a lock mar ujra lockolodik. Nem hiszem, hogy a lock a legjobb megoldas ilyen esetben, mert hamar bottleneck lesz a rendszerben az, hogy 10 percenkent par percig (amig a feldolgozas php oldalon megtortenik) egy szalasitjuk az alkalmazast.

-
First impressions of the new Cloud Native programming language Ballerina

Ezért javasoltam neki SQL stored procedure-t, az futhat tranzakcióként és nem a kliens oldalon old meg DB feladatot/üzleti logikát (persze konkrét feladat nélkül nehéz lesz ez).
--
https://naszta.hu

Miről beszélsz?
Szerintem nem olvastad el, hogy mit írtam. Vagy nem értetted meg.

A feldolgozásnak egyszálúnak kell lennie, ez volt az elvárás (= ne futhasson egyszerre több példányban a feldolgozó). Nem lockolhatja _azt_ a táblát, amin más tevékenységek is vannak, de én nem is ezt írtam javaslatként.

Elolvasod, amit írtam?

Elolvastam, es te magad valaszoltad meg a felreertesunket :) "egyszálúnak kell lennie, ez volt az elvárás" nem ez volt. Az volt, hogy ugyanazt a rekordot ne kapja fel tobb szal! Ami egeszen mas tema.

-
First impressions of the new Cloud Native programming language Ballerina

Tulajdonképpen mindkét megoldás jó ide.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Egy kis otleteles, avagy a szegeny ember compareAndSet-je.

- Hozzaadunk egy lock uuid mezot a tablahoz, ami default kap egy random erteket
- PHP sckriptben
-- 1: Kivalasztok egy sort feldolgozni -> regi_uuid = lock
-- 2: Generalok egy random uuid erteket -> uj_uuid = random()
-- 3: UPDATE tabla SET lock = uj_uuid, status = 'in progress' WHERE lock = regi_uuid
-- 4: Ha 1 sor lett updatelve -> feldolgozas indulhat, ha 0 sor lett updatelve -> goto 1 (ha tobb, mint 1 sor lett updatelve -> vegyel egy lottoszelvenyt)

szerk: most latom, hogy write-only vagyok

Érdekes, köszönöm!

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Ezt mi használtuk többször, működő megoldás.
Annyi eltérés volt, hogy nekünk az OR maping miatt egy "version" mező volt amit az update where tartalmazott mindíg.
update ... set version=2 where recordid=xxxx and version=1
ha 0 sor volt updatelve valaki elhalászta előled a sort.

Mi HA kornyezetben hasznaljuk leader election-re ezt a technikat (egy syncel a tobbi meg feldolgoz). Mivel strong consistent a db sima liba.

-
First impressions of the new Cloud Native programming language Ballerina

[Feliratkozás]

Egy egyszerű megoldás, bízd a kizárást az adatbázisra:

Hozz létre egy táblát, amiben egy rekord van összesen, és egy mező: futas number(1, 0).
0 az érték, ha nem fut a folyamat, 1 értékkel ha fut.

A programod induláskor beupdate-eli ezt az egy sort 1-re, úgy hogy a where-be beteszed: where futas = 0;

Ha 0 sort update-elt, akkor van konkurens folyamat, kilép, ha 1-et, akkor megszerezte a futatás jogát. A PHP-t nem ismerem, de rákerestem gyorsan, és lekérdezhető, hogy hány sor volt érintett az update-ben.

Kilépéskor visszaupdate-eled 0-ra.

És ha elhasal futás közben/nem jut el a normál kilépési pontra, akkor beragadt a lock.

A kilépéskort úgy értettem, hogy ha elhasal valahol a program, akkor is, itt ha kivétel van, azt is érdemes lehet lekezelni, hogy ne maradjon lock.

(De attól is függ mi a cél, elhasalás esetén lehet hogy jó is, hogy nem fut megint.)

Ha a lock leszedés az, ami nem sikerült, akkor arra valóban nem megoldás, mert pl nem megy az adatbázis, vagy nincs vele kapcsolat, ami induláskor még ment. De az már régen rossz. :)

Köszönöm.

--
Kum Gábor
Linux póló | Ciprus | Matek korrepetálás

Nem lehetne veremmel?
A párhuzamos processzek a verem terejére pakolják a feladatokat, míg a csak egy példányban /gyorsan/ futó másik program a verem aljáról kivéve az utasításokat, kezeli a tényleges adatbázist.

Azaz queue...

rabbitmq-ba tolni a feladatokat es onnan inditgatni a scriptet?

MySQL adatbázis esetén, ha a folyamatnak csak egy szálon kellene futnia, akkor GET_LOCK() függvényt érdemes lehet megnézni:
https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html

Ha soronkénti lock kell, akkor a SELECT ... FOR UPDATE -tel próbálkoznék:
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html