Grundvallaratriði gagnagrunnshönnunar - Samanburður á PostgreSQL, Cassandra og MongoDB

Hæ vinir. Áður en lagt er af stað í seinni hluta maífrísins deilum við með þér efninu sem við þýddum í aðdraganda nýs straums á námskeiðinu "Venslabundin DBMS".

Grundvallaratriði gagnagrunnshönnunar - Samanburður á PostgreSQL, Cassandra og MongoDB

Forritahönnuðir eyða miklum tíma í að bera saman marga rekstrargagnagrunna til að velja þann sem hentar best fyrirhuguðu vinnuálagi. Þarfir geta falið í sér einfaldaða gagnalíkanagerð, viðskiptaábyrgð, lestur/skrifafköst, lárétta mælikvarða og bilanaþol. Hefð er fyrir því að valið byrjar á gagnagrunnsflokknum, SQL eða NoSQL, þar sem hver flokkur sýnir skýrt sett af málamiðlun. Mikil afköst hvað varðar litla leynd og mikla afköst er almennt litið á sem kröfu sem ekki er skipt út og er því nauðsynleg fyrir hvaða sýnishorn sem er.

Tilgangur þessarar greinar er að hjálpa forriturum að velja rétt á milli SQL og NoSQL í samhengi við forritagagnalíkanagerð. Við skoðum einn SQL gagnagrunn, nefnilega PostgreSQL, og tvo NoSQL gagnagrunna, Cassandra og MongoDB, til að fara yfir grunnatriði gagnagrunnshönnunar, eins og að búa til töflur, fylla þær út, lesa gögn úr töflu og eyða þeim. Í næstu grein munum við vera viss um að skoða vísitölur, viðskipti, JOINs, TTL tilskipanir og JSON-byggða gagnagrunnshönnun.

Hver er munurinn á SQL og NoSQL?

SQL gagnagrunnar auka sveigjanleika forrita með ACID viðskiptaábyrgð, sem og getu þeirra til að spyrjast fyrir um gögn með því að nota JOINs á óvæntan hátt ofan á núverandi staðlaða venslagagnagrunnslíkön.

Í ljósi einhæfs/einshnúta arkitektúrs þeirra og notkunar á master-slave afritunarlíkani fyrir offramboð, skortir hefðbundna SQL gagnagrunna tvo mikilvæga eiginleika - línulegan skrifstærðleika (þ.e. sjálfvirk skipting yfir marga hnúta) og sjálfvirkt / núll gagnatap. Þetta þýðir að magn gagna sem berast getur ekki farið yfir hámarks skrifafköst eins hnúts. Að auki verður að taka tillit til tímabundins gagnataps í bilunarþoli (í sameiginlegri ekkert-arkitektúr). Hér þarftu að hafa í huga að nýlegar skuldbindingar hafa ekki enn komið fram í þrælaeintakinu. Uppfærslur sem ekki eru niður í miðbæ er einnig erfitt að ná í SQL gagnagrunnum.

NoSQL gagnagrunnar eru venjulega dreifðir í eðli sínu, þ.e. í þeim er gögnum skipt í hluta og dreift yfir nokkra hnúta. Þeir krefjast afeðlunar. Þetta þýðir að gögnin sem slegin eru inn verða einnig að afrita nokkrum sinnum til að svara tilteknum beiðnum sem þú sendir. Heildarmarkmiðið er að ná háum afköstum með því að fækka brotum sem eru tiltækar við lestur. Þetta gefur til kynna að NoSQL krefst þess að þú líkir fyrirspurnum þínum, en SQL krefst þess að þú líkir gögnunum þínum.

NoSQL leggur áherslu á að ná háum afköstum í dreifðum þyrpingum og þetta er undirliggjandi rökstuðningur fyrir mörgum gagnagrunnshönnunarskiptum sem fela í sér ACID viðskiptatap, JOINs og stöðugar alþjóðlegar aukavísitölur.

Það eru rök fyrir því að þó að NoSQL gagnagrunnar veiti línulegan skrifstærðanleika og mikið bilunarþol, þá gerir tap á viðskiptaábyrgðum þá óhentuga fyrir mikilvæg gögn.

Eftirfarandi tafla sýnir hvernig gagnalíkanagerð í NoSQL er frábrugðin SQL.

