Upotreba Redis-a kao LRU keša

O Redis-u

Zamislite da je potrebno opslužiti na stotine hiljada korisničkih zahteva u okviru jedne aplikacije. Dodajte na to još da je većina tih zahteva upućeno ka istim podacima, tačnije da zahtevaju isti skup podataka iz baze. Na primer, možda svi žele da pregledaju određenu objavu, ili pak da pogledaju informacije o najpopularnijim filmovima. Rešenje koje uključuje distribuirane podatke spregnute na više servera može da donese prednosti, ali po cenu uvećane kompleksnosti. Ukoliko nam je potrebno rešenje koje nudi visoku dostupnost podataka, skalabilnost i performanse uz jednostavnost upotrebe, onda vredi razmisliti o upotrebi Redis-a. Ovaj članak će objasniti koncept korišćenja Redis-a u svrhu keširanja podataka na centralizovanom sistemu.

Šta je Redis?

Redis, (engl. Remote Dictionary Server), je projekat otvorenog koda, višenamenske upotrebe, koji može služiti za skladištenje podataka, za njihovo keširanje, ali i kao medijum za prenos poruka između odgovarajućih protokola pošiljaoca i primaoca. U ove svrhe, Redis koristi RAM kao resurs. Ono što je od značaja za našu priču jeste njegova mogućnost da kešira podatke.

Redis je napisan u programskom jeziku C, sa zahtevom za dobrim performansama i tokom godina je izgradio reputaciju iza koje stoje velike, poznate, firme poput GitHub-a, Twitter-a i StackOverflow-a. Zanimljivost je i to da je Redis već petu godinu zaredom „najomiljenija“ baza podataka među anketiranim StackOverflow programerima.

Redis logo Svi Redis podaci se nalaze u memoriji (in-memory database), što omogućava brz protok podataka uz minimalno kašnjenje. Za razliku od tradicionalnih baza podataka, skladišta podataka u memoriji ne zahtevaju dopremanje podataka sa diska, čime smanjuju kašnjenje zahteva na mikrosekunde. Rezultat ovakvog pristupa jesu izuzetno dobre performanse.

Kada koristiti Redis?

Redis se u praksi najčešće koristi kao pomoćno skladište podataka, što znači da će postojati i glavna baza podataka (npr. PostgreSQL). Redis se u takvom slučaju koristi za privremene podatke, za keširanje vrednosti u cilju boljih performansi i bržeg pristupa, i za podatke koji se mogu rekonstruisati (npr. podaci o sesiji). Redis podrazumevano čuva podatke na izvesno vreme u vidu snapshot-a na disku, zato nije preporučljivo da se u okviru Redis-a skladišti nešto što ne možete da priuštite da izgubite. Takođe, pošto se podaci skladište u radnu memoriju, veličina dostupne memorije za skladištenje i veličina podataka koji se tu čuvaju igraju veliku ulogu.

Kako Redis funkcioniše?

Aplikacija koja koristi Redispodrazumeva korišćenje Redis klijenta. Klijent nije ništa drugo neko biblioteka implementirana u određenom programskom jeziku. Ovde možete videti listu, zvaničnih i nezvaničnih, podržanih Redis klijenata.

Kako postoji klijent, logično je i da je ovakvoj arhitekturi potreban server. Redis server je zadužen za skladištenje podataka i čitavu logiku vezanu za njihovu obradu. On čuva podatke u primarnoj memoriji, što omogućava veoma brze operacije čitanja i pisanja u bazu podataka. S obzirom na to da se podaci koji se nalaze u primarnoj memoriji gube pri gašenju servera, Redis sadrži mehanizme za čuvanje podataka u sekundarnoj, trajnoj, memoriji na neki od dva načina – čuvanjem svih podataka iz baze kada se zadati uslovi ispune (broj upisa u bazu, zakazano vreme itd.) ili čuvanjem svake od izvršenih naredbi nad bazom (sigurnija, ali skuplja opcija kada su u pitanju performanse). Primer Redis keširanja

