2022-03-16 / Bartłomiej Kurek
Redis sharding (partycjonowanie)

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.