MongoDB Sharding: Praktyczny przewodnik krok po kroku

Photo of author

By maciekx

Fragmentacja, znana również jako sharding, to technika podziału obszernego zbioru danych na mniejsze, zarządzalne części, które są rozlokowywane między różnymi instancjami MongoDB w środowisku rozproszonym.

Czym jest fragmentacja danych?

Sharding w MongoDB stanowi efektywną metodę skalowania, umożliwiającą przechowywanie ogromnych ilości danych na wielu serwerach, zamiast ograniczać się do jednego, centralnego serwera.

W praktyce, wraz ze wzrostem ilości danych, staje się niemożliwe ich przechowywanie na pojedynczej maszynie. Przeszukiwanie ogromnych baz danych na jednym serwerze może prowadzić do przeciążenia zasobów oraz niezadowalającej szybkości odczytu i zapisu.

Istnieją dwa główne podejścia do skalowania systemów gromadzenia danych:

Skalowanie wertykalne, czyli zwiększanie mocy obliczeniowej pojedynczego serwera poprzez dodawanie szybszych procesorów, zwiększanie pamięci RAM lub przestrzeni dyskowej. Jednakże, istnieją praktyczne ograniczenia w stosowaniu tej metody, wynikające z dostępnych technologii i konfiguracji sprzętowych.

Skalowanie horyzontalne, czyli rozkładanie obciążenia na wiele serwerów. Każdy serwer obsługuje podzbiór całości danych, co zapewnia wyższą wydajność i jest bardziej opłacalne niż inwestycja w zaawansowany sprzęt. Wymaga to jednak bardziej złożonej infrastruktury i jej konserwacji.

Sharding w MongoDB opiera się na koncepcji skalowania horyzontalnego.

Elementy składowe shardingu

Aby zaimplementować sharding w MongoDB, potrzebne są następujące komponenty:

Shard (fragment) – instancja MongoDB, która przechowuje podzbiór danych. Fragmenty powinny być wdrażane w ramach zestawu replik.

Mongos – instancja MongoDB działająca jako pośrednik między aplikacją kliencką a klastrem podzielonym na fragmenty. Działa jako router przekierowujący zapytania do odpowiednich fragmentów.

Config Server – instancja MongoDB przechowująca metadane oraz informacje konfiguracyjne klastra. Serwer konfiguracji musi być wdrożony jako zestaw replik.

Architektura shardingu

Klaster MongoDB składa się z kilku zestawów replik.

Każdy zestaw replik obejmuje co najmniej 3 instancje MongoDB. Klaster podzielony na fragmenty może składać się z wielu instancji fragmentów, przy czym każda z nich działa w swoim własnym zestawie replik. Aplikacje komunikują się z serwerem Mongos, który następnie kieruje zapytania do odpowiednich fragmentów. Aplikacje nie wchodzą w bezpośrednią interakcję z węzłami fragmentów. Router zapytań rozdziela dane między węzły fragmentów w oparciu o klucz fragmentu.

Implementacja shardingu

Poniżej przedstawiono kroki niezbędne do skonfigurowania shardingu:

Krok 1

  • Uruchom serwer konfiguracyjny w zestawie replik i włącz replikację pomiędzy jego instancjami.

mongod –configsvr –port 27019 –replSet rs0 –dbpath C:datadata1 –bind_ip localhost

mongod –configsvr –port 27018 –replSet rs0 –dbpath C:datadata2 –bind_ip localhost

mongod –configsvr –port 27017 –replSet rs0 –dbpath C:datadata3 –bind_ip localhost

Krok 2

  • Zainicjuj zestaw replik na jednym z serwerów konfiguracyjnych.

rs.initiate( { _id : “rs0”, configsvr: true, członkowie: [ { _id: 0, host: “IP:27017” }, { _id: 1, host: “IP:27018” }, { _id: 2, host: “IP:27019” } ] })

rs.initiate( { _id : "rs0",  configsvr: true,  members: [   { _id: 0, host: "IP:27017" },   { _id: 1, host: "IP:27018" },   { _id: 2, host: "IP:27019" }    ] })
{
        "ok" : 1,
        "$gleStats" : {
                "lastOpTime" : Timestamp(1593569257, 1),
                "electionId" : ObjectId("000000000000000000000000")
        },
        "lastCommittedOpTime" : Timestamp(0, 0),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593569257, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1593569257, 1)
}

Krok 3

  • Uruchom serwery fragmentów w zestawie replik i włącz replikację między nimi.

mongod –shardsvr –port 27020 –replSet rs1 –dbpath C:datadata4 –bind_ip localhost

