Distribuita ŝlosado uzante Redis

Hej Habr!

Hodiaŭ ni atentigas pri vi tradukon de kompleksa artikolo pri la efektivigo de distribuita ŝlosado uzante Redis kaj invitas vin paroli pri la perspektivoj de Redis kiel temo. Analizo de la Redlock-algoritmo koncerna de Martin Kleppmann, verkinto de la libro "Altaj Ŝarĝaj Aplikoj", donita tie.

Distribuita ŝlosado estas tre utila primitivo uzata en multaj medioj kie malsamaj procezoj devas labori sur komunaj resursoj en reciproke ekskluziva maniero.

Ekzistas kelkaj bibliotekoj kaj afiŝoj tie, kiuj priskribas kiel efektivigi DLM (Distributed Lock Manager) uzante Redis, sed ĉiu biblioteko prenas malsaman aliron kaj la garantioj, kiujn ili provizas, estas sufiĉe malfortaj kompare kun tio, kio estas atingebla kun iomete pli kompleksa dezajno.

En ĉi tiu artikolo, ni provos priskribi kondiĉan kanonan algoritmon, kiu montras kiel efektivigi distribuitan ŝlosadon uzante Redis. Ni parolos pri algoritmo nomata Redlock, ĝi efektivigas distribuitan serurmanaĝeron kaj, laŭ nia opinio, ĉi tiu algoritmo estas pli sekura ol la kutima unuokaza aliro. Ni esperas, ke la komunumo analizos ĝin, donos komentojn kaj uzos ĝin kiel deirpunkton por pli kompleksaj aŭ alternativaj projektoj.

Efektivigo

Antaŭ ol pluiri al la priskribo de la algoritmo, ni provizas plurajn ligilojn al pretaj efektivigoj. Ili povas esti uzataj por referenco.

Sekureco kaj Havebleco Garantioj

Ni modeligos nian dezajnon kun nur tri propraĵoj, kiujn ni opinias, ke ni donas la minimumajn garantiojn necesajn por efike uzi distribuitan ŝlosadon.

  1. Sekurecposedaĵo: Reciproka ekskludo. En ajna momento, nur unu kliento povas teni la seruron.
  2. Havebleco Propraĵo A: Neniu blokiĝo. Ĉiam eblas eventuale akiri seruron, eĉ se la kliento, kiu ŝlosis la rimedon malsukcesas aŭ alteriĝas sur malsama disko-segmento.
  3. Havebleco Propraĵo B: Faŭltoleremo. Dum la plimulto de Redis-nodoj funkcias, klientoj povas akiri kaj liberigi serurojn.

Kial efektivigo bazita sur malsukcesa reakiro ne sufiĉas en ĉi tiu kazo
Por kompreni, kion ni plibonigos, ni analizu la nunan staton de la plej multaj distribuitaj ŝlosaj bibliotekoj bazitaj sur Redis.

La plej simpla maniero ŝlosi rimedon uzante Redis estas krei ŝlosilon en la kazo. Tipe, ŝlosilo estas kreita kun limigita vivdaŭro, ĉi tio estas atingita uzante la eksvalidiĝan funkcion provizitan en Redis, do baldaŭ aŭ malfrue ĉi tiu ŝlosilo estas liberigita (posedaĵo 2 en nia listo). Kiam la kliento bezonas liberigi la rimedon, ĝi forigas la ŝlosilon.

Unuavide, ĉi tiu solvo funkcias sufiĉe bone, sed estas problemo: nia arkitekturo kreas ununuran punkton de fiasko. Kio okazas se la gastiganto Redis-instanco malsukcesas? Ni do aldonu sklavon! Kaj ni uzos ĝin se la prezentisto ne estas disponebla. Bedaŭrinde, ĉi tiu opcio ne estas realigebla. Farante tion, ni ne povos ĝuste efektivigi la reciprokan ekskludan posedaĵon, kiun ni bezonas por certigi sekurecon, ĉar reproduktado en Redis estas nesinkrona.

Evidente, en tia modelo okazas raskondiĉo:

  1. Kliento A akiras seruron sur la majstro.
  2. La majstro malsukcesas antaŭ ol la ŝlosila eniro estas transdonita al la sklavo.
  3. La sekvanto estas promociita al gvidanto.
  4. Kliento B akiras seruron sur la sama rimedo kiun A jam ŝlosis. SEKURECO-Malobservo!

