Responsabili:
17 aprilie 2021 22:52: Corectare test12-16.ref
22 aprilie 2021 10:00: Corectare checker
22 aprilie 2021 20:55: Corectare schelet (main.c) + test5,12-16.ref
22 aprilie 2021 23:30: Adăugare instanţă de upload pe vmchecker.
În urma realizării acestei teme veţi:
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.
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.
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.
<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.
server_id = hash(data) % NUM_SERVERS
Astfel, load balancer-ul îşi îndeplineşte scopul şi reuşeşte să distribuie uniform datele spre toate cele NUM_SERVERS servere din sistem. În schimb, dezavantajul acestei metode este faptul că într-un sistem real numărul de servere nu este niciodata constant. Dacă unul din servere dispare, din cauza acestei metode simpliste ar trebui să redirijam toate datele din sistem.
Exemplu:
NUM_SERVERS = 10
server_id = 0x65b71f5b % 10 = 1
NUM_SERVERS = 9
Vor trebui verificate toate obiectele din sistem pentru că hash(data) % NUM_SERVERS va avea altă valoare față de cea precedentă.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.Observăm că această metodă este foarte ineficientă.
Î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.
Client - Are la dispozitie 2 operaţii:
Load Balancer (Main Server) - Are la dispoziţie 4 operaţii:
Server - Are la dispoziţei 3 operaţii:
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.
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.
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.
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:
ID Server | Hash |
---|---|
2 | 2269549488 |
0 | 5572014558 |
1 | 8077113362 |
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.
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.
Key / ID Server | Hash | Stored on |
---|---|---|
Jade | 1633428562 | 2 |
2 | 2269549488 | - |
Pearl | 3421657995 | 0 |
Sapphire | 5000799124 | 0 |
0 | 5572014558 | - |
Amethyst | 7594634739 | 1 |
1 | 8077113362 | - |
Ruby | 9787173343 | 2 |
Î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ă:
eticheta = replica_id * 10^5 + server_id;
Key / Tag Server | Hash | Stored on |
---|---|---|
100000 | 1093130520 | - |
Jade | 1633428562 | 2 |
000002 | 2269549488 | - |
200002 | 2717580620 | - |
Pearl | 3421657995 | 1 |
200001 | 4633428562 | - |
Sapphire | 5000799124 | 0 |
000000 | 5572014558 | - |
100002 | 6252163898 | - |
Amethyst | 7594634739 | 1 |
100001 | 7613173320 | - |
200000 | 7893130520 | - |
000001 | 8077113362 | - |
Ruby | 9787173343 | 0 |
Î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 |
---|---|---|
Jade | 1633428562 | 2 |
000002 | 2269549488 | - |
200002 | 2717580620 | - |
Pearl | 3421657995 | 1 |
200001 | 4633428562 | - |
Sapphire | 5000799124 | 2 |
100002 | 6252163898 | - |
Amethyst | 7594634739 | 1 |
100001 | 7613173320 | - |
000001 | 8077113362 | - |
Ruby | 9787173343 | 2 |
Î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 |
---|---|---|
Jade | 1633428562 | 2 |
000002 | 2269549488 | - |
200002 | 2717580620 | - |
Pearl | 3421657995 | 1 |
200001 | 4633428562 | - |
Sapphire | 5000799124 | 4 |
000004 | 5421603348 | - |
200004 | 6017580621 | - |
100002 | 6252163898 | - |
Amethyst | 7594634739 | 1 |
100001 | 7613173320 | - |
000001 | 8077113362 | - |
Ruby | 9787173343 | 4 |
100004 | 9945918562 | - |
Î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 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.
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.
Puteti vizualiza aici fiecare operatie din test1.in.
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.
Q: Ce funcţii de hashing putem folosi pentru server?
A: Puteţi folosi orice funcţie doriţi (inclusiv pe cele din laborator).
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.