Grundvallaratriði gagnagrunnshönnunar - Samanburður á PostgreSQL, Cassandra og MongoDB

SQL og NoSQL: Af hverju er bæði þörf?

Raunveruleg forrit með fjölda notenda, eins og Amazon.com, Netflix, Uber og Airbnb, hafa það verkefni að framkvæma flókin, margþætt verkefni. Til dæmis þarf netviðskiptaforrit eins og Amazon.com að geyma létt og mikilvæg gögn eins og notendaupplýsingar, vörur, pantanir, reikninga, ásamt þungum, minna viðkvæmum gögnum eins og vöruumsagnir, stuðningsskilaboð, notendavirkni, umsagnir og ráðleggingar notenda. Auðvitað treysta þessi forrit á að minnsta kosti einum SQL gagnagrunni ásamt að minnsta kosti einum NoSQL gagnagrunni. Í svæðisbundnum og alþjóðlegum kerfum starfar NoSQL gagnagrunnur sem landfræðilegt dreift skyndiminni fyrir gögn sem eru geymd í traustum SQL gagnagrunni sem keyrir á tilteknu svæði.

Hvernig sameinar YugaByte DB SQL og NoSQL?

YugaByte DB er byggður á log-stilla blandaðri geymsluvél, sjálfvirkri sundrun, dreifðri samstöðuafritun og ACID dreifðum viðskiptum (innblásin af Google Spanner), og er fyrsti opinn uppspretta gagnagrunnur heims sem er samtímis samhæfður við NoSQL (Cassandra & Redis ) og SQL (PostgreSQL). Eins og sést í töflunni hér að neðan, bætir YCQL, YugaByte DB API samhæft við Cassandra, hugmyndum um einstaka og fjöllykla ACID viðskipti og alþjóðlegar aukavísitölur við NoSQL API, og þar með hefja tímabil viðskipta NoSQL gagnagrunna. Að auki bætir YCQL, YugaByte DB API samhæft við PostgreSQL, hugmyndum línulegrar skrifstærðar og sjálfvirkrar villuþols við SQL API, sem færir dreifða SQL gagnagrunna til heimsins. Þar sem YugaByte DB er viðskiptalegs eðlis, er nú hægt að nota NoSQL API í samhengi við mikilvæg gögn.

Grundvallaratriði gagnagrunnshönnunar - Samanburður á PostgreSQL, Cassandra og MongoDB

Eins og áður hefur komið fram í greininni „Við kynnum YSQL: PostgreSQL samhæft dreift SQL API fyrir YugaByte DB“, valið á milli SQL eða NoSQL í YugaByte DB fer algjörlega eftir einkennum undirliggjandi vinnuálags:

  • Ef aðalvinnuálagið þitt er fjöllykla JOIN-aðgerðir, þá þegar þú velur YSQL skaltu skilja að lyklunum þínum gæti verið dreift yfir marga hnúta, sem leiðir til meiri leynd og/eða minni afköst en NoSQL.
  • Annars skaltu velja annaðhvort NoSQL API, hafðu í huga að þú munt fá betri árangur vegna fyrirspurna sem sendar eru frá einum hnút í einu. YugaByte DB getur þjónað sem einn rekstrargagnagrunnur fyrir raunveruleg, flókin forrit sem þurfa að stjórna mörgu vinnuálagi samtímis.

Gagnalíkanastofan í næsta hluta er byggð á PostgreSQL og Cassandra API samhæfðum YugaByte DB gagnagrunnum, öfugt við innfædda gagnagrunna. Þessi nálgun leggur áherslu á auðveld samskipti við tvö mismunandi API (á tveimur mismunandi höfnum) sama gagnagrunnsklasans, í stað þess að nota algjörlega sjálfstæða klasa tveggja mismunandi gagnagrunna.
Í eftirfarandi köflum munum við skoða gagnalíkanastofuna til að sýna muninn og sumt af því sem er sameiginlegt í gagnagrunnunum sem fjallað er um.

Gagnalíkanarannsóknarstofa

Uppsetning gagnagrunns

Miðað við áhersluna á hönnun gagnalíkana (frekar en flókinn dreifingararkitektúr), munum við setja upp gagnagrunna í Docker gámum á staðbundinni vél og hafa síðan samskipti við þá með því að nota viðkomandi skipanalínuskel.

