【Kubernetes,Redis】Redisの冗長化のためにOperatorについて調べる。

# はじめに

このところ、Kubernetes(k8s)でRedisに触っています。サービスの一部としてRedisを使うとき、ノード障害やミドルウェアのアップデートで、落ちてしまわないかが気になります。Redis自体には冗長化方式はあるのですが、k8sで動かす場合、Operatorの選択肢があります。
この記事ではRedisのOperatorについて調べたことをまとめます。

# Redisの冗長化方式

まずRedis自体にどのような冗長化方式があるかです。Redis OperatorはRedisの方式をベースとしているため、ここで整理しておきます。
Redisはマスターとレプリカの構成をとることができ、データを複製できます（Replication）。しかし、Replicationはフェイルオーバーの機能を持たず、マスターがダウンしてもレプリカがマスターに昇格しません。
Redis Sentinelはマスターとレプリカに加え、監視役のSentinelがいます。Sentinelがマスターの状態を監視し、マスターがダウンすればフェイルオーバーを実行します。
Redis Clusterもフェイルオーバーします。また、マルチマスター構成を取り、データを複数のマスターに分散するシャーディングを行っています。しかし、複数のDBを持てずSELECTコマンドが使えません。

|方式|構成|フェイルオーバー|論理DB|シャーディング|
|---|---|---|---|---|
|Replication|マスター、レプリカ|×|〇|×|
|Sentinel|マスター、レプリカ、Sentinel|〇|〇|×|
|Cluster|マルチマスター、レプリカ|〇|×|〇|

他にも外部ツールと組み合わせる方法もあるようです。

※参考

https://www.sraoss.co.jp/tech-blog/redis/redis-ha/

http://mogile.web.fc2.com/redis/topics/cluster-spec.html

# Redis,Sentinelの機能

この記事ではRedis Sentinelについて記載します。
先にRedis,Sentinelの機能について触れ、後述でその機能がOperatorでどのように変わるのかを記載したいと思います。

## Redis Sentinelのフェイルオーバーの流れ

Redis Sentinelのフェイルオーバーの流れを図で記載します。
Sentinelがマスターのダウンを検知し、レプリカをマスターに昇格させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/b6470923-bb18-24e4-869f-a2eccebf02f4.png)

ダウンの検知は各Sentinelで行われます。SentinelからRedisに対して2秒ごとにhelloメッセージを送り、状態を確認します。この時、マスターのダウンを検知すると主観ダウン状態(SDOWN)にあると判断します。
その後、他のSentinelの判断を確認し、マスターが客観的ダウン状態(ODOWN)にあると判断します。ODOWNであるかどうかは、SDOWNと判断したSentinelの台数が、Sentinelの設定中のQUORUMの値以上であるかで決定されます。
ODOWNとなった後は、フェイルオーバーが実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/3ccce6bf-8fea-2706-5c27-447b7386ba2e.png)

フェイルオーバーでは、各Sentinelが次のRedisマスター候補に投票します。投票後、１台のSentinelがリーダーとなり、投票結果を取りまとめ、QUORUMの値以上の投票を集めたRedisを次のマスターとします。その後、リーダーとなったSentinelがRedis Sentinel構成を更新し、レプリカをマスターに昇格させます。もし投票数がQUORUMだけ集めたRedisが存在しない場合、フェイルオーバーは失敗し、一定時間後、再度試行されます。

※参考

http://mogile.web.fc2.com/redis/topics/sentinel.html

## Redisのデータ永続化

Redisのデータをディスク上に保存し、ダウン後の再起動でデータを復元する方法について記載します。
データを保存する方法にはRDBとAOF(Append Only File)があります。
RDBではデータをファイルに圧縮して保存します。指定時間に変更されたデータ数に対する保存頻度を設定できます。
AOFではデータとコマンドを圧縮せずにファイルに保存します。以下に簡単な例を示します。

```bash
root@redis-pod:/data# redis-cli set test 1
root@redis-pod:/data# cat appendonlydir/appendonly.aof.1.incr.aof
*2
$6
SELECT
$1
0
*3
$3
set
$4
test
$1
1
```

それぞれの比較について記載します。

