Tere, Habr!
TĂ€na pakume teie tĂ€helepanu keerulise artikli tĂ”lget jaotatud lukustuste rakendamisest Redis abil ning arutame Redis'e perspektiivikust teemana. AnalĂŒĂŒsime Redlock algoritmi Martin Kleppmann'i poolt, kes on raamatu autor "", on esitatud .
Jaotatud lukustused on vÀga kasulik primitiiv, mida kasutatakse paljudes keskkondades, kus erinevad protsessid peavad töötama jagatud ressursside kallal vastavalt vastastikuse vÀlistamise pÔhimÔttele.
On olemas mitmeid teeke ja postitusi, mis kirjeldavad, kuidas rakendada DLM-i (jaotatud lukustuste haldurit) Redis abil, kuid igas teegis kasutatakse oma lĂ€henemist ning antud garantiid on vĂ”rreldes keerukama projekteerimisega ĂŒsna nĂ”rgad.
Selles artiklis pĂŒĂŒame kirjeldada tinglikult kanonilist algoritmi, mis demonstreerib, kuidas rakendada jaotatud lukustusi Redis'i abil. RÀÀgime algoritmist nimega Redlock, mis rakendab jaotatud lukustuste haldurit ning meie arvates on see algoritm turvalisem kui traditsiooniline lĂ€henemine, kus on ainult ĂŒks instants. Loodame, et kogukond analĂŒĂŒsib seda, annab tagasisidet ja hakkab seda kasutama keerukamate vĂ”i alternatiivsete projektide teostamise lĂ€htepunktina.
Rakendused
Enne algoritmi kirjelduse juurde minekut toome vÀlja mÔned lingid juba valminud rakendustele. Neid saab kasutada viitamiseks.
- (Ruby rakendus). Samuti on olemas Redlock-rb, lisades paketi (gem) mugavuse jaoks ja mitte ainult selleks.
- (Python rakendus).
- (Asyncio Python rakendus).
- (PHP rakendus).
- (veel ĂŒks PHP rakendus)
- (PHP teek lukustuste jaoks)
- (Go rakendus).
- (Java rakendus).
- (Perl rakendus).
- (C++ rakendus).
- (C#/.NET rakendus).
- (C#/.NET rakendus). Toetab asĂŒnkroonset ja lukustamise laiendust.
- (C# .NET rakendus konfigureeritava andmete salvestusega)
- (C# .NET rakendus)
- (NodeJS rakendus). Sisaldab lukustuste pikendamise tuge.
Turvalisuse ja kÀttesaadavuse garantiid
Me kavatseme modelleerida meie projekti ainult kolme omaduse abil, mis meie arvates pakuvad vÀhemalt minimaalset garantiid, mis on vajalikud jaotatud lukustuste tÔhusaks kasutamiseks.
- Turvaomadus: Vastastikune vĂ€listamine. Igal hetkel vĂ”ib lukustust hoida vaid ĂŒks klient.
- KÀttesaadavuse omadus A: Vastastikuste lukustuste puudumine. LÔppkokkuvÔttes on alati vÔimalik lukustus saada, isegi kui ressurssi lukustav klient keeldub vÔi satub teisele kettasegmentile.
- KÀttesaadavuse omadus B: Rikete taluvus. Niikaua kui enamus Redis'e sÔlmedest töötab, saavad kliendid lukustusi omandada ja vabastada.
Miks on taastumisele keskenduvad rakendused antud juhul ebapiisavad
Kuna mÔistame, mida me soovime parandada, vaatame hetkeolukorda, mis enamiku Redis'e pÔhiste jaotatud lukustuste raamatukogude puhul on tekkinud.
Lihtsaim viis ressurssi Redis'e abil lukustada on luua key instantsis. Ăldiselt luuakse key piiratud elueaga, mida saavutatakse Redis'e vĂ”imalusega expires, seega varem vĂ”i hiljem vabastatakse see key (omadus 2 meie nimekirjas). Kui kliendil on vajalik ressurss vabastada, delete'ib ta key.
Esmapilgul töötab see lahendus kĂŒllalt hĂ€sti, kuid on probleem: meie arhitektuuris tekib ĂŒhtne tĂ”rkepunkt. Mis juhtub, kui pÀÀstiku Redis'e instants ebaĂ”nnestub? Lisame alluva! Ja kasutame seda, kui pÀÀstik on mitteaktiivne. Kahjuks on see vĂ”imalus elujĂ”uetu. Nii tehes ei suuda me Ă”igesti rakendada vastastikuse vĂ€listamise omadust, mis on turvalisuse tagamiseks vajalik, kuna replikeerimine Redis's on asĂŒnkroonsete.
Ilmselgelt tekib sellises mudelis vÔidujooksu olek:
- Klient A omandab lukustuse pÀÀstikus.
- PÀÀstik ebaÔnnestub, enne kui kirje key'sse kantakse alluvale.
- Alluv tÔuseb pÀÀstikuks.
- Klient B omandab lukustuse samale ressursele, mis on juba A poolt lukustatud. TURVALISUSE RIKKUMINE!
MÔnikord on tÀiesti normaalne, et erilistes olukordades, nÀiteks rikke puhul, vÔivad mitmed kliendid korraga lukustuse hoida. Sellistel juhtudel saab rakendada replikatsioonipÔhist lahendust. Teistes olukordades soovitame artiklis kirjeldatud lahendust.
Ăige teostus ĂŒhe instantsiga
Enne kui proovime ĂŒletada ĂŒheaastase konfiguratsiooni puudusi, mis on eespool kirjeldatud, vaatame, kuidas Ă”igesti tegutseda selles lihtsas olukorras, kuna selline lahendus on tegelikult lubatav rakendustes, kus vĂ”idusĂ”idud on aeg-ajalt lubatud, samuti seetĂ”ttu, et ĂŒhekordne lukustus teenib aluseks, mida kasutatakse siin kirjeldatud jaotatud algoritmis.
Lukustuse saamiseks teeme jÀrgmist:
SET resource_name my_random_value NX PX 30000
See kĂ€sk seadistab vĂ”tme ainult siis, kui see ei eksisteeri (valik NX), kehtivusajaga 30000 millisekundit (valik PX). VĂ”tmele mÀÀratakse vÀÀrtus âmyrandomvalueâ. See vÀÀrtus peab olema ainulaadne kĂ”igi klientide ja lukustuse taotluste seas.
PÔhimÔtteliselt kasutatakse juhuslikku vÀÀrtust lukustuse ohutuks vabastamiseks skripti abil, mis teatab Redis'ile: kustuta vÔti, ainult kui see eksisteerib, ja vÀÀrtus, mis seal on, on see, mida oodati. See saavutatakse jÀrgmise Lua skripti abil:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
endSee on oluline, et vÀltida lukustuse eemaldamist, mille on teinud teine klient. NÀiteks vÔib klient omada lukustust ja seejÀrel lukustuda operatsiooni kÀigus, mis kestab kauem kui esimese lukustuse kehtivusaeg (n nii, et vÔtme aegumise aeg on möödas), ja hiljem eemaldada lukustuse, mille pani mÔni teine klient.
Lihtsa DEL kasutamine ei ole ohutu, kuna klient vĂ”ib eemaldada lukustuse, mille on seadnud teine klient. Vastupidi, kasutades ĂŒlaltoodud skripti, on iga lukustus âallkirjastatudâ juhusliku stringiga, seega suudab selle eemaldada vaid see klient, kes selle varem seadistatud lukustuse kehtestanud.
Mis peab see juhuslik tekst olema? Arvan, et see peaks olema 20 baiti /dev/urandom'ist, kuid on olemas ka odavamaid viise, et luua tekst, mis on piisavalt ainulaadne teie esitatud eesmĂ€rkide jaoks. NĂ€iteks vĂ”ib olla sobiv alustada RC4'ga /dev/urandom'ist ja seejĂ€rel luua selle pĂ”hjal pseudojuhuslik voog. Lihtsam lahendus on kombineerida unix-i aega mikroskogustes pluss kliendi ID; see ei ole nii turvaline, kuid tĂ”enĂ€oliselt vastab see ĂŒlesannete tasemele enamikes kontekstides.
Aeg, mida kasutame vĂ”tme eluea nĂ€itajana, nimetatakse âlukustamise kehtivuse ajaksâ. See vÀÀrtus on samaaegselt periood, mille möödumisel lukustus automaatselt vabastatakse, ja aeg, mille jooksul kliendil on vĂ”imalus operatsioon lĂ€bi viia, enne kui teine klient saab omakorda selle ressursi lukustada, rikkuvat aga tegelikult vastastikuse vĂ€listamise garantiisid. Selline garantii on piiratud ainult kindla ajavahemikuga, mis algab lukustuse omandamise hetkest.
Nii siis, oleme arutanud head viisi lukustuse omandamiseks ja vabastamiseks. SĂŒsteem (kui rÀÀgime mittejaotatud sĂŒsteemist, mis koosneb ainsast ja alati kergesti ligipÀÀsetavast instantsist) on turvaline. Laiendame seda kontseptsiooni jaotatud sĂŒsteemile, kus selliseid garantiisid meil ei ole.
Redlock'i algoritm
Jaotatud versioonis algoritmist eeldatakse, et meil on N juhtivat Redis'i. Need sĂ”lmed on tĂ€ielikult sĂ”ltumatud ĂŒksteisest, seega ei kasuta me replikatsiooni ega ĂŒhtegi muud kaudset koordineerimissĂŒsteemi. Oleme juba rÀÀkinud, kuidas turvaliselt omandada ja vabastada lukustust ĂŒhes ainsas instantsis. Eeldame, et algoritm ĂŒhes instantsis töötab just selle meetodiga. Meie nĂ€idetes seadistame N-ks 5, mis on tĂ€iesti mĂ”istlik vÀÀrtus. Seega, peame kasutama 5 juhtivat Redis'i erinevates arvutites vĂ”i virtuaalmasinates, et tagada nende peamine sĂ”ltumatus ĂŒksteisest.
Lukustuse omandamiseks peab klient teostama jÀrgmised toimingud:
- Saama praeguse aja millisekundites.
- JÀrjestelmÀllisesti yritetÀÀn hankkia lukitus kaikilta N-instanssilta kÀyttÀen kaikissa tapauksissa samaa avaimen nimeÀ ja satunnaisia arvoja. Vaiheessa 2, asettaen lukituksen jokaiselle instanssille, asiakas kÀyttÀÀ viivettÀ, joka on riittÀvÀn lyhyt verrattuna aikaan, jonka kuluttua lukitus poistuu automaattisesti. Esimerkiksi, jos lukituksen kesto on 10 sekuntia, viive voi olla alueella ~ 5-50 millisekuntia. TÀllÀ tavoin vÀltetÀÀn tilanne, jossa asiakas voisi jÀÀdÀ pitkÀksi aikaa lukituksi yrittÀessÀÀn ottaa yhteyttÀ epÀonnistuneeseen Redis-solmuun: jos instanssi ei ole kÀytettÀvissÀ, yritÀmme mahdollisimman pian yhdistÀÀ toiseen instanssiin.
- Lukituksen ottamiseksi asiakas laskee, kuinka kauan aikaa on kulunut; hÀn vÀhentÀÀ nykyisestÀ aikavÀlistÀ sen aikaleiman, joka saatiin vaiheessa 1. Vasta silloin, kun asiakas on onnistunut saamaan lukituksen useimmista instansseista (vÀhintÀÀn 3), ja kokonaisaika, joka tarvittiin lukituksen saamiseen, on lyhyempi kuin lukituksen voimassaoloaika, katsotaan, ettÀ lukituksen saaminen on onnistunut.
- Jos lukitus saatiin, lukituksen voimassaoloaikaa lasketaan alkuperÀisestÀ lukituksen kestosta vÀhentÀmÀllÀ vaiheessa 3 laskettu kulunut aika.
- Jos asiakas ei jostain syystÀ onnistunut saamaan lukitusta (tai hÀn ei saanut lukittua N/2+1 instanssia, tai lukituksen voimassaoloaika oli negatiivinen), hÀn yrittÀÀ avata lukituksen kaikilta instansseilta (myös niiltÀ, joita hÀn luuli, ettei hÀn voinut lukita).
Onko algoritmi asynkroninen?
TÀmÀ algoritmi perustuu oletukseen, ettÀ vaikka ei ole synkronoituja kelloja, joiden mukaan kaikki prosessit toimisivat, jokaisen prosessin paikallinen aika kulkee silti suurin piirtein samaa tahtia, ja virhe on pieni verrattuna kokonaisaikaan, jonka kuluttua lukitus poistuu automaattisesti. TÀmÀ oletus muistuttaa paljon tilannetta, joka on tyypillinen tavallisille tietokoneille: jokaisessa tietokoneessa on paikalliset kellot, ja yleensÀ voimme laskea, ettÀ aikahÀiriö eri tietokoneilla on pieni.
Selle etapi juures peame selgemalt sÔnastama meie vastastikuse vÀlistamise reegli: vastastikune vÀlistamine on garanteeritud ainult siis, kui klient, kes hoiab lukku, lÔpetab oma tegevuse ajavahemikus, mil lukustus on kehtiv (see vÀÀrtus on saadud 3. sammust), miinus veel mÔni hetk (mitu millisekundit, et kompenseerida protsesside vahelisi ajavahemikke).
Rohkem sarnastest sĂŒsteemidest, mis nĂ”uavad ajavahemike kooskĂ”lastamist, rÀÀgib jĂ€rgmine huvitav artikkel: .
Uuesti proovimine peale ebaÔnnestumist
Kui kliendil ei Ă”nnestu lukku saada, peab ta proovima seda uuesti, tehes juhusliku viivituse; seda tehakse, et desĂŒnkroniseerida mitmed kliendid, kes ĂŒritavad sama ressursi lukku samaaegselt osta (mis vĂ”ib viia olukorra «jagatud aju» tekkimiseni, kus vĂ”itjaid ei ole). Lisaks, mida kiiremini klient ĂŒritab enamikku Redis'i instantsidest lukku saada, seda kitsam on aken, kus «jagatud aju» olukord vĂ”ib tekkida (ja seda vĂ€hem on uuesti proovimise vajadust). Seega peaks klient ideaaljuhul ĂŒritama samal ajal saata SET kĂ€sklusi N instantsile mitmekesistamise abil.
Siinkohal on oluline rĂ”hutada, kui tĂ€htis on, et kliendid, kes ei suutnud enamikke lukustusi saada, vabastaksid (osaliselt) saadud lukustusi, et ei peaks ootama vĂ”tme aegumise lĂ”ppu, enne kui ressursi lukku saab uuesti saada (tegelikult, kui toimub vĂ”rgu killustumine ja klient kaotab ĂŒhenduse Redis'i instantsidega, tuleb maksta tasu saadavuse rikkumise eest, kuni vĂ”tme aegumisel ootamine kestab).
Luku vabastamine
Luku vabastamine on lihtne operatsioon, mis nÔuab lihtsalt kÔigi instantside vabastamist, olenemata sellest, kas klient arvab, et ta suutis konkreetse instantsi edukalt lukustada.
Turvakaalutlused
Kas algoritm on turvaline? Proovime kujutada, mis juhtub erinevates stsenaariumides.
Alustuseks eeldame, et klient suutis saada lukustuse enamusinstantside ĂŒle. Igal instantsil on sama eluiga, kuid iga selle vĂ”tme kehtivus algas eri ajal, mistĂ”ttu nende aegumine toimub erinevatel hetkedel. Kui esimene vĂ”tme seati mitte hiljem kui T1 (aeg, mille me valime enne ĂŒhenduse loomist esimesse serverisse), ja viimane vĂ”tme seati mitte hiljem kui T2 (aeg, mil saadi vastus viimase serveri poolt), siis saame olla kindlad, et esimene vĂ”tme, millel aegumine toimub, kehtib vĂ€hemalt MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT. KĂ”ik ĂŒlejÀÀnud vĂ”tmed aeguvad hiljem, seega saame kindlad olla, et kĂ”ik vĂ”tmed kehtivad vĂ€hemalt selle aja jooksul.
Kuna enamus vÔtmeid on kehtivad, ei saa teine klient lukustust osta, kuna N/2+1 SET NX operatsioon ei saa lÔpetuda edukalt, kui juba on olemas N/2+1 vÔtmeid. Seega, kui lukustus on juba omandatud, ei saa seda samal hetkel uuesti omandada (see rikuks vastastikuse tÔrjumise omadust).
TĂ”si, me tahame olla kindlad, et hulk kliente, kes ĂŒritavad samal ajal lukustust omandada, ei Ă”nnestu sellega samaaegselt.
Kui klient lukustab enamusinstantsidest, kulutades sellele aega, mis on vĂ”rdne vĂ”i pikem kui maksimaalne lukustuse kestus, siis peab ta lukustuse kehtetuks ja vabastab instantsid. Seega peame arvesse vĂ”tma ainult olukorda, kus kliendil Ă”nnestus lukustada enamus instantsidest ajavahemikus, mis on lĂŒhem kui kehtivusaeg. Sel juhul, seoses eelpooltoodud argumendiga, ajavahemiku MIN_VALIDITY kui ĂŒkski klient ei peaks olema suuteline lukustust uuesti saama. Seega saavad paljud kliendid lukustada N/2+1 instantsi samal hetkel (mis lĂ”ppeb etapp 2 lĂ”ppemise hetkel), ainult siis, kui enamus lukustuse aeg oli pikem kui TTL aeg, muutes lukustuse kehtetuks.
Kas sa saaksid esitada formaalse turvalisuse tÔendi, viidata olemasolevatele sarnastele algoritmidele vÔi leida tÔrke eeltoodud tekstis?
Kergusega seotud kaalutlused
SĂŒsteemi kergus sĂ”ltub kolmest pĂ”hijoonest:
- Automaatne lukustuse lÔpetamine (kuna vÔtmete kehtivusaeg lÔpeb): lÔpuks on vÔtmed taas kergesti kÀttesaadavad, et kasutada lukustuse jaoks.
- Tegelikkus on see, et kliendid aitavad tavaliselt ĂŒksteist lukustusi eemaldades, kui vajalik lukustus pole soetatud, vĂ”i kui see on soetatud ja töö on lĂ”petatud; seega on tĂ”enĂ€oline, et me ei pea ootama vĂ”tmete kehtivuse lĂ”ppemist, et lukustust uuesti soetada.
- Tegelikkus on see, et kui kliendil on vaja lukustuse uuesti soetamist, oodatakse tavaliselt tunduvalt pikemat aega kui enamik lukustuste soetamiseks vajalik aeg. See vÀhendab tÔenÀosust, et tekib olukord, kus kokkulepitud ressursid on jagatud.
Siiski tuleb kerguse vĂ€henemise eest maksta trahvi, mis on vĂ”rreldav TTL-iga vĂ”rgusegmentides, seega, kui vĂ”rgus on pidevad segmendid, vĂ”ib see trahv saavutada piiramatu suuruse. See juhtub iga kord, kui klient soetab lukustuse ja siis katkestatakse ĂŒhendus teise segmendi, enne kui ta on selle vabastanud.
PĂ”himĂ”tteliselt, kui vĂ”rgu lĂ”pmatuid pidevaid segmente on, vĂ”ib sĂŒsteem olla kergelt kĂ€ttesaamatu lĂ”pmatuks ajaks.
TÔhusus, talitlushÀiretest taastumine ja fsync
Paljud kasutavad Redis't, kuna on vajalik tagada kĂ”rge lukustusserveri tĂ”husus, lĂ€htudes latentsusaegadest, mis on vajalikud lukustuste soetamiseks ja vabastamiseks, samuti nendega seotud operatsioonide arvust, mida suudetakse sekundis teha. Selle nĂ”ude tĂ€itmiseks on kommunikatsioonistrateegia N Redis serveriga, et vĂ€hendada latentsusaega. See on mitmeĂŒlekande strateegia (vĂ”i 'vaeste mitmeĂŒlekande' strateegia, kus sokkel seatakse mitteblokeerivasse reĆŸiimi, saadetakse kĂ”ik kĂ€sud ja kĂ€sud loetakse hiljem, lĂ€htudes arvamusest, et kliendi ja iga instantsi pöörlemisaeg on sarnane).
TÔsi, peame arvesse vÔtma ka pikaajalise andmete sÀilitamise kaalutluse, kui soovime luua mudeli, mis tagab usaldusvÀÀrse talitlushÀiretest taastumise.
PĂ”himĂ”tteliselt, et selgitada probleemi, oletame, et konfigureerime Redis'i ilma pikaajalise andmesalvestuseta. Klient suudab lukustada 3 5 instantsist. Ăks neist instantsidest, mille klient on suutnud lukustada, taaskĂ€ivitub, ja sel hetkel tekivad taas 3 instantsi sama ressursi jaoks, mida me saame lukustada, ja teine klient vĂ”ib omakorda lukustada taaskĂ€itatud instantsi, rikkudes lukustamise eksklusiivsuse turvafunktsiooni.
Kui sisse lĂŒlitada eelteavitav andmete salvestamine (AOF), siis olukord paraneb veidi. NĂ€iteks saame serverit tĂ”sta, edastades SHUTDOWN kĂ€sku ja taaskĂ€ivitades selle. Kuna Redis'is on aeg semantiliselt rakendatud nii, et see jookseb ka siis, kui server on vĂ€ljas, on kĂ”ik meie nĂ”udmised normaalsed. Normaalne seni, kuni tagatakse riist- ja tarkvaralise katkestamise korral normaalne sulgemine. Aga mis siis, kui on toitekatkestus? Kui Redis on vaikimisi konfigureeritud, koos fsync'iga, mis sĂŒnkroniseeritakse kettale iga sekundi jĂ€rel, siis vĂ”ib olla nii, et pĂ€rast taaskĂ€ivitamist jÀÀb meie vĂ”tme otsa. Teoreetiliselt, kui tahame tagada lukustamise turvalisuse igas instantsi taaskĂ€ivitamises, peame lĂŒlitama fsync=always pikaajalise andmesalvestuse seadetes. See tapab tĂ€ielikult jĂ”udluse, viies selle selliste CP-sĂŒsteemide tasemele, mida traditsiooniliselt kasutatakse jagatud lukustamise turvalisuse rakendamiseks.
Aga olukord on parem, kui esmapilgul tundub. PÔhimÔtteliselt sÀilib algoritmi turvalisus, kuna kui instants taaskÀivitub pÀrast riket, ei osale see enam aktiivsetes lukustamistes.
Selle tagamiseks on vajalik vaid tagada, et pĂ€rast riket jÀÀks instants kĂ€ttesaamatuks ajaks, mis veidi ĂŒletab maksimaalse TTL, mida me kasutame. Nii ootame, kuni aegumistĂ€htaeg möödub ja automaatne vabastamine toimub kĂ”ikide vĂ”tmete puhul, mis olid aktiivsed rikke hetkel.
Kasutades edasi lĂŒkatud taaskĂ€ivitusi, on pĂ”himĂ”tteliselt vĂ”imalik saavutada turvalisust ka siis, kui Redis ei sĂ€ilita andmeid pĂŒsivalt. Tuleb siiski mĂ€rkida, et see vĂ”ib kaasa tuua karistuse kĂ€ttesaadavuse rikkumise eest. NĂ€iteks juhul, kui enamik instantsidest ebaĂ”nnestub, muutub sĂŒsteem TTL ajal globaalselt kĂ€ttesaamatuks (ja sel ajal ei saa ĂŒhtegi ressurssi blokeerida).
Suurendame algoritmi kÀttesaadavust: pikendame blokeeringut
Kui klientide teostatav töö koosneb vÀikestest etappidest, on vÔimalik vÀhendada vaikimisi mÀÀratud blokeeringu aegumisaega ja rakendada blokeeringute pikendamise mehhanismi. PÔhimÔtteliselt, kui klient on arvutustega hÔivatud ja blokeeringu aegumisaeg on ohtlikult madal, saab kÔigile instantsidele saata Lua skripti, mis pikendab vÔtme TTL-i, kui see veel eksisteerib ja selle vÀÀrtus on endiselt juhuslik, saadud siis, kui blokeering saavutati.
Klient peab arvestama blokeeringut uuesti saavutatuks ainult juhul, kui tal Ônnestus blokeerida enamik instantsidest enne aegumisaega.
TÔsi, tehniliselt algoritm sellega ei muutu, seega peab maksimum korduste arv blokeeringute saavutamiseks olema piiratud, vastasel juhul rikkuvad kÀttesaadavuse omadused.
Allikas: habr.com
