LoginSignup
4
0

More than 1 year has passed since last update.

KubernetesとRedis Sentinelで自動フェイルオーバー&レプリケーション再構成するセッションストアの構築

Last updated at Posted at 2021-11-16

こんにちは。

インメモリデータストアであるRedisRedis Sentinelと組み合わせることで自動フェイルオーバーが可能になります。

それなら「Kubernetesに冗長化構成を楽々作れる?」と思ったのですがひと手間必要だったので、やってみたことをまとめます。
(手元のシングルノード環境でお手軽に試しただけなので、本番運用は考慮していません)

注意事項

筆者の完全な勘違いで「Redisのslave-read-onlynoに設定すれば、スレーブ側でも書き込み&レプリケーション全体で同期される」と思っていたのですが、実際にはスレーブ側で書き込んだ内容はそのスレーブ限定になるのでこの記事で紹介する構成はフェイルオーバー時にクライアント側でアレコレする必要があります……

ほとんど書いてしまった後に気づいたのですが、せっかくなので投稿します。RedisやSentinel設定の参考になれば幸いです。

RedisとRedis Sentinelについて

https://redis.io/

Redisはオープンソースのインメモリデータストアで、AWS、GCP、Azureなど各社でマネージドサービスも提供されています。

(そういう意味では自分で冗長構成組必要あるか?とも思いますが、とりあえずやってみたいのエンジニアですよね?)

Redisでは標準機能でマスターとスレーブのレプリケーション構成を組むことができます。Redisのレプリケーションはスレーブ側からも書き込みが可能です。

一方でマスター側に障害が発生した場合に「スレーブをマスターに昇格させる」というようなフェイルオーバー機能はないため、別途監視用のプロセスを用いて自動フェイルオーバーを実現するのがRedis Sentinelの機能です。

出来上がった構成

下記のような構成になりました。

k8s構成.png

RedisレプリケーションはStatefulSetの順序性の保証により、最初に起動したpodをmasterとして構成します。

Redis Sentinelは1台のサーバー内にプロセス3つ起動でも問題ありませんがDeploymentで配置するほうが簡単そうなのでこのようにしました。

「Redis Sentinelのイベントトリガーを使って、フェイルオーバー時に共有ストレージに現在のマスターIPを更新していく」という部分がポイントです。

環境

  • Docker Desktop v20.10.7
  • Kubernetes on Docker Desktop v1.21.2
  • Redis 6.2.6

自動フェイルオーバーのための設定

ソースはGitHubにアップロードしています。

Redis本体

Kubernetesマニュフェスト

Redis本体のPodイメージはredis公式イメージをベースに、後述する設定ファイルと起動スクリプトを内包させたものです。

マスター情報を保持するためのPVはローカルホストを使っているので、マルチノード対応は考慮が必要です。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: session-store-shared-pv
  namespace: myapp
  labels:
    app: myapp
    type: storage
    subtype: shared
spec:
  storageClassName: slow
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteMany
  hostPath:
    path: /run/desktop/mnt/host/c/Users/takah/project/session-store
    type: Directory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: session-store-shared-pvc
  namespace: myapp
spec:
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  storageClassName: slow
  selector:
    matchLabels:
      app: myapp
      type: storage
      subtype: shared
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: session-store
  namespace: myapp
spec:
  selector:
    matchLabels:
      app: myapp
      type: session-store
  serviceName: session-store-svc
  replicas: 3
  template:
    metadata:
      labels:
        app: myapp
        type: session-store
        kube-svc: session-store-svc
      namespace: myapp
    spec:
      containers:
      - name: redis
        image: session-store
        imagePullPolicy: Never
        ports:
          - containerPort: 6379
            name:  redis
        command:
        - "/scripts/entrypoint.sh"
        volumeMounts:
          - name:  shared-volume
            mountPath: /redis/share
      volumes:
        - name:  shared-volume
          persistentVolumeClaim:
            claimName: session-store-shared-pvc
