Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Uvod

Prije nekog vremena dobio sam zadatak da razvijem failover cluster za PostgreSQL, koji rade u nekoliko podatkovnih centara povezanih optičkim vlaknima unutar jednog grada, a sposobni su izdržati kvar (primjerice, blackout) jednog podatkovnog centra. Kao softver koji je odgovoran za toleranciju grešaka, izabrao sam Pejsmejkerjer je ovo službeno rješenje RedHata za stvaranje failover klastera. Dobro je jer za to podržava RedHat i jer je ovo rješenje univerzalno (modularno). Uz njegovu pomoć bit će moguće osigurati toleranciju na pogreške ne samo PostgreSQL-a, već i drugih usluga, bilo korištenjem standardnih modula ili njihovom izradom za specifične potrebe.

Ova odluka potaknula je razumno pitanje: koliko će failover cluster biti otporan na greške? Kako bih to istražio, razvio sam testni uređaj koji simulira različite kvarove na čvorovima klastera, čeka da se usluga obnovi, oporavlja neispravni čvor i nastavlja testiranje u petlji. Ovaj se projekt izvorno zvao hapgsql, no s vremenom mi je dosadio naziv koji je imao samo jedan samoglasnik. Stoga sam počeo nazivati ​​baze podataka otporne na greške (i float IP koji je upućivao na njih) krogan (lik iz računalne igre u kojoj su duplicirani svi bitni organi), a čvorovi, klasteri i sam projekt su tučanka (planeta na kojoj žive krogani).

Sada je uprava dopustila otvorite projekt zajednici otvorenog koda pod licencom MIT-a. README će uskoro biti preveden na engleski (jer se očekuje da će glavni potrošači biti Pacemaker i PostgreSQL programeri), a ja sam odlučio predstaviti staru rusku verziju README-a (djelomično) u obliku ovog članka.

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Klasteri su raspoređeni na virtualnim strojevima VirtualBox. Ukupno će se postaviti 12 virtualnih strojeva (ukupno 36GiB), koji tvore 4 klastera otporna na greške (različite opcije). Prva dva klastera sastoje se od dva PostgreSQL poslužitelja koji se nalaze u različitim podatkovnim centrima i zajedničkog poslužitelja svjedok c kvorum uređaj (hostiran na jeftinom virtualnom stroju u trećem podatkovnom centru), što rješava nesigurnost 50% / 50%, dajući svoj glas jednoj od stranaka. Treći klaster u tri podatkovna centra: jedan master, dva slave, br kvorum uređaj. Četvrti klaster sastoji se od četiri PostgreSQL poslužitelja, dva po podatkovnom centru: jedan glavni, ostali replike, a također koristi svjedok c kvorum uređaj. Četvrti može izdržati kvar dva poslužitelja ili jednog podatkovnog centra. Ovo se rješenje može skalirati na veći broj replika ako je potrebno.

Usluga točnog vremena ntpd također rekonfiguriran za toleranciju grešaka, ali koristi samu metodu ntpd (način rada bez roditelja). Zajednički poslužitelj svjedok djeluje kao središnji NTP poslužitelj, raspodjeljujući svoje vrijeme na sve klastere, sinkronizirajući tako sve poslužitelje međusobno. Ako svjedok zakaže ili postane izoliran, tada će jedan od poslužitelja klastera (unutar klastera) početi raspoređivati ​​svoje vrijeme. Pomoćno predmemoriranje HTTP proxy također podignuta na svjedok, uz njegovu pomoć drugi virtualni strojevi imaju pristup Yum repozitoriju. U stvarnosti, usluge kao što su točno vrijeme i proxy poslužitelji najvjerojatnije će biti smještene na namjenskim poslužiteljima, ali u kabini su smještene na svjedok samo radi uštede broja virtualnih strojeva i prostora.

verzije

v0. Radi s CentOS 7 i PostgreSQL 11 na VirtualBoxu 6.1.

Struktura klastera

