Elosztott zárolás Redis segítségével

Szia Habr!

Ma figyelmébe ajánljuk egy összetett cikk fordítását az elosztott zárolás Redis segítségével történő megvalósításáról, és felkérjük Önt, hogy beszéljen a Redis lehetőségeiről. A kérdéses Redlock algoritmus elemzése Martin Kleppmanntól, a könyv szerzőjétőlNagy terhelésű alkalmazások", adott itt.

Az elosztott zárolás egy nagyon hasznos primitív, amelyet számos környezetben használnak, ahol a különböző folyamatoknak kölcsönösen kizáró módon kell működniük a megosztott erőforrásokon.

Számos könyvtár és bejegyzés található, amelyek leírják, hogyan kell megvalósítani a DLM-et (Distributed Lock Manager) a Redis használatával, de mindegyik könyvtár más megközelítést alkalmaz, és az általuk nyújtott garanciák meglehetősen gyengék ahhoz képest, ami egy kicsit kifinomultabb tervezéssel elérhető.

Ebben a cikkben megpróbálunk leírni egy feltételes kanonikus algoritmust, amely bemutatja, hogyan valósítható meg az elosztott zárolás a Redis használatával. Egy algoritmusról fogunk beszélni redlock, elosztott zárkezelőt valósít meg, és véleményünk szerint ez az algoritmus biztonságosabb, mint a szokásos egypéldányos megközelítés. Reméljük, hogy a közösség elemzi, visszajelzést ad, és kiindulópontként fogja felhasználni bonyolultabb vagy alternatív projektekhez.

Végrehajtás

Прежде, чем перейти к описанию алгоритма, приведем несколько ссылок на уже готовые реализации. Ими можно пользоваться для справки.

Biztonsági és elérhetőségi garanciák

Terveinket mindössze három olyan tulajdonsággal fogjuk modellezni, amelyekről úgy gondoljuk, hogy minimális garanciát nyújtanak az elosztott zárolás hatékony használatához.

  1. Biztonsági tulajdonság: Kölcsönös kizárás. Egy adott időpontban csak egy ügyfél tarthatja a zárat.
  2. Elérhetőség A tulajdonság: Nincs holtpont. Mindig lehetséges a zárolás beszerzése, még akkor is, ha az erőforrást zároló kliens meghibásodik, vagy egy másik lemezszegmensre kerül.
  3. Rendelkezésre állás B tulajdonság: Hibatűrés. Amíg a Redis csomópontok többsége fut, az ügyfelek beszerezhetnek és feloldhatnak zárolásokat.

Miért nem elég ebben az esetben a hibajavításon alapuló megvalósítás?
Ahhoz, hogy megértsük, mit fogunk javítani, elemezzük a legtöbb Redis-alapú elosztott zárolási könyvtár helyzetét.

Az erőforrások Redis segítségével történő zárolásának legegyszerűbb módja egy kulcs létrehozása a példányban. Jellemzően egy kulcs korlátozott élettartammal jön létre, ez a Redisben biztosított expires funkcióval érhető el, így előbb-utóbb ez a kulcs felszabadul (a listánk 2. tulajdonsága). Amikor az ügyfélnek fel kell szabadítania az erőforrást, törli a kulcsot.

Első ránézésre ez a megoldás egész jól működik, de van egy probléma: architektúránk egyetlen hibapontot hoz létre. Mi történik, ha a gazdagép Redis-példány meghibásodik? Akkor adjunk hozzá egy rabszolgát! És akkor használjuk, ha az előadó nem elérhető. Sajnos ez a lehetőség nem életképes. Ezzel nem tudjuk megfelelően megvalósítani azt a kölcsönös kizárási tulajdonságot, amelyre a biztonság érdekében szükségünk van, mivel a Redisben a replikáció aszinkron.

Nyilvánvaló, hogy egy ilyen modellben versenyfeltétel fordul elő:

  1. Az A kliens zárolást szerez a mesteren.
  2. A master meghibásodik, mielőtt a kulcsbevitel átkerülne a slave-hez.
  3. A követőt vezetővé léptetik elő.
  4. A B kliens ugyanazon az erőforráson szerez zárolást, amelyet A már zárolt. BIZTONSÁGI MEGSÉRTÉS!

Néha teljesen normális, hogy különleges körülmények között, például meghibásodás esetén sok ügyfél egyszerre tudja tartani a zárat. Ilyen esetekben replikáció alapú megoldás alkalmazható. Ellenkező esetben a cikkben leírt megoldást javasoljuk.

