Introducing Consistent Hashing
Hajautettujen arkkitehtuureiden yleistymisen myötä johdonmukaisesta hashinasta tuli valtavirtaa. Mutta mitä se tarkalleen ottaen on ja miten se eroaa tavallisesta hashausalgoritmista? Mitkä ovat sen tarkat motiivit?
Aluksi kuvataan tärkeimmät käsitteet. Sen jälkeen perehdymme olemassa oleviin algoritmeihin ymmärtääkseen johdonmukaiseen hashaukseen liittyviä haasteita.
Hashing
Hashing on prosessi, jossa mielivaltaisen kokoista dataa kuvataan kiinteän kokoisiin arvoihin. Jokaisella olemassa olevalla algoritmilla on oma spesifikaationsa:
- MD5 tuottaa 128-bittisiä hash-arvoja.
- SHA-1 tuottaa 160-bittisiä hash-arvoja.
- jne.
Hashingilla on monia sovelluksia tietotekniikassa. Esimerkiksi yksi näistä sovelluksista on nimeltään tarkistussumma. Tietoaineiston eheyden tarkistamiseen voidaan käyttää hashausalgoritmia. Palvelin hashaa tietokokonaisuuden ja ilmoittaa hash-arvon asiakkaalle. Tämän jälkeen asiakas hashaa oman versionsa tietokokonaisuudesta ja vertaa hash-arvoja. Jos ne ovat samat, eheys pitäisi varmistaa.
”pitäisi” on tässä tärkeä. Pahimmassa tapauksessa törmäys tapahtuu. Törmäys on se, kun kahdella eri datalla on sama hash-arvo. Otetaanpa käytännön esimerkki määrittelemällä seuraava hashausfunktio: kun annetaan henkilö, se palauttaa hänen syntymäpäivänsä (päivä & syntymäkuukausi). Syntymäpäiväparadoksi kertoo, että jos huoneessa on vain 23 henkilöä, todennäköisyys sille, että kahdella henkilöllä on sama syntymäpäivä (eli törmäys), on yli 50 %. Siksi syntymäpäiväfunktio ei todennäköisesti ole hyvä hashifunktio.
Nopeana johdantona hashifiointiin on tärkeää ymmärtää, että pääidea on arvojen hajauttaminen tietylle alueelle. Esimerkiksi:
- MD5 levittää arvot 128-bittiseen avaruusalueeseen
- Hashtable (tai hashmap), jonka tukena on 32-alkioinen array, on sisäinen hashing-funktio, joka levittää arvot mihin tahansa indeksiin (0:sta 31:een).
Kuormituksen jakaminen
Kuormituksen jakaminen voidaan määritellä kuormituksen levittämiseksi solmupisteiden kesken. Termi solmu on tässä yhteydessä vaihdettavissa palvelimen tai instanssin kanssa. Se on laskentayksikkö.
Kuormituksen tasaus on yksi esimerkki kuormituksen jakamisesta. Siinä on kyse joukon tehtävien jakamisesta resurssien kesken. Kuormantasausta käytetään esimerkiksi API-pyyntöjen jakamiseen web-palvelininstanssien kesken.
Kun on kyse datasta, käytetään mieluummin termiä sharding. Tietokannan shard on tietokannan datan horisontaalinen osio. Tyypillinen esimerkki on tietokanta, joka on jaettu kolmeen shardiin, joissa kussakin shardissa on osajoukko koko datasta.
Kuormituksen tasaamisella ja shardoinnilla on joitakin yhteisiä haasteita. Datan jakaminen tasaisesti esimerkiksi sen takaamiseksi, että jokin solmu ei ole ylikuormittunut muihin verrattuna. Joissain yhteyksissä kuormantasauksen ja shardauksen on myös jatkuvasti yhdistettävä tehtäviä tai dataa samaan solmuun:
- Jos meidän on serialisoitava, käsiteltävä yksi kerrallaan, tietyn kuluttajan operaatiot, meidän on reititettävä pyyntö samaan solmuun.
- Jos meidän on jaettava dataa, meidän on tiedettävä, mikä shard on tietyn avaimen omistaja.
Kuulostaako tutulta? Näissä kahdessa esimerkissä levitämme arvoja toimialueelle. Olipa kyseessä sitten palvelinsolmuihin levitettävä tehtävä tai tietokannan shardiin levitettävä data, löydämme takaisin hashingiin liittyvän ajatuksen. Tästä syystä hashingia voidaan käyttää yhdessä kuormanjaon kanssa. Katsotaanpa miten.
Mod-n-hashing
Mod-n-hashingin periaate on seuraava. Jokainen avain hashataan käyttämällä hashausfunktiota, joka muuttaa syötteen kokonaisluvuksi. Sitten suoritetaan solmujen lukumäärään perustuva modulo.
Katsotaanpa konkreettinen esimerkki, jossa on 3 solmua. Tässä meidän on jaettava kuorma näiden solmujen kesken avaintunnisteen perusteella. Jokainen avain hashataan, minkä jälkeen suoritetaan modulo-operaatio:
Tämän lähestymistavan etuna on sen tilattomuus. Meidän ei tarvitse säilyttää mitään tilaa muistuttamassa meitä siitä, että foo
ohjattiin solmuun 2. Silti meidän on tiedettävä, kuinka monta solmua on konfiguroitu modulo-operaation soveltamiseksi.
Miten mekanismi sitten toimii, jos se skaalautuu ulos- tai sisäänpäin (solmuja lisätään tai poistetaan)? Jos lisäämme toisen solmun, modulo-operaatio perustuu nyt 3:n sijasta 4:ään:
Kuten huomaamme, avaimia foo
ja baz
ei enää liitetä samaan solmuun. Mod-n-hashaussa ei ole mitään takeita siitä, että avaimen ja solmun välinen yhteys säilyy johdonmukaisena. Onko tämä ongelma? Saattaa olla.
Mitä jos toteutamme tietovaraston käyttäen shardingia ja perustuen mod-n-strategiaan tietojen jakamiseen? Jos skaalaamme solmujen määrää, meidän on suoritettava tasapainotusoperaatio. Edellisessä esimerkissä uudelleentasapainotus tarkoittaa:
- Siirretään
foo
solmusta 2 solmuun 0. - Siirretään
baz
solmusta 2 solmuun 3.
Mutta mitä tapahtuu, jos olisimme tallentaneet miljoonia tai jopa miljardeja tietoja ja meidän pitäisi tasapainottaa melkein kaikki uudelleen? Kuten voimme kuvitella, tämä olisi raskas prosessi. Siksi meidän on muutettava kuormanjakotekniikkaamme varmistaaksemme, että uudelleentasapainotuksen yhteydessä:
- Jakauma pysyy mahdollisimman tasaisena solmujen uuden lukumäärän perusteella.
- Migroitavien avainten lukumäärän tulisi olla rajoitettu. Ihannetapauksessa se olisi vain 1/n prosenttia avaimista, missä n on solmujen lukumäärä.
Juuri tämä on johdonmukaisten hashausalgoritmien tarkoitus.
Termi johdonmukainen saattaa tosin olla hieman sekava. Tapasin insinöörejä, jotka olettivat, että tällaiset algoritmit yhdistävät tietyn avaimen aina samaan solmuun jopa skaalautuvuudesta huolimatta. Näin ei kuitenkaan ole. Sen täytyy olla johdonmukainen tiettyyn pisteeseen asti, jotta jakauma pysyy tasaisena.
Nyt on aika kaivaa joitakin ratkaisuja.
Rendezvous
Rendezvous oli ensimmäinen algoritmi, jota koskaan ehdotettiin ongelmamme ratkaisemiseksi. Vaikka alkuperäisessä, vuonna 1996 julkaistussa tutkimuksessa ei mainita termiä consistent hashing, se tarjoaa ratkaisun kuvaamiimme haasteisiin. Katsotaanpa yksi mahdollinen toteutus Go:ssa:
Miten se toimii? Kierrämme jokaisen solmun ja laskemme sen hash-arvon. Hash-arvo palautetaan hash
-funktiolla, joka tuottaa kokonaisluvun avaimen (syötteemme) ja solmun tunnisteen perusteella (helpoin tapa on hashata näiden kahden merkkijonon ketjutus). Sitten palautamme solmun, jonka hash-arvo on suurin. Tästä syystä algoritmi tunnetaan myös nimellä korkeimman satunnaispainon hashaus.
Rendezvous-algoritmin suurin haittapuoli on sen O(n)-aikakompleksisuus, jossa n on solmujen lukumäärä. Se on erittäin tehokas, jos tarvitsemme rajallisen määrän solmuja. Kuitenkin jos alamme ylläpitää tuhansia solmuja, se saattaa alkaa aiheuttaa suorituskykyongelmia.
Ring Consistent Hash
Seuraava algoritmi julkaistiin vuonna 1997 Karger et al. artikkelissa. Tässä tutkimuksessa mainittiin ensimmäistä kertaa termi consistent hashing.
Se perustuu renkaaseen (päästä päähän yhdistetty joukko). Vaikka se on suosituin consistent hashing -algoritmi (tai ainakin tunnetuin), periaatetta ei aina ymmärretä hyvin. Sukelletaanpa siihen.
Aluksi luodaan rengas. Rengas on kiinteän pituinen. Esimerkissämme jaamme sen 12 osaan:
Sitten sijoitamme solmut. Esimerkissämme määrittelemme N0
, N1
ja N2
.
Solmut jaetaan toistaiseksi tasaisesti. Palaamme tähän kohtaan myöhemmin.
Sitten on aika katsoa, miten avaimet esitetään. Ensin tarvitsemme funktion f
, joka palauttaa rengasindeksin (0-11) avaimen perusteella. Voimme käyttää siihen mod-n-hashingia. Koska renkaan pituus on vakio, se ei aiheuta meille ongelmia.
Esimerkissämme määrittelemme 3 avainta: a
, b
ja c
. Sovellamme f
jokaiseen niistä. Oletetaan, että saamme seuraavat tulokset:
f(a) = 1
f(a) = 5
f(a) = 7
Voidaan siis sijoittaa näppäimet rinkiin näin:
Miten liitämme tietyn avaimen solmuun? Tärkein logiikka on eteneminen. Annetusta avaimesta palautamme ensimmäisen solmun, jonka löydämme seuraavaksi edetessämme eteenpäin:
Tässä esimerkissä assosioimme solmun a
solmuun N1
, solmuun b
ja solmuun c
solmuun N2
.
Katsotaanpa seuraavaksi, miten tasapainottaminen onnistuu. Määrittelemme toisen solmun N3
. Mihin meidän pitäisi sijoittaa se? Enää ei ole tilaa sille, että kokonaisjakauma olisi tasainen. Pitäisikö meidän järjestää solmut uudelleen? Ei, muuten emme olisi enää yhdenmukaisia, eikö niin? Solmun sijoittamiseksi käytämme uudelleen samaa hash-funktiota f
, jonka otimme käyttöön. Sen sijaan, että sitä kutsuttaisiin avaimella, sitä voidaan kutsua solmun tunnisteella. Uuden solmun sijainti päätetään siis satunnaisesti.
Tällöin herää yksi kysymys: mitä teemme a
:lle, kun seuraava solmu ei ole enää N1
:
Ratkaisu on seuraava: Meidän on muutettava sen assosiaatio ja saatava a
assosioitumaan N3
:
Leave a Reply