Svi su klasteri dizajnirani da budu smješteni u više podatkovnih centara, kombinirani u jednu ravnu mrežu i moraju izdržati kvar ili mrežnu izolaciju jednog podatkovnog centra. Zato je nemoguće koristiti za zaštitu od podijeljeni mozak standardna Pacemaker tehnologija tzv KAMENI (Pucaj drugom čvoru u glavu) ili mačevanje. Njegova suština: ako čvorovi u klasteru počnu sumnjati da s nekim čvorom nešto nije u redu, ne reagira ili se nepravilno ponaša, onda ga prisilno isključuju putem "vanjskih" uređaja, na primjer, IPMI ili UPS kontrolne kartice . Ali to će raditi samo u slučajevima kada, u slučaju jednog kvara, IPMI ili UPS poslužitelj nastavlja raditi. Ovdje planiramo zaštititi se od puno katastrofalnijeg kvara, kada cijeli podatkovni centar zakaže (na primjer, izgubi struju). A s takvim odbijanjem sve kameniti-uređaji (IPMI, UPS itd.) također neće raditi.

Umjesto toga, sustav se temelji na ideji kvoruma. Svi čvorovi imaju glas, a mogu raditi samo oni koji mogu vidjeti više od polovice svih čvorova. Ova količina "pola + 1" se zove kvorum. Ako kvorum nije postignut, tada čvor odlučuje da je u mrežnoj izolaciji i mora isključiti svoje resurse, tj. ovo je to zaštita podijeljenog mozga. Ako softver koji je odgovoran za ovo ponašanje ne radi, tada će morati raditi pas čuvar, na primjer, temeljen na IPMI-ju.

Ako je broj čvorova paran (klaster u dva podatkovna centra), može doći do tzv. nesigurnosti 50% / 50% (pola pola) kada mrežna izolacija dijeli klaster točno na pola. Stoga za paran broj čvorova zbrajamo kvorum uređaj je nezahtjevan daemon koji se može pokrenuti na najjeftinijem virtualnom stroju u trećem podatkovnom centru. Daje svoj glas jednom od segmenata (koje vidi) i time rješava nesigurnost 50%/50%. Imenovao sam poslužitelj na kojem će se pokrenuti kvorum uređaj svjedok (terminologija od repmgr, svidjela mi se).

Resursi se mogu premještati s mjesta na mjesto, na primjer, s neispravnih poslužitelja na ispravne ili na naredbu administratora sustava. Kako bi klijenti znali gdje se nalaze potrebni resursi (gdje se povezati?), plutajući IP (float IP). To su IP adrese koje Pacemaker može premještati oko čvorova (sve je na ravnoj mreži). Svaki od njih simbolizira resurs (uslugu) i bit će smješten tamo gdje se morate spojiti kako biste dobili pristup ovoj usluzi (u našem slučaju, bazi podataka).

Tuchanka1 (krug sa zbijanjem)

Struktura

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Ideja je bila da imamo mnogo malih baza podataka s malim opterećenjem, za koje je neisplativo održavati namjenski podređeni poslužitelj u vrućem stanju pripravnosti za transakcije samo za čitanje (nema potrebe za takvim rasipanjem resursa).

Svaki podatkovni centar ima jedan poslužitelj. Svaki poslužitelj ima dvije PostgreSQL instance (u PostgreSQL terminologiji nazivaju se klasteri, ali da ne bude zabune ja ću ih zvati instance (po analogiji s drugim bazama podataka), a Pacemaker klastere ću zvati samo klasteri). Jedna instanca radi u glavnom načinu rada i samo ona pruža usluge (do nje vodi samo float IP). Druga instanca radi kao podređena za drugi podatkovni centar i pružat će usluge samo ako njegov glavni zakaže. Budući da će većinu vremena samo jedna od dvije instance (master) pružati usluge (izvoditi zahtjeve), svi resursi poslužitelja optimizirani su za master (memorija je alocirana za shared_buffers cache, itd.), ali tako da druga instanca također ima dovoljno resursa (iako za neoptimalan rad kroz predmemoriju datotečnog sustava) u slučaju kvara jednog od podatkovnih centara. Slave ne pruža usluge (ne izvodi read only zahtjeve) tijekom normalnog rada klastera, tako da nema rata za resurse s masterom na istom stroju.