PostgreSQL & Cassandra samhæfður YugaByte DB gagnagrunnur

mkdir ~/yugabyte && cd ~/yugabyte
wget https://downloads.yugabyte.com/yb-docker-ctl && chmod +x yb-docker-ctl
docker pull yugabytedb/yugabyte
./yb-docker-ctl create --enable_postgres

MongoDB

docker run --name my-mongo -d mongo:latest

Skipanalínuaðgangur

Við skulum tengjast gagnagrunnunum með því að nota skipanalínuskelina fyrir samsvarandi API.

PostgreSQL

psql er skipanalínuskel til að hafa samskipti við PostgreSQL. Til að auðvelda notkun kemur YugaByte DB með psql beint í bin möppunni.

docker exec -it yb-postgres-n1 /home/yugabyte/postgres/bin/psql -p 5433 -U postgres

Cassandra

cqlsh er skipanalínuskel til að hafa samskipti við Cassandra og samhæfa gagnagrunna hennar í gegnum CQL (Cassandra Query Language). Til að auðvelda notkun fylgir YugaByte DB cqlsh í vörulistanum bin.
Athugaðu að CQL var innblásið af SQL og hefur svipaðar hugmyndir um töflur, raðir, dálka og vísitölur. Hins vegar, sem NoSQL tungumál, bætir það ákveðnum takmörkunum við, sem við munum einnig fjalla um í öðrum greinum.

docker exec -it yb-tserver-n1 /home/yugabyte/bin/cqlsh

MongoDB

Mongó er skipanalínuskel til að hafa samskipti við MongoDB. Það er að finna í bin möppunni í MongoDB uppsetningunni.

docker exec -it my-mongo bash 
cd bin
mongo

Búðu til töflu

Nú getum við haft samskipti við gagnagrunninn til að framkvæma ýmsar aðgerðir með því að nota skipanalínuna. Byrjum á því að búa til töflu sem geymir upplýsingar um lög skrifuð af mismunandi listamönnum. Þessi lög gætu verið hluti af plötu. Einnig eru valfrjálsir eiginleikar lags útgáfuár, verð, tegund og einkunn. Við þurfum að gera grein fyrir frekari eiginleikum sem gætu verið nauðsynlegar í framtíðinni í gegnum reitinn „merki“. Það getur geymt hálfuppbyggð gögn í formi lykilgildapöra.

PostgreSQL

CREATE TABLE Music (
    Artist VARCHAR(20) NOT NULL, 
    SongTitle VARCHAR(30) NOT NULL,
    AlbumTitle VARCHAR(25),
    Year INT,
    Price FLOAT,
    Genre VARCHAR(10),
    CriticRating FLOAT,
    Tags TEXT,
    PRIMARY KEY(Artist, SongTitle)
);	

Cassandra

Að búa til töflu í Cassandra er mjög svipað PostgreSQL. Einn helsti munurinn er skortur á heiðarleikaþvingunum (t.d. NOT NULL), en þetta er á ábyrgð forritsins, ekki NoSQL gagnagrunnsins. Aðallykillinn samanstendur af skiptingarlykli (listamannsdálkurinn í dæminu hér að neðan) og setti af klasadálkum (SongTitle dálkurinn í dæminu hér að neðan). Skiptingalykillinn ákvarðar í hvaða skipting/brot röðin á að vera sett í og ​​þyrpingardálkarnir gefa til kynna hvernig gögnin eiga að vera skipulögð innan núverandi brots.

CREATE KEYSPACE myapp;
USE myapp;
CREATE TABLE Music (
    Artist TEXT, 
    SongTitle TEXT,
    AlbumTitle TEXT,
    Year INT,
    Price FLOAT,
    Genre TEXT,
    CriticRating FLOAT,
    Tags TEXT,
    PRIMARY KEY(Artist, SongTitle)
);

MongoDB

MongoDB skipuleggur gögn í gagnagrunna (Database) (svipað og Keyspace í Cassandra), þar sem eru Söfn (svipað og töflur) sem innihalda Skjöl (svipað og raðir í töflu). Í MongoDB er í grundvallaratriðum engin þörf á að skilgreina upphafsskema. Lið "nota gagnagrunn", sýnt hér að neðan, sýnir gagnagrunninn við fyrsta símtalið og breytir samhenginu fyrir nýstofnaða gagnagrunninn. Jafnvel söfn þurfa ekki að vera beinlínis búin til; þau verða til sjálfkrafa, einfaldlega þegar þú bætir fyrsta skjalinu við nýtt safn. Athugaðu að MongoDB notar prófunargagnagrunninn sjálfgefið, þannig að allar aðgerðir á safnstigi án þess að tilgreina sérstakan gagnagrunn mun sjálfgefið keyra á honum.