Helyes megvalósítás egyetlen példányban

Mielőtt megpróbálnánk kiküszöbölni a fent leírt egypéldányos konfiguráció hiányosságait, értsük meg, hogyan kell megfelelően kezelni ezt az egyszerű esetet, mivel ez a megoldás valójában olyan alkalmazásokban érvényes, ahol időről időre elfogadható a versenyfeltétel, valamint azért is, mert a blokkolás egy egyetlen példány szolgál alapul az itt leírt elosztott algoritmusban.

A zár beszerzéséhez tegye a következőket:

SET resource_name my_random_value NX PX 30000

Ez a parancs csak akkor telepít kulcsot, ha az még nem létezik (NX opció), érvényességi ideje 30000 XNUMX ezredmásodperc (PX opció). A kulcs a „myrandomvalue" Ennek az értéknek egyedinek kell lennie minden ügyfélnél és minden zárolási kérésnél.
Alapvetően véletlenszerű értéket használnak a zár biztonságos feloldására, a szkript azt mondja a Redisnek: csak akkor távolítsa el a kulcsot, ha az létezik, és a benne tárolt érték pontosan az elvárt érték. Ez a következő Lua szkripttel érhető el:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Ez azért fontos, hogy megakadályozza egy másik ügyfél által tartott zár eltávolítását. Előfordulhat például, hogy egy kliens beszerez egy zárat, majd egy olyan műveletben zárolódik, amely tovább tart, mint az első zárolás (hogy a kulcsnak legyen ideje lejárni), majd később eltávolítja a másik ügyfél által elhelyezett zárat.
Az egyszerű DEL használata nem biztonságos, mert egy kliens eltávolíthatja egy másik ügyfél által tartott zárolást. Ezzel szemben a fenti szkript használatakor minden zár „aláírásra” kerül egy véletlenszerű karakterlánccal, így azt csak az előzőleg elhelyező kliens tudja eltávolítani.

Mi legyen ez a véletlenszerű karakterlánc? Feltételezem, hogy 20 bájtnak kell lennie a /dev/urandomból, de találhatsz olcsóbb módszereket is, amelyekkel a karakterláncot kellően egyedivé teheted a céljaidhoz. Például jó lenne beindítani az RC4-et a /dev/urandom paranccsal, majd létrehozni belőle egy pszeudo-véletlen adatfolyamot. Egy egyszerűbb megoldás a mikroszekundumos felbontású unix idő és az ügyfélazonosító kombinációja; nem olyan biztonságos, de valószínűleg a legtöbb kontextusban megfelel a feladatnak.

Azt az időt, amelyet a kulcs élettartamának mérésére használunk, „zár élettartamának” nevezzük. Ez az érték a zárolás automatikus feloldása előtt eltelt idő és az az idő, ameddig az ügyfélnek befejeznie kell egy műveletet, mielőtt egy másik ügyfél zárolhatja az erőforrást anélkül, hogy ténylegesen megsértené a kölcsönös kizárási garanciákat. Ez a garancia csak egy bizonyos időtartamra korlátozódik, amely a zár megvásárlásának pillanatától kezdődik.

Tehát megbeszéltük a zár beszerzésének és feloldásának jó módját. A rendszer (ha egy nem elosztott rendszerről beszélünk, amely egyetlen és mindig elérhető példányból áll) biztonságos. Terjesszük ki ezt a fogalmat egy elosztott rendszerre, ahol nincs ilyen garanciánk.

Redlock algoritmus

Az algoritmus elosztott változata azt feltételezi, hogy N darab Redis-mesterünk van. Ezek a csomópontok teljesen függetlenek egymástól, így nem használunk replikációt vagy más implicit koordinációs rendszert. Már leírtuk, hogyan lehet biztonságosan megszerezni és feloldani a zárat egyetlen példányon. Magától értetődőnek tartjuk, hogy az algoritmus, ha egyetlen példánysal dolgozik, pontosan ezt a módszert fogja használni. Példáinkban az N-t 5-re állítjuk, ami ésszerű érték. Így 5 Redis mestert kell használnunk különböző számítógépeken vagy virtuális gépeken, hogy biztosítsuk, hogy ezek egymástól nagyrészt függetlenül működjenek.