U slučaju dva čvora, tolerancija grešaka moguća je samo s asinkronom replikacijom, budući da će kod sinkrone replikacije kvar podređenog dovesti do zaustavljanja nadređenog.

Nesvjedočenje

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Nesvjedočenje (kvorum uređaj) Uzet ću u obzir samo klaster Tuchanka1, sa svim ostalim će biti ista priča. Ako svjedok ne uspije, ništa se neće promijeniti u strukturi klastera, sve će nastaviti raditi na isti način kao i prije. Ali kvorum će postati 2 od 3, pa će svaki sljedeći neuspjeh biti fatalan za klaster. Morat će se to ipak hitno popraviti.

Tuchanka1 odbijanje

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Kvar jednog od podatkovnih centara za Tuchanka1. U ovom slučaju svjedok daje svoj glas drugom čvoru u drugom podatkovnom centru. Tamo se bivši rob pretvara u gospodara, kao rezultat toga, oba mastera rade na istom poslužitelju i obje njihove float IP adrese upućuju na njih.

Tučanka2 (klasična)

Struktura

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Klasična shema dva čvora. Gospodar radi na jednom, rob na drugom. Oba mogu izvršavati zahtjeve (slave je samo za čitanje), tako da na oba ukazuje float IP: krogan2 je master, krogan2s1 je slave. I master i slave će imati toleranciju na greške.

U slučaju dva čvora, tolerancija grešaka moguća je samo kod asinkrone replikacije, jer će kod sinkrone replikacije kvar slave dovesti do zaustavljanja mastera.

Tuchanka2 odbijanje

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Ako jedan od podatkovnih centara zakaže svjedok glasa za drugu. Na jedinom podatkovnom centru koji radi, master će biti podignut, a obje float IP adrese pokazivat će na njega: master i slave. Naravno, instanca mora biti konfigurirana na takav način da ima dovoljno resursa (ograničenja veze, itd.) da istovremeno prihvati sve veze i zahtjeve od glavnog i podređenog float IP-a. To jest, tijekom normalnog rada trebao bi imati dovoljnu zalihu ograničenja.

Tuchanka4 (mnogo robova)

Struktura

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Već druga krajnost. Postoje baze podataka koje primaju puno zahtjeva samo za čitanje (tipičan slučaj web mjesta s velikim opterećenjem). Tuchanka4 je situacija u kojoj mogu postojati tri ili više robova za obradu takvih zahtjeva, ali još uvijek ne previše. S vrlo velikim brojem robova, bit će potrebno izmisliti hijerarhijski sustav replikacije. U minimalnom slučaju (na slici), svaki od dva podatkovna centra ima dva poslužitelja, svaki s PostgreSQL instancom.

Još jedna značajka ove sheme je da je već moguće organizirati jednu sinkronu replikaciju. Konfiguriran je za replikaciju, ako je moguće, u drugi podatkovni centar, a ne u repliku u istom podatkovnom centru kao glavni. Glavni i svaki podređeni su usmjereni prema float IP-u. Srećom, između robova bit će potrebno nekako uravnotežiti zahtjeve sql proxy, na primjer, na strani klijenta. Različite vrste klijenata mogu zahtijevati različite vrste sql proxy, a samo programeri klijenata znaju tko treba što. Ovu funkcionalnost može implementirati ili vanjski demon ili klijentska biblioteka (skup veze), itd. Sve ovo nadilazi temu failover klastera baze podataka (failover SQL proxy može se implementirati neovisno, zajedno s tolerancijom grešaka klijenta).

Tuchanka4 odbijanje

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Ako jedan podatkovni centar (tj. dva poslužitelja) zakaže, svjedok glasuje za drugi. Kao rezultat toga, u drugom podatkovnom centru rade dva poslužitelja: jedan pokreće glavni, a glavni float IP pokazuje na njega (za primanje zahtjeva za čitanje i pisanje); a na drugom poslužitelju postoji podređeni poslužitelj koji radi sa sinkronom replikacijom i jedan od podređenih float IP-ova upućuje na njega (za zahtjeve samo za čitanje).