use myNewDatabase;

Að fá upplýsingar um borð
PostgreSQL

d Music
Table "public.music"
    Column    |         Type          | Collation | Nullable | Default 
--------------+-----------------------+-----------+----------+--------
 artist       | character varying(20) |           | not null | 
 songtitle    | character varying(30) |           | not null | 
 albumtitle   | character varying(25) |           |          | 
 year         | integer               |           |          | 
 price        | double precision      |           |          | 
 genre        | character varying(10) |           |          | 
 criticrating | double precision      |           |          | 
 tags         | text                  |           |          | 
Indexes:
    "music_pkey" PRIMARY KEY, btree (artist, songtitle)

Cassandra

DESCRIBE TABLE MUSIC;
CREATE TABLE myapp.music (
    artist text,
    songtitle text,
    albumtitle text,
    year int,
    price float,
    genre text,
    tags text,
    PRIMARY KEY (artist, songtitle)
) WITH CLUSTERING ORDER BY (songtitle ASC)
    AND default_time_to_live = 0
    AND transactions = {'enabled': 'false'};

MongoDB

use myNewDatabase;
show collections;

Að slá inn gögn í töflu
PostgreSQL

INSERT INTO Music 
    (Artist, SongTitle, AlbumTitle, 
    Year, Price, Genre, CriticRating, 
    Tags)
VALUES(
    'No One You Know', 'Call Me Today', 'Somewhat Famous',
    2015, 2.14, 'Country', 7.8,
    '{"Composers": ["Smith", "Jones", "Davis"],"LengthInSeconds": 214}'
);
INSERT INTO Music 
    (Artist, SongTitle, AlbumTitle, 
    Price, Genre, CriticRating)
VALUES(
    'No One You Know', 'My Dog Spot', 'Hey Now',
    1.98, 'Country', 8.4
);
INSERT INTO Music 
    (Artist, SongTitle, AlbumTitle, 
    Price, Genre)
VALUES(
    'The Acme Band', 'Look Out, World', 'The Buck Starts Here',
    0.99, 'Rock'
);
INSERT INTO Music 
    (Artist, SongTitle, AlbumTitle, 
    Price, Genre, 
    Tags)
VALUES(
    'The Acme Band', 'Still In Love', 'The Buck Starts Here',
    2.47, 'Rock', 
    '{"radioStationsPlaying": ["KHCR", "KBQX", "WTNR", "WJJH"], "tourDates": { "Seattle": "20150625", "Cleveland": "20150630"}, "rotation": Heavy}'
);

Cassandra

Heildar tjáning INSERT í Cassandra lítur mjög svipað út og í PostgreSQL. Hins vegar er einn stór munur á merkingarfræði. Í Cassandra INSERT er í rauninni aðgerð UPSERT, þar sem síðustu gildin eru bætt við röðina ef röðin er þegar til.

Gagnafærsla er svipuð og PostgreSQL INSERT ofan

.

MongoDB

Jafnvel þó að MongoDB sé NoSQL gagnagrunnur eins og Cassandra, þá á innsetningaraðgerðin ekkert sameiginlegt með merkingarlegri hegðun Cassandra. Í MongoDB setja inn () hefur engin tækifæri UPSERT, sem gerir það svipað og PostgreSQL. Bætir við sjálfgefnum gögnum án _idspecified mun valda því að nýju skjali bætist við safnið.

db.music.insert( {
artist: "No One You Know",
songTitle: "Call Me Today",
albumTitle: "Somewhat Famous",
year: 2015,
price: 2.14,
genre: "Country",
tags: {
Composers: ["Smith", "Jones", "Davis"],
LengthInSeconds: 214
}
}
);
db.music.insert( {
artist: "No One You Know",
songTitle: "My Dog Spot",
albumTitle: "Hey Now",
price: 1.98,
genre: "Country",
criticRating: 8.4
}
);
db.music.insert( {
artist: "The Acme Band",
songTitle: "Look Out, World",
albumTitle:"The Buck Starts Here",
price: 0.99,
genre: "Rock"
}
);
db.music.insert( {
artist: "The Acme Band",
songTitle: "Still In Love",
albumTitle:"The Buck Starts Here",
price: 2.47,
genre: "Rock",
tags: {
radioStationsPlaying:["KHCR", "KBQX", "WTNR", "WJJH"],
tourDates: {
Seattle: "20150625",
Cleveland: "20150630"
},
rotation: "Heavy"
}
}
);