Prikazana slika ilustruje način putem kojeg se Redis može upotrebiti za keširanje podataka. Kada korisnik uputi zahtev za podacima, Redis server vrši proveru postojanja takvih podataka. Ukoliko su oni dostupni (cache hit), podaci se vraćaju bez potrebe za daljim kontaktiranjem baze podataka. Ukoliko nisu dostupni (cache miss), dopremaju se iz baze da bi se potom servirali korisniku. Pre serviranja podataka korisniku, oni se beleže ponovo u memoriji (kešu) kako bi bili dostupni za neki od sledećih zahteva.

Instalacija

Instalacija, primeri upotrebe kao i svi ostali tehnički detalji u ovom članku biće prilagođeni Linux sistemima, tačnije Ubuntu derivatu. Komanda za instalaciju Redis paketa, u okviru kojeg između ostalog dolaze Redis server kao i Redis CLI (Command Line Interface), klijent za izvršavanje naredbi putem terminala, jeste:

$ apt install redis-server 

Podrazumevano, Redis server je već pokrenut. Proveru statusa servera možemo izvršiti na sledeći način:

$ service redis-server status 

dok komande za zaustavljanje i pokretanje servera zahtevaju minimalnu izmenu:

$ service redis-server stop 
$ service redis-server start 

Verzije servera i CLI klijenta možemo dobiti na sledeći način:

$ redis-cli --version
redis-cli 6.0.11
$ redis-server --version
Redis server v=6.0.11 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=83fe9b039c768864

Da bismo izvršili proveru rada Redis servera, možemo izvršiti „kontaktiranje“ pokrenutog servera putem CLI klijenta, i to na sledeći način:

$ redis-cli
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> QUIT

Komandom redis-cli ulazimo u interaktivni režim klijenta odakle možemo dalje navoditi komande za rad sa Redis serverom. Naredbe koje navodimo u okviru klijenta nisu osetljive na velika i mala slova, ali je praksa da ih uglavnom navodimo velikim slovima. U primeru iznad, naredbom PING smo kontaktirali server, nakon čega smo dobili potvrdni odgovor od servera PONG, a potom i izašli iz režima klijenta putem naredbe QUIT. Primećujemo i to da je Redis server podrazumevano pokrenut na portu 6379, kao i da CLI klijent podrazumevano koristi ovaj port.

Tipovi podataka

Redis je NoSQL baza podataka, ili kako je često nazivaju skladište (engl. data store), kod kojeg se podaci organizuju u ključ-vrednost (engl. key-value) parove. Ključ je tekstualni podatak koji jedinstveno određuje jedan zapis, dok vrednost može biti tipa:

  • tekstualna vrednost (String)
  • lista (List) – kolekcija tekstualnih vrednosti sortirana na osnovu redosleda dodavanja i formiranja liste
  • set (Set) – kolekcija jedinstvenih tekstualnih vrednosti bez ikakvog redosleda
  • uređeni skup (Ordered set) – sličan kao običan skup, ali prati poredak zasnovan na nekoj datoj vrednosti, od manje ka većoj, pri čemu se one mogu ponavljati
  • heš (Hash) – koristi mapiranje tekstualne vrednosti ključa i tekstualnog podatka, čineći ih odličnim za čuvanje vrednosti nekog objekta (npr. podaci o korisniku)
  • bitmapa (Bitmap) i HyperLogLog – tipovi podataka zasnovani na tekstualnoj vrednosti, ali imaju svoju sopstvenu semantiku.

Redis CLI klijent

U ovom odeljku objasnićemo neke jednostavne primere za rad sa Redis CLI klijentom, a koji će nam biti od značaja prilikom dalje implementacije Redis-a kao keša u okviru naše aplikacije. Počnimo od čuvanja najjednostavnije, tekstualne, vrednosti:

$ redis-cli
127.0.0.1:6379> SET message “Hello”
OK
127.0.0.1:6379> GET message
“Hello”
127.0.0.1:6379> KEYS *
1) “message”
127.0.0.1:6379> QUIT