Kelkfoje estas tute normale, ke en specialaj cirkonstancoj, kiel fiasko, multaj klientoj povas teni la seruron samtempe. En tiaj kazoj, reproduktad-bazita solvo povas esti aplikita. Alie, ni rekomendas la solvon priskribitan en ĉi tiu artikolo.

Ĝusta efektivigo kun ununura kazo

Antaŭ ol provi venki la mankojn de la unuopa agordo priskribita supre, ni komprenu kiel ĝuste trakti ĉi tiun simplan kazon, ĉar ĉi tiu solvo efektive validas en aplikoj kie raskondiĉo estas akceptebla de tempo al tempo, kaj ankaŭ ĉar blokado sur ununura kazo funkcias kiel la bazo kiu estas uzita en la distribuita algoritmo priskribita ĉi tie.

Por akiri seruron, faru ĉi tion:

SET resource_name my_random_value NX PX 30000

Ĉi tiu komando instalas ŝlosilon nur se ĝi ne jam ekzistas (opcio NX), kun valideca periodo de 30000 milisekundoj (opcio PX). La ŝlosilo estas agordita al "myrandomvalue" Ĉi tiu valoro devas esti unika tra ĉiuj klientoj kaj ĉiuj ŝlosilaj petoj.
Esence, hazarda valoro estas uzata por sekure liberigi la seruron, kun skripto diranta al Redis: nur forigu la ŝlosilon se ĝi ekzistas kaj la valoro konservita en ĝi estas ĝuste tia, kio estis atendita. Ĉi tio estas atingita per la sekva Lua-skripto:

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

Ĉi tio gravas por eviti forigon de seruro tenita de alia kliento. Ekzemple, kliento povus akiri seruron, tiam iĝi ŝlosita en iu operacio kiu daŭras pli longe ol la unua seruro (tiel ke la ŝlosilo havas tempon eksvalidiĝi), kaj poste forigi la seruron kiun iu alia kliento metis.
Uzi simplan DEL estas nesekura ĉar kliento povas forigi seruron tenitan de alia kliento. Kontraste, kiam oni uzas la supran skripton, ĉiu seruro estas "subskribita" per hazarda ŝnuro, do nur la kliento, kiu antaŭe metis ĝin, povas forigi ĝin.

Kio devus esti ĉi tiu hazarda ŝnuro? Mi supozas, ke ĝi devus esti 20 bajtoj de /dev/urandom, sed vi povas trovi malpli multekostajn manierojn fari la ĉenon sufiĉe unika por viaj celoj. Ekzemple, estus bone semi RC4 per /dev/urandom kaj poste generi pseŭdo-hazardan fluon el ĝi. Pli simpla solvo implikas kombinaĵon de uniksa tempo en mikrosekunda rezolucio kaj plie la klientidentigilo; ĝi ne estas tiel sekura, sed verŝajne ĝi estas laŭ la tasko en la plej multaj kuntekstoj.

La tempo, kiun ni uzas kiel mezurilon de la vivdaŭro de la ŝlosilo, estas nomata "ŝlosila vivdaŭro". Ĉi tiu valoro estas kaj la kvanto de tempo antaŭ ol la seruro estas aŭtomate liberigita kaj la kvanto de tempo kiun kliento devas kompletigi operacion antaŭ ol alia kliento povas en victurno ŝlosi tiun rimedon, sen fakte malobservi reciprokajn ekskludajn garantiojn. Ĉi tiu garantio estas limigita nur al certa tempoperiodo, kiu komenciĝas de la momento, kiam la seruro estas aĉetita.

Do ni diskutis pri bona maniero akiri kaj liberigi seruron. La sistemo (se ni parolas pri ne-distribuita sistemo konsistanta el ununura kaj ĉiam havebla okazo) estas sekura. Ni etendi ĉi tiun koncepton al distribuita sistemo, kie ni ne havas tiajn garantiojn.

Redlock-algoritmo

La distribuita versio de la algoritmo supozas, ke ni havas N Redis-mastrojn. Ĉi tiuj nodoj estas tute sendependaj unu de la alia, do ni ne uzas reproduktadon aŭ ajnan alian implican kunordigan sistemon. Ni jam priskribis kiel sekure akiri kaj liberigi seruron sur ununura kazo. Ni prenas ĝin por koncedite, ke la algoritmo, kiam oni laboras kun ununura kazo, uzos ĝuste ĉi tiun metodon. En niaj ekzemploj ni starigas N al 5, kio estas racia valoro. Tiel, ni devos uzi 5 Redis-mastrojn en malsamaj komputiloj aŭ virtualaj maŝinoj por certigi, ke ili agas plejparte sendepende unu de la alia.