|項目|比較|メモ|
|---|---|---|
|必要なディスクサイズ|RDB<AOF|AOFは圧縮せずコマンドとデータを保存するため。|
|Redisダウン時のデータロスの大きさ|RDB>AOF|AOFは追記分だけ保存する。そのため保存頻度を大きくしてロスを小さくしやすい。|
|ファイル読み込み速度|RDB>AOF|AOFではデータに加えコマンドを読み込んでいるため。|
|永続化失敗の可能性|RDB<AOF|RDBはアトミックに保存が可能。AOFはバグで復元が失敗したことがあるらしい。|
|ファイル復元のしやすさ|RDB<AOF|AOFでは破損を修正するツールがある。また直接ファイルを修正しやすいため。|

どちらもメリット、デメリットがあり、使う場面に応じて、片方もしくは両方を採用するとよいそうです。

※参考

https://www.sraoss.co.jp/tech-blog/redis/redis-persistdata/

https://redis-documentasion-japanese.readthedocs.io/ja/latest/topics/persistence.html

https://blog.denet.co.jp/redismaxmemory/

## Redisのセキュリティ

Redisではアクセス制限を設定できます。bindの設定では、アクセスできるクライアントのIPアドレスを制限できます。
また、ユーザーを作成し、認証と権限を設定できます。以下、setとgetコマンドの権限を持つユーザー「myaccount」を作成する例です。

```bash
root@redis-pod:/data# redis-cli
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
127.0.0.1:6379> acl setuser myaccount on ~* &* >mypassword -@all +get +set
OK
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user myaccount on #89e01536ac207279409d4de1e5253e01f4a1769e696db0d6062ca9b8f56767c8 ~* &* -@all +set +get"
127.0.0.1:6379> exit
root@redis-pod:/data# redis-cli --user myaccount --pass mypassword
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> set test aaa
OK
127.0.0.1:6379> get test
"aaa"
127.0.0.1:6379> config get dir
(error) NOPERM this user has no permissions to run the 'config|get' command
```

※参考

http://mogile.web.fc2.com/redis/topics/security.html

http://mogile.web.fc2.com/redis/commands/acl-setuser.html

# Redis Operatorの機能

今回はSentinelを使った以下のgithubのOperatorについて記載します。

https://github.com/spotahome/redis-operator

k8sの機能を使えるようになっており、便利になっています。

## Redis Sentinelの構成、設定の監視

Operatorは以下のことを確認し、問題があれば自動で修正してくれます。

・起動しているRedisの台数が設定通りである。
・起動しているSentinelの台数が設定通りである。
・マスターは１台のみ起動している。
・レプリカが同じマスターに接続されている。
・Sentinelが同じマスターを監視している。
・SentinelはダウンしたRedis、Sentinelを監視していない。

Operatorなしとの違いとしては、Redisの台数を確認してくれることが挙げられると思います。Operatorなしでは、マスターダウン時のフェイルオーバー後、何もしないとレプリカが存在しない構成となってしまいます。Operatorを使うと、新しくレプリカとしてRedisのPODが立ち上がり、マスターレプリカ構成が保たれます。
また、Redis、Sentinelの接続先を監視しているため、コマンドミスなどで接続先が変わった際は修正してくれます。

※参考

https://github.com/spotahome/redis-operator/blob/master/docs/logic.md

## カスタムリソースの一部として設定できる

Operatorでは、データ永続化のためのPV(PersistentVolume)をカスタムリソースの一部として定義できます。
PVを使うことによりRedis外部にデータ永続化のためのファイルを保存できます。OperatorなしではPVの定義はカスタムリソースと別々でしなければならないため、Operatorによりリソースの管理が楽になると思います。

# RedisのOperatorを使ってみる

実際にk8s環境を作ってOperatorを使ってみます。

## k8s環境を作成する

まずはk8s環境を作ります。基本はこれまでと同じですが、パッケージのバージョンを指定しないと困った経験があるので、指定します。

https://qiita.com/kokohei39/items/4a018e74ca851eaeb20e

```bash:setup-docker-and-k8s.sh
#!/bin/bash -ex
echo 'install docker'
sudo yum install -y docker-20.10.13-2.amzn2.x86_64
sudo systemctl start docker
sudo systemctl enable docker

echo 'install k8s'
echo "`ip -4 a show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'` `hostname`" | sudo tee -a /etc/hosts
echo "
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
" | sudo tee -a /etc/yum.repos.d/kubernetes.repo > /dev/null
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
sudo yum install -y kubelet-1.24.0-0.x86_64 kubeadm-1.24.0-0.x86_64 kubectl-1.24.0-0.x86_64 --disableexcludes=kubernetes
sudo swapoff -a
sudo systemctl start kubelet
sudo systemctl enable kubelet