---
kind: Service
apiVersion: v1
metadata:
  name:  session-store-svc
  namespace: myapp
spec:
  selector:
    app:  myapp
    type: session-store
  type:  ClusterIP
  clusterIP: None
  ports:
  - name:  redis
    port:  6379
    targetPort:  6379

設定ファイル

Redis用の設定ファイルのテンプレートです。外からの接続を受け付けるためにbind 0.0.0.0 としています。

スレーブの場合は後述の起動用スクリプト内で、このファイルに対して設定を追記します。

redis.conf
daemonize no
bind 0.0.0.0
port 6379

起動用スクリプト

Redis本体のエントリーポイントです。
最初の配置では共有ストレージのマスター情報が作られていないため、その場合には自分のIPを書き込みます。

それ以外は既に存在するマスター情報からIPを読み取り、スレーブとして参加します。

Redis Sentinelはデフォルト30秒のマスターダウンでフェイルオーバーの判断をします。
一方でStatefulSetはPodの異常を検知すると即新しいIPのPodを再配置するため、podの再配置がフェイルオーバー判定を先回りして、共有ストレージ上のマスター情報が更新される前に古いマスターIPに対してスレーブ参加してしまいます。

それを回避するためにエントリーポイント内で30秒の待機時間を設けています。

entrypoint.sh

#!/bin/bash
#!/bin/bash
set -e

if [[ -z ${SHARED_MASTER_INFO_FILE} ]]; then
    export SHARED_MASTER_INFO_FILE=/redis/share/master
fi

# masterで再起動された場合,Sentinelのフェイルオーバーを追い越してしまう可能性があるため
# 起動まで30秒待機(Sentinelのフェイルオーバー判断するまでの待機時間がデフォルト30秒)
echo "waiting for start in 30 seconds..."
sleep 30

if [[ $(hostname) =~ (.+)-([0-9]+)$ ]]; then
    podname=${BASH_REMATCH[1]}
    ordinal=${BASH_REMATCH[2]}
    if [[ ${ordinal} -eq 0 ]] && [[ ! -f ${SHARED_MASTER_INFO_FILE} ]]; then
        # マスターの場合は共有ストレージのファイルにIPアドレスを書き込む
        master_ip=$(hostname -i)
        echo "This is master, set own ip: ${master_ip}"
        echo ${master_ip} >> ${SHARED_MASTER_INFO_FILE}
    else
        # スレーブの場合はマスターのIPアドレスを指定してslaveofを設定
        master_ip=$(cat ${SHARED_MASTER_INFO_FILE})
        echo "This is slave, use master info file: ${master_ip}"
        echo "slaveof ${master_ip} 6379" >> ${REDIS_CONF}
    fi
    redis-server ${REDIS_CONF}
fi

Redis Sentinel

続いてRedis Sentinelの各種設定です。

Kubernetesマニュフェスト

Redis Sentinelもredis公式をベースにエントリーポイントとイベント通知用スクリプト、設定ファイルをもたせたカスタムイメージです。
DeploymentのPodで使っているPVCは前段に定義したものです。

initContainers最初のRedisが立ち上がるまで=共有ストレージのマスター情報が書き込まれるまで待機させます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  session-store-redis-sentinel
  namespace: myapp
  labels:
    app: myapp
    type:  session-store-sidecar