Por akiri seruron, la kliento faras la jenajn operaciojn:

  1. Akiras la nunan tempon en milisekundoj.
  2. Sinsekve provas akiri seruron en ĉiuj N okazoj, uzante la saman ŝlosilnomon kaj hazardajn valorojn en ĉiuj kazoj. En Ŝtupo 2, kiam la kliento starigas seruron po-okastan, la kliento uzas prokraston por akiri ĝin, kiu estas sufiĉe mallonga kompare kun la tempo post kiu la seruro estas aŭtomate liberigita. Ekzemple, se la blokado daŭro estas 10 sekundoj, tiam la prokrasto povus esti en la intervalo de ~5-50 milisekundoj. Ĉi tio forigas la situacion en kiu la kliento povus resti blokita dum longa tempo provante atingi malsukcesan Redis-nodon: se la petskribo estas neatingebla, tiam ni provas konektiĝi al alia petskribo kiel eble plej baldaŭ.
  3. Por preni seruron, la kliento kalkulas kiom da tempo pasis; Por fari tion, ĝi subtrahas de la reala tempovaloro la tempomarkon kiu estis akirita en la paŝo 1. Se kaj nur se la kliento povis akiri la seruron ĉe la plimulto de okazoj (almenaŭ 3), kaj la totalan tempon ĝi bezonis por fari tion. akiri la seruro, malpli ol la seruro daŭro, la seruro estas konsiderita esti akirita.
  4. Se seruro estis akirita, la serurdaŭro estas konsiderata kiel la origina serurdaŭro minus la pasinta tempo kalkulita en paŝo 3.
  5. Se la kliento ne sukcesas akiri la seruron ial (aŭ ĝi ne povis ŝlosi N/2+1 okazojn, aŭ la seruro daŭro estis negativa), tiam ĝi provos malŝlosi ĉiujn okazojn (eĉ tiujn kiujn ĝi pensis ne povis bloki). ).

Ĉu la algoritmo estas nesinkrona?

Ĉi tiu algoritmo baziĝas sur la supozo ke, kvankam ekzistas neniu sinkronigita horloĝo sur kiu ĉiuj procezoj funkcius, loka tempo en ĉiu procezo daŭre fluas je proksimume la sama rapideco, kaj la eraro estas malgranda kompare kun la tuta tempo post kiu la seruro estas. aŭtomate liberigita. Tiu ĉi supozo estas tre simila al la situacio tipa por ordinaraj komputiloj: ĉiu komputilo havas lokan horloĝon, kaj oni povas kutime kalkuli je tio, ke la tempodiferenco inter malsamaj komputiloj estas malgranda.

Je ĉi tiu punkto, ni devas pli zorge formuli nian reciprokan ekskludon regulon: reciproka ekskludo estas garantiita nur se la kliento tenanta la seruron eliras dum la tempo la seruro estas valida (ĉi tiu valoro akirita en la paŝo 3), minus iom pli da tempo (entute kelkaj milisekundoj por kompensi la tempodiferencon inter procezoj).

La sekva interesa artikolo rakontas pli pri tiaj sistemoj, kiuj postulas kunordigon de tempintervaloj: Luadoj: efika mistolerema mekanismo por distribuita dosierkaŝkonsekvenco.

Reprovu ĉe malsukceso

Kiam kliento ne sukcesas akiri seruron, ĝi devas provi denove post hazarda prokrasto; ĉi tio estas farita por malsinkronigi plurajn klientojn provantajn akiri seruron sur la sama rimedo samtempe (kiu povas konduki al "split-cerba" situacio en kiu ekzistas neniuj gajnintoj). Plie, ju pli rapide kliento provas akiri seruron sur plimulto de Redis-kazoj, des pli mallarĝa la fenestro en kiu povas okazi disig-cerba situacio (kaj des malpli la bezono de reprovoj). Tial, ideale, la kliento devus provi sendi SET-komandojn al N okazoj samtempe uzante multipleksadon.

Indas substreki ĉi tie kiom gravas por klientoj, kiuj ne sukcesas akiri la plimulton de seruroj, liberigi la (parte) akiritajn serurojn, por ke ili ne devu atendi ke la ŝlosilo eksvalidiĝos antaŭ ol la seruro sur la rimedo povas esti akirita denove. (kvankam se reta fragmentiĝo okazas, kaj la kliento perdas kontakton kun la Redis-instancoj, tiam vi devas pagi haveblecan punon atendante ke la ŝlosilo eksvalidiĝos).