mongod –shardsvr –port 27021 –replSet rs1 –dbpath C:datadata5 –bind_ip localhost

mongod –shardsvr –port 27022 –replSet rs1 –dbpath C:datadata6 –bind_ip localhost

MongoDB ustawi pierwszy serwer shardingu jako podstawowy. Użyj metody movePrimary, aby przenieść użycie podstawowego serwera shardingu.

Krok 4

  • Zainicjuj zestaw replik na jednym z serwerów fragmentów.

rs.initiate( { _id : “rs0”, członkowie: [ { _id: 0, host: “IP:27020” }, { _id: 1, host: “IP:27021” }, { _id: 2, host: “IP:27022” } ] })

rs.initiate( { _id : "rs0",  members: [   { _id: 0, host: "IP:27020" },   { _id: 1, host: "IP:27021" },   { _id: 2, host: "IP:27022" }    ] })
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593569748, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1593569748, 1)
}

Krok 5

  • Uruchom instancję mongos dla klastra podzielonego na fragmenty

mongos –port 40000 –configdb rs0/localhost:27019,localhost:27018,localhost:27017

Krok 6

  • Połącz się z serwerem routingu mongos.

mongo – port 40000

  • Teraz dodaj serwery shardingu.

sh.addShard( „rs1/hostlokalny:27020,hostlokalny:27021,hostlokalny:27022”)

sh.addShard( "rs1/localhost:27020,localhost:27021,localhost:27022")
{
        "shardAdded" : "rs1",
        "ok" : 1,
        "operationTime" : Timestamp(1593570212, 2),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570212, 2),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Krok 7

  • W powłoce mongo włącz sharding dla bazy danych i kolekcji.
  • Włącz sharding dla bazy danych.

sh.enableSharding(„geekFlareDB”)