Tafla fyrirspurn

Kannski er mikilvægasti munurinn á SQL og NoSQL hvað varðar smíði fyrirspurna tungumálið sem er notað FROM и WHERE. SQL leyfir eftir tjáningu FROM veldu margar töflur og tjáningu með WHERE getur verið hvers kyns flókið (þar á meðal aðgerðir JOIN á milli borða). Hins vegar hefur NoSQL tilhneigingu til að setja alvarlegar takmarkanir á FROM, og vinna aðeins með eina tilgreinda töflu, og í WHERE, aðallykill verður alltaf að vera tilgreindur. Þetta tengist NoSQL frammistöðuþrýstingnum sem við ræddum um áðan. Þessi löngun leiðir til allrar mögulegrar minnkunar á hvaða samspili sem er í krosstöflum og krosslyklum. Það getur leitt til mikillar seinkun á samskiptum milli hnúta þegar svarað er beiðni og er því best að forðast almennt. Til dæmis, Cassandra krefst þess að fyrirspurnir séu takmarkaðar við ákveðna rekstraraðila (aðeins =, IN, <, >, =>, <=) á skiptingarlykla, nema þegar óskað er eftir aukavísitölu (aðeins = stjórnandinn er leyfður hér).

PostgreSQL

Hér að neðan eru þrjú dæmi um fyrirspurnir sem auðvelt er að framkvæma með SQL gagnagrunni.

  • Birta öll lög eftir listamann;
  • Birta öll lög eftir listamanninn sem passa við fyrsta hluta titilsins;
  • Sýndu öll lög eftir listamann sem hafa ákveðið orð í titlinum og hafa verð undir 1.00.
SELECT * FROM Music
WHERE Artist='No One You Know';
SELECT * FROM Music
WHERE Artist='No One You Know' AND SongTitle LIKE 'Call%';
SELECT * FROM Music
WHERE Artist='No One You Know' AND SongTitle LIKE '%Today%'
AND Price > 1.00;

Cassandra

Af PostgreSQL fyrirspurnunum sem taldar eru upp hér að ofan mun aðeins sú fyrsta virka óbreytt í Cassandra, þar sem rekstraraðilinn LIKE er ekki hægt að nota á þyrpingardálka eins og SongTitle. Í þessu tilviki eru aðeins rekstraraðilar leyfðir = и IN.

SELECT * FROM Music
WHERE Artist='No One You Know';
SELECT * FROM Music
WHERE Artist='No One You Know' AND SongTitle IN ('Call Me Today', 'My Dog Spot')
AND Price > 1.00;

MongoDB

Eins og sýnt er í fyrri dæmum er aðalaðferðin til að búa til fyrirspurnir í MongoDB db.collection.find(). Þessi aðferð inniheldur beinlínis nafn safnsins (music í dæminu hér að neðan), þannig að það er bannað að spyrjast fyrir um mörg söfn.

db.music.find( {
  artist: "No One You Know"
 } 
);
db.music.find( {
  artist: "No One You Know",
  songTitle: /Call/
 } 
);

Að lesa allar línur töflu

Að lesa allar línur er einfaldlega sérstakt tilfelli af fyrirspurnarmynstrinu sem við skoðuðum áðan.

PostgreSQL

SELECT * 
FROM Music;

Cassandra

Svipað og PostgreSQL dæmið hér að ofan.

MongoDB

db.music.find( {} );

Að breyta gögnum í töflu

PostgreSQL

PostgreSQL veitir leiðbeiningar UPDATE að breyta gögnum. Hún hefur engin tækifæri UPSERT, þannig að þessi setning mun mistakast ef línan er ekki lengur í gagnagrunninum.

UPDATE Music
SET Genre = 'Disco'
WHERE Artist = 'The Acme Band' AND SongTitle = 'Still In Love';