Liberigu la seruron

Liberigi ŝlosilon estas simpla operacio, kiu simple postulas malŝlosi ĉiujn okazojn, sendepende de ĉu la kliento ŝajnas esti sukcese ŝlosinta apartan okazon.

Sekurecaj Konsideroj

Ĉu la algoritmo estas sekura? Ni provu imagi, kio okazas en malsamaj scenaroj.

Komence, ni supozu, ke la kliento povis akiri seruron ĉe la plimulto de okazoj. Ĉiu okazo enhavos ŝlosilon kun la sama vivdaŭro por ĉiuj. Tamen, ĉiu el ĉi tiuj ŝlosiloj estis instalita en malsama tempo, do ili eksvalidiĝos en malsamaj tempoj. Sed, se la unua ŝlosilo estis instalita samtempe ne pli malbona ol T1 (la tempo, kiun ni elektas antaŭ kontakti la unuan servilon), kaj la lasta ŝlosilo estis instalita samtempe ne pli malbona ol T2 (la tempo, kiam la respondo estis ricevita). de la lasta servilo), tiam ni certas, ke la unua ŝlosilo en la aro, kiu eksvalidiĝas, pluvivos almenaŭ MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT. Ĉiuj aliaj ŝlosiloj eksvalidiĝos poste, do ni povas esti certaj, ke ĉiuj ŝlosiloj estos samtempe validaj por almenaŭ ĉi tiu tempo.

Dum la tempo, kiam la plej multaj ŝlosiloj restas validaj, alia kliento ne povos akiri la seruron, ĉar N/2+1 SET NX-operacioj ne povas sukcesi se N/2+1 ŝlosiloj jam ekzistas. Tial, post kiam seruro estis akirita, estas neeble akiri ĝin denove en la sama momento (ĉi tio malobservus la reciprokan ekskludan posedaĵon).
Tamen ni volas certigi, ke pluraj klientoj, kiuj provas akiri seruron samtempe, ne povas sukcesi samtempe.

Se la kliento ŝlosis la plimulton de okazoj por proksimume aŭ pli ol la maksimuma seruro daŭro, ĝi konsideros la seruron nevalida kaj malŝlosos la okazojn. Sekve, ni nur devas konsideri la kazon, en kiu la kliento sukcesis bloki la plimulton da okazoj en malpli da tempo ol la limdato. En ĉi tiu kazo, koncerne la supran argumenton, dum la tempo MIN_VALIDITY neniu kliento devus povi reakiri la seruron. Tial, multaj klientoj povos ŝlosi N/2+1 okazojn en la sama tempo (kiu finiĝas ĉe la fino de la etapo 2) nur kiam la tempo por ŝlosi la plimulton estis pli granda ol la TTL-tempo, kio malvalidigas la seruron.

Ĉu vi povas doni formalan pruvon de sekureco, indiki ekzistantajn similajn algoritmojn aŭ trovi cimon en ĉi-supra?

Konsideroj pri alirebleco

Sistemo havebleco dependas de tri ĉefaj karakterizaĵoj:

  1. Aŭtomate liberigu serurojn (kiel ŝlosiloj eksvalidiĝas): Ŝlosiloj poste estos disponeblaj denove por esti uzataj por seruroj.
  2. La fakto, ke klientoj kutime helpas unu la alian forigante serurojn kiam la dezirata seruro ne estis akirita, aŭ estis akirita kaj la laboro finiĝis; do verŝajne ni ne devos atendi ke la ŝlosiloj eksvalidiĝos por reakiri la seruron.
  3. La fakto, ke kiam kliento devas reprovi akiri seruron, ĝi atendas kompare pli longan tempon ol la periodo necesa por akiri la plej multajn serurojn. Ĉi tio reduktas la verŝajnecon de disig-cerba situacio aperanta dum konkurado pri resursoj.

Tamen, ekzistas havebleca puno egala al la TTL de la retsegmentoj, do se ekzistas apudaj segmentoj, la puno povas esti nedifinita. Ĉi tio okazas kiam ajn kliento akiras seruron kaj tiam ŝiras al alia segmento antaŭ ol ĝi povas liberigi ĝin.

Principe, donitaj senfinaj apudaj retsegmentoj, sistemo povas resti neatingebla por senfina tempodaŭro.

Efikeco, malfunkciigo kaj fsync