spec:
  replicas: 3
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  selector:
    matchLabels:
        app: myapp
        type:  session-store-sidecar
  template:
    metadata:
      labels:
        app: myapp
        type:  session-store-sidecar
    spec:
      initContainers:
      - name: wait-container
        image: redis-sentinel
        imagePullPolicy: Never
        command: ['sh','-c','until [ -f /redis/share/master ]; do echo waiting for redis server... >> /redis/share/sentinel.log; cat /redis/share/master >> /redis/share/sentinel.log; sleep 10; done']
        volumeMounts:
          - name:  shared-volume
            mountPath: /redis/share
      containers:
      - name: redis-sentinel
        image: redis-sentinel
        imagePullPolicy: Never
        command:
        - "/scripts/entrypoint-sentinel.sh"
        env:
        - name: SHARED_MASTER_INFO_FILE
          value: /redis/share/master
        ports:
          - containerPort: 23679
            name:  sentinel
        volumeMounts:
          - name:  shared-volume
            mountPath: /redis/share
      volumes:
        - name:  shared-volume
          persistentVolumeClaim:
            claimName: session-store-shared-pvc

設定ファイル

Sentinelの起動時に指定する設定ファイルです。sentinel notification-scriptを設定することで、Sentinelのイベント発生時にスクリプトを起動します。

監視するRedisのマスター情報を起動用スクリプト内でこの設定ファイルに追記します。

redis-sentinel.conf
daemonize no
protected-mode no
port 26379
pidfile /redis/sentinel.pid
logfile /redis/sentinel.log
sentinel notification-script mymaster /scripts/notify.sh

起動用スクリプト

エントリーポイントは単純で、共有ストレージのマスターIPを指定してRedis Sentinelを起動します。

entrypoint-sentinel.sh
#!/bin/bash
set -e

if [[ -z ${SHARED_MASTER_INFO_FILE} ]]; then
    export SHARED_MASTER_INFO_FILE=/redis/share/master
fi

master_ip=""
if [[ ! -f ${SHARED_MASTER_INFO_FILE} ]]; then
    echo "master info file not found"
    exit 1
else
    master_ip=$(cat ${SHARED_MASTER_INFO_FILE})
    echo "sentinel monitor mymaster ${master_ip} 6379 2" >> ${REDIS_SENTINEL_CONF}
    redis-sentinel ${REDIS_SENTINEL_CONF}
fi

フェイルオーバー通知用スクリプト

ここがポイントです。

Sentinel設定ファイルにnotify−scriptを設定するとSentinelのイベント発生時、第1パラメータにイベント種別、第2パラメータにイベントの詳細が渡されます。

具体的には、フェイルオーバーが発生しマスターが切り替わるイベントは以下のようになります。

# 第1パラメータ
+switch-master

# 第2パラメータ
mymaster 10.1.2.74 6379 10.1.2.76 6379

このログから正規表現で切り替え後のマスターIPを抽出して共有ストレージのマスター情報を書き換え、master以外のpodはこの情報を見てスレーブとして参加する、という流になります。

notify.sh
#!/bin/bash