U primeru iznad, postavljamo key-value par (message-Hello), a potom i dopremamo vrednost za određeni ključ. Listu svih ključeva možemo dopremiti naredbom KEYS, kojoj prosleđujemo kriterijum za pretragu, što je u ovom slučaju zvezda (*****) koja govori da želimo da dopremimo sve. Ukoliko hoćemo da čuvamo više stvari pod istim ključem, nešto što bi moglo da reprezentuje objekat, onda možemo koristiti heš strukturu i odgovarajuće komande:

$ redis-cli
127.0.0.1:6379> HSET user_id:10 name “Alice”
(integer) 1
127.0.0.1:6379> HGET user_id:10 name
“Alice”
127.0.0.1:6379> HSET user_id:10 name “Alice” gender “f” age 25
(integer) 2
127.0.0.1:6379> HGET user_id:10 gender
“f”
127.0.0.1:6379> HGETALL user_id:10
1) “name”
2) “Alice”
3) “gender”
4) “f”
5) “age”
6) “25”
127.0.0.1:6379> KEYS *
1) “user_id:10”
127.0.0.1:6379> QUIT

Za brisanje svih key-value parova možemo koristiti:

$ redis-cli
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> QUIT

ili još kraće (jedna komanda):

$ redis-cli FLUSHALL 

Vrednosti u Redis-u se čuvaju onoliko dugo koliko mi to odredimo, a podrazumevano nemaju rok isticanja. Životni vek (engl. Time to live, TTL) jednog key-value para se može postaviti na sledeći način:

$ redis-cli
127.0.0.1:6379> SETEX “message” 50 “Hi”
OK
127.0.0.1:6379> GET message
“Hi”
127.0.0.1:6379> TTL message
(integer) 39
127.0.0.1:6379> TTL message
(integer) -2
127.0.0.1:6379> GET message
(nil)
127.0.0.1:6379> QUIT

U primeru iznad postavljamo par sa rokom trajanja od 50 sekundi. Za to vreme moguće je dopremiti vrednost tog ključa, a nakon toga dobijamo negativan broj kao indikator da ovaj par više ne postoji. Ukoliko je ipak potrebno, možemo u nekom trenutku trajno sačuvati par kojem je već podešen TTL.

$ redis-cli
127.0.0.1:6379> SETEX “message” 50 “Hi”
OK
127.0.0.1:6379> GET message
“Hi”
127.0.0.1:6379> TTL message
(integer) 42
127.0.0.1:6379> PERSIST message
(integer) 1
127.0.0.1:6379> TTL message
(integer) -1
127.0.0.1:6379> GET message
“Hi”
127.0.0.1:6379> QUIT

U praksi

Keširanje podataka pomoću Redis-a ćemo prikazati na praktičnom i jednostavnom primeru. Takvih primera može biti puno, ali da ne bismo komplikovali i kako bismo pokazali suštinu keširanja, kreiraćemo minimalnu, NodeJS aplikaciju putem koje će korisnik moći da zatraži informacije o najpopularnijem filmu za određeni period. Pošto ćemo Redis koristiti kao posrednika u dobavljanju podataka, kao što je opisano u trećoj sekciji, koristićemo i SQLite bazu podataka za primarno skladištenje. Skup podataka o filmu koji ćemo koristiti za potrebe naše demonstrativne aplikacije se može pogledati i preuzeti ovde. Skup sadrži 5000 filmova, što je sasvim dovoljno za potrebe demonstracije. Zamislimo da korisnici naše aplikacije zahtevaju prikaz najpopularnijih filmova, ili jednostavnije, najpopularniji film. Da bismo radili sa više key-value parova i kako Redis ne bi čuvao samo jedan ključ (vezan za najpopularniji film), uvešćemo da korisnici zahtevaju najpopularniji film za izvestan vremenski period. Primer SQL upita kojim bismo iz baze pročitali određeni film je dat u nastavku.

SELECT original_title, MAX(popularity) as popularity
FROM movies
WHERE release_date BETWEEN (?) AND (?)
GROUP BY id, original_title
LIMIT 1