echo '
{
  "exec-opts": ["native.cgroupdriver=systemd"]
}
' | sudo tee /etc/docker/daemon.json > /dev/null
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl restart kubelet
sudo kubeadm init

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

echo 'setup control-plane node'
K8S_VERSION=$(kubectl version | base64 | tr -d '\n')
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$K8S_VERSION"
CONTROL_PLANE=`kubectl get node | grep 'control-plane' | cut -d' ' -f1`
kubectl taint nodes $CONTROL_PLANE node-role.kubernetes.io/master:NoSchedule-
kubectl taint nodes $CONTROL_PLANE node-role.kubernetes.io/control-plane:NoSchedule-
```

それますが、マスター、スレーブの呼び方は変わる流れとなっているようで、コマンドも少し変わっています。

https://amateur-engineer-blog.com/kubernetes-cluster/

## Redis Operatorをインストールする

Helmを使ってRedis Operatorをインストールします。

```bash:setup-helm-and-redis-operator.sh
#!/bin/bash -ex
SCRIPT_DIR=$(cd $(dirname $0); pwd)

echo 'install helm'
HELM_VERSION='v3.8.1'
cd $HOME
curl "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" -o helm.tar.gz
tar -zxvf helm.tar.gz
mv linux-amd64 helm
echo 'export PATH=$PATH:$HOME/helm' >> $HOME/.bashrc
source .bashrc
helm version

echo 'install redis-operator'
REDIS_OPERATOR_VERSION='v1.1.1'
REDIS_NAMESPACE='redis'
cd $SCRIPT_DIR
helm repo add redis-operator https://spotahome.github.io/redis-operator
helm repo update
helm install redis-operator redis-operator/redis-operator \
--set image.tag=$REDIS_OPERATOR_VERSION \
-n $REDIS_NAMESPACE \
--create-namespace \
-f config/redis-operator-values.yaml
```

```yaml:config/redis-operator-values.yaml
image:
  repository: quay.io/spotahome/redis-operator
  pullPolicy: Always

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 100m
    memory: 128Mi
```

インストール時にOperatorのメモリ、CPUなどを指定できます。

## Redis Sentinelを作成する

サンプルを参考にyamlファイルを作成し、Redis Sentinelを作成します。

https://raw.githubusercontent.com/spotahome/redis-operator/v1.1.1/example/redisfailover/basic.yaml

また、カスタムリソース中に、PODと同様に各種設定を入れることができます。

```yaml:rf-basic.yaml
apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
  name: redisfailover
  namespace: redis
spec:
  sentinel:
    replicas: 3
    resources:
      requests:
        cpu: 100m
      limits:
        memory: 100Mi
  redis:
    replicas: 2
    resources:
      requests:
        cpu: 100m
        memory: 100Mi
      limits:
        cpu: 400m
        memory: 500Mi
    storage:
      persistentVolumeClaim:
        metadata:
          name: rf-data
        spec:
          accessModes:
            - ReadWriteOnce
          storageClassName: local-storage
          resources:
            requests:
              storage: 1Gi
    affinity:
      podAntiAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            topologyKey: kubernetes.io/hostname
```

カスタムリソース以外のリソースの定義は省略します。
今回はカスタムリソース中に各種の設定を入れました。
また、Redis Operatorの構成の作成自体はyamlファイルを適用するだけでできます。楽でよいと思います。

```bash
kubectl apply -f rf-basic.yaml
```

以下、カスタムリソース中の設定について記載します。

・データ永続化としてPVを使用している。
　`/data`配下PVがマウントされ、ファイルが書き込まれます。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfr-redisfailover-0 -it -- sh
/data $ ls
dump.rdb
/data $ pwd
/data
```

　今回は記事を書く時間の都合上、ローカルストレージを使いましたが、AWSのEBSやEFSを使うことが可能です。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-ebs-csi-self-managed-add-on.html

・アフィニティを使用してPODの配置をノード単位で分散できるようにしている。
　ただ今回はノードは１つしかないので特に意味はありません。

https://kubernetes.io/ja/docs/concepts/scheduling-eviction/assign-pod-node/

また、githubのページではSecretを使って認証の設定が行えることが書いてありましたが、エラーで実施できませんでした。