Cassandra

Cassandra hefur UPDATE svipað og PostgreSQL. UPDATE hefur sömu merkingarfræði UPSERT, svipað INSERT.

Svipað og PostgreSQL dæmið hér að ofan.

MongoDB
Operation uppfæra () í MongoDB getur alveg uppfært núverandi skjal eða uppfært aðeins ákveðna reiti. Sjálfgefið er að það uppfærir aðeins eitt skjal með merkingarfræði óvirka UPSERT. Uppfærsla á mörgum skjölum og svipuð hegðun UPSERT hægt að nota með því að setja viðbótarflögg fyrir aðgerðina. Til dæmis, í dæminu hér að neðan, er tegund tiltekins listamanns uppfærð út frá lagi hans.

db.music.update(
  {"artist": "The Acme Band"},
  { 
    $set: {
      "genre": "Disco"
    }
  },
  {"multi": true, "upsert": true}
);

Að fjarlægja gögn úr töflu

PostgreSQL

DELETE FROM Music
WHERE Artist = 'The Acme Band' AND SongTitle = 'Look Out, World';

Cassandra

Svipað og PostgreSQL dæmið hér að ofan.

MongoDB

MongoDB hefur tvenns konar aðgerðir til að eyða skjölum - eyðaEinni() /deleteMany() и fjarlægja (). Báðar tegundir eyða skjölum en skila mismunandi niðurstöðum.

db.music.deleteMany( {
        artist: "The Acme Band"
    }
);

Eyðir töflu

PostgreSQL

DROP TABLE Music;

Cassandra

Svipað og PostgreSQL dæmið hér að ofan.

MongoDB

db.music.drop();

Ályktun

Umræðan um að velja á milli SQL og NoSQL hefur verið í gangi í meira en 10 ár. Það eru tveir meginþættir í þessari umræðu: gagnagrunnsvélararkitektúr (einhverfa, viðskipta SQL vs dreifð, non-transactional NoSQL) og gagnagrunnshönnunaraðferð (líkan gögnin þín í SQL vs líkanagerð fyrirspurna þinna í NoSQL).

Með dreifðum viðskiptagagnagrunni eins og YugaByte DB er auðvelt að stöðva umræðuna um gagnagrunnsarkitektúr. Þar sem gagnamagn verður stærra en hægt er að skrifa í einn hnút, verður að fullu dreifðri arkitektúr sem styður línulega skrifstærðleika með sjálfvirkri sundrun/endurjafnvægi nauðsynleg.

Þar að auki, eins og segir í einni af greinunum Google Cloud,Transactional, sterklega samkvæmur arkitektúr er nú meira notaður til að veita betri þróun lipurð en non-transactional, , loksins samkvæmur arkitektúr.

Þegar ég fer aftur að gagnagrunnshönnunarumræðunni er rétt að segja að báðar hönnunaraðferðirnar (SQL og NoSQL) eru nauðsynlegar fyrir hvers kyns flókið raunverulegt forrit. SQL „gagnalíkön“ nálgunin gerir forriturum kleift að mæta breyttum viðskiptakröfum á auðveldari hátt, en NoSQL „fyrirspurnarlíkön“ nálgunin gerir sömu forriturum kleift að vinna á miklu magni gagna með lítilli leynd og mikilli afköst. Það er af þessari ástæðu sem YugaByte DB veitir SQL og NoSQL API í sameiginlegum kjarna, frekar en að kynna eina af aðferðunum. Að auki, með því að veita eindrægni við vinsæl gagnagrunnstungumál, þar á meðal PostgreSQL og Cassandra, tryggir YugaByte DB að forritarar þurfi ekki að læra annað tungumál til að vinna með dreifðri, mjög samkvæmri gagnagrunnsvél.

Í þessari grein skoðuðum við hvernig grundvallaratriði gagnagrunnshönnunar eru mismunandi á milli PostgreSQL, Cassandra og MongoDB. Í framtíðargreinum munum við kafa ofan í háþróuð hönnunarhugtök eins og vísitölur, viðskipti, JOINs, TTL tilskipanir og JSON skjöl.

Við óskum ykkur góðrar hvíldar um helgina og bjóðum ykkur velkomin ókeypis vefnámskeið, sem fer fram 14. maí.

Heimild: www.habr.com

Bæta við athugasemd