Redis wspiera zarówno partycjonowanie (dzielenie danych przez wiele instancji), jak i replikację (duplikację danych).
Pierwsze rozwiązanie pozwala nam na dystrybucję danych na wiele serwerów. Drugie rozwiązanie pozwala nam na obsługę serwerów
zapasowych read-only, lub takich, które możemy promować do roli master, kiedy serwer master przestanie być dostępny.
W tym artykule przyjrzymy się klastrowi Redis na przykładzie klastra złożonego wyłącznie z instancji master. Repliki omówimy w kolejnej części.
Instancje wystartujemy na osobnych portach z poziomu konta zwykłego użytkownika.
Zachęcam również do zapoznania się z dokumentacją/tutorialem na redis.io.
Struktura katalogów
Wszystkie pliki konfiguracyjne umieścimy w jednym katalogu configs
.
Pliki pidfile
poszczególnych instancji umieścimy w katalogu run
, a każda z instancji będzie posiadać swój dedykowany katalog run/nodes
.
Konfigurację ograniczamy tutaj do minimum.
$ mkdir -p configs run/nodes
$ tree
.
├── configs
└── run
└── nodes
3 directories, 0 files
Dla prostoty instancje będziemy referować jako:
- master-A (port 16001)
- master-B (port 16002)
- master-C (port 16003)
Konfiguracja instancji
master-A
Zacznijmy od konfiguracji pojedynczego serwera redis.
Plik configs/master-A.conf
:
# Master node:
daemonize yes
bind 0.0.0.0
port 16001
pidfile ./run/master-A.pid
cluster-enabled yes
cluster-config-file ./run/nodes/master-A.conf
cluster-node-timeout 15000
Uruchamiamy serwer w trybie zdemonizowanym na porcie 16001.
Identyfikator uruchomionego procesu znajdzie się w pliku run/master-A.pid
,
Opcja cluster-enabled włącza obsługę klastra dla tej instancji serwera.
Opcja cluster-config-file wskazuje lokalizację pliku, w którym Redis przechowuje informacje na temat konfiguracji klastra (m.in. informacje o pozostałych węzłach klastra oraz ich stan).
Opcja cluster-node-timeout ustawia czas (w milisekundach), po którym instancja oznaczana jest jako niedostępna.
Startujemy tę pojedynczą instancję i obserwujemy wyniki.
Start:
$ redis-server configs/master-A.conf
Identyfikator procesu i sam proces:
$ cat run/master-A.pid
693256
$ ps 693256
PID TTY STAT TIME COMMAND
693256 ? Ssl 0:00 redis-server 0.0.0.0:16001 [cluster]
Informacje o klastrze, które Redis przechowuje samodzielnie we wskazanym pliku.
$ cat run/nodes/master-A.conf
65833ba4eccbac75c9ef9d500df64a6c554fcca4 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
Sprawdzamy klaster:
$ redis-cli -p 16001 cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:1
cluster_size:0
cluster_current_epoch:0
cluster_my_epoch:0
cluster_stats_messages_sent:0
cluster_stats_messages_received:0
Do poprawnego działania klastra wymagane są minimum 3 węzły w trybie master.
Obecnie nasz klaster jest w stanie fail.
Dodajmy zatem konfigurację węzłów master-B oraz master-C.
Konfiguracje te są niemal identyczne względem konfiguracji master-A. Różnią się jedynie numerami portów oraz lokalizacją pliku informacji o klastrze.
master-B
Plik configs/master-B.conf
.
# Master node:
daemonize yes
bind 0.0.0.0
port 16002
pidfile ./run/master-B.pid
cluster-enabled yes
cluster-config-file ./run/nodes/master-B.conf
cluster-node-timeout 15000
master-C
Plik configs/master-C.conf
.
# Master node:
daemonize yes
bind 0.0.0.0
port 16003
pidfile ./run/master-C.pid
cluster-enabled yes
cluster-config-file ./run/nodes/master-C.conf
cluster-node-timeout 15000
Startujemy wszystkie instancje master
$ redis-server configs/master-A.conf
$ redis-server configs/master-B.conf
$ redis-server configs/master-C.conf
Sprawdzamy procesy:
$ cat run/*pid | xargs ps
PID TTY STAT TIME COMMAND
694792 ? Ssl 0:00 redis-server 0.0.0.0:16001 [cluster]
704083 ? Ssl 0:00 redis-server 0.0.0.0:16002 [cluster]
704151 ? Ssl 0:00 redis-server 0.0.0.0:16003 [cluster]
Tworzymy klaster instancji master
W celu utworzenia klastra wydajemy odpowiednią komendę wskazując właściwe instancje:
$ redis-cli --cluster create \
--cluster-yes \
127.0.0.1:16001 \
127.0.0.1:16002 \
127.0.0.1:16003
Przełącznik --cluster-yes pozwala pominąć interaktywny tryb, w którym redis oczekuje odpowiedzi na pytanie:
Can I set the above configuration? (type 'yes' to accept):
Tworzenie klastra wygląda następująco:
$ redis-cli --cluster create \
127.0.0.1:16001 \
127.0.0.1:16002 \
127.0.0.1:16003
>>> Performing hash slots allocation on 3 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: 65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001
slots:[0-5460] (5461 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[5461-10922] (5462 slots) master
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[10923-16383] (5461 slots) master
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
..
>>> Performing Cluster Check (using node 127.0.0.1:16001)
M: 65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001
slots:[0-5460] (5461 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[5461-10922] (5462 slots) master
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[10923-16383] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
Teraz nasz klaster złożony z trzech instancji powinien być w stanie "ok":
$ redis-cli -p 16001 cluster info | grep cluster_state
cluster_state:ok
$ redis-cli -p 16002 cluster info | grep cluster_size
cluster_size:3
W celu wyświetlenia informacji o klastrze możemy użyć komendy:
$ redis-cli --cluster check 127.0.0.1:16001
Sprawdzamy:
$ redis-cli --cluster check 127.0.0.1:16001
127.0.0.1:16001 (65833ba4...) -> 0 keys | 5461 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 0 keys | 5462 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 0 keys | 5461 slots | 0 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:16001)
M: 65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001
slots:[0-5460] (5461 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[5461-10922] (5462 slots) master
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[10923-16383] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
Podobne informacje możemy uzyskać będąc już połączonym do któregoś z węzłów:
$ redis-cli -u redis://127.0.0.1:16001
127.0.0.1:16001> CLUSTER NODES
65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001@26001 myself,master - 0 1647441764000 1 connected 0-5460
0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002@26002 master - 0 1647441767054 2 connected 5461-10922
41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003@26003 master - 0 1647441765048 3 connected 10923-1638
Redis posiada cały zestaw komend obsługi klastra:
127.0.0.1:16001> cluster help
1) CLUSTER <subcommand> arg arg ... arg. Subcommands are:
2) ADDSLOTS <slot> [slot ...] -- Assign slots to current node.
3) BUMPEPOCH -- Advance the cluster config epoch.
4) COUNT-failure-reports <node-id> -- Return number of failure reports for <node-id>.
5) COUNTKEYSINSLOT <slot> - Return the number of keys in <slot>.
6) DELSLOTS <slot> [slot ...] -- Delete slots information from current node.
7) FAILOVER [force|takeover] -- Promote current replica node to being a master.
8) FORGET <node-id> -- Remove a node from the cluster.
9) GETKEYSINSLOT <slot> <count> -- Return key names stored by current node in a slot.
10) FLUSHSLOTS -- Delete current node own slots information.
11) INFO - Return information about the cluster.
12) KEYSLOT <key> -- Return the hash slot for <key>.
13) MEET <ip> <port> [bus-port] -- Connect nodes into a working cluster.
14) MYID -- Return the node id.
15) NODES -- Return cluster configuration seen by node. Output format:
16) <id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>
17) REPLICATE <node-id> -- Configure current node as replica to <node-id>.
18) RESET [hard|soft] -- Reset current node (default: soft).
19) SET-config-epoch <epoch> - Set config epoch of current node.
20) SETSLOT <slot> (importing|migrating|stable|node <node-id>) -- Set slot state.
21) REPLICAS <node-id> -- Return <node-id> replicas.
22) SAVECONFIG - Force saving cluster configuration on disk.
23) SLOTS -- Return information about slots range mappings. Each range is made of:
24) start, end, master and replicas IP addresses, ports and ids
Możemy też uzyskać identyfikator węzła klastra oraz listę jego replik.
127.0.0.1:16001> cluster myid
"65833ba4eccbac75c9ef9d500df64a6c554fcca4"
127.0.0.1:16001> CLUSTER REPLICAS 65833ba4eccbac75c9ef9d500df64a6c554fcca4
(empty array)
W tym przypadku nie mamy węzłów replik, ograniczamy się do klastra złożonego z instancji master.
Zapisujemy dane do jednego z węzłów i odczytujemy z pozostałych.
Zapis (master-A).
$ redis-cli -h 127.0.0.1 -p 16001 -c set x 123
OK
Odczyt (z wszystkich węzłów):
$ redis-cli -h 127.0.0.1 -p 16001 -c get x
"123"
$ redis-cli -h 127.0.0.1 -p 16002 -c get x
"123"
$ redis-cli -h 127.0.0.1 -p 16003 -c get x
"123"
Usuwamy dane z jednego z węzłów i sprawdzamy pozostałe instancje:
$ redis-cli -h 127.0.0.1 -p 16003 -c del x
(integer) 1
$ redis-cli -h 127.0.0.1 -p 16002 -c get x
(nil)
$ redis-cli -h 127.0.0.1 -p 16001 -c get x
(nil)
Kiedy węzeł master ginie i nie ma repliki
Sprawdźmy co się stanie, kiedy jeden z węzłów zginie. Do poprawnego działania klastra Redis wymagana minimum 3 instancji master.
Zatrzymujemy którąś z nich.
$ kill $(cat run/master-A.pid )
$ redis-cli --cluster info 127.0.0.1:16003
Could not connect to Redis at 127.0.0.1:16001: Connection refused
127.0.0.1:16003 (41a7a2de...) -> 0 keys | 5461 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 0 keys | 5462 slots | 0 slaves.
[OK] 0 keys in 2 masters.
0.00 keys per slot on average.
$ redis-cli --cluster check 127.0.0.1:16003
Could not connect to Redis at 127.0.0.1:16001: Connection refused
127.0.0.1:16003 (41a7a2de...) -> 0 keys | 5461 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 0 keys | 5462 slots | 0 slaves.
[OK] 0 keys in 2 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:16003)
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[10923-16383] (5461 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[5461-10922] (5462 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[ERR] Not all 16384 slots are covered by nodes.
Powyższa komenda w ostatniej linii informuje nas o problemie: instancja dla części slotów (hash slots
) jest nieosiągalna.
Możemy wyświetlić mapping wszystkich slotów w klastrze:
127.0.0.1:16003> CLUSTER slots
1) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 16002
3) "0a5591f7f78013d5def353a09cd58c7c1cc7dc07"
2) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 16001
3) "65833ba4eccbac75c9ef9d500df64a6c554fcca4"
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 16003
3) "41a7a2deed09d01b63af33c138d5308ace04e92b"
Redis posługuje się slotami w celu shardingu/dystrybucji kluczy na wiele węzłów. Każdy z węzłów obsługuje pewien ich zakres. W przypadku naszego klastra złożonego z trzech instancji master - każda z nich odpowiadała za jeden zakres. Kiedy zabrakło jednego z wymaganych serwerów master - zabrakło możliwości dystrybucji kluczy, a same dane przechowywane przez ten węzeł stały się niedostępne.
W przypadku kiedy brakuje wymaganej liczby instancji master, klaster staje się niedostępny po upłynięciu czasu określonego w konfiguracji opcją cluster-node-timeout
.
$ redis-cli -h 127.0.0.1 -p 16003 -c set y 321
(error) CLUSTERDOWN The cluster is down
Jeśli węzeł powróci, to działanie klastra zostanie wznowione:
$ redis-server configs/master-A.conf
$ redis-cli -h 127.0.0.1 -p 16003 -c set y 321
OK
$ redis-cli -h 127.0.0.1 -p 16001 -c get y
"321"
Dodawanie węzłów do klastra
Węzły do klastra możemy dodać komendą redis-cli --cluster add-node ...
lub: CLUSTER MEET. Dla przykładu stworzymy nową instancję (master-D, na porcie 16004) i dodamy ją do klastra:
Start:
$ redis-server configs/master-D.conf
$ redis-cli -p 16004 cluster nodes
ed6f2b8a5f4b54320f1426c4e5031241e14d9356 :16004@26004 myself,master - 0 0 0 connected
Dodanie węzła do klastra:
$ redis-cli -p 16004
127.0.0.1:16004> cluster meet 127.0.0.1 16001
OK
Wyświetlenie węzłów klastra.
$ redis-cli -p 16004 cluster nodes
0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002@26002 master - 0 1647444826455 2 connected 5461-10922
65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001@26001 master - 0 1647444827457 1 connected 0-5460
41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003@26003 master - 0 1647444825452 3 connected 10923-16383
ed6f2b8a5f4b54320f1426c4e5031241e14d9356 127.0.0.1:16004@26004 myself,master - 0 1647444826000 0 connected
Widzimy jednak, że nowy węzeł nie ma przypisanych żadnych slotów.
Musimy dokonać rebalansowania.
$ redis-cli --cluster rebalance 127.0.0.1:16004 --cluster-use-empty-masters
>>> Performing Cluster Check (using node 127.0.0.1:16004)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Rebalancing across 4 nodes. Total weight = 4.00
Moving 1366 slots from 127.0.0.1:16002 to 127.0.0.1:16004
### ...
Moving 1365 slots from 127.0.0.1:16003 to 127.0.0.1:16004
### ...
Moving 1365 slots from 127.0.0.1:16001 to 127.0.0.1:16004
### ...
Sprawdzamy klaster - każdy węzeł obsługuje teraz równy zakres slotów:
edis-cli --cluster check 127.0.0.1:16004
127.0.0.1:16004 (ed6f2b8a...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:16001 (65833ba4...) -> 0 keys | 4096 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 1 keys | 4096 slots | 0 slaves.
[OK] 3 keys in 4 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:16004)
M: ed6f2b8a5f4b54320f1426c4e5031241e14d9356 127.0.0.1:16004
slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[6827-10922] (4096 slots) master
M: 65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001
slots:[1365-5460] (4096 slots) master
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[12288-16383] (4096 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
Usuwanie węzłów, reshard
Kiedy chcemy usunąć jakiś węzeł z klastra, musimy dokonać migracji slotów.
W tej chwili klaster składa się z 4 instancji:
$ redis-cli --cluster check 127.0.0.1:16004
127.0.0.1:16004 (ed6f2b8a...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:16001 (65833ba4...) -> 0 keys | 4096 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 1 keys | 4096 slots | 0 slaves.
[OK] 3 keys in 4 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:16004)
M: ed6f2b8a5f4b54320f1426c4e5031241e14d9356 127.0.0.1:16004
slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master
M: 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 127.0.0.1:16002
slots:[6827-10922] (4096 slots) master
M: 65833ba4eccbac75c9ef9d500df64a6c554fcca4 127.0.0.1:16001
slots:[1365-5460] (4096 slots) master
M: 41a7a2deed09d01b63af33c138d5308ace04e92b 127.0.0.1:16003
slots:[12288-16383] (4096 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
Dla przykładu usuniemy instancję master-B (port 16002, 0a5591f7f78013d5def353a09cd58c7c1cc7dc07
).
Przed wyłączeniem instancji zmigrujemy sloty na instancję master-C (port 16003, 41a7a2deed09d01b63af33c138d5308ace04e92b
).
$ redis-cli --cluster reshard 127.0.0.1:16002 \
--cluster-yes \
--cluster-from 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 \
--cluster-to 41a7a2deed09d01b63af33c138d5308ace04e92b \
--cluster-slots 4096
Teraz instancja master-B na porcie 16002 nie ma przypisanych żadnych slotów:
$ redis-cli --cluster check 127.0.0.1:16004
127.0.0.1:16004 (ed6f2b8a...) -> 1 keys | 4096 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 0 keys | 0 slots | 0 slaves.
127.0.0.1:16001 (65833ba4...) -> 0 keys | 4096 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 2 keys | 8192 slots | 0 slaves.
[OK] 3 keys in 4 masters.
Możemy też ponownie wykonać rebalansowanie w celu równomiernej dystrybucji slotów.
$ redis-cli --cluster rebalance 127.0.0.1:16001
Sprawdzamy:
$ redis-cli --cluster check 127.0.0.1:16001
127.0.0.1:16001 (65833ba4...) -> 1 keys | 5462 slots | 0 slaves.
127.0.0.1:16004 (ed6f2b8a...) -> 1 keys | 5461 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 1 keys | 5461 slots | 0 slaves.
127.0.0.1:16002 (0a5591f7...) -> 0 keys | 0 slots | 0 slaves.
[OK] 3 keys in 4 masters.
Usuwamy węzeł master-B z klastra:
$ redis-cli --cluster del-node 127.0.0.1:16002 0a5591f7f78013d5def353a09cd58c7c1cc7dc07
>>> Removing node 0a5591f7f78013d5def353a09cd58c7c1cc7dc07 from cluster 127.0.0.1:16002
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.
Sprawdzamy:
$ redis-cli --cluster check 127.0.0.1:16001
127.0.0.1:16001 (65833ba4...) -> 1 keys | 5462 slots | 0 slaves.
127.0.0.1:16004 (ed6f2b8a...) -> 1 keys | 5461 slots | 0 slaves.
127.0.0.1:16003 (41a7a2de...) -> 1 keys | 5461 slots | 0 slaves.
[OK] 3 keys in 3 masters.
0.00 keys per slot on average.
Podsumowanie
W tej częście zajęliśmy się omówieniem podstaw shardingu w Redis i obsługi klastra. Po drodze widzieliśmy klaster w stanie fail kiedy jedna z wymaganych instancji przestała być dostępna. Do rozwiązania problemu tego typu możemy skorzystać z replikacji, w której instancja master może posiadać jedną lub więcej replik. W takim scenariuszu - replika jest w stanie zastąpić węzeł master (failover), w przypadku kiedy stanie się on niedostępny. Zajmiemy się tym zagadnieniem w następnej części.