```bash
[ec2-user@ip-10-0-0-53 ~]$ kubectl get pod -n redis
NAME                                 READY   STATUS    RESTARTS   AGE
redis-operator-777cdb655b-f2qxq      1/1     Running   0          143m
rfr-redisfailover-0                  0/1     Running   0          35m
rfr-redisfailover-1                  0/1     Running   0          35m
rfs-redisfailover-55b5fd485b-g9kz9   1/1     Running   0          35m
rfs-redisfailover-55b5fd485b-ldlvn   1/1     Running   0          35m
rfs-redisfailover-55b5fd485b-qhsct   1/1     Running   0          35m
[ec2-user@ip-10-0-0-53 ~]$ kubectl logs -n redis redis-operator-777cdb655b-f2qxq | tail -n 3
time="2022-05-21T14:09:35Z" level=info msg="podDisruptionBudget updated" namespace=redis podDisruptionBudget=rfs-redisfailover service=k8s.podDisruptionBudget src="poddisruptionbudget.go:79"
time="2022-05-21T14:09:35Z" level=info msg="deployment updated" deployment=rfs-redisfailover namespace=redis service=k8s.deployment src="deployment.go:102"
time="2022-05-21T14:09:35Z" level=error msg="error on object processing: WRONGPASS invalid username-password pair or user is disabled." controller-id=redisfailover object-key=redis/redisfailover operator=redisfailover service=kooper.controller src="controller.go:279"
```

## Redis Operatorにアクセスする

作成したRedis Sentinelにクライアントからアクセスしてみます。redis-cliコマンドが使えるPODを作成してやってみます。

```bash
[ec2-user@ip-10-0-0-53 ~]$ kubectl apply -f redis.yaml
pod/redis-pod created
[ec2-user@ip-10-0-0-53 ~]$ kubectl get svc -n redis
NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
redis-operator      ClusterIP   10.105.178.232   <none>        9710/TCP    164m
rfs-redisfailover   ClusterIP   10.100.210.109   <none>        26379/TCP   13m
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis redis-pod -it -- bash
root@redis-pod:/data# redis-cli -h rfs-redisfailover -p 26379 sentinel get-master-addr-by-name mymaster
1) "10.32.0.8"
2) "6379"
root@redis-pod:/data# redis-cli -h 10.32.0.8 set testdata 100
OK
root@redis-pod:/data# redis-cli -h rfs-redisfailover -p 26379 sentinel replicas mymaster
1)  1) "name"
    2) "10.32.0.9:6379"
    3) "ip"
    4) "10.32.0.9"
    5) "port"
    6) "6379"
    7) "runid"
    8) "5dd7d0cb8c3aabea01c5ec95cab15ec89a992db8"
    9) "flags"
   10) "slave"
   11) "link-pending-commands"
   12) "0"
   13) "link-refcount"
   14) "1"
   15) "last-ping-sent"
   16) "0"
   17) "last-ok-ping-reply"
   18) "109"
   19) "last-ping-reply"
   20) "109"
   21) "down-after-milliseconds"
   22) "5000"
   23) "info-refresh"
   24) "3605"
   25) "role-reported"
   26) "slave"
   27) "role-reported-time"
   28) "826627"
   29) "master-link-down-time"
   30) "0"
   31) "master-link-status"
   32) "ok"
   33) "master-host"
   34) "10.32.0.8"
   35) "master-port"
   36) "6379"
   37) "slave-priority"
   38) "100"
   39) "slave-repl-offset"
   40) "381554"
   41) "replica-announced"
   42) "1"
root@redis-pod:/data# redis-cli -h 10.32.0.9 get testdata
"100"
```

SentinelのServiceからマスターのアドレスを確認することで、Redisへデータの読み書きができました。

## フェイルオーバー時の挙動を確認する

次にRedisマスターがダウンした際のフェイルオーバーの挙動を見てみます。ダウンの状況としてRedisマスターのPODを削除します。

```bash
[ec2-user@ip-10-0-0-53 ~]$ kubectl get pod -n redis -o wide
NAME                                 READY   STATUS    RESTARTS   AGE    IP           NODE                                           NOMINATED NODE   READINESS GATES
redis-operator-777cdb655b-f2qxq      1/1     Running   0          170m   10.32.0.4    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
redis-pod                            1/1     Running   0          12m    10.32.0.10   ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-0                  1/1     Running   0          19m    10.32.0.8    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-1                  1/1     Running   0          19m    10.32.0.9    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-2967z   1/1     Running   0          19m    10.32.0.5    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-6pw27   1/1     Running   0          19m    10.32.0.6    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-fds5w   1/1     Running   0          19m    10.32.0.7    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-2967z -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.8:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-53 ~]$ kubectl delete pod -n redis rfr-redisfailover-0
pod "rfr-redisfailover-0" deleted
```