A zár megszerzéséhez az ügyfél a következő műveleteket hajtja végre:

  1. Az aktuális időt ezredmásodpercben kapja meg.
  2. Sorozatosan próbál zárolást szerezni az összes N példányon, minden esetben ugyanazt a kulcsnevet és véletlenszerű értékeket használva. A 2. szakaszban, amikor az ügyfél példányonként beállít egy zárolást, az ügyfél olyan késleltetést használ, amely elég rövid ahhoz az időhöz képest, amely után a zárolás automatikusan feloldódik. Például, ha a blokkolás időtartama 10 másodperc, akkor a késleltetés ~5-50 ezredmásodperc tartományban lehet. Ez kiküszöböli azt a helyzetet, amikor a kliens hosszú ideig blokkolva maradhat egy meghibásodott Redis-csomópont elérésekor: ha a példány nem elérhető, akkor a lehető leghamarabb megpróbálunk csatlakozni egy másik példányhoz.
  3. A zároláshoz az ügyfél kiszámítja, hogy mennyi idő telt el; Ehhez levonja a tényleges időértékből az 1. lépésben kapott időbélyeget. Akkor és csak akkor, ha a kliens a legtöbb példányon (legalább 3 esetben) meg tudta szerezni a zárolást, valamint a teljes időtartamot A zárolás megszerzéséhez a zárolás időtartamánál rövidebb ideig a zárat megszerezettnek kell tekinteni.
  4. Ha zárolás történt, a zárolás időtartama az eredeti zárolási időtartam mínusz a 3. lépésben kiszámított eltelt idő.
  5. Ha az ügyfél valamilyen okból nem tudja megszerezni a zárolást (vagy nem tudott zárolni N/2+1 példányt, vagy a zárolás időtartama negatív volt), akkor megpróbálja feloldani az összes példányt (még azokat is, amelyeket úgy gondolt, hogy nem tudja blokkolni). ).

Aszinkron az algoritmus?

Ez az algoritmus azon a feltételezésen alapul, hogy bár nincs szinkronizált óra, amelyen az összes folyamat működne, a helyi idő az egyes folyamatokban továbbra is megközelítőleg azonos ütemben folyik, és a hiba kicsi ahhoz az időhöz képest, amely után a zárolás befejeződik. automatikusan felszabadul. Ez a feltevés nagyon hasonlít a hétköznapi számítógépekre jellemző helyzethez: minden számítógépnek van helyi órája, és általában azzal számolhatunk, hogy a különböző számítógépek között kicsi az időeltolódás.

Ezen a ponton alaposabban meg kell fogalmaznunk a kölcsönös kizárási szabályunkat: a kölcsönös kizárás csak akkor garantált, ha a zárat tartó ügyfél a zár érvényességi ideje alatt kilép (ezt az értéket kaptuk a 3. lépésben), mínusz még egy kis idő (összesen néhány ezredmásodperc a folyamatok közötti időkülönbség kompenzálására).

A következő érdekes cikk többet mond azokról a rendszerekről, amelyek megkövetelik az időintervallumok összehangolását: Bérletek: hatékony hibatűrő mechanizmus az elosztott fájl-gyorsítótár konzisztenciájához.

Próbálja újra sikertelenség esetén

Ha egy kliens nem szerez zárolást, véletlenszerű késleltetés után újra meg kell próbálnia; ez a több kliens szinkronizálásának megszüntetésére szolgál, akik egyidejűleg ugyanahhoz az erőforráshoz próbálnak zárolást szerezni (ami "megosztott agy" helyzethez vezethet, amelyben nincsenek nyertesek). Ezenkívül minél gyorsabban próbálja meg az ügyfél zárolást szerezni a Redis-példányok többségén, annál szűkebb az az ablak, amelyben felosztott agyi helyzet fordulhat elő (és annál kisebb az újrapróbálkozási igény). Ezért ideális esetben a kliensnek meg kell próbálnia SET parancsokat egyszerre N példánynak küldeni multiplexelés segítségével.

Itt érdemes hangsúlyozni, mennyire fontos azoknak az ügyfeleknek, akik a zárak többségét nem szerzik be, a (részben) beszerzett zárakat feloldják, hogy ne kelljen megvárniuk a kulcs lejártát, mielőtt az erőforráson lévő zárat újra megszerezhetik. (ha azonban a hálózat töredezett, és az ügyfél elveszíti a kapcsolatot a Redis-példányokkal, akkor rendelkezésre állási bírságot kell fizetnie, amíg a kulcs lejártára vár).

