2022-10-02 18:35 Czas czytania: 24 min

MongoDB Sharding: Praktyczny przewodnik krok po kroku

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.

Anna Nowak
Autor
Polska

Tworzy zwięzłe wyjaśnienia, zamieniając złożone tematy w praktyczne wnioski.

Poprzedni artykuł
Jak zainstalować Jenkins na Windows, Ubuntu i CentOS?
Następny artykuł
12 najlepszych darmowych gier na Androida w 2022 roku