sh.enableSharding("geekFlareDB")
{
        "ok" : 1,
        "operationTime" : Timestamp(1591630612, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1591630612, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Krok 8

  • Do fragmentacji kolekcji wymagany jest klucz fragmentu (opisany w dalszej części artykułu).

Składnia: sh.shardCollection(“dbName.collectionName”, { “key” : 1 } )

sh.shardCollection("geekFlareDB.geekFlareCollection", { "key" : 1 } )
{
        "collectionsharded" : "geekFlareDB.geekFlareCollection",
        "collectionUUID" : UUID("0d024925-e46c-472a-bf1a-13a8967e97c1"),
        "ok" : 1,
        "operationTime" : Timestamp(1593570389, 3),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570389, 3),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Uwaga: jeśli kolekcja nie istnieje, utwórz ją w następujący sposób:

db.createCollection("geekFlareCollection")
{
        "ok" : 1,
        "operationTime" : Timestamp(1593570344, 4),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570344, 5),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Krok 9

Wstaw dane do kolekcji. Logi Mongo zaczną się rozrastać, sygnalizując aktywność mechanizmu równoważenia i próby rozłożenia danych między fragmentami.

Krok 10

Ostatnim krokiem jest sprawdzenie stanu shardingu. Można to zrobić, uruchamiając poniższe polecenie w węźle trasy Mongos.

Status fragmentowania

Sprawdź status fragmentowania za pomocą poniższego polecenia w węźle routingu mongo:

sh.status()

mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("5ede66c22c3262378c706d21")
  }
  shards:
        {  "_id" : "rs1",  "host" : "rs1/localhost:27020,localhost:27021,localhost:27022",  "state" : 1 }
  active mongoses:
        "4.2.7" : 1
  autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  no
        Failed balancer rounds in last 5 attempts:  5
        Last reported error:  Could not find host matching read preference { mode: "primary" } for set rs1
        Time of Reported error:  Tue Jun 09 2020 15:25:03 GMT+0530 (India Standard Time)
        Migration Results for the last 24 hours:
                No recent migrations
  databases:
        {  "_id" : "config",  "primary" : "config",  "partitioned" : true }
                config.system.sessions
                        shard key: { "_id" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1024
                        too many chunks to print, use verbose if you want to force print
        {  "_id" : "geekFlareDB",  "primary" : "rs1",  "partitioned" : true,  "version" : {  "uuid" : UUID("a770da01-1900-401e-9f34-35ce595a5d54"),  "lastMod" : 1 } }
                geekFlareDB.geekFlareCol
                        shard key: { "key" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1
                        { "key" : { "$minKey" : 1 } } -->> { "key" : { "$maxKey" : 1 } } on : rs1 Timestamp(1, 0)
                geekFlareDB.geekFlareCollection
                        shard key: { "product" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1
                        { "product" : { "$minKey" : 1 } } -->> { "product" : { "$maxKey" : 1 } } on : rs1 Timestamp(1, 0)
        {  "_id" : "test",  "primary" : "rs1",  "partitioned" : false,  "version" : {  "uuid" : UUID("fbc00f03-b5b5-4d13-9d09-259d7fdb7289"),  "lastMod" : 1 } }

mongos>

Dystrybucja danych

Mongos rozdziela obciążenie między fragmenty w oparciu o klucz fragmentu, a równomierną dystrybucją danych zajmuje się Balancer.

Kluczowe elementy dystrybucji danych między shardami:

  • Balancer odpowiada za równomierne rozłożenie podzbiorów danych między węzłami fragmentów. Uruchamia się, gdy serwer Mongos zaczyna przekazywać obciążenia między shardami, i stara się zapewnić jak najbardziej równomierny rozkład. Aby sprawdzić stan balansera, można użyć polecenia sh.status(), sh.getBalancerState() lub sh.isBalancerRunning().
mongos> sh.isBalancerRunning()
true
mongos>

LUB

mongos> sh.getBalancerState()
true
mongos>

Po wstawieniu danych można zaobserwować aktywność w logach demona Mongos, który informuje o przenoszeniu porcji danych między shardami. Balancer próbuje równoważyć dane, a jego działanie może wpływać na wydajność. Dlatego zaleca się uruchamianie balansera w określonym oknie balansowania.

mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("5efbeff98a8bbb2d27231674")
  }
  shards:
        {  "_id" : "rs1",  "host" : "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022",  "state" : 1 }
        {  "_id" : "rs2",  "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",  "state" : 1 }
  active mongoses:
        "4.2.7" : 1
  autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  yes
        Failed balancer rounds in last 5 attempts:  5
        Last reported error:  Could not find host matching read preference { mode: "primary" } for set rs2
        Time of Reported error:  Wed Jul 01 2020 14:39:59 GMT+0530 (India Standard Time)
        Migration Results for the last 24 hours:
                1024 : Success
  databases:
        {  "_id" : "config",  "primary" : "config",  "partitioned" : true }
                config.system.sessions
                        shard key: { "_id" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs2     1024
                        too many chunks to print, use verbose if you want to force print
        {  "_id" : "geekFlareDB",  "primary" : "rs2",  "partitioned" : true,  "version" : {  "uuid" : UUID("a8b8dc5c-85b0-4481-bda1-00e53f6f35cd"),  "lastMod" : 1 } }
                geekFlareDB.geekFlareCollection
                        shard key: { "key" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs2     1
                        { "key" : { "$minKey" : 1 } } -->> { "key" : { "$maxKey" : 1 } } on : rs2 Timestamp(1, 0)
        {  "_id" : "test",  "primary" : "rs2",  "partitioned" : false,  "version" : {  "uuid" : UUID("a28d7504-1596-460e-9e09-0bdc6450028f"),  "lastMod" : 1 } }

mongos>
  • Klucz fragmentu definiuje logikę dystrybucji dokumentów pomiędzy shardami. Może to być pole indeksowane lub indeksowane pole złożone, które musi występować we wszystkich dokumentach w danej kolekcji. Dane są dzielone na porcje, a każda z nich jest przypisywana do klucza fragmentu w oparciu o zakres. Na podstawie zakresu zapytania router wybiera shard, który będzie przechowywał daną porcję danych.

Wybierając klucz fragmentu, należy wziąć pod uwagę następujące pięć właściwości:

  • Kardynalność
  • Dystrybucja zapisu
  • Dystrybucja odczytu
  • Kierowanie odczytu
  • Lokalność odczytu

Idealny klucz fragmentu sprawia, że MongoDB równomiernie rozkłada obciążenie pomiędzy wszystkie fragmenty. Dlatego właściwy wybór klucza jest bardzo istotny.

Obraz: MongoDB

Usuwanie węzła fragmentu

Przed usunięciem fragmentu z klastra należy upewnić się, że dane zostały bezpiecznie przeniesione do pozostałych fragmentów. MongoDB automatycznie przesyła dane do innych węzłów przed usunięciem wybranego węzła fragmentu.

Aby usunąć fragment, wykonaj poniższe kroki:

Krok 1

Najpierw należy zidentyfikować nazwę hosta fragmentu, który ma zostać usunięty. Poniższe polecenie wyświetli listę wszystkich fragmentów obecnych w klastrze, wraz z ich stanem.

db.adminCommand( { listShards: 1 } )

mongos> db.adminCommand( { listShards: 1 } )
{
        "shards" : [
                {
                        "_id" : "rs1",
                        "host" : "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022",
                        "state" : 1
                },
                {
                        "_id" : "rs2",
                        "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",
                        "state" : 1
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1593572866, 15),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593572866, 15),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Krok 2

Wydaj poniższe polecenie, aby usunąć wybrany fragment z klastra. Po jego wykonaniu Balancer zajmie się usunięciem porcji danych z opróżnianego węzła fragmentu i zrównoważy ich dystrybucję pomiędzy pozostałymi węzłami.

db.adminCommand( { removeShard: “shardedReplicaNodes” } )

mongos> db.adminCommand( { removeShard: "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022" } )
{
        "msg" : "draining started successfully",
        "state" : "started",
        "shard" : "rs1",
        "note" : "you need to drop or movePrimary these databases",
        "dbsToMove" : [ ],
        "ok" : 1,
        "operationTime" : Timestamp(1593572385, 2),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593572385, 2),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Krok 3

Aby sprawdzić stan opróżnianego fragmentu, wprowadź ponownie to samo polecenie.

db.adminCommand( { removeShard: “rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022” } )

Należy poczekać, aż transfer danych zostanie zakończony. Pola „msg” i „state” informują o postępie procesu.

"msg" : "draining ongoing",
"state" : "ongoing",

Możemy również sprawdzić status za pomocą polecenia sh.status(). Po usunięciu węzła fragmentu nie będzie on widoczny w wynikach. Jeśli proces opróżniania trwa, węzeł będzie oznaczony statusem „draining” jako true.

Krok 4

Kontynuuj sprawdzanie stanu opróżniania, aż do całkowitego usunięcia fragmentu. Po zakończeniu procesu dane wyjściowe polecenia powinny odzwierciedlać komunikat i stan jako zakończone.

"msg" : "removeshard completed successfully",
"state" : "completed",
"shard" : "rs1",
"ok" : 1,

Krok 5

Na koniec należy sprawdzić, które fragmenty pozostały w klastrze. Aby sprawdzić stan, wpisz sh.status() lub db.adminCommand( { listShards: 1 } )

mongos> db.adminCommand( { listShards: 1 } )
{
        "shards" : [
                {
                        "_id" : "rs2",
                        "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",
                        "state" : 1
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1593575215, 3),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593575215, 3),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

Widać, że usunięty fragment nie jest już wyświetlany na liście.

Korzyści fragmentacji nad replikacją

  • W przypadku replikacji węzeł podstawowy obsługuje wszystkie operacje zapisu, a serwery pomocnicze są przeznaczone do tworzenia kopii zapasowych lub obsługi zapytań tylko do odczytu. W shardingu obciążenie jest rozkładane na wiele serwerów.
  • Pojedynczy zestaw replik ma ograniczenie do 12 węzłów, natomiast nie ma limitu liczby fragmentów.
  • Replikacja wymaga zaawansowanego sprzętu do obsługi dużych zbiorów danych, a skalowanie pionowe jest kosztowne w porównaniu z dodawaniem serwerów w shardingu.
  • W replikacji wydajność odczytu można zwiększyć, dodając serwery podrzędne, natomiast sharding pozwala zwiększyć zarówno wydajność odczytu, jak i zapisu przez dodawanie większej liczby węzłów fragmentów.

Ograniczenia shardingu

  • Klaster podzielony na fragmenty nie obsługuje unikatowych indeksów w fragmentach, jeśli nie są one poprzedzone kluczem fragmentu.
  • Wszystkie operacje aktualizacji dla kolekcji podzielonej na fragmenty muszą zawierać klucz fragmentu lub pole _id w zapytaniu.
  • Kolekcje mogą być fragmentowane tylko wtedy, gdy ich rozmiar nie przekracza określonego progu. Próg ten zależy od średniego rozmiaru klucza fragmentu i skonfigurowanego rozmiaru fragmentu.
  • Sharding ma limity operacyjne dotyczące maksymalnego rozmiaru kolekcji lub liczby podziałów.
  • Nieprawidłowy wybór kluczy fragmentów może mieć negatywny wpływ na wydajność.

Podsumowanie

MongoDB oferuje wbudowany mechanizm shardingu do implementacji dużych baz danych bez obniżania wydajności. Mam nadzieję, że powyższe informacje pomogą w skonfigurowaniu shardingu w MongoDB. Warto również zapoznać się z popularnymi poleceniami MongoDB.


newsblog.pl