Oldja ki a zárat

A zárolás feloldása egy egyszerű művelet, amelyhez egyszerűen fel kell oldani az összes példányt, függetlenül attól, hogy az ügyfél sikeresen zárolt-e egy adott példányt.

Biztonsági szempontok

Biztonságos az algoritmus? Próbáljuk elképzelni, mi történik különböző forgatókönyvekben.

Kezdésként tegyük fel, hogy az ügyfél a legtöbb példányon zárolást tudott szerezni. Minden példány tartalmaz egy kulcsot, amelynek élettartama mindenki számára azonos. Azonban ezeknek a kulcsoknak a telepítése eltérő időpontban történt, így eltérő időpontban járnak le. De ha az első kulcs telepítése nem rosszabb időpontban, mint a T1 (az az időpont, amelyet az első szerverrel való kapcsolatfelvétel előtt választunk), és az utolsó kulcsot nem rosszabb időpontban telepítették, mint a T2 (a válasz beérkezésének időpontja) az utolsó szerverről), akkor biztosak vagyunk benne, hogy a készlet első kulcsa, amely lejár, legalább életben marad MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT. Az összes többi kulcs később lejár, így biztosak lehetünk benne, hogy minden kulcs egyidejűleg lesz érvényes legalább ennyi ideig.

Amíg a legtöbb kulcs érvényben marad, egy másik kliens nem tudja megszerezni a zárolást, mivel az N/2+1 SET NX műveletek nem sikeresek, ha már létezik N/2+1 kulcs. Ezért a zár megszerzése után lehetetlen ugyanabban a pillanatban újra megszerezni (ez sértené a kölcsönös kizáró tulajdonságot).
Szeretnénk azonban megbizonyosodni arról, hogy több kliens, aki egyszerre próbál zárolást szerezni, nem sikerülhet egyszerre.

Ha az ügyfél a példányok többségét körülbelül a maximális zárolási időtartamra vagy annál hosszabb ideig zárolta, a zárolást érvénytelennek tekinti, és feloldja a példányok zárolását. Ezért csak azt az esetet kell figyelembe vennünk, amikor az ügyfélnek a lejárati dátumnál rövidebb idő alatt sikerült letiltani a példányok többségét. Ebben az esetben a fenti érvelésre tekintettel az idő alatt MIN_VALIDITY egyetlen ügyfél sem tudja újra megszerezni a zárat. Ezért sok kliens csak akkor tud zárolni N/2+1 példányt ugyanabban az időben (ami a 2. szakasz végén ér véget), ha a legtöbb zárolás ideje hosszabb volt, mint a TTL idő, ami érvénytelenné teszi a zárolást.

Tud-e hivatalosan igazolni a biztonságot, jelezni a meglévő hasonló algoritmusokat, vagy találni hibát a fentiekben?

Hozzáférhetőségi szempontok

A rendszer rendelkezésre állása három fő jellemzőtől függ:

  1. Zárak automatikus feloldása (a kulcsok lejártakor): A kulcsok idővel újra elérhetők lesznek zárakhoz.
  2. Az a tény, hogy az ügyfelek általában segítik egymást a zárak eltávolításával, amikor a kívánt zárat nem szerezték be, vagy megszerezték és a munka befejeződött; így valószínű, hogy nem kell megvárnunk a kulcsok lejártát, hogy visszaszerezzük a zárat.
  3. Az a tény, hogy amikor az ügyfélnek újra meg kell próbálnia egy zárat, akkor viszonylag hosszabb ideig vár, mint a legtöbb zár megszerzéséhez szükséges időtartam. Ez csökkenti annak a valószínűségét, hogy az erőforrásokért való versengés során megosztott agyi helyzet alakuljon ki.

Van azonban egy rendelkezésre állási büntetés, amely megegyezik a hálózati szegmensek TTL-jével, így ha vannak összefüggő szegmensek, a büntetés határozatlan idejű lehet. Ez akkor fordul elő, amikor az ügyfél megszerez egy zárolást, majd egy másik szegmensre szakad, mielőtt feloldhatná.

Elvileg végtelen összefüggő hálózati szegmensek esetén egy rendszer végtelen ideig elérhetetlen maradhat.

Teljesítmény, feladatátvétel és fsync