Ovakav upit, izvršen nad obimnom bazom podataka može potrajati izvesno vreme, čineći ga pogodnim kandidatom za ubrzanje. Način na koji bismo ovakav upit izvršili u našoj NodeJS aplikaciji je sledeći:

const mp_movie_sql = `SELECT original_title, MAX(popularity) as popularity
FROM movies
WHERE release_date BETWEEN (?) AND (?)
GROUP BY id, original_title
ORDER BY popularity DESC
LIMIT 1`;

const dbEntry = await db.get(mp_movie_sql, [startDate, endDate]);

Brzina izvršavanja zahteva će u mnogome zavisiti od server mašine, opsega pretrage filmova, same baze podataka i drugih faktora, ali to neće promeniti činjenicu da je potrebno pretražiti par hiljada zapisa u okviru baze. Za potrebe naše aplikacije, merićemo brzinu dohvatanja odgovara na standardan način, beleženjem trenutnog vremena pre i nakon obrade zahteva.

$ node popular_movie.js
{
data: { original_title: ‘Minions’, popularity: 875.581305 },
source: ‘database’,
responseTime: ‘30ms’
}

Imajući na umu da rezultati pretrage zavise od zadatog vremenskog opsega, konstruisaćemo ključ za keširanje na sledeći način:

const cacheKey = `popular:${startDate}:${endDate}`; 

Jednom kada imamo formirani ključ, možemo da proverimo da li je odgovarajuća vrednost već keširana, odnosno postoji, na Redis serveru.

let cacheEntry = await redisClient.get(cacheKey); 

Ukoliko traženi ključ postoji, serviraćemo korisniku podatak pridružen ključu, dok bismo u suprotnom pročitali podatak iz baze.

let cacheEntry = await redisClient.get(cacheKey);
If (cacheEntry) {
cacheEntry = JSON.parse(cacheEntry);
return { ...cacheEntry, source: ‘cache’ };
}

/* Čitanje podatka iz baze */
const mp_movie_sql = `SELECT original_title, MAX(popularity) as popularity
FROM movies
WHERE release_date BETWEEN (?) AND (?)
GROUP BY id, original_title
ORDER BY popularity DESC
LIMIT 1`;

await db.open();
const dbEntry = await db.get(mp_movie_sql, [startDate, endDate]);

redisClient.set(cacheKey, JSON.stringify(dbEntry));

return { ...dbEntry, source: ‘database’ };

SQLite driver vraća rezultat kao JavaScript objekat, pa je neophodno izvršiti serijalizaciju objekta u vidu JSON String-a pre nego što rezultat sačuvamo na Redis serveru. Nema potrebe za višestrukom dnevnom kalkulacijom popularnosti filma jer se rezultati neće promeniti, pa stoga možemo rezultat zadržati u okviru Redis-a, nakon inicijalne kalkulacije, sa rokom trajanja (TTL) od jednog dana.

redisClient.setex(cacheKey, 60 * 60 * 24, JSON.stringify(dbEntry));

return { ...dbEntry, source: ‘database’ };

Korišćenje keširanog, zabeleženog ključa će sada rezultovati u mnogo bržem odgovoru u okviru aplikacije. Pored korišćenje brze baze podataka, uz svega par hiljada zapisa, uspeli smo da ubrzamo ovakav zahtev korisnika dva i po puta.

$ node popular_movie.js
{
data: { original_title: ‘Minions’, popularity: 875.581305 },
source: ‘cache’,
responseTime: ‘12ms’
}

Redis memorija

Redis Out of Memory Dolazimo do suštine keširanja podataka uz dva ključna pitanja koja se odnose na to sa koliko maksimalno radne memorije za skladištenje raspolažemo i šta se dešava jednom kada ponestane memorije. Ukoliko želimo, možemo da proverimo trenutnu zauzetost memorije na sledeći način:

redis-cli --bigkeys

[00.00%] Biggest string found so far ’“popular:2000-01-01:2020-01-01“’ with 52 bytes

-------- summary --------