マスターがダウンすると、フェイルオーバーが行われます。フェイルオーバーによりマスターが変わったことを確認します。

```bash
[ec2-user@ip-10-0-0-53 ~]$ kubectl get pod -n redis -o wide
NAME                                 READY   STATUS    RESTARTS   AGE    IP           NODE                                           NOMINATED NODE   READINESS GATES
redis-operator-777cdb655b-f2qxq      1/1     Running   0          173m   10.32.0.4    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
redis-pod                            1/1     Running   0          15m    10.32.0.10   ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-0                  1/1     Running   0          93s    10.32.0.8    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-1                  1/1     Running   0          23m    10.32.0.9    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-2967z   1/1     Running   0          23m    10.32.0.5    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-6pw27   1/1     Running   0          23m    10.32.0.6    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-fds5w   1/1     Running   0          23m    10.32.0.7    ip-10-0-0-53.ap-northeast-1.compute.internal   <none>           <none>
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-2967z -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.9:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-2967z -- redis-cli -p 26379 sentinel replicas mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
name
10.32.0.8:6379
ip
10.32.0.8
port
6379
runid
4ae072844f52afdffff3f166a0f4dfbdf603c40c
flags
slave
link-pending-commands
0
link-refcount
1
last-ping-sent
0
last-ok-ping-reply
568
last-ping-reply
568
down-after-milliseconds
5000
info-refresh
2217
role-reported
slave
role-reported-time
192467
master-link-down-time
0
master-link-status
ok
master-host
10.32.0.9
master-port
6379
slave-priority
100
slave-repl-offset
485233
replica-announced
1
```

結果から、Redisマスターがrfr-redisfailover-0(10.32.0.8)からrfr-redisfailover-1(10.32.0.9)に変わっていることが確認できました。rfr-redisfailover-0はスレーブとなっていました。
Redis Operatorでは、PODが削除されてもIPアドレスが変わらないようにされているようです。RedisとSentinelの両方でIPアドレスが変わりません。これでRedisにとってIPアドレスが変わって都合の悪い諸々が解決されているように思います。

## 誤った設定となったときの挙動を確認する

OperatorはRedisとSentinelの設定面も監視しているとのことなので、確認してみます。
今回はSentinelのマスターの監視対象を削除してみます。

```bash
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-2967z -- redis-cli -p 26379 sentinel remove mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
OK
[ec2-user@ip-10-0-0-53 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-2967z -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.9:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-53 ~]$ kubectl logs -n redis rfs-redisfailover-55b5fd485b-2967z
・・・
1:X 21 May 2022 14:45:59.027 # -monitor master mymaster 10.32.0.9 6379
1:X 21 May 2022 14:46:06.318 # +monitor master mymaster 10.32.0.9 6379 quorum 2
・・・
```

結果から、設定変更後、約7秒後、再設定されていました。
これはRedis Sentinelにはない機能なので、ありがたいと思います。

## スプリットブレイン時の挙動を確認する

最後にスプリットブレインが起こった際の挙動について記載します。
Redis Sentinelでのスプリットブレインでは、各Sentinelが別々のRedisをマスターだと認識していることになります。簡単に図で例を示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/20f72a0e-2809-5971-2097-be86f841bf8a.png)

上図ではSentinel間とSentinelとRedis間の接続障害が同時に発生した場合を考えています。
この時、Sentinel1にとって、Sentinel2,3とRedis2(レプリカ)がダウンしているように見えます。
Sentinel2,3にとっては、Sentinel1とRedis1(マスター)がダウンしているように見えます。
Sentinel2,3はマスターがダウンしているように見えるため、フェイルオーバーを実行してRedis2をマスターに昇格させます。しかし、Sentinel1はそのことを知らないため、Redis1がマスターだと思い続けています。結果、Sentinel1とSentinel2,3で異なるRedisのマスターと接続していることになります。もし、この時クライアントから接続されると、データロスの可能性があります。
Redis Operatorではスプリットブレインが起きた時、どうなるか確認します。

準備としてchaos-meshをインストールします。

