- Praktický úvod do MongoDB (1): NoSQL opravdu snadno
- Praktický úvod do MongoDB (2): Indexy a agregace
- Praktický úvod do MongoDB (3): clustering
Nejprve jsme se v seriálu o MongoDB seznámili se základním ovládáním, následně jsme se zaměřili na dolování agregovaných dat a dnes si ukážeme clustering.
Clustering bude využívat dvou základních principů, které je možné (a vhodné) kombinovat. Replikace, která zajišťuje, že na vícero MongoDB nodech jsou stejná data z důvodu redundance a sharding, tedy rozprostření jedné databáze a kolekce dokumentů přes vícero nodů z důvodu škálování.
Replikace
Začneme tím jednodušším na nastavení – replikací. MongoDB v zásadě používá principu active/standby (to je rozdíl třeba proti CouchDB, kterou na cloudsvet rozebereme jindy – je taky dokumentová, ale má odlišný přístup k CP vs. AP, takže na rozdíl od MongoDB se hodí třeba pro držení lokálních replik v pobočkách apod.). Ve výchozím nastavení bude jeden z nodů master a tento bude sloužit k zápisům i čtení a na pozadí bude replikovat asynchronně záznamy na ostatní nody. Každý zápis jednoho dokumentu je atomický (tedy buď se zapíše celý nebo vůbec), ale pokud potřebujete atomické transakce nad hned několika dokumenty, tak s MongoDB máte ve výchozím stavu smůlu (je možné použít funkci $isolated, která zajistí, že pokud modifikujete tři dokumenty v transakci, tak vám do toho nemůže vstoupit jiný proces – nicméně tato možnost neplatí pro sharded scénáře – pokud nutně potřebujete ACID vlastnosti, použijte relační databázi).
Výchozí asynchronní replikace má dva důsledky. Prvním je časový posun – po úspěšném zapsaní na primární nod se to na ostatních může objevit až po nějaké době. Pokud vás krátká prodleva trápí, můžete to vyřešit tak, že z ne-primárních nodů nebudete číst (to je výchozí nastavení MongoDB – musíte explicitně povolit čtení z ne-primárních uzlů a vědět co to znamená). Druhá obtíž může být, že pokud máte zapsáno a primární node umře dřív, než stihne nová data zreplikovat, tak o ně přijdete. To je výchozí nastavení MongoDB, které ovšem nabízí největší výkon pro vaše aplikace, pokud vám toto riziko nevadí (v praxi vadí často daleko méně, než si lidé zprvu myslí). Chování ale můžete modifikovat, takže vám databáze nevrátí “ok” hned po zapsání na primární node, ale až po té, co byla provedena replikace. Můžete si přímo říct jak přísní chcete být – stačí vám pro klidné spaní, že proběhla jedna replika? Nebo je replika hotová na většině nodu? Nebo na všech? To všechno můžete určit vy a to dokonce pro každou write operaci zvlášť (! nejde o nastavení databáze, ale zvlášť každé zapisovací operace, což je fantastická možnost). Podle charakteru vašeho zápisu tak můžete volit mezi rychlostí a bezpečností (chcete-li garancí). Výborná vlastnost!
Jak si to vyzkoušíme? Nechce se mi dělat několik VM, pro testování využijeme víc spuštěných procesů v jedné mašině.
Vytvořte adresáře pro data:
cloudsvet@ubuntu:~$ mkdir data1 data2 data3
Spusťte tři MongoDB procesy a namapujte na specifické porty a adresáře a všechny přiřadíme do jednoho replication setu:
cloudsvet@ubuntu:~$ mongod --port 9001 --dbpath data1 --replSet mujset & cloudsvet@ubuntu:~$ mongod --port 9002 --dbpath data2 --replSet mujset & cloudsvet@ubuntu:~$ mongod --port 9003 --dbpath data3 --replSet mujset &
Připojte se na první node a inicializujte replication set:
cloudsvet@ubuntu:~$ mongo --port 9001 MongoDB shell version: 3.0.6 connecting to: 127.0.0.1:9001/test ...> rs.initiate() ... 2015-09-01T11:49:16.943-0700 I REPL [ReplicationExecutor] New replica set config in use: { _id: "mujset", version: 1, members: [ { _id: 0, host: "ubuntu:9001", arbiterOnly: false, buildIndexes: true, hidden: false, priority: 1.0, tags: {}, slaveDelay: 0, votes: 1 } ], settings: { chainingAllowed: true, heartbeatTimeoutSecs: 10, getLastErrorModes: {}, getLastErrorDefaults: { w: 1, wtimeout: 0 } } } ... 2015-09-01T11:49:16.963-0700 I REPL [ReplicationExecutor] transition to SECONDARY 2015-09-01T11:49:16.963-0700 I REPL [ReplicationExecutor] transition to PRIMARY mujset:SECONDARY> 2015-09-01T11:49:17.962-0700 I REPL [rsSync] transition to primary complete; database writes are now permitted mujset:PRIMARY>
Tak a je to, náš node je teď primární. Můžeme se podívat na jeho konfiguraci a všimněte si záznamu “host” (jde o hostname a port):
mujset:PRIMARY> rs.conf() { "_id" : "mujset", "version" : 1, "members" : [ { "_id" : 0, "host" : "ubuntu:9001", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : 0, "votes" : 1 } ], "settings" : { "chainingAllowed" : true, "heartbeatTimeoutSecs" : 10, "getLastErrorModes" : { }, "getLastErrorDefaults" : { "w" : 1, "wtimeout" : 0 } } }
Teď už můžeme přidat další členy – jsme na stejném hostname, jen na odlišných portech:
mujset:PRIMARY> rs.add("ubuntu:9002") ... 2015-09-01T11:54:12.539-0700 I REPL [ReplicationExecutor] Member ubuntu:9001 is now in state PRIMARY 2015-09-01T11:54:14.537-0700 I REPL [ReplicationExecutor] Member ubuntu:9002 is now in state STARTUP2 ... 2015-09-01T11:54:17.596-0700 I REPL [ReplicationExecutor] syncing from: ubuntu:9001 ... 2015-09-01T11:54:17.646-0700 I REPL [ReplicationExecutor] transition to RECOVERING ... 2015-09-01T11:54:17.655-0700 I REPL [ReplicationExecutor] transition to SECONDARY 2015-09-01T11:54:18.538-0700 I REPL [ReplicationExecutor] Member ubuntu:9002 is now in state SECONDARY mujset:PRIMARY>
Přidejte i další node:
mujset:PRIMARY> rs.add("ubuntu:9003") ... 2015-09-01T11:54:48.586-0700 I REPL [ReplicationExecutor] Member ubuntu:9003 is now in state SECONDARY mujset:PRIMARY>
Vložíme nějaká data
mujset:PRIMARY> db.pokus.insert({"mujdokument":"je replikovany"}) db.pokus.find() { "_id" : ObjectId("55e5f4b4be21a5c50ee7bc44"), "mujdokument" : "je replikovany" }
Vyskočte z primárního nodu a připojte se na druhý:
mujset:PRIMARY> quit() cloudsvet@ubuntu:~$ mongo --port 9002
Zkusíme číst
mujset:SECONDARY> db.pokus.find() 2015-09-01T11:57:37.328-0700 I QUERY [conn23] assertion 13435 not master and slaveOk=false ns:test.pokus query:{} Error: error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
No jasně – výchozí nastavení zabraňujeme nejen zápisům na ostatních nodech (přes to nejede vlak dokud primár žije), ale i čtení, což ale můžeme změnit:
mujset:SECONDARY> rs.slaveOk() mujset:SECONDARY> db.pokus.find() { "_id" : ObjectId("55e5f4b4be21a5c50ee7bc44"), "mujdokument" : "je replikovany" } mujset:SECONDARY> quit()
Jak je vidět, data jsou zreplikována v pořádku. Co kdybychom chtěli zapsat dokument a mít garanci, že po přijetí odpovědi o úspěšném zápisu už je ve skutečnosti i hotová replika na většině nodů? Jednoduché – připojte se na primár a zkuste si to:
mujset:PRIMARY> db.pokus.insert({"mujdokument":"je bezpecne replikovany"}, { writeConcern: { w: "majority", wtimeout: 5000 } }) WriteResult({ "nInserted" : 1 })
Pokud chcete, otevřete si do VM druhé okno (ať tam nemáte mraky hlášek), najděte číslo procesu, na kterém běží primár a zabijte ho.
cloudsvet@ubuntu:~$ ps aux | grep mongod mongodb 1048 0.8 5.0 362804 51480 ? Ssl 11:41 0:09 /usr/bin/mongod --config /etc/mongod.conf hp 1545 0.9 7.9 9266540 80456 pts/0 Sl 11:44 0:08 mongod --port 9001 --dbpath data1 --replSet mujset hp 1561 0.9 7.5 9263460 76496 pts/0 Sl 11:44 0:08 mongod --port 9002 --dbpath data2 --replSet mujset hp 1575 0.9 7.5 9249104 76420 pts/0 Sl 11:44 0:08 mongod --port 9003 --dbpath data3 --replSet mujset hp 2009 0.0 0.0 11740 932 pts/0 S+ 11:59 0:00 grep --color=auto mongod cloudsvet@ubuntu:~$ kill 1545
Připojte na 9002 – bude novým primárem.
Takhle tedy funguje replikace v MongoDB – na to, co se tam všechno děje, je to docela jednoduché, ne?
Sharding
Relikace nám pomohla s redundancí, ale všechna data stále držíme na jednom místě (nebo v kopiích na několika nodech). To samozřejmě neřeší škálovatelnost. Sharding vám umožní rozprostřít jednu databázi a jednu kolekci dokumentů přes vícero nodů. Použít lze víc algoritmů, jak to rozdělení dělat (třeba hash nebo podle jiného klíče), ale to je pro naše účely teď jedno. V praxi budete provádět sharding mezi replication sety – takže zkombinujete redundanci se škálováním. Pro ukázku ale budeme dělat sharding jen přes single node MongoDB instance.
Jednodušší systémy jako je Redis (brzy se objeví články na cloudsvet) sice rozprostřou záznamy, ale je na aplikaci, aby se zápisem oslovila ten správný node (Redis vám při pokusu o zápis může říct: hele fajn, ale tenhle record na mě nepatří, zapiš to do node2, takže je potřeba to implementovat v aplikaci resp. je to součástí knihoven pro běžné jazyky). MongoDB tohle umí řešit jinak. Potřebujeme tak tři kategorie instancí – konfigurační, router a shard.
Nejprve si připravte datové adresáře (opět to celé uděláme na jediné VM vícero procesy, ať se to dobře zkouší).
cloudsvet@ubuntu:~$ mkdir config1 config2 config3 shard1 shard2 shard3
Rozjeďte tři konfigurační instance (ty drží konfigurační údaje pro celý sharding systém):
cloudsvet@ubuntu:~$ mongod --configsvr --dbpath config1 --port 10001 & cloudsvet@ubuntu:~$ mongod --configsvr --dbpath config2 --port 10002 & cloudsvet@ubuntu:~$ mongod --configsvr --dbpath config3 --port 10003 & ... 2015-09-01T21:15:22.850-0700 I NETWORK [initandlisten] waiting for connections on port 10002 2015-09-01T21:15:22.849-0700 I NETWORK [initandlisten] waiting for connections on port 10001 2015-09-01T21:15:22.850-0700 I NETWORK [initandlisten] waiting for connections on port 10003
Následně vytvoříme dva redundantní routery – s těmi bude komunikovat aplikace a jejich úlohou je posílat dotazy na ta správná místa, tedy shardy. Spusťte je, řekněte jim, kde jsou konfigurační servery a rozjedeme je jako procesy na zase dalších portech.
cloudsvet@ubuntu:~$ mongos --configdb ubuntu:10001,ubuntu:10002,ubuntu:10003 --port 11001 & cloudsvet@ubuntu:~$ mongos --configdb ubuntu:10001,ubuntu:10002,ubuntu:10003 --port 11002 &
Teď uděláme tři běžné instance MongoDB (v praxi to možná budou tři replication sety) – je to stejné, jako když děláme samostatné instance:
cloudsvet@ubuntu:~$ mongod --dbpath shard1 --port 12001 & cloudsvet@ubuntu:~$ mongod --dbpath shard2 --port 12002 & cloudsvet@ubuntu:~$ mongod --dbpath shard3 --port 12003 &
Můžete si ověřit, že nám všechny procesy běží:
cloudsvet@ubuntu:~$ hp@ubuntu:~$ ps aux | grep mongo mongodb 1048 0.8 5.0 362804 51476 ? Ssl 20:41 0:19 /usr/bin/mongod --config /etc/mongod.conf cloudsvet 1561 1.0 5.7 9263460 58372 ? Sl 20:44 0:21 mongod --port 9002 --dbpath data2 --replSet mujset cloudsvet 1575 0.9 5.4 9257300 54800 ? Sl 20:44 0:20 mongod --port 9003 --dbpath data3 --replSet mujset cloudsvet 5060 7.4 4.7 357596 48332 pts/2 Sl 21:14 0:20 mongod --configsvr --dbpath config1 --port 10001 cloudsvet 5061 7.4 4.0 357596 41292 pts/2 Sl 21:14 0:20 mongod --configsvr --dbpath config2 --port 10002 cloudsvet 5062 7.4 4.2 357592 43364 pts/2 Sl 21:14 0:20 mongod --configsvr --dbpath config3 --port 10003 cloudsvet 5275 0.6 1.2 108908 12480 pts/2 Sl 21:15 0:01 mongos --configdb ubuntu:10001,ubuntu:10002,ubuntu:10003 --port 11001 cloudsvet 5276 0.6 1.2 108904 12316 pts/2 Sl 21:15 0:01 mongos --configdb ubuntu:10001,ubuntu:10002,ubuntu:10003 --port 11002 cloudsvet 5977 1.0 5.2 365688 53100 pts/2 Sl 21:18 0:00 mongod --dbpath shard1 --port 12001 cloudsvet 6130 5.0 5.3 366708 54100 pts/2 Sl 21:19 0:00 mongod --dbpath shard2 --port 12002 cloudsvet 6131 5.1 5.2 366708 53056 pts/2 Sl 21:19 0:00 mongod --dbpath shard3 --port 12003 cloudsvet 6177 0.0 0.0 11744 936 pts/3 S+ 21:19 0:00 grep --color=auto mongo
Fajn, začneme konfigurovat sharding. Připojte se na “router” a přidejte naše uzly:
cloudsvet@ubuntu:~$ mongo --port 11001 sh.addShard( "ubuntu:12001" ) sh.addShard( "ubuntu:12002" ) sh.addShard( "ubuntu:12003" )
Vytvoříme databázi, zapneme na ní sharding a mrkneme se, co nám MongoDB o tom řekne:
mongos> use cloudsvet switched to db cloudsvet mongos> sh.enableSharding("cloudsvet") { "ok" : 1 } mongos> sh.status() --- Sharding Status --- sharding version: { "_id" : 1, "minCompatibleVersion" : 5, "currentVersion" : 6, "clusterId" : ObjectId("55e677fd2f990a71856215db") } shards: { "_id" : "shard0000", "host" : "ubuntu:12001" } { "_id" : "shard0001", "host" : "ubuntu:12002" } { "_id" : "shard0002", "host" : "ubuntu:12003" } balancer: Currently enabled: yes Currently running: no Failed balancer rounds in last 5 attempts: 0 Migration Results for the last 24 hours: No recent migrations databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "cloudsvet", "partitioned" : true, "primary" : "shard0000" }
Budeme pracovat s kolekcí “lide”, ale nejprve na ní zapneme sharding s algoritmem hash:
mongos> sh.shardCollection("cloudsvet.lide", { "_id": "hashed" } ) { "collectionsharded" : "cloudsvet.lide", "ok" : 1 }
To by bylo – vložíme tam nějaká data.
mongos> db.lide.insert({"jmeno":"tomas"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.insert({"jmeno":"marek"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.insert({"jmeno":"franta"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.insert({"jmeno":"jana"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.insert({"jmeno":"anicka"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.insert({"jmeno":"iveta"}) WriteResult({ "nInserted" : 1 }) mongos> db.lide.find() { "_id" : ObjectId("55e67f82e4d1c78422f083f6"), "jmeno" : "tomas" } { "_id" : ObjectId("55e67f89e4d1c78422f083f8"), "jmeno" : "franta" } { "_id" : ObjectId("55e67f8ee4d1c78422f083fa"), "jmeno" : "anicka" } { "_id" : ObjectId("55e67f86e4d1c78422f083f7"), "jmeno" : "marek" } { "_id" : ObjectId("55e67f8be4d1c78422f083f9"), "jmeno" : "jana" } { "_id" : ObjectId("55e67f93e4d1c78422f083fb"), "jmeno" : "iveta" }
Jak teď vypadá sharding status?
mongos> sh.status() --- Sharding Status --- sharding version: { "_id" : 1, "minCompatibleVersion" : 5, "currentVersion" : 6, "clusterId" : ObjectId("55e677fd2f990a71856215db") } shards: { "_id" : "shard0000", "host" : "ubuntu:12001" } { "_id" : "shard0001", "host" : "ubuntu:12002" } { "_id" : "shard0002", "host" : "ubuntu:12003" } balancer: Currently enabled: yes Currently running: no Failed balancer rounds in last 5 attempts: 0 Migration Results for the last 24 hours: 2 : Success 1 : Failed with error 'migration already in progress', from shard0000 to shard0002 databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "cloudsvet", "partitioned" : true, "primary" : "shard0000" } cloudsvet.lide shard key: { "_id" : "hashed" } chunks: shard0000 2 shard0001 2 shard0002 2 { "_id" : { "$minKey" : 1 } } -->> { "_id" : NumberLong("-6148914691236517204") } on : shard0000 Timestamp(3, 2) { "_id" : NumberLong("-6148914691236517204") } -->> { "_id" : NumberLong("-3074457345618258602") } on : shard0000 Timestamp(3, 3) { "_id" : NumberLong("-3074457345618258602") } -->> { "_id" : NumberLong(0) } on : shard0001 Timestamp(3, 4) { "_id" : NumberLong(0) } -->> { "_id" : NumberLong("3074457345618258602") } on : shard0001 Timestamp(3, 5) { "_id" : NumberLong("3074457345618258602") } -->> { "_id" : NumberLong("6148914691236517204") } on : shard0002 Timestamp(3, 6) { "_id" : NumberLong("6148914691236517204") } -->> { "_id" : { "$maxKey" : 1 } } on : shard0002 Timestamp(3, 7) { "_id" : "test", "partitioned" : false, "primary" : "shard0001" }
Zdá se, že je vše v pořádku. Všimněte si, že z routeru jsme byli schopni dostat všechny záznamy. Teď se přesvědčíme, že jsou ve skutečnosti rozprostřeny přes instance:
mongos> mongos> quit() cloudsvet@ubuntu:~$ mongo --port 12001 MongoDB shell version: 3.0.6 connecting to: 127.0.0.1:12001/test ... > use cloudsvet switched to db cloudsvet > db.lide.find() { "_id" : ObjectId("55e67f8ee4d1c78422f083fa"), "jmeno" : "anicka" } > quit() cloudsvet@ubuntu:~$ mongo --port 12002 MongoDB shell version: 3.0.6 connecting to: 127.0.0.1:12002/test ... > use cloudsvet switched to db cloudsvet > db.lide.find() { "_id" : ObjectId("55e67f82e4d1c78422f083f6"), "jmeno" : "tomas" } { "_id" : ObjectId("55e67f86e4d1c78422f083f7"), "jmeno" : "marek" } { "_id" : ObjectId("55e67f93e4d1c78422f083fb"), "jmeno" : "iveta" } > quit() cloudsvet@ubuntu:~$ mongo --port 12003 MongoDB shell version: 3.0.6 connecting to: 127.0.0.1:12003/test ... > use cloudsvet switched to db cloudsvet > db.lide.find() { "_id" : ObjectId("55e67f89e4d1c78422f083f8"), "jmeno" : "franta" } { "_id" : ObjectId("55e67f8be4d1c78422f083f9"), "jmeno" : "jana" } > quit()
Sharding nám tedy zafungoval !
Blížíme se ke konci představení první z NoSQL databází a to té v současnosti nejoblíbenější. Jde tedy o distribuovanou redundantní a škálovatelnou open source databázi postavenou na principu “dokumentů”, tedy JSON struktur, bez nutnosti definovat schéma (nabízí obrovskou flexibilitu). Je ideální pro čtení toho, co jste zapsali (tedy dokumentů), ale podporuje i agregované vyhledávání včetně map/reduce operací (když chcete číst z jiného úhlu, než po dokumentech). MongoDB preferuje konzistenci, proto je active/standby a pro každý záznam můžete navíc naladit sílu konzistence.
Příště nás čeká poslední díl – ukážeme si primitivní případ MEAN stacku, tedy MongoDB, ExpressJS, AngularJS, NodeJS (oblíbená kombinace pro tvorbu webových aplikací, tedy Mongo databáze, Express jako webový server, Angular jako klientská část aplikace a Node jako serverová). Také se budeme věnovat dalším zástupcům fascinujícího světa NoSQL jako je Redis, Cassandra nebo CouchDB.