Sampled 1 keys in the keyspace!
Total key length in bytes is 29 (avg len 29.00)

Biggest string found ’“popular:2000-01-01:2020-01-01“’ has 52 bytes

1 strings with 52 bytes (100.00% of keys, avg size 52.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

Takođe, možemo i koristiti neki od GUI programa za lakše analiziranje zauzete memorije. Primer jednog takvog programa jeste RedisInsight, koji nam omogućava da se na jednostavan način konektujemo na Redis bazu i da pregledamo sve dostupne ključeve. Redis Insight Na slici iznad, vidimo isti ključ kao i prilikom korišćenja Redis CLI klijenta, čija je veličina 52 B, dok je vrednost koja je pridružena tom ključu 136 B, a TTL, izražen u sekundama, nešto manje od jednog dana. Redis nam omogućava da konfigurišemo veličinu dostupne radne memorije, kao i mehanizam kojim će se memorija ponašati u slučaju zauzetosti svih resursa, u okviru konfiguracionog fajla.

nano /etc/redis/redis.conf 

Podrazumevano, Redis nema ograničenje radne memorije kada je u pitanju 64-bitna serverska mašina, dok je za 32-bitne mašine to ograničenje postavljeno na 3 GB. U okviru konfiguracionog fajla možemo postaviti maksimalnu raspoloživu memoriju uz podešavanje:

maxmemory 200mb 

Ukoliko se dostupna memorija popuni, Redis će, podrazumevano, vraćati grešku prilikom pokušaja upisa i kreiranja novih ključeva (noeviction). Ovo ponašanje diktira politika zamene ili odbacivanje (engl. Eviction Policy) koje takođe možemo podesiti u okviru konfiguracionog fajla.

maxmemory-policy noeviction 

Politika zamene koja se najčešće koristi u praksi jeste LRU (engl. Least Recently Used). Redis zapravo koristi modifikovanu LRU politiku koja funkcioniše tako što uzorkuje malu grupu ključeva i određuje najboljeg kandidata na osnovu toga kojem je ključu najdavnije pristupano. Količina ključeva koja je zahvaćena ovom aproksimacijom se takođe može podesiti. Možemo izabrati jednu od dve LRU politike:

  • allkeys-lru – izbaci ključeve prema LRU politici kako bi se napravilo mesta za nove podatke
  • volatile-lru – izbaci ključeve prema LRU politici, ali uzimajući u obzir izbacivanje samo onih ključeva koji imaju TTL postavljen

Pošto u okviru naše demonstrativne aplikacije koristimo TTL za ključeve i kako ne generišemo nikakve trajne ključeve, obe politike će se ponašati identično.

maxmemory-policy volatile-lru 

Nakon postavljanja politike i izmene konfiguracije potrebno je restartovati Redis server kako bi izmene bile vidljive.

$ service redis-server restart 

LRU simulacija

Za potrebe velike, komercijalne, aplikacije, merenje brzine odgovora zahteva korisnika nije dovoljno relevantan. Realniji podaci se dobijaju simulacijom. Na primer, simuliranje može da podrazumeva da se uz odgovarajuću politiku zamene i maksimalnu dozvoljenu memoriju izvršavaju niti (korisnici) koje bi nasumično izdavale zahtev Redis serveru simulirajući tako rad više korisnika. Prilikom simulacije, merili bismo procenat pogodaka keša (engl. cache hit) kao i procenat promašaja keša (engl. cache miss).

$ redis-cli info stats

...
keyspace_hits:142 # broj pogodaka keša
keyspace_misses:26 # broj promašaja keša
...

Ponekad, simuliranje rada keša je veoma korisno radi korekcije same konfiguracije keša.

Redis CLI nudi pogodan režim za simuliranje GET i SET operacija, koristeći 80-20% zakon raspodele, što znači da će 20% ključeva biti zatraženo u 80% slučajeva, a što je učestala raspodela kada je u pitanju scenario keširanja. Logično, korišćenje ovakvog režima zahteva prethodno konfigurisane opcije za maksimalnu količinu radne memorije u okviru Redis-a, kao i postavljanje odgovarajuće LRU politike. Za potrebe testiranja, podesićemo Redis konfiguraciju na sledeći način:

$ nano /etc/redis/redis.conf

...
maxmemory 100mb
maxmemory-policy allkeys-lru
...

Preostaje još da podesimo broj ključeva koji se nalaze u okviru Redis-a, a za koje će biti simulirane GET i SET operacije. U primeru ispod, u pitanju je 10 miliona ključeva.

$ redis-cli FLUSHALL
OK
$ redis-cli --lru-test 10000000
156000 Gets/sec | Hits: 4552 (2.92%) | Misses: 151448 (97.08%)
153750 Gets/sec | Hits: 12906 (8.39%) | Misses: 140844 (91.61%)
159250 Gets/sec | Hits: 21811 (13.70%) | Misses: 137439 (86.30%)
151000 Gets/sec | Hits: 27615 (18.29%) | Misses: 123385 (81.71%)
145000 Gets/sec | Hits: 32791 (22.61%) | Misses: 112209 (77.39%)
157750 Gets/sec | Hits: 42178 (26.74%) | Misses: 115572 (73.26%)
154500 Gets/sec | Hits: 47418 (30.69%) | Misses: 107082 (69.31%)
151250 Gets/sec | Hits: 51636 (34.14%) | Misses: 99614 (65.86%)

Simulacija prikazuje statistiku svake sekunde. Kao što vidimo, u prvih par sekundi keš se popunjava, čineći da postižemo sve veći broj pogodaka i sve manji broj promašaja. U kasnijoj fazi simulacije dobijamo nešto stabilizovaniju statistiku koju bismo mogli da očekujemo kao realan scenario:

120750 Gets/sec | Hits: 48774 (40.39%) | Misses: 71976 (59.61%)
122500 Gets/sec | Hits: 49052 (40.04%) | Misses: 73448 (59.96%)
127000 Gets/sec | Hits: 50870 (40.06%) | Misses: 76130 (59.94%)
124250 Gets/sec | Hits: 50147 (40.36%) | Misses: 74103 (59.64%)

Procenat promašaja keša od 59% možda nije zadovoljavajuć za naše potrebe. Sada znamo da je inicijalnih 100 MB za raspoloživost radne memorije malo, pa ćemo pokušati sa istom konfiguracijom uz 500 MB raspoložive radne memorije.

140000 Gets/sec | Hits: 135376 (96.70%) | Misses: 4624 (3.30%)
141250 Gets/sec | Hits: 136523 (96.65%) | Misses: 4727 (3.35%)
140250 Gets/sec | Hits: 135457 (96.58%) | Misses: 4793 (3.42%)
140500 Gets/sec | Hits: 135947 (96.76%) | Misses: 4553 (3.24%)

Dakle, uz ovakvu konfiguraciju možemo očekivati solidno ponašanje u teoriji za 10 miliona ključeva, uz 80-20% zakon raspodele.

Zaključak

U poređenju sa svojim konkurentima na tržištu, Redis daje dobre rezultate u terminima brzine pristupa podacima. U poređenju sa keš tehnologijama, on ima podršku trajnosti podataka (engl. PERIST) i kompleksnije strukture podataka kojima se lakše i prirodnije modeluju strukture. U ovom članku opisane su neke od ključnih osobine Redis platforme, njegove strukture podataka, zatim na koji način možemo konfigurisati Redis server. Na kraju je demonstrirana upotreba i benefit Redis keširanja. Kao primarni medijum za smeštanje podataka korišćena je relaciona baza podataka, dok je Redis poslužio, kao posrednik, za keširanje podataka koji su se dopremali iz nje. Time se brzina izvršavanja prvobitnog korisnikovog zahteva značajno povećala.

Korisni linkovi

Autor: Aleksandar Miladinović

Student završne godine Prirodno-matematičkog fakulteta.

Aleksandar Miladinović

Student završne godine Prirodno-matematičkog fakulteta.