This shows you the differences between two versions of the page.
sd-ca:teme:tema2-2024 [2024/03/30 18:18] andrei.otetea created |
sd-ca:teme:tema2-2024 [2024/04/25 18:18] (current) andrei.otetea [Actualizări] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | <hidden> | + | ====== Tema 2 - Distributed Database ====== |
- | + | ||
- | ====== Tema 2 - Load Balancer ====== | + | |
**Responsabili:** | **Responsabili:** | ||
- | * [[topala.andrei@gmail.com | Andrei Topală]] | + | * [[andreiotetea23@gmail.com | Andrei Oțetea]] |
- | * [[armand.nicolicioiu@gmail.com | Armand Nicolicioiu]] | + | * [[marin.eduard.c@gmail.com | Eduard Marin]] |
- | * [[coriciiulia76@gmail.com | Iulia Corici]] | + | |
- | * Data publicării: **14.04.2021** | + | * Data publicării: **08.04.2024** |
- | * Deadline **HARD**: **09.05.2021 23:55:00** | + | * Deadline **HARD**: **07.05.2024 23:59:59** |
===== Actualizări ===== | ===== Actualizări ===== | ||
- | ** 17 aprilie 2021 22:52:** Corectare test12-16.ref | + | * **10 aprilie** - Am actualizat deadline-ul hard - **7 mai, ora 23:59** |
- | + | * **11 aprilie** - Am adăugat un exemplu de test, cu ref-ul corespunzător | |
- | ** 22 aprilie 2021 10:00:** Corectare checker | + | * **17 aprilie** - Am [[https://github.com/sd-pub/Tema2-2024/commit/3691b822d621efe458862bbb7c2af7e4493402d1|actualizat scheletul]] - am reglat erorile de coding style din schelet - vezi **utils.h** - și am modificat valoarea constantei **MAX_LOG_LENGTH** |
- | + | * **17 aprilie** - Am adăugat [[https://github.com/sd-pub/Tema2-2024/commit/c086e5776b80c920d299337c4f00c1e40ed024e5|checker-ul și testele]] | |
- | ** 22 aprilie 2021 20:55:** Corectare schelet (main.c) + test5,12-16.ref | + | * **25 aprilie** - Am actualizat [[https://github.com/sd-pub/Tema2-2024/commit/630febcb72493b1350859563d8044189362c83f9|ref-urile testelor 26-30]]. |
- | + | ||
- | ** 22 aprilie 2021 23:30:** Adăugare instanţă de upload pe vmchecker. | + | |
===== Obiective ===== | ===== Obiective ===== | ||
În urma realizării acestei teme veţi: | În urma realizării acestei teme veţi: | ||
Line 24: | Line 19: | ||
* Exersa implementarea unui sistem mai complex urmând o descriere detaliată a fiecărei componente. | * Exersa implementarea unui sistem mai complex urmând o descriere detaliată a fiecărei componente. | ||
* Căpăta intuiţie despre limitările unui volum mare de date şi despre sistemele distribuite. | * Căpăta intuiţie despre limitările unui volum mare de date şi despre sistemele distribuite. | ||
+ | * Înțelege utilitatea cache-ului în sistemele reale. | ||
===== Introducere ===== | ===== Introducere ===== | ||
- | Roby, mare antreprenor, doreşte să lanseze o companie de e-Commerce care să rivalizeze Amazon. O primă problemă pe care o întâmpină este aceea de a gestiona toate produsele care vor fi puse la dispoziţie pe site. | + | După ce a învățat să implementeze propriul alocator de memorie, Marcel, student în anul I, își dorește să iasă temporar din lumea sistemelor de operare și să aibă o privire de ansamblu asupra modului în care companiile mari oferă servicii de stocare în cloud. Om simplu, el a decis să rivalizeze cu AWS și să își creeze propriul sistem distribuit de stocare, pe care să îl poată folosi împreună cu colegii de facultate. |
+ | |||
+ | Având un volum foarte mare de date, Marcel a intuit că trebuie să folosească mai multe servere, scopul fiecăruia fiind să stocheze un subset din datele pe care vrea să le păstreze. Lovindu-se pentru prima dată de un sistem format din mai multe entități în afara de propriul laptop, Marcel a început să citească despre concepte de system design folosite frecvent în proiectarea eficientă a sistemelor distribuite: **caching**, **load balancing**, **consistent hashing**, **task queues**. Rolul vostru este să îl ajutați pe Marcel să pună în practică tot ceea ce a aflat în incursiunea lui prin lumea sistemelor distribuite. | ||
+ | |||
+ | ===== Cerință ===== | ||
+ | Scopul proiectului este de a dezvolta o bază de date distribuită în care se păstrează documente. Pentru a optimiza accesul la documentele utilizate frecvent, se dorește implementarea unui sistem de caching bazat pe **[[https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)|algoritmul LRU]]** (Least Recently Used). Acesta este mecanism de evacuare a intrărilor din cache, conform căruia, în momentul în care nu mai există spațiu suficient pentru a insera noi date, sunt eliminate cele mai vechi valori existente. | ||
+ | |||
+ | În plus, pentru a concepe un sistem cât mai eficient, ne dorim ca baza noastra de date sa foloseasca multiple servere, între care sa se distribuie uniform documentele stocate. Pentru aceasta, vom folosi conceptul de **Load Balancing**, folosind **Consistent Hashing**. Acesta este un mecanism frecvent utilizat în cadrul sistemelor distribuite și are avantajul de a îndeplini //minimal disruption constraint//, adică minimizarea numărului de transferuri necesare atunci când un server este oprit sau pornit. Mai exact, când un server este oprit, doar obiectele aflate pe acesta trebuie redistribuite către servere apropiate. Analog, când un nou server este adăugat, va prelua obiecte doar de la un număr limitat de servere, cele vecine. | ||
- | Deoarece compania lui Roby deţine un număr foarte mare de produse, acestea nu vor putea fi stocate pe un singur server. De aceea, el va folosi un sistem distribuit în care dorește împărțirea uniformă de produse pe fiecare server. Într-un sistem dinamic precum acesta trebuie ținut cont de faptul că pe parcurs pot fi adăugate servere noi sau oprite servere vechi. Un server nou trebuie să preia o parte din load-ul existent în sistem, iar un server scos va trebui să își transfere produsele către alte servere rămase disponibile. | + | <note tip> **Cache-ul** reprezintă un layer de stocare utilizat pentru **memorarea temporară a datelor** care au o probabilitate mare de a fi refolosite, astfel încât accesările ulterioare la aceste date să necesite un timp mult mai scurt față de obținerea lor din locația originală de memorie.</note> |
- | ===== Cerinta ===== | + | |
- | Scopul vostru este de a implementa un **Load Balancer** folosind **Consistent Hashing**. Acesta este un mecanism frecvent utilizat în cadrul sistemelor distribuite şi are avantajul de a îndeplini //minimal disruption constraint//, adică minimizarea numărului de transferuri necesare atunci când un server este oprit sau unul este pornit. Mai exact, când un server este oprit, doar obiectele aflate pe acesta trebuie redistribuite către servere apropiate. Analog, când un nou server este adugat, va prelua obiecte doar de la un număr limitat de servere, cele vecine. | + | |
- | <note tip>**Load Balancer** este componenta care are rolul de a dirija **uniform** traficul către un set de servere cu o putere limitata de procesare. Acesta are misiunea de a asigura că toate serverele stochezează și procesezează un volum similar date pentru a maximiza eficienta întregului sistem. În cazul nostru, **obiecte din sistem vor fi perechi ''<cheie, valoare>''** unde cheia va fi id-ul unui produs iar valoarea va salva detaliile despre produs (pentru simplitate doar un string cu numele, dar poate fi extins la o structura ce conține nume, preț, specificații, rating, etc). Load balancer-ul e responsabil de a decide pe ce server va fi salvat un obiect în funcție de cheia acestuia. Mecanismul eficient prin care acesta mapează un obiect <cheie, valoare> unui server_id poartă numele de **Consistent Hashing** şi este descris în detaliu într-o secțiune următoare.</note> | + | <note tip>**Load Balancer** este componenta care are rolul de a dirija **uniform** traficul către un set de servere cu o putere limitata de procesare. Acesta are misiunea de a asigura că toate serverele stochezează și procesezează un volum similar date pentru a maximiza eficienta întregului sistem. În cazul nostru, **obiecte din sistem vor fi perechi ''<doc_name, doc_content>''** unde cheia va fi numele unui document, iar valoarea va salva conţinutul acestuia. Load balancer-ul e responsabil de a decide pe ce server va fi salvat un obiect în funcție de cheia acestuia. Mecanismul eficient prin care acesta mapează un obiect <doc_name, doc_content> unui server_id poartă numele de **Consistent Hashing** şi este descris în detaliu într-o secțiune următoare.</note> |
<note> | <note> | ||
Line 41: | Line 42: | ||
Exemplu: | Exemplu: | ||
- ''NUM_SERVERS = 10'' | - ''NUM_SERVERS = 10'' | ||
- | - Vream sa stocam cheia “123” cu valoarea “iPhone 12” in sistem. hash(“123”) = 0x65b71f5b. Serverul care va stoca această informaţie va fi: ''server_id = 0x65b71f5b % 10 = 1'' | + | - Vream sa stocam documentul denumit “123” cu conţinutul “iPhone 12” în sistem. hash(“123”) = 0x65b71f5b. Serverul care va stoca această informaţie va fi: ''server_id = 0x65b71f5b % 10 = 1'' |
- Un server dispare. ''NUM_SERVERS = 9'' Vor trebui verificate toate obiectele din sistem pentru că hash(data) % NUM_SERVERS va avea altă valoare față de cea precedentă. | - Un server dispare. ''NUM_SERVERS = 9'' Vor trebui verificate toate obiectele din sistem pentru că hash(data) % NUM_SERVERS va avea altă valoare față de cea precedentă. | ||
- | - Pentru cheia “123” adăugată anterior avem: ''server_id = hash(“123”) % NUM_SERVERS = 0x65b71f5b % 9 = 4'' ⇒ În trecut era salvată pe serverul 1, dar acum va trebui mutată pe serverul cu id-ul 4. | + | - Pentru documentul cu numele “123” adăugat anterior, avem: ''server_id = hash(“123”) % NUM_SERVERS = 0x65b71f5b % 9 = 4'' ⇒ În trecut era salvat pe serverul 1, dar acum va trebui mutat pe serverul cu id-ul 4. |
- De asemenea, și id-urile serverelor se vor shifta pentru a avea indici consecutivi. | - De asemenea, și id-urile serverelor se vor shifta pentru a avea indici consecutivi. | ||
Line 50: | Line 51: | ||
===== Detaliile Sistemului ===== | ===== Detaliile Sistemului ===== | ||
- | În cadrul acestei teme, vă propunem organizarea întregului task sub forma unei ierarhii cu următoarele componente: un client, un load balancer şi mai multe servere. | + | În cadrul acestei teme, vă propunem organizarea întregului proiect sub forma unei ierarhii cu următoarele componente: un client, un load balancer și mai multe servere. |
- | {{ :sd-ca:teme:arhitectura.png?550 |}} | + | **Load balancer-ul** este prima componentă din sistem. Acesta se ocupă de distribuirea request-urilor primite de la clienți. În cazul nostru, există 2 tipuri de request-uri: |
+ | * **adresate serverelor**- care au ca scop manipularea documentelor stocate pe servere (**EDIT_DOCUMENT**, **GET_DOCUMENT**) | ||
+ | * **adresate load balancer-ului** - care au ca scop administrarea topologiei de servere (**ADD_SERVER**, **REMOVE_SERVER**) | ||
- | **Client** - | + | În primul caz, load balancer-ul decide spre ce server trebuie redirecționat request-ul, prelucrarea acestuia fiind făcută la nivelul serverului. |
- | Are la dispozitie 2 operaţii: | + | |
- | - client_store(char* key, char* value): Stochează un produs (value) în sistem şi îl asociază unui ID (key). | + | |
- | - client_retrieve(char* key): Returnează numele produsului asociat unui ID (key). | + | |
- | **Load Balancer (Main Server)** - | + | În cel de-al doilea caz, se adaugă sau se șterg servere din sistem și se rebalansează stocarea serverelor astfel încât să nu se piardă documentele, utilizând logica explică la [[sd-ca:teme:tema2-2024#consistent_hashing|Consistent Hashing]]. |
- | Are la dispoziţie 4 operaţii: | + | |
- | - loader_store(char* key, char* value): Stochează un produs (cheia - ID, valoarea - numele produsului) pe unul dintre serverele disponibile folosind Consistent Hashing. | + | |
- | - loader_retrieve(char* key): Calculează pe ce server este stocat key şi îi extrage valoarea. | + | |
- | - loader_add_server(int server_id): Adaugă un nou server în sistem şi rebalansează obiectele. | + | |
- | - loader_remove_server(int server_id): Scoate un server din sistem şi rebalansează obiectele. | + | |
- | **Server** - | + | ==== Server(50p) ==== |
- | Are la dispoziţei 3 operaţii: | + | Pentru fiecare server se respectă următoarea arhitectură, bazată pe o coadă de task-uri, un cache și o memorie principală (o bază de date): |
- | - server_store(char* key, char* value): Stochează într-un Hashtable datele primite de la Load Balancer. | + | |
- | - server_retrieve(char* key): Returnează valoarea asociată lui key din Hashtable. | + | |
- | - server_remove(char* key): Şterge o intrare din Hashtable. | + | |
- | <note important>Puteţi adăuga şi alte funcţii noi cât timp nu schimbaţi semnăturile celor deja existente în schelet. De exemplu, dacă este necesar puteţi adăuga o funcţie server_retrieve_all care să returneze toate cheile & valorile de pe un server.</note> | + | {{ :sd-ca:teme:inside_server.png?800 }} |
- | <note tip>În fișierul ''main.c'' din schelet este deja implementată partea ce ține de client si modul în care acesta apelează sistemul distribuit, dar și partea de gestiune a sistemului ce va apela funcțiile de pornire si oprire a serverelor atunci când este cazul. Trebuie să implementați doar funcționalitățile din ''load_balancer.c'' si din ''server.c'', entry point-ul către sistemul distribuit fiind prin **Load Balancer** (conform diagramei de mai sus), iar toate apelurile către acesta fiind deja implementate în schelet.</note> | + | Interacțiunea cu server-ul se face prin intermediul unui request. Există două tipuri de request-uri. Pentru ca server-ul să facă minimul de operații necesare, vom folosi o coadă, a cărei dimensiune maximă este TASK_QUEUE_SIZE = 1000, în care păstram request-urile care ar trebui să modifice conținutul unui document. Astfel, execuția lor este amânată până în momentul în care un request de tip ** GET_DOCUMENT ** este primit, urmând a fi rulate toate request-urile stocate până atunci în coadă, trebuind să se afișeze un răspuns corespunzător pentru fiecare request. |
- | ===== Consistent Hashing ===== | + | Odată ce un request este executat, server-ul returnează o structura de tip **response**, cu următoarele câmpuri: |
- | Consistent Hashing este o metoda de hashing distribuit prin care atunci când tabelul este redimensionat, se vor remapa în medie doar ''n / m'' chei, unde //n// este numărul curent de chei, iar //m// este numărul de slot-uri (în cazul nostru servere). În implementare, se va folosi un cerc imaginar numit //hash ring// pe care sunt mapate atât cheile obiectelor, cât și id-urile serverlor. Această mapare se realizează cu ajutorul unei funcții de hashing cu valori între MIN_HASH și MAX_HASH, punctele din acest interval fiind distribuite la distante egale în intervalul logic [0°, 360°] de pe cercul imaginar. | + | * server_id - id-ul server-ului care a executat request-ul |
+ | * server_response - un string de lungime maximă **MAX_RESPONSE_LENGTH**, putând conține fie conținutul documentului cerut, fie un mesaj tip în funcție de flow-ul request-ului în server, fiind definit prin macro-urile: **MSG_A**, **MSG_B**, **MSG_C** | ||
+ | * server_log - un string de lungime maximă **MAX_LOG_LENGTH**, care indică informații relevante referitoare la starea curentă a serverului, fiind definit prin macro-urile: **LOG_HIT**, **LOG_MISS**, **LOG_EVICT**, **LOG_FAULT**, **LOG_LAZY_EXEC** | ||
- | Fiecare obiect trebuie sa aparţină unui singur server. Astfel, **serverul responsabil să stocheze un anumit obiect va fi cel mai apropiat de pe hash ring în direcţia acelor de ceas**. | + | La nivelul server-ului sunt definite următoarele operații: |
+ | * editează un document (**"EDIT <document_name> <conținut_document>"**) | ||
+ | * obține conținutul unui document (**"GET <document_name>"**) | ||
- | Să presupunem că avem următorul set de servere ce urmează să primească obiecte şi rezultatul funcției de hashing după ce a fost aplicată pe id-ul acestora: | + | Când se primește orice request: |
+ | * executat lazy - se returnează **"Request- <request_type> <document_name> - has been added to queue"** | ||
+ | * executat direct - se returnează conținutul efectiv | ||
+ | |||
+ | În continuare, explicăm flow-ul execuției fiecărui tip de request în momentul în care este rulat. | ||
+ | |||
+ | {{ :sd-ca:teme:cache_flow.v2.png?800 }} | ||
+ | |||
+ | * **EDIT** | ||
+ | - Se adaugă request-ul în coada de task-uri si se intoarce un struct response, in care: | ||
+ | * server_response = MSG_A: **“Request- EDIT <doc_name> - has been added to queue”** | ||
+ | * server_log = LOG_LAZY_EXEC: **"Task queue size is <size_of_task_queue>"** | ||
+ | - În momentul în care trebuie executat, se scoate task-ul din front-ul cozii și se stochează conținutul documentului în cache. | ||
+ | - Dacă nu există, se dă un **CACHE MISS**, este adus din memoria principală în cache conținutul asociat documentului existent; | ||
+ | - Se scrie in cache si in baza de date continutul asociat cu numele documentului | ||
+ | - Membrul server_response al structurii response returnate trebuie asociat cu unul dintre urmatoarele mesaje: | ||
+ | * MSG_B: **“Document <doc_name> has been overridden”** | ||
+ | * MSG_C: **“Document <doc_name> has been created”** | ||
+ | - Câmpul server_log din response trebuie asociat, in functie de caz, cu unul dintre mesajele: | ||
+ | * LOG_HIT: **“Cache HIT for <doc_name>”** | ||
+ | * LOG_MISS: **“Cache MISS for <doc_name>”** | ||
+ | * LOG_EVICT: **“Cache MISS for <doc_name> - cache entry for <evicted_doc_name> has been evicted”** | ||
+ | * **GET** | ||
+ | - Se execută fiecare operație stocată în coada de task-uri (respectând ordinea impusă de coadă) şi se afişează response-ul returnat de fiecare operaţie, folosind macro-ul **PRINT_RESPONSE**. | ||
+ | - După ce se execută toate task-urile din coadă, server-ul verifică dacă există o intrare în cache: | ||
+ | - Dacă există, se returnează. | ||
+ | - Altfel, server-ul se uită în memoria principală: | ||
+ | - dacă există, îl aduce în cache și îl returnează; | ||
+ | - dacă nu există, se returnează un mesaj de eroare. | ||
+ | - Câmpul server_response al structurii response returnate trebuie sa fie un pointer către o clonă a conținutului documentului, în cazul în care acesta există pe server. Altfel, trebuie să fie NULL | ||
+ | - server_log contine unul din mesajele prezentate la **GET** sau: | ||
+ | - LOG_FAULT: **“Document <doc_name> doesn't exist”**, in cazul in care documentul cautat nu exista | ||
+ | |||
+ | === LRU Cache === | ||
+ | În cazul sistemului nostru, va trebui să implementăm un cache software, care să se bazeze pe un hashtable, în care numele documentelor reprezintă cheile, iar conținutul - valoarea. Fiecare server din topologie va avea o anumită dimensiune a cache-ului, exprimată ca număr de intrări, și pasată ca argument la crearea unui nou server (vezi operația de **ADD_SERVER**). | ||
+ | |||
+ | <note> La nivel conceptual, acest cache reduce timpul de acces al fiecărui server la documentele păstrate în baza de date locală. Practic, în cazul nostru, atât baza de date, cât și cache-ul fiecărui server vor fi stocate în RAM, pentru ușurința implementării, așa că avantajele utilizării unui cache software nu vor fi vizibile dacă urmărim timpii de acces la documente. </note> | ||
+ | |||
+ | Având o dimensiune limitată, la un moment dat vom ajunge în situația în care dorim să adăugăm o nouă intrare în cache, însă acesta este deja plin. Pentru a trata acest scenariu, va trebui să decidem ce intrare eliminăm din cache pentru a face spațiu pentru noile date. | ||
+ | |||
+ | **Least Recently Used (LRU) Cache** se bazează pe principiul că există o probabilitate foarte mare ca datele accesate cel mai recent să fie accesate din nou în viitorul apropiat. Astfel, în momentul în care cache-ul s-a umplut, politica de evacuare a elementelor din cache specifică faptul că trebuie eliminată intrarea care a fost accesată în urmă cu cel mai mult timp. | ||
+ | |||
+ | <note>Implementarea unui astfel de cache necesită și folosirea unei structuri sau mai multe structuri de date care să rețină ordonate documentele în funcție de momentul accesării</note> | ||
+ | |||
+ | Cea mai uzuală metodă de a implementa acest cache este prin: | ||
+ | - **o listă dublu înlănțuită**, care va reține **ordinea** în care au fost accesate intrările din cache, la finalul listei fiind poziționate intrările cu care am interacționat cel mai recent, iar la începutul listei fiind cele mai vechi intrări | ||
+ | - **un hashmap**, în care se păstrează cheile (intrărilor din cache) asociate cu pointeri la nodurile corespunzătoare din listă | ||
+ | |||
+ | <note>Aveți libertatea de a implementa în ce mod doriți, cât timp respectați proprietățile unui LRU Cache(adăugarea și ștergea să fie în O(1), iar, de fiecare dată când trebuie eliminată o intrare, să fie eliminată cea mai veche)</note> | ||
+ | |||
+ | În momentul in care o valoare asociata unei chei deja existente in cache trebuie actualizata, vom aplica o politica de scriere **write-through**, actualizand valoarea atat in cache, cat si in baza de date din memoria locala a serverului, pentru a pastra coerenta dintre cache si memorie. | ||
+ | |||
+ | Funcțiile principale pe care cache-ul vostru trebuie să le implementeze sunt: | ||
+ | * **put(document_name, document_content)** | ||
+ | * verifică dacă există deja în cache un document cu numele primit | ||
+ | * dacă există, actualizează conținutul | ||
+ | * dacă nu există: | ||
+ | * în cazul în care este suficient spațiu liber în cache, introduce o nouă intrare | ||
+ | * altfel, elimină cea mai veche intrare | ||
+ | * **get(document_name)** | ||
+ | * returnează conținutul asociat cu numele primit ca parametru | ||
+ | |||
+ | <note> Pentru ambele operații trebuie luat în calcul că, după execuția lor, intrarea asociată cu numele documentului primit ca parametru este cea mai recentă din cache.</note> | ||
+ | |||
+ | |||
+ | ==== Load Balancer(30p) ==== | ||
+ | |||
+ | {{ :sd-ca:teme:arhitectura.png?550 }} | ||
+ | |||
+ | |||
+ | Distribuția serverelor poate fi modificată folosind următoarele comenzi: | ||
+ | * **ADD_SERVER <server_id> <number_of_cached_documents>** | ||
+ | * **REMOVE_SERVER <server_id>** | ||
+ | |||
+ | Inițial, nu există niciun server în sistem. În momentul în care load balancer-ul primește una dintre comenzi, anumite documente și task-uri sunt redistribuite între servere (vezi logica de la Consistent Hashing). | ||
+ | |||
+ | |||
+ | Astfel, flow-ul inerent fiecărei comenzi referitoare la topologie este: | ||
+ | - **(Doar pentru ADD_SERVER)** Se creează server-ul cu un cache gol. | ||
+ | - Se dau pop-uri succesive pe coada de task-uri a serverului sursă, se executa fiecare comandă eliminată din coadă, şi se foloseşte macro-ul PRINT_RESPONSE pe fiecare rezultat returnat | ||
+ | - Se parcurg toate documentele stocate în baza de date locală a serverului sursă, iar cele care trebuie mutate sunt eliminate din cache şi mutate | ||
+ | în baza de date a serverului destinaţie (fară a fi adăugate în cache). | ||
+ | |||
+ | <note> | ||
+ | Prin **server sursă**, ne referim la: | ||
+ | - serverul vecin de pe care vom prelua documente, pentru ADD_SERVER | ||
+ | - serverul care este eliminat din topologie, în cazul operaţiei de REMOVE_SERVER | ||
+ | |||
+ | Prin **server destinaţie**, ne referim la: | ||
+ | - serverul nou adăugat în topologie, pentru ADD_SERVER | ||
+ | - serverul vecin care va primi toate documentele din baza de date a serverului şters, în cazul operaţiei de REMOVE_SERVER | ||
+ | </note> | ||
+ | |||
+ | === Consistent Hashing === | ||
+ | Consistent Hashing este o metoda de hashing distribuit prin care atunci când tabelul este redimensionat, se vor remapa în medie doar ''n / m'' chei, unde //n// este numărul curent de chei, iar //m// este numărul de slot-uri (în cazul nostru servere). În implementare, se va folosi un cerc imaginar numit //hash ring// pe care sunt mapate atât numele documentelor, cât și id-urile serverelor. Această mapare se realizează cu ajutorul unei funcții de hashing cu valori între MIN_HASH și MAX_HASH, punctele din acest interval fiind distribuite la distante egale în intervalul logic [0°, 360°] de pe cercul imaginar. | ||
+ | |||
+ | Fiecare document trebuie sa aparţină unui singur server. Astfel, **serverul responsabil să stocheze un anumit document va fi cel mai apropiat de pe hash ring în direcţia acelor de ceas**. | ||
+ | |||
+ | Să presupunem că avem următorul set de servere ce urmează să primească documente şi rezultatul funcției de hashing după ce a fost aplicată pe id-ul acestora: | ||
^ ID Server ^ Hash ^ | ^ ID Server ^ Hash ^ | ||
| 2 | 2269549488 | | | 2 | 2269549488 | | ||
Line 89: | Line 183: | ||
{{ :sd-ca:teme:2021-tema2-hash-ring-1.png?450 }} | {{ :sd-ca:teme:2021-tema2-hash-ring-1.png?450 }} | ||
- | Acum vom adăuga în sistem un set de obiecte ce urmează a fi distribuite către serverele deja existente. În momentul în care un obiect este adăugat în sistem, va trebui să căutăm primul server a cărui funcţie hash este mai "mare" (în sensul acelor de ceas, atenţie la logica circulară) decât funcţia hash a obiectului. | + | Acum vom adăuga în sistem o mulţime de documente ce urmează a fi distribuite către serverele deja existente. În momentul în care un document este adăugat în sistem, va trebui să căutăm primul server al cărui ID hash-uit este mai "mare" (în sensul acelor de ceas, atenţie la logica circulară) decât hash-ul numelui documentului. |
- | Pentru a produce interogări cât mai eficiente, hash ring-ul va fi reţinut în memorie ca un array ordonat crescător de etichete de servere. Ordonarea se va face în funcţie de hash, iar în cazul în care 2 etichete de servere au aceeaşi valoare hash, se va sorta crescător după ID-ul serverului. | + | Pentru a produce interogări cât mai eficiente, hash ring-ul va fi reţinut în memorie ca un **array ordonat crescător** de etichete de servere. Ordonarea se va face în funcţie de hash, iar în cazul în care 2 etichete de servere au aceeaşi valoare hash, se va sorta crescător după ID-ul serverului. |
- | <note important>**Atentie!** În implementare obiectele **NU** vor fi puse pe hash ring. Ele vor fi stocate pe servere. Reprezentările grafice vor pune obiectele pe hash ring pentru a explica mai uşor conceptul şi a putea vizualiza care este cel mai apropiat server.</note> | + | <note important>**Atentie!** În implementare documentele **NU** vor fi stocate pe hash ring. Ele vor fi păstrate pe servere. Reprezentările grafice vor pune numele documentelor pe hash ring pentru a explica mai uşor conceptul şi a putea vizualiza care este cel mai apropiat server.</note> |
- | ^ Key / ID Server ^ Hash ^ Stored on ^ | + | ^ Doc Name / ID Server ^ Hash ^ Stored on ^ |
| Jade | 1633428562 | 2 | | | Jade | 1633428562 | 2 | | ||
| 2 | 2269549488 | - | | | 2 | 2269549488 | - | | ||
Line 108: | Line 202: | ||
- | ==== Replici ==== | + | == Eliminare == |
- | În practică, pentru a ne asigura că obectele sunt distribuite cât mai uniform pe servere se foloseşte următorul artificiu: Fiecare server va fi adaugat de mai multe ori pe hash ring (Aceste servere se vor numi “replici”). Acest mecanism se va realiza prin asocierea unei etichete artificiale fiecarei replici, plecând de la id-ul server-ului de bază: | + | În cazul în care unul din servere este eliminat din sistem, toate replicile sale sunt eliminate de pe hash ring, iar documentele salvate pe acestea sunt remapate la cele mai apropiate servere ramase (în sensul acelor de ceas). |
+ | |||
+ | În exemplul nostru vom presupune că serverul cu id 0 va fi eliminat. În acest moment documentele denumite “Pearl” şi “Sapphire” vor fi redistribuite către serverul 1. | ||
+ | |||
+ | ^ Doc Name / ID Server ^ Hash ^ Stored on ^ | ||
+ | | Jade | 1633428562 | 2 | | ||
+ | | 2 | 2269549488 | - | | ||
+ | | Pearl | 3421657995 | 1 | | ||
+ | | Sapphire | 5000799124 | 1 | | ||
+ | | Amethyst | 7594634739 | 1 | | ||
+ | | 1 | 8077113362 | - | | ||
+ | | Ruby | 9787173343 | 2 | | ||
+ | |||
+ | {{ :sd-ca:teme:2021-tema2-hash-ring-99.png?450 }} | ||
+ | |||
+ | == Adăugare == | ||
+ | În cazul adăugării unui nou server în sistem, se vor lua toate documentele de pe serverele vecine (succesoare în sensul acelor de ceas) si se va verifica dacă vor fi remapate către serverul nou sau nu. Dacă un document trebuie să fie mapat pe serverul nou, valoarea sa va fi transferată de pe serverul vechi, iar serverul vechi o va şterge. | ||
+ | |||
+ | Să presupunem că se adaugă un server cu id 3. Serverul care va fi vecin celui cu id 3 pe hash ring (serverul 1, în cazul de faţă) va trebui să îşi redistribuie documentele: | ||
+ | |||
+ | ^ Doc Name / ID Server ^ Hash ^ Stored on ^ | ||
+ | | Jade | 1633428562 | 2 | | ||
+ | | 2 | 2269549488 | - | | ||
+ | | Pearl | 3421657995 | 3 | | ||
+ | | 3 | 4514215965 | - | | ||
+ | | Sapphire | 5000799124 | 1 | | ||
+ | | Amethyst | 7594634739 | 1 | | ||
+ | | 1 | 8077113362 | - | | ||
+ | | Ruby | 9787173343 | 2 | | ||
+ | |||
+ | {{ :sd-ca:teme:2021-tema2-hash-ring-100.png?450 }} | ||
+ | |||
+ | |||
+ | |||
+ | ==== Bonus - Replici (20p) ==== | ||
+ | |||
+ | În practică, pentru a ne asigura că obiectele sunt distribuite cât mai uniform pe servere se foloseşte următorul artificiu: Fiecare server va fi adaugat de mai multe ori pe hash ring (Aceste servere se vor numi “replici” sau “noduri virtuale”). Acest mecanism se va realiza prin asocierea unei etichete artificiale fiecarei replici, plecând de la id-ul server-ului de bază: | ||
<code>eticheta = replica_id * 10^5 + server_id;</code> | <code>eticheta = replica_id * 10^5 + server_id;</code> | ||
Line 117: | Line 247: | ||
<note important>În implementarea noastră, fiecare server va fi replicat de fix 3 ori (în total va fi reprezentat în 3 puncte).</note> | <note important>În implementarea noastră, fiecare server va fi replicat de fix 3 ori (în total va fi reprezentat în 3 puncte).</note> | ||
- | ^ Key / Tag Server ^ Hash ^ Stored on ^ | + | Fie următorul exemplu: |
+ | |||
+ | ^ Doc Name / Tag Server ^ Hash ^ Stored on ^ | ||
| 100000 | 1093130520 | - | | | 100000 | 1093130520 | - | | ||
| Jade | 1633428562 | 2 | | | Jade | 1633428562 | 2 | | ||
Line 134: | Line 266: | ||
{{ :sd-ca:teme:2021-tema2-hash-ring-3.png?450 }} | {{ :sd-ca:teme:2021-tema2-hash-ring-3.png?450 }} | ||
- | ==== Eliminare ==== | ||
- | În cazul în care unul din servere este eliminat din sistem, toate replicile sale sunt eliminate de pe hash ring, iar obiectele salvate pe acestea sunt remapate la cele mai apropiate servere ramase (în sensul acelor de ceas). | ||
- | În exemplul nostru vom presupune că serverul cu id 0 va fi eliminat. În acest moment obiectele “Sapphire” şi “Ruby” vor fi redistribuite | ||
- | ^ Key / Tag Server ^ Hash ^ Stored on ^ | + | În cazul cazul în care vrem să eliminăm serverul cu id 0, documentele denumite “Sapphire” şi “Ruby” vor fi redistribuite: |
+ | |||
+ | ^ Doc Name / Tag Server ^ Hash ^ Stored on ^ | ||
| Jade | 1633428562 | 2 | | | Jade | 1633428562 | 2 | | ||
| 000002 | 2269549488 | - | | | 000002 | 2269549488 | - | | ||
Line 154: | Line 285: | ||
{{ :sd-ca:teme:2021-tema2-hash-ring-4.png?450 }} | {{ :sd-ca:teme:2021-tema2-hash-ring-4.png?450 }} | ||
- | ==== Adăugare ==== | ||
- | În cazul adăugării unui nou server în sistem, se vor lua toate obiectele de pe serverele vecine (succesoare în sensul acelor de ceas) si se va verifica dacă vor fi remapate către serverul nou sau nu. Dacă un obiect trebuie să fie mapat pe serverul nou, valoarea sa va fi transferată de pe serverul vechi, iar serverul vechi o va şterge. | ||
- | Să presupunem că se adaugă un server cu id 4. Serverele care vor fi vecine celui cu id 4 (oricare replică a lui) vor trebui să îşi redistribuie obiectele: | ||
- | ^ Key / Tag Server ^ Hash ^ Stored on ^ | + | Să presupunem că se adaugă un server cu id 4. Serverele care vor fi vecine celui cu id 4 (oricare replică a lui) vor trebui să îşi redistribuie documentele: |
+ | |||
+ | ^ Doc Name / Tag Server ^ Hash ^ Stored on ^ | ||
| Jade | 1633428562 | 2 | | | Jade | 1633428562 | 2 | | ||
| 000002 | 2269549488 | - | | | 000002 | 2269549488 | - | | ||
Line 177: | Line 307: | ||
{{ :sd-ca:teme:2021-tema2-hash-ring-5.png?450 }} | {{ :sd-ca:teme:2021-tema2-hash-ring-5.png?450 }} | ||
- | În situaţia de mai sus, serverul 2 a trebuit să distribuie serverului 4 obiectele “Ruby” si “Sapphire”. Este o coincidenţă că ambele obiecte redistribuite vin de la acelasi server, puteau veni de la replici ale unor servere diferite. | + | În situaţia de mai sus, serverul 2 a trebuit să distribuie serverului 4 documentele cu numele “Ruby” si “Sapphire”. Este o coincidenţă că ambele documente redistribuite vin de la acelasi server, puteau veni de la replici ale unor servere diferite. |
===== Implementare ===== | ===== Implementare ===== | ||
- | În {{:sd-ca:teme:2021-tema2-v3.zip|scheletul de cod}} veţi avea de implementat funcţiile din fişierele ''load_balancer.c'' şi ''server.c''. Citiţi cu atenţie semnăturile acestor funcţii! Scheletul vine deja cu logica pentru client, parsarea fisierului de intrare şi generarea fişierului de ieşire. | ||
- | <note important>**ATENȚIE!** Functia ''unsigned int hash_function_servers(void *a)'' va fi folosită pentru a genera hash-ul etichetei unui server, iar funcţia ''unsigned int hash_function_key(void *a)'' va fi folosită pentru a genera hash-ul unui obiect. **Folosiți aceste funcții pentru a calcula pozitiile pe hashring**, dar nu le modificaţi deoarece veţi obţine alte rezultate decât cele aşteptate de checker. </note> | + | În fișierul main.c din schelet este deja implementată partea ce ține de client si modul în care acesta apelează sistemul distribuit, dar și partea de gestiune a sistemului ce va apela funcțiile de pornire si oprire a serverelor atunci când este cazul. Trebuie să implementați doar funcționalitățile din load_balancer.c, server.c şi lru_cache.c, entry point-ul către sistemul distribuit fiind prin Load Balancer, iar toate apelurile către acesta fiind deja implementate în schelet. |
+ | |||
+ | În headere, veţi întâlni macro-uri pentru mesajele pe care trebuie sa le printaţi, cât şi alte macro-uri utile: | ||
+ | * **MAX_LOG_LENGTH** - dimensiunea maximă pe care o poate avea field-ul **server_log** din structura **response** | ||
+ | * **MAX_RESPONSE_LENGTH** - dimensiunea maximă pe care o poate avea field-ul **server_response** din structura **response** | ||
+ | * **PRINT_RESPONSE** - primeşte un pointer la o structură de tipul **response**, şi printează server_id-ul serverului care a returnat-o, cât şi cele doua field-uri menţionate mai sus (în plus, dezalocă complet memoria structurii trimise ca parametru) | ||
+ | * **TASK_QUEUE_SIZE** - dimeansiunea maximă a cozii de task-uri a fiecărui server | ||
+ | * **MAX_SERVERS** - numărul maxim de servere din topologie | ||
+ | |||
+ | Load balancer-ul ar trebui sa interactioneze cu serverele folosind functia **server_handle_request**. Functiile specifice executiei fiecarui tip de request (**server_edit_document**, **server_get_document**) nu sunt expuse in server.h, si nu ar trebui sa fie apelate direct de catre o alta entitate, in afara de server. | ||
+ | |||
+ | **server_handle_request** va returna load balancer-ului o structura response, care va fi returnata la randul ei in main, acolo unde este printata. | ||
+ | |||
+ | Structura response va fi folosita pentru a pastra atat mesajul care trebuie printat de server la finalul executiei unei operatii, cat si alte date auxiliare pe care vi se pare important sa le memorati. | ||
+ | |||
+ | <note> | ||
+ | La bonus, pentru mesajele de logging, campul **server_id** din structura **response** returnata de server dupa primirea unui request trebuie sa se refere la eticheta pe care o are nodul virtual catre care se adreseaza operatia. | ||
+ | </note> | ||
+ | |||
+ | <note> | ||
+ | Pentru a face conversia din tipul request-ului(acest tip este stocat într-un enum) în numele lui efectiv, vă puteți folosi de funcția **get_request_type_str** din utils.h. | ||
+ | |||
+ | Aceasta poate fi folosită pentru anumite mesaje de răspuns ce necesită folosirea tipului request-ului. | ||
+ | </note> | ||
+ | |||
+ | |||
+ | Puteţi începe implementând logica de tratare a request-urilor în server, şi folosind câmpul temporar **test_server** din structura de load_balancer. | ||
+ | |||
+ | |||
+ | Odată ce aţi finalizat implementarea serverului, puteţi elimina field-ul **test_server**, şi să vă implementaţi propria logică de load balancing. | ||
+ | |||
+ | |||
+ | Testele sunt formate dintr-o linie unde se specifică numărul total de request-uri, urmat de **ENABLE_VNODES** (opţional, doar pentru testele bonus, în care se verifică utilizarea replicilor), respectiv cate o linie pentru fiecare request. | ||
+ | |||
+ | <spoiler Click pentru input-ul unui test> | ||
+ | <code json> | ||
+ | 6 | ||
+ | ADD_SERVER 100 5 | ||
+ | EDIT "manager.txt" "Box understand feel." | ||
+ | GET "manager.txt" | ||
+ | EDIT "manager.txt" "Mouth attorney." | ||
+ | GET "manager.txt" | ||
+ | GET "produce.txt" | ||
+ | </code> | ||
+ | </spoiler> | ||
+ | |||
+ | <spoiler Click pentru output-ul unui test> | ||
+ | <code json> | ||
+ | [Server 100]-Response: Request- EDIT manager.txt - has been added to queue | ||
+ | [Server 100]-Log: Task queue size is 1 | ||
+ | |||
+ | [Server 100]-Response: Document manager.txt has been created | ||
+ | [Server 100]-Log: Cache MISS for manager.txt | ||
+ | |||
+ | [Server 100]-Response: Box understand feel. | ||
+ | [Server 100]-Log: Cache HIT for manager.txt | ||
+ | |||
+ | [Server 100]-Response: Request- EDIT manager.txt - has been added to queue | ||
+ | [Server 100]-Log: Task queue size is 1 | ||
+ | |||
+ | [Server 100]-Response: Document manager.txt has been overridden | ||
+ | [Server 100]-Log: Cache HIT for manager.txt | ||
+ | |||
+ | [Server 100]-Response: Mouth attorney. | ||
+ | [Server 100]-Log: Cache HIT for manager.txt | ||
+ | |||
+ | [Server 100]-Response: (null) | ||
+ | [Server 100]-Log: Document produce.txt doesn't exist | ||
+ | |||
+ | |||
+ | </code> | ||
+ | </spoiler> | ||
+ | |||
+ | |||
+ | ===== Restricţii ===== | ||
+ | |||
+ | * Se execută **cel puţin** o operaţie de ADD_SERVER înainte de orice prelucrare de fişiere. | ||
+ | * Pentru a stabili poziţia unui server pe hash ring, se va folosi funcţia **hash_uint()**, aplicată pe ID-ul serverului (în cazul bonusului, se va aplica pe eticheta nodului virtual). | ||
+ | * Pentru a stabili unde s-ar poziţiona un document pe hash ring, se va folosi funcţia **hash_string()**, aplicată pe numele documentului. | ||
+ | * Numărul de request-uri care ar trebui stocate la un moment dat in task_queue nu va depaşi niciodată dimensinea maximă a queue-ului, **TASK_QUEUE_SIZE**. | ||
+ | * La bonus, pentru nodurile virtuale nu trebuie alocate un cache şi o bază de date noi | ||
- | ===== Explicații adiționale ===== | + | ===== Schelet ===== |
- | Puteti vizualiza [[https://docs.google.com/presentation/d/1gdy9Al3KtNzUL2a42d9lbDxnlUfn4P0FUX62FR9OPcs/edit?usp=sharing |aici]] fiecare operatie din test1.in. | + | Scheletul și checker-ul se regăsesc în [[https://github.com/sd-pub/Tema2-2024|acest repository de Github]]. |
===== Punctaj ===== | ===== Punctaj ===== | ||
* 80p teste: **fiecare** test este verificat cu valgrind. Dacă un test are memory leaks, nu va fi punctat. | * 80p teste: **fiecare** test este verificat cu valgrind. Dacă un test are memory leaks, nu va fi punctat. | ||
+ | * Testele 0-10 testează implementarea serverului | ||
+ | * Testele 11-20 testează implementarea load balancer-ului | ||
* 10p coding style | * 10p coding style | ||
* 10p README | * 10p README | ||
+ | * 20p BONUS 😀 | ||
+ | * Testele 21-30 testează implementarea replicilor în load balancer | ||
* O tema care nu compilează va primi 0 puncte. | * O tema care nu compilează va primi 0 puncte. | ||
<note warning> | <note warning> | ||
- | **Nu copiați!** Toate soluțiile vor fi verificate folosind o unealtă de detectare a plagiatului. În cazul detectării unui astfel de caz, atât plagiatorul cât și autorul original (nu contează cine e) vor primi punctaj 0 pe toate temele! | + | **Nu copiați!** Toate soluțiile vor fi verificate folosind o unealtă (⛏️) de detectare a plagiatului. În cazul detectării unui astfel de caz, atât plagiatorul cât și autorul original (nu contează cine e) vor primi punctaj 0 pe toate temele! |
De aceea, vă sfătuim să nu vă lăsați rezolvări ale temelor pe calculatoare partajate (la laborator etc), pe mail/liste de discuții/grupuri etc. | De aceea, vă sfătuim să nu vă lăsați rezolvări ale temelor pe calculatoare partajate (la laborator etc), pe mail/liste de discuții/grupuri etc. | ||
</note> | </note> | ||
Line 201: | Line 413: | ||
**Q**: Putem modifica scheletul / adăuga funcţii? \\ | **Q**: Putem modifica scheletul / adăuga funcţii? \\ | ||
- | **A**: Da. În schimb, nu puteti modifica antetele functiilor deja existente in load_balancer.h / server.h. | + | **A**: Da. |
**Q**: Ce funcţii de hashing putem folosi pentru server? \\ | **Q**: Ce funcţii de hashing putem folosi pentru server? \\ | ||
- | **A**: Puteţi folosi orice funcţie doriţi (inclusiv pe cele din laborator). | + | **A**: Puteţi folosi orice funcţie doriţi atât pentru baza de date a serverului, cât şi pentru cache (inclusiv pe cele din laborator/schelet). |
- | + | ||
- | **Q**: Cum funcţionează checker-ul? \\ | + | |
- | **A**: Checker-ul verifică faptul că obiectele adăugate de voi pe servere să respecte proprietăţile aflate în cerinţă. În fişierele ref vor apărea mesaje de forma //"Stored #product_name on server #server_id."// şi //"Retrieved #product_name from server #server_id."// În cazul schimbării funcţiei de hashing pentru servere / obiecte sau al schimbării criteriului de matching dintre un server şi un obiect, vor apărea diferenţe între rezultatul vostru şi cel din fişierul ref. | + | |
- | + | ||
- | **Q**: Putem implementa tema în C++? \\ | + | |
- | **A**: Nu. | + | |
===== Link-uri utile ===== | ===== Link-uri utile ===== | ||
[[https://www.toptal.com/big-data/consistent-hashing]] \\ | [[https://www.toptal.com/big-data/consistent-hashing]] \\ | ||
[[https://www.youtube.com/watch?v=zaRkONvyGr8&ab_channel=GauravSe]] | [[https://www.youtube.com/watch?v=zaRkONvyGr8&ab_channel=GauravSe]] | ||
- | |||
- | </hidden> |