```bash:setup-chaos-mesh.sh
#!/bin/bash -ex
SCRIPT_DIR=$(cd $(dirname $0); pwd)

echo 'install chaos-mesh'
CHAOS_MESH_VERSION='2.1.3'
INSTALL_NAMESPACE='chaos-testing'
cd $SCRIPT_DIR
helm repo add chaos-mesh https://charts.chaos-mesh.org
helm install chaos-mesh chaos-mesh/chaos-mesh \
--version $CHAOS_MESH_VERSION \
-n $INSTALL_NAMESPACE \
--create-namespace \
-f config/chaos-mesh-values.yaml

echo 'annotate redis node'
CONTROL_PLANE=`kubectl get node | grep 'control-plane' | cut -d' ' -f1`
kubectl annotate node $CONTROL_PLANE chaos-mesh.org/inject=enabled
```

```yaml:config/chaos-mesh-values.yaml
controllerManager:
  enableFilterNamespace: 'true'

chaosDaemon:
  runtime: 'containerd'
  socketPath: '/var/run/containerd/containerd.sock'
```

Redis Operatorに接続の障害を起こします。
以下のyamlファイルを作成します。POD名を指定して接続を切断するものです。

```yaml:rf-netchaos.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: Workflow
metadata:
  name: rf-chaos
  namespace: chaos-testing
spec:
  entry: rf-networkchaos
  templates:
    - name: rf-networkchaos
      templateType: Parallel
      children:
        - redis-master
        - redis-slave
        - sentinel
    - name: redis-master
      templateType: NetworkChaos
      networkChaos:
        action: partition
        mode: all
        selector:
          pods:
            redis:
              - rfr-redisfailover-0
        direction: to
        target:
          mode: all
          selector:
            pods:
              redis:
                - rfs-redisfailover-55b5fd485b-jks7t
                - rfs-redisfailover-55b5fd485b-pp6qg
    - name: redis-slave
      templateType: NetworkChaos
      networkChaos:
        action: partition
        mode: all
        selector:
          pods:
            redis:
              - rfr-redisfailover-1
        direction: to
        target:
          mode: all
          selector:
            pods:
              redis:
                - rfs-redisfailover-55b5fd485b-4cltl
    - name: sentinel
      templateType: NetworkChaos
      networkChaos:
        action: partition
        mode: all
        selector:
          pods:
            redis:
              - rfs-redisfailover-55b5fd485b-4cltl
        direction: to
        target:
          mode: all
          selector:
            pods:
              redis:
                - rfs-redisfailover-55b5fd485b-jks7t
                - rfs-redisfailover-55b5fd485b-pp6qg
```

上記yamlファイルを適用します。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl apply -f rf-netchaos.yaml
workflow.chaos-mesh.org/rf-chaos created
```

Redis Operatorの状態を確認します。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-4cltl -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.13:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-jks7t -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.14:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-pp6qg -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.14:6379,slaves=1,sentinels=3
```

SentinelからRedisマスターのIPアドレスを確認してみると、1台だけIPアドレスが異なり、スプリットブレインが起きていることがわかります。
また、PODの状態を見てみます。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl get pod -n redis -o wide
NAME                                 READY   STATUS    RESTARTS   AGE     IP           NODE                                            NOMINATED NODE   READINESS GATES
redis-operator-777cdb655b-2w546      1/1     Running   0          24m     10.32.0.4    ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-0                  1/1     Running   0          4m24s   10.32.0.13   ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
rfr-redisfailover-1                  1/1     Running   0          4m24s   10.32.0.14   ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-4cltl   1/1     Running   0          4m23s   10.32.0.11   ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-jks7t   1/1     Running   0          4m23s   10.32.0.10   ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
rfs-redisfailover-55b5fd485b-pp6qg   1/1     Running   0          4m23s   10.32.0.12   ip-10-0-0-199.ap-northeast-1.compute.internal   <none>           <none>
```

READYの欄が1/1となっており、クライアントからアクセス可能であることがわかります。データロスが問題となる場合、何かしら対策が必要だと思います。
Operatorのログを見てみます。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl logs -n redis redis-operator-777cdb655b-2w546 | tail
time="2022-05-22T02:26:21Z" level=info msg="configMap updated" configMap=rfr-redisfailover namespace=redis service=k8s.configMap src="configmap.go:78"
W0522 02:26:21.493347       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0522 02:26:21.497182       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
time="2022-05-22T02:26:21Z" level=info msg="podDisruptionBudget updated" namespace=redis podDisruptionBudget=rfr-redisfailover service=k8s.podDisruptionBudget src="poddisruptionbudget.go:79"
time="2022-05-22T02:26:21Z" level=info msg="statefulSet updated" namespace=redis service=k8s.statefulSet src="statefulset.go:102" statefulSet=rfr-redisfailover
W0522 02:26:21.507657       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0522 02:26:21.511551       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
time="2022-05-22T02:26:21Z" level=info msg="podDisruptionBudget updated" namespace=redis podDisruptionBudget=rfs-redisfailover service=k8s.podDisruptionBudget src="poddisruptionbudget.go:79"
time="2022-05-22T02:26:21Z" level=info msg="deployment updated" deployment=rfs-redisfailover namespace=redis service=k8s.deployment src="deployment.go:102"
time="2022-05-22T02:26:21Z" level=error msg="error on object processing: More than one master, fix manually" controller-id=redisfailover object-key=redis/redisfailover operator=redisfailover service=kooper.controller src="controller.go:279"
```