Sokan azért használják a Redis-t, mert nagy teljesítményre van szükségük a zárolási szerver teljesítményére a zárolások megszerzéséhez és feloldásához szükséges késleltetés, valamint a másodpercenként végrehajtható beszerzések/kiadások száma tekintetében. Ennek a követelménynek a teljesítése érdekében létezik egy stratégia az N Redis-kiszolgálókkal való kommunikációra a késleltetés csökkentése érdekében. Ez egy multiplexelési stratégia (vagy "szegény ember multiplexelése", ahol a socket nem blokkoló módba kerül, elküldi az összes parancsot, és később olvassa be a parancsokat, feltételezve, hogy a kliens és az egyes példányok közötti oda-vissza út hasonló). .

Azonban a hosszú távú adattárolással járó megfontolásokat is figyelembe kell vennünk, ha olyan modellt akarunk létrehozni, amely megbízhatóan helyreáll a hibák után.

Alapvetően a probléma tisztázása érdekében tegyük fel, hogy a Redis-t hosszú távú adattárolás nélkül konfiguráljuk. Az ügyfélnek 3-ből 5-at sikerül blokkolnia. Az egyik példány, amelyet az ügyfélnek sikerült blokkolnia, újraindul, és ebben a pillanatban ismét 3 példány van ugyanahhoz az erőforráshoz, amit blokkolhatunk, egy másik kliens pedig blokkolhatja az újraindított példányt, megsértve a biztonsági tulajdonságot, a zárak kizárólagosságát feltételezi.

Ha engedélyezi az adatszolgáltatást (AOF), a helyzet némileg javulni fog. Például előléptethet egy szervert a SHUTDOWN parancs elküldésével és újraindításával. Mivel a lejárati műveletek a Redisben szemantikailag úgy vannak megvalósítva, hogy az idő akkor is folyjon, ha a kiszolgáló ki van kapcsolva, minden követelményünk megfelelő. Ez normális mindaddig, amíg a normál leállás biztosított. Mi a teendő áramkimaradás esetén? Ha a Redis alapból úgy van beállítva, hogy az fsync másodpercenként szinkronizál a lemezen, akkor előfordulhat, hogy újraindítás után nem lesz meg a kulcsunk. Elméletileg, ha garantálni akarjuk a zárolás biztonságát bármely példány újraindításakor, engedélyeznünk kell fsync=always a hosszú távú adattárolás beállításaiban. Ez teljesen megöli a teljesítményt, egészen a hagyományosan az elosztott zárolások biztonságos megvalósítására használt CP-rendszerek szintjéig.

De a helyzet jobb, mint amilyennek első pillantásra tűnik. Elvileg az algoritmus biztonsága megmarad, mert amikor a példányt hiba után újraindítják, már nem vesz részt az éppen aktív zárolásban.

Ennek biztosításához csak azt kell biztosítanunk, hogy a példány egy hiba után elérhetetlen maradjon az általunk használt maximális TTL-t kissé meghaladó ideig. Így megvárjuk a lejárati dátumot és az összes, a hiba pillanatában aktív kulcs automatikus felszabadítását.

A késleltetett újraindítások használatával elvileg a Redis hosszú távú fennmaradása nélkül is biztonságot lehet elérni. Ne feledje azonban, hogy ez pénzbírsággal járhat az akadálymentesítés megsértéséért. Például, ha a példányok többsége meghiúsul, a rendszer globálisan elérhetetlenné válik a TTL számára (és ezalatt az erőforrások nem blokkolhatók).

Növeljük az algoritmus elérhetőségét: kiterjesztjük a blokkolást

Ha az ügyfelek által végzett munka kis lépésekből áll, akkor csökkenthető az alapértelmezett zárolási időtartam, és bevezethető egy mechanizmus a zárak meghosszabbításához. Elvileg, ha a kliens a számítástechnikával van elfoglalva, és a zárolás lejárati értéke veszélyesen alacsony, akkor küldhet egy Lua-szkriptet minden példánynak, amely kiterjeszti a kulcs TTL-jét, ha a kulcs még mindig létezik, és az értéke továbbra is véletlenszerű érték, amelyet a zárat szereztek be.

A kliensnek csak akkor kell újra beszereznie a zárolást, ha a példányok többségét az érvényességi időn belül sikerült zárolnia.

Igaz, technikailag az algoritmus nem változik, így korlátozni kell a zárak megszerzésére irányuló ismételt kísérletek maximális számát, különben az akadálymentesítési tulajdonságok sérülnek.

Forrás: will.com

Hozzászólás