Prvo što treba primijetiti je da neće svi podređeni float IP-ovi biti radnici, već samo jedan. A za ispravan rad s njim bit će potrebno to sql proxy preusmjerio sve zahtjeve na jedini preostali float IP; i ako sql proxy ne, tada možete ispisati sve float IP slave odvojene zarezima u URL-u veze. U ovom slučaju, sa libpq veza će biti na prvi radni IP, to se radi u sustavu za automatsko testiranje. Možda u drugim knjižnicama, na primjer JDBC, to neće raditi i potrebno je sql proxy. To je učinjeno jer je zabranjeno istovremeno podizanje float IP-ova za podređene poslužitelje na jednom poslužitelju, tako da su ravnomjerno raspoređeni među podređenim poslužiteljima ako ih je nekoliko pokrenuto.

Drugo: čak i u slučaju kvara podatkovnog centra, sinkrona replikacija će se održati. Čak i ako dođe do sekundarnog kvara, odnosno zakaže jedan od dva poslužitelja u preostalom podatkovnom centru, klaster će, iako će prestati pružati usluge, i dalje zadržati informacije o svim izvršenim transakcijama za koje je dao potvrdu predaje. (neće biti informacija o gubitku u slučaju sekundarnog kvara).

Tuchanka3 (3 podatkovna centra)

Struktura

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Ovo je klaster za situaciju u kojoj postoje tri potpuno funkcionalna podatkovna centra, od kojih svaki ima potpuno funkcionalan poslužitelj baze podataka. U ovom slučaju kvorum uređaj nije potrebno. U jednom podatkovnom centru radi master, u druga dva rade slave. Replikacija je sinkrona, tipa ANY (slave1, slave2), odnosno klijent će dobiti potvrdu predaje kada bilo koji od slave-a prvi odgovori da je prihvatio predaju. Resursi su označeni jednom float IP za master i dvije za slave. Za razliku od Tuchanka4, sva tri float IP-a su tolerantna na pogreške. Za ravnotežu SQL upita samo za čitanje možete koristiti sql proxy (s odvojenom tolerancijom greške), ili dodijelite jednu slave float IP polovici klijenata, a drugu polovicu drugom.

Tuchanka3 odbijanje

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Ako jedan od podatkovnih centara zakaže, ostaju dva. U jednom se podižu glavni i float IP od mastera, u drugom - slave i obje slave float IP (instanca mora imati dvostruku rezervu resursa kako bi prihvatila sve veze s oba slave float IP-a). Sinkrona replikacija između gospodara i robova. Također, klaster će spremati informacije o izvršenim i potvrđenim transakcijama (neće biti gubitka informacija) u slučaju uništenja dva podatkovna centra (ako se ne unište istovremeno).

Odlučio sam ne uključiti detaljan opis strukture datoteke i implementacije. Svatko tko se želi igrati može sve pročitati u README-u. Dajem samo opis automatiziranog testiranja.

Automatski sustav testiranja

Kako bi se ispitala otpornost klastera na greške simulacijom različitih grešaka, kreiran je sustav za automatsko testiranje. Pokrenuto skriptom test/failure. Skripta može uzeti kao parametre brojeve klastera koje želite testirati. Na primjer ova naredba:

test/failure 2 3