ログの最後の1文で、マスターが複数いるため手動での修正が必要といったエラー文が出ています。
最後に接続障害を取り除いてみます。

```bash
[ec2-user@ip-10-0-0-199 ~]$ kubectl delete -f rf-netchaos.yaml
workflow.chaos-mesh.org "rf-chaos" deleted
[ec2-user@ip-10-0-0-199 ~]$ kubectl logs -n redis redis-operator-777cdb655b-2w546 | tail -n 20
・・・
time="2022-05-22T02:28:51Z" level=error msg="error on object processing: More than one master, fix manually" controller-id=redisfailover object-key=redis/redisfailover operator=redisfailover service=kooper.controller src="controller.go:279"
time="2022-05-22T02:29:21Z" level=info msg="configMap updated" configMap=rfs-redisfailover namespace=redis service=k8s.configMap src="configmap.go:78"
time="2022-05-22T02:29:21Z" level=info msg="configMap updated" configMap=rfr-s-redisfailover namespace=redis service=k8s.configMap src="configmap.go:78"
time="2022-05-22T02:29:21Z" level=info msg="configMap updated" configMap=rfr-readiness-redisfailover namespace=redis service=k8s.configMap src="configmap.go:78"
time="2022-05-22T02:29:21Z" level=info msg="configMap updated" configMap=rfr-redisfailover namespace=redis service=k8s.configMap src="configmap.go:78"
W0522 02:29:21.474784       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0522 02:29:21.478397       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
time="2022-05-22T02:29:21Z" level=info msg="podDisruptionBudget updated" namespace=redis podDisruptionBudget=rfr-redisfailover service=k8s.podDisruptionBudget src="poddisruptionbudget.go:79"
time="2022-05-22T02:29:21Z" level=info msg="statefulSet updated" namespace=redis service=k8s.statefulSet src="statefulset.go:102" statefulSet=rfr-redisfailover
W0522 02:29:21.487929       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0522 02:29:21.491568       1 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
time="2022-05-22T02:29:21Z" level=info msg="podDisruptionBudget updated" namespace=redis podDisruptionBudget=rfs-redisfailover service=k8s.podDisruptionBudget src="poddisruptionbudget.go:79"
time="2022-05-22T02:29:21Z" level=info msg="deployment updated" deployment=rfs-redisfailover namespace=redis service=k8s.deployment src="deployment.go:102"
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-4cltl -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.14:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-jks7t -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.14:6379,slaves=1,sentinels=3
[ec2-user@ip-10-0-0-199 ~]$ kubectl exec -n redis rfs-redisfailover-55b5fd485b-pp6qg -- redis-cli -p 26379 info | grep mymaster
Defaulted container "sentinel" out of: sentinel, sentinel-config-copy (init)
master0:name=mymaster,status=ok,address=10.32.0.14:6379,slaves=1,sentinels=3
```

少し経って構成が更新されました。全Sentinelの接続先のRedisマスターがフェイルオーバー後のものになっていました。接続障害がなくなれば自動で復旧を試みるようです。

以上でスプリットブレインの確認を終わります。

# おわりに

今回、Redis Operatorを使ってみて、実際にOperatorを採用するかどうかは状況次第だなと思いました。
ただ、Redisを使って実現したいことがOperatorでもできるなら、便利ではないかと思います。
まだまだインフラで知らないことが多いです。これからも勉強します。