Multaj homoj uzas Redis ĉar ili bezonas altan serurservilan agadon laŭ la latenteco necesa por akiri kaj liberigi serurojn, kaj la nombron da akiroj/eldonoj kiuj povas esti kompletigitaj sekundo. Por plenumi ĉi tiun postulon, ekzistas strategio por komuniki kun N Redis-serviloj por redukti latencian. Tio estas multipleksadstrategio (aŭ "multiplexado de malriĉulo", kie la ingo estas metita en ne-blokan reĝimon, sendas ĉiujn komandojn, kaj legas la komandojn poste, supozante ke la rondvetura tempo inter la kliento kaj ĉiu kazo estas simila) .

Tamen, ni ankaŭ devas konsideri la konsideron asociitan kun longdaŭra konservado de datumoj, se ni strebas krei modelon kun fidinda reakiro de fiaskoj.

Esence, por klarigi la aferon, ni supozu, ke ni agordas Redis tute sen longtempa konservado de datumoj. La kliento sukcesas bloki 3 el 5 okazoj. Unu el la okazoj, kiujn la kliento sukcesis bloki, estas rekomencita, kaj ĉi-momente ekzistas 3 okazoj denove por la sama rimedo, kiun ni povas bloki, kaj alia kliento povas, siavice, bloki la rekomencitan kazon, malobservante la sekurecan posedaĵon, kiu supozas ekskluzivecon de seruroj.

Se vi ebligas datumojn antaŭen (AOF), la situacio iomete pliboniĝos. Ekzemple, vi povas promocii servilon sendante la SHUTDOWN komandon kaj rekomencante ĝin. Ĉar eksvalidiĝaj operacioj en Redis estas semantike efektivigitaj tiel, ke tempo daŭre fluas eĉ kiam la servilo estas malŝaltita, ĉiuj niaj postuloj estas bone. Ĉi tio estas normala kondiĉe ke normala ĉesigo estas certigita. Kion fari en kazo de elektropaneo? Se Redis estas agordita defaŭlte, kun fsync sinkroniganta sur disko ĉiun sekundon, tiam eblas, ke post rekomenco ni ne havos nian ŝlosilon. Teorie, se ni volas garantii ŝlosan sekurecon dum iu ajn rekomenco, ni devus ebligi fsync=always en la agordoj por longtempa konservado de datumoj. Ĉi tio tute mortigos rendimenton, ĝis la nivelo de CP-sistemoj, kiuj estas tradicie uzataj por sekure efektivigi distribuitajn serurojn.

Sed la situacio estas pli bona ol ŝajnas unuavide. Principe, la sekureco de la algoritmo estas konservita ĉar kiam la petskribo estas rekomencita post fiasko, ĝi ne plu partoprenas en ajna seruro kiu estas nuntempe aktiva.

Por certigi tion, ni nur bezonas certigi, ke post malsukceso la kazo restas neatingebla dum tempodaŭro iomete superanta la maksimuman TTL kiun ni uzas. Tiel ni atendos ĝis la limdato kaj aŭtomata liberigo de ĉiuj ŝlosiloj, kiuj estis aktivaj ĉe la tempo de fiasko.

Uzante prokrastajn rekomencojn, principe eblas atingi sekurecon eĉ sen iu longdaŭra persisto en Redis. Notu, tamen, ke tio povas rezultigi monpunon pro malobservo de alirebleco. Ekzemple, se la plimulto de kazoj malsukcesos, la sistemo fariĝos tutmonde neatingebla por la TTL (kaj neniu rimedo povas esti blokita dum ĉi tiu tempo).

Ni pliigas la haveblecon de la algoritmo: ni etendas la blokadon

Se la laboro farita de klientoj konsistas el malgrandaj paŝoj, eblas redukti la defaŭltan serurdaŭron kaj efektivigi mekanismon por etendi serurojn. Principe, se la kliento estas okupata pri komputado kaj la valoro de eksvalidiĝo de la seruro estas danĝere malalta, vi povas sendi Lua-skripton al ĉiuj okazoj, kiuj etendas la TTL de la ŝlosilo, se la ŝlosilo ankoraŭ ekzistas kaj ĝia valoro ankoraŭ estas hazarda valoro akirita kiam la ŝlosilo. seruro estis akirita.

Kliento nur konsideru seruron reakirita se ĝi sukcesis ŝlosi la plimulton de kazoj ene de la validecperiodo.

Vere, teknike la algoritmo ne ŝanĝas, do la maksimuma nombro da ripetaj provoj akiri serurojn devas esti limigita, alie la alireblecoj estos malobservitaj.

fonto: www.habr.com

Aldoni komenton