testirat će samo drugi i treći klaster. Ako parametri nisu navedeni, testirat će se svi klasteri. Svi se klasteri testiraju paralelno, a rezultat se prikazuje na ploči tmux. Tmux koristi namjenski tmux poslužitelj, tako da se skripta može pokrenuti ispod zadanog tmuxa, što rezultira ugniježđenim tmuxom. Preporučam korištenje terminala u velikom prozoru i s malim fontom. Prije početka testiranja, svi virtualni strojevi se vraćaju na snimku u trenutku završetka skripte setup.

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Terminal je podijeljen u stupce prema broju klastera koji se testiraju; standardno (na snimci zaslona) postoje četiri. Opisat ću sadržaj kolumni na primjeru Tuchanka2. Ploče na snimci zaslona su numerirane:

  1. Ovdje se prikazuju statistike testa. Stupci:
    • neuspjeh — naziv testa (funkcija u skripti) koji emulira grešku.
    • reakcija — aritmetičko prosječno vrijeme u sekundama tijekom kojeg je klaster oporavio svoju funkcionalnost. Mjeri se od početka skripte koja emulira grešku do trenutka kada klaster vrati svoju funkcionalnost i može nastaviti pružati usluge. Ako je vrijeme vrlo kratko, na primjer, šest sekundi (to se događa u klasterima s nekoliko podređenih uređaja (Tuchanka3 i Tuchanka4)), to znači da je greška bila na asinkronom podređenom uređaju i da ni na koji način nije utjecala na performanse; nije bilo prekidači stanja klastera.
    • odstupanje — pokazuje širenje (točnost) vrijednosti reakcija koristeći metodu standardne devijacije.
    • računati — koliko je puta ovo ispitivanje provedeno.
  2. Kratak zapisnik omogućuje procjenu onoga što klaster trenutno radi. Prikazuje se broj iteracije (testiranja), vremenska oznaka i naziv operacije. Predugo trčanje (> 5 minuta) ukazuje na problem.
  3. srce (srce) - trenutno vrijeme. Za vizualnu procjenu performansi ovladati; majstorski Trenutno vrijeme se stalno upisuje u njegovu tablicu pomoću float IP mastera. Ako je uspješno, rezultat se prikazuje na ovoj ploči.
  4. pobijediti (puls) - "trenutno vrijeme", koje je prethodno zabilježilo skripta srce svladati, sad čitaj iz rob preko svoje float IP adrese. Omogućuje vam vizualnu procjenu izvedbe slave i replikacije. U Tuchanka1 nema podređenih uređaja s float IP-om (nema podređenih uređaja koji pružaju usluge), ali postoje dvije instance (DB-ovi), tako da neće biti prikazano ovdje pobijeditiI srce drugostupanjski.
  5. Praćenje stanja klastera pomoću uslužnog programa pcs mon. Prikazuje strukturu, raspodjelu resursa po čvorovima i druge korisne informacije.
  6. Ovdje se prikazuje praćenje sustava sa svakog virtualnog računala u klasteru. Može biti više takvih panela ovisno o tome koliko virtualnih strojeva ima klaster. Dva grafikona CPU opterećenje (virtualni strojevi imaju dva procesora), naziv virtualnog stroja, Opterećenje sustava (nazvan Prosječno opterećenje jer je prosjek za 5, 10 i 15 minuta), podaci o procesu i dodjela memorije.
  7. Trag skripte koja izvodi testiranje. U slučaju kvara - iznenadnog prekida rada ili beskrajnog ciklusa čekanja - ovdje možete vidjeti razlog ovakvog ponašanja.

Testiranje se provodi u dvije faze. Prvo, skripta prolazi kroz sve vrste testova, nasumično odabirući virtualni stroj na koji će primijeniti ovaj test. Zatim se provodi beskrajni ciklus testiranja, virtualni strojevi i kvar se odabiru nasumično svaki put. Iznenadni prekid testne skripte (donja ploča) ili beskrajna petlja čekanja nečega (> 5 minuta vrijeme izvršenja za jednu operaciju, to se može vidjeti u praćenju) označava da neki od testova na ovom klasteru nisu uspjeli.

Svaki test sastoji se od sljedećih operacija:

  1. Pokrenite funkciju koja emulira grešku.
  2. Spremni? — čekanje da se klaster obnovi (kada su sve usluge pružene).
  3. Prikazuje vrijeme čekanja za oporavak klastera (reakcija).
  4. Popraviti — klaster se "popravlja". Nakon čega bi se trebao vratiti u potpuno operativno stanje i biti spreman za sljedeći kvar.

Evo popisa testova s ​​opisom onoga što rade:

  • ForkBomb: Stvara "Out of memory" koristeći bombu s viljuškom.
  • OutOfSpace: Tvrdi disk je pun. Ali test je prilično simboličan; uz beznačajno opterećenje koje se stvara tijekom testiranja, PostgreSQL obično ne pada kada je tvrdi disk pun.
  • Postgres-UBITI: ubija PostgreSQL naredbom killall -KILL postgres.
  • Postgres-STOP: prekida PostgreSQL naredbu killall -STOP postgres.
  • Isključivanje: "isključuje" virtualni stroj naredbom VBoxManage controlvm "виртуалка" poweroff.
  • Reset: preopterećuje virtualni stroj naredbom VBoxManage controlvm "виртуалка" reset.
  • SBD-STOP: naredbom obustavlja SBD demona killall -STOP sbd.
  • Ugasiti: šalje naredbu virtualnom stroju putem SSH-a systemctl poweroff, sustav se elegantno gasi.
  • Prekini vezu: izolacija mreže, naredba VBoxManage controlvm "виртуалка" setlinkstate1 off.