echo $1 >> /redis/event.log
echo $2 >> /redis/event.log
# +switch-masterが発生したら詳細情報から切替先のマスターIPを抽出して共有ストレージのファイルを上書きする
if [[ $1 == "+switch-master" ]]; then
    if [[ $2 =~ .+[[:space:]][0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]][0-9]+[[:space:]]([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then
        master_ip=${BASH_REMATCH[1]}
        echo "Over write master ip on shared file: ${master_ip}" >> /redis/event.log
        echo  ${master_ip} > ${SHARED_MASTER_INFO_FILE}
    fi
fi

実際に動かしてみる

それでは動かしてみましょう。

$ kubectl apply -f .\sample\k8s-sample.yml

Redisのpodが3つ、Sentinelのpodが3つ立ち上がりました。

$ kubectl get all --namespace=myapp
NAME                                                READY   STATUS    RESTARTS   AGE
pod/session-store-0                                 1/1     Running   0          49s
pod/session-store-1                                 1/1     Running   0          47s
pod/session-store-2                                 1/1     Running   0          41s
pod/session-store-redis-sentinel-66c49d5955-8pzcw   1/1     Running   0          49s
pod/session-store-redis-sentinel-66c49d5955-f77qv   1/1     Running   0          49s
pod/session-store-redis-sentinel-66c49d5955-mfx4g   1/1     Running   0          49s

NAME                        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/session-store-svc   ClusterIP   None         <none>        6379/TCP   49s

NAME                                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/session-store-redis-sentinel   3/3     3            3           49s

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/session-store-redis-sentinel-66c49d5955   3         3         3       49s

NAME                             READY   AGE
statefulset.apps/session-store   3/3     49s

初回起動なので予定どおりstatefulset の1つめ(インデックス0)がマスターになりました。

root@session-store-0:/redis# redis-cli
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=10.1.2.150,port=6379,state=online,offset=17182,lag=1
slave1:ip=10.1.2.151,port=6379,state=online,offset=16912,lag=1
master_failover_state:no-failover
master_replid:ebf677a9f4611c50ac75aee1dd1b9380491980cc
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:17317
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:17317

ここでマスターを削除してみます。

$ kubectl delete pod session-store-0 --namespace=myapp

statefulset なので削除されたインデックス0のpodは復帰しました。

$ kubectl get all --namespace=myapp
NAME                                                READY   STATUS    RESTARTS   AGE
pod/session-store-0                                 1/1     Running   0          26s
pod/session-store-1                                 1/1     Running   0          4m27s
pod/session-store-2                                 1/1     Running   0          4m21s
pod/session-store-redis-sentinel-66c49d5955-8pzcw   1/1     Running   0          4m29s
pod/session-store-redis-sentinel-66c49d5955-f77qv   1/1     Running   0          4m29s
pod/session-store-redis-sentinel-66c49d5955-mfx4g   1/1     Running   0          4m29s

ここでsentinelのイベントログを見てみると「redisのマスターが10.1.2.146から10.1.2.150に切り替わった」ことがわかります。

$ root@session-store-redis-sentinel-66c49d5955-8pzcw:/redis# cat /redis/sentinel.log
...
9:X 14 Nov 2021 15:15:53.290 # +sdown master mymaster 10.1.2.146 6379
9:X 14 Nov 2021 15:15:53.400 # +new-epoch 1
9:X 14 Nov 2021 15:15:53.414 # +vote-for-leader 70918ed35e8a13c470ac2eb16067d893750691fd 1
9:X 14 Nov 2021 15:15:53.852 # +config-update-from sentinel 70918ed35e8a13c470ac2eb16067d893750691fd 10.1.2.147 26379 @ mymaster 10.1.2.146 6379
9:X 14 Nov 2021 15:15:53.852 # +switch-master mymaster 10.1.2.146 6379 10.1.2.150 6379
9:X 14 Nov 2021 15:15:53.852 * +slave slave 10.1.2.151:6379 10.1.2.151 6379 @ mymaster 10.1.2.150 6379
9:X 14 Nov 2021 15:15:53.852 * +slave slave 10.1.2.146:6379 10.1.2.146 6379 @ mymaster 10.1.2.150 6379
9:X 14 Nov 2021 15:16:03.965 * +slave slave 10.1.2.152:6379 10.1.2.152 6379 @ mymaster 10.1.2.150 6379
9:X 14 Nov 2021 15:16:23.891 # +sdown slave 10.1.2.146:6379 10.1.2.146 6379 @ mymaster 10.1.2.150 6379

復帰したインデックス0のpodに入ってレプリケーション情報を見てみると、10.1.2.150のスレーブとして参加しているのが分かります。

root@session-store-0:/redis# redis-cli
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:10.1.2.150
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:112386
slave_repl_offset:112386
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:905df3a7cd6654a6cd525593e7d4a0920b789e2a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:112386
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:41809
repl_backlog_histlen:70578

動作確認

「Kubernetesのサービスにアクセスすればフェイルオーバー後も後ろを意識せずに読み書きできるぞ!」とやりたかったのですが、冒頭に書いたとおりRedis ResplicationはSlave側で書き込んでも同期されないためダメでした。

中途半端になってしまいましたが、Sentinelの使い方など何かの参考になれば幸いです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0