Dovršavanje testiranja pomoću standardne tmux naredbe "kill-window" Ctrl-b &, ili naredbu "detach-client". Ctrl-b d: u ovoj točki testiranje završava, tmux se zatvara, virtualni strojevi se isključuju.

Problemi uočeni tijekom testiranja

  • U ovom trenutku demon čuvar sbd radi na zaustavljanju promatranih demona, ali ih ne zamrzava. I, kao rezultat, greške koje dovode samo do smrzavanja Corosync и Pejsmejker, ali ne visi sbd... Za provjeru Corosync već jesam PR#83 (na GitHubu na sbd), prihvaćeno u temu majstor. Obećali su (u PR#83) da će biti nešto slično za Pacemaker, nadam se do Crveni šešir 8 učinit će. Ali takvi su "kvarovi" spekulativni i mogu se lako umjetno simulirati korištenjem, na primjer, killall -STOP corosync, ali nikad se ne susreću u stvarnom životu.

  • У Pejsmejker u verziji za 7 CentOS pogrešno postavljen vrijeme_prekida_sinhronizacije у kvorum uređaj, kao rezultat ako jedan čvor nije uspio, s određenom vjerojatnošću drugi čvor se također ponovno pokreće, u koju se trebao preseliti majstor. Izliječen povećanjem vrijeme_prekida_sinhronizacije у kvorum uređaj tijekom implementacije (u skripti setup/setup1). Ovaj amandman programeri nisu prihvatili Pejsmejker, umjesto toga obećali su redizajnirati infrastrukturu na takav način (u nekoj neodređenoj budućnosti) da će se taj timeout automatski izračunati.

  • Ako konfiguracija baze podataka to navodi LC_MESSAGES (tekstualne poruke) Unicode se može koristiti, npr. ru_RU.UTF-8, zatim pri pokretanju postgres u okruženju gdje lokalizacija nije UTF-8, recimo u praznom okruženju (ovdje pejsmejker+pgsqlms(paf) trči postgres), onda dnevnik će sadržavati upitnike umjesto UTF-8 slova. Programeri PostgreSQL-a nisu se složili što učiniti u ovom slučaju. Košta, morate instalirati LC_MESSAGES=en_US.UTF-8 prilikom konfiguriranja (kreiranja) instance baze podataka.

  • Ako je wal_receiver_timeout postavljen (prema zadanim postavkama je 60s), tada tijekom PostgreSQL-STOP testa na masteru u tuchanka3 i tuchanka4 klasterima replikacija se ne povezuje ponovno s novim masterom. Replikacija je sinkrona, tako da se ne zaustavlja samo slave, već i novi master. Zaobilazi postavljanjem wal_receiver_timeout=0 prilikom konfiguriranja PostgreSQL-a.

  • Povremeno sam primijetio zamrzavanje replikacije u PostgreSQL-u u testu ForkBomb (prelijevanje memorije). Nakon ForkBomba, ponekad se podređeni uređaji možda neće ponovno povezati s novim glavnim. Na ovo sam naišao samo u klasterima tuchanka3 i tuchanka4, gdje se master zamrznuo zbog sinkrone replikacije. Problem je nestao sam od sebe nakon dužeg vremena (oko dva sata). Potrebno je više istraživanja kako bi se to ispravilo. Simptomi su slični prethodnoj grešci, koja je uzrokovana drugim razlogom, ali s istim posljedicama.

Krogan slika preuzeta sa devijantno Art uz dopuštenje autora:

Modeliranje failover klastera temeljenih na PostgreSQL i Pacemakeru

Izvor: www.habr.com

Dodajte komentar