Kubernetes 上に Redis Sentinel 構成 を構築する方法

0 件のコメント

今回は「Kubernetes上にRedisのSentinel構成を構築する方法」についてまとめます。

今回はRedisの冗長化構成3種(Replication、Sentinel、Cluster)のうち「Sentinel」構成について、構築する方法をまとめました。 ただ…残念ながら現状のRedis(v5.0.5)では標準付属の Sentinel のみを利用した Kubernetes上での"完全な" 冗長構成は作れなさそうです。 もし完全な冗長構成を作りたい場合は少し自作が必要そうです。

概要

今回は以下のような構成で Redis Sentinel 構成を組み上げていきます。

構成としては「マスター1台、スレーブ1台、センチネル3台」で、「パスワードを共有」するような構成にしています。

「Redis Seerver の StatefulSet に Sentinel を同梱」するような実装だと Pod 内に Redis Server と Sentinel が一緒になってしまってPodの再起動がしづらくなってしまいます。 まぁ、そもそも役割違うのでPod分けてあげるのが妥当かと思って上のような構成にしています。

なお、Sentinel側もStatefulSetで構築していますが、こちらはDeploymentでもいいかもしれません(Sentinel は順序性の意味がないので)。

構築(共通)

まずは共有するパスワードをSecretリソースに準備します。 作成方法は単独サーバーのときと同じなのでほぼ再掲になります。 一応、今回は作成リソースが多くなるのであとから検索しやすくするようラベルを追加しておきます。

  1. パスワードファイルを作成

    OpenSSL を利用してランダムなパスワード文字列を作成します。 デフォルト指定する base64 オプションだと 約3割り増し の文字列生成されるので、改行削除して先頭512文字をパスワードとして利用します。

    1
    openssl rand -base64 512 | tr -d '\r\n' | cut -c 1-512 > redis-password

    この後で動作確認をする際に毎回このパスワードを入力します。 パスワードが長いとテスト向けに動作確認するのはつらくなるので、本番まで間はいったん適当にしておくのもあり。

  2. KubernetesにSecreetリソースを作成

    1
    kubectl create secret generic redis-secret --from-file=redis-password
  3. Secreetリソースをラベルを追加

    1
    kubectl label secrets/redis-secret app=redis

構築(Replication)

Redis Server 用の Service を作成していきます。

  1. マニュフェストの作成

    redis-server-service.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    apiVersion: v1
    kind: Service
    metadata:
      name: cache-service
      labels:
        app: redis
    spec:
      ports:
      - port: 6379
        targetPort: 6379
      clusterIP: None               # Headless Service
      selector:
        app: redis                  # match "StatefulSet spec.template.metadata.labels"
        type: server

    selector には app 以外にも type を指定しています。 今回は Redis Server と Sentinel の2つがあるので、その違いを type で区別するようにしています。

  2. リソースの作成

    1
    kubectl apply -f redis-server-service.yml

Redisサーバーの実態を作成します。

  1. マニュフェストの作成

    redis-server-statefulset.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: redis
      namespace: default
      labels:
        app: redis
        ver: "0.0.1"
        env: staging
    spec:
      selector:
        matchLabels:
          app: redis
          type: server
      serviceName: cache-service    # match "Service metadata.name"
      replicas: 2
      template:
        metadata:
          name: redis
          namespace: default
          labels:
            app: redis
            type: server
            ver: "0.0.1"
            env: staging
        spec:
          terminationGracePeriodSeconds: 10
          containers:
          - name: redis
            image: redis:5.0.5-alpine3.10
            ports:
            - containerPort: 6379
            command:
            - "/bin/sh"
            - "-c"
            args:
            - |
              CONFIG_FILE_PATH=/data/redis.conf
               
              create_config() {
              if [ $HOSTNAME = ${REDIS_STATEFULSETNAME}-0 ]; then
               
              #
              # Master server config file
              #
              cat <<-EOF > "${CONFIG_FILE_PATH}"
              ########### NETWORK ##########
              bind 0.0.0.0
              port 6379
              protected-mode no
     
              ########### GENERAL ##########
              daemonize no
              logfile ""
     
              ######## SNAPSHOTTING ########
              save 900 1
              save 300 10
              save 60 10000
              dir "/data"
              dbfilename "dump.rdb"
     
              ######### REPLICATION ########
              masterauth ${REDIS_PASSWORD}
              replica-announce-ip "${HOSTNAME}.${REDIS_SERVICENAME}"
              replica-announce-port 6379
     
              ########## SECURITY ##########
              requirepass ${REDIS_PASSWORD}
     
              ##### MEMORY MANAGEMENT ######
              maxmemory 100mb
              maxmemory-policy allkeys-lru
              EOF
               
              else
               
              #
              # Slave server config file
              #
              cat <<-EOF > "${CONFIG_FILE_PATH}"
              ########### NETWORK ##########
              bind 0.0.0.0
              port 6379
              protected-mode no
     
              ########### GENERAL ##########
              daemonize no
              logfile ""
     
              ######## SNAPSHOTTING ########
              save 900 1
              save 300 10
              save 60 10000
              dir "/data"
              dbfilename "dump.rdb"
     
              ######### REPLICATION ########
              replicaof ${REDIS_STATEFULSETNAME}-0.${REDIS_SERVICENAME} 6379
              masterauth ${REDIS_PASSWORD}
              replica-announce-ip "${HOSTNAME}.${REDIS_SERVICENAME}"
              replica-announce-port 6379
     
              ########## SECURITY ##########
              requirepass ${REDIS_PASSWORD}
     
              ##### MEMORY MANAGEMENT ######
              maxmemory 100mb
              maxmemory-policy allkeys-lru
              EOF
               
              fi
              }
     
              if [ ! -e ${CONFIG_FILE_PATH} ]; then
                create_config
              fi
     
              redis-server "${CONFIG_FILE_PATH}"
            env:
            - name: "REDIS_STATEFULSETNAME"
              value: "redis"
            - name: "REDIS_SERVICENAME"
              value: "cache-service"
            - name: "REDIS_PASSWORD"
              valueFrom:
                secretKeyRef:
                  name: redis-secret
                  key: redis-password

    あまり特筆事項はありませんが…もし設定ファイルを ConfigMap から与えたいのであれば、initContainerやShellで別途コピーして書き込みできるようにする必要があります。

  2. リソースの作成

    1
    kubectl apply -f redis-server-statefulset.yml

構築(Sentinel)

Redisレプリケーションを監視するセンチネルを構築します。 センチネルはステートフルである必要ありませんが・・・サーバー本体と同じ作りのほうがわかりやすいかと思うので、今回は StatefulSet で準備します。

サーバーを個別に特定できるようHeadlessサービスを作っておきます。

  1. マニュフェストの作成

    redis-sentinel-service.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    apiVersion: v1
    kind: Service
    metadata:
      name: cache-monitoring
      labels:
        app: redis
    spec:
      ports:
      - port: 26379
        targetPort: 26379
      clusterIP: None               # Headless Service
      selector:
        app: redis                  # match "StatefulSet spec.template.metadata.labels"
        type: sentinel
  2. リソースの作成

    1
    kubectl apply -f redis-sentinel-service.yml

センチネルを構築します。

  1. マニュフェストの作成

    redis-sentinel-statefulset.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: sentinel
      namespace: default
      labels:
        app: redis
        ver: "0.0.1"
        env: staging
    spec:
      selector:
        matchLabels:
          app: redis
          type: sentinel
      serviceName: cache-monitoring # match "Service metadata.name"
      replicas: 3
      template:
        metadata:
          name: redis
          namespace: default
          labels:
            app: redis
            type: sentinel
            ver: "0.0.1"
            env: staging
        spec:
          terminationGracePeriodSeconds: 10
          containers:
          - name: sentinel
            image: redis:5.0.5-alpine3.10
            ports:
            - containerPort: 26379
            command:
            - "/bin/sh"
            - "-c"
            args:
            - |
              CONFIG_FILE_PATH=/etc/redis.conf
     
              while ! ping -c 1 ${REDIS_MASTER_HOST} &> /dev/null
              do
                echo "Waiting for redis master server boot ..."
                sleep 1
              done
     
              cat <<-EOF > "${CONFIG_FILE_PATH}"
              ########### NETWORK ##########
              bind 0.0.0.0
              port 26379
              protected-mode no
               
              ########### GENERAL ##########
              daemonize no
              logfile ""
              sentinel announce-ip "${HOSTNAME}.${REDIS_SENTINEL_SERVICENAME}"
              sentinel announce-port 26379
               
              ########## SECURITY ##########
              requirepass ${REDIS_PASSWORD}
               
              ########## SENTINEL ##########
              sentinel monitor mymaster ${REDIS_MASTER_HOST} 6379 2
              sentinel auth-pass mymaster ${REDIS_PASSWORD}
              sentinel down-after-milliseconds mymaster 5000
              sentinel failover-timeout mymaster 180000
              sentinel parallel-syncs mymaster 1
              EOF
     
              redis-server "${CONFIG_FILE_PATH}" --sentinel
            env:
            - name: "REDIS_MASTER_HOST"
              value: "redis-0.cache-service"
            - name: "REDIS_SENTINEL_SERVICENAME"
              value: "cache-monitoring"
            - name: "REDIS_PASSWORD"
              valueFrom:
                secretKeyRef:
                  name: redis-secret
                  key: redis-password

    センチネルは3台以上が推奨なので、 replication は 3以上 を指定します。

  2. リソースの作成

    1
    kubectl apply -f redis-sentinel-statefulset.yml

確認

ここまでに作成したクラスタの動作確認をしていきます。

  1. ラベル指定で作成リソースを確認

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    [root@k8s-master ~]# kubectl get sts,svc,po,secrets -l app=redis
    NAME                        READY   AGE
    statefulset.apps/redis      2/2     13h
    statefulset.apps/sentinel   3/3     12h
     
    NAME                       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
    service/cache-monitoring   ClusterIP   None         <none>        26379/TCP   12h
    service/cache-service      ClusterIP   None         <none>        6379/TCP    13h
     
    NAME             READY   STATUS    RESTARTS   AGE
    pod/redis-0      1/1     Running   0          146m
    pod/redis-1      1/1     Running   0          13h
    pod/sentinel-0   1/1     Running   0          12h
    pod/sentinel-1   1/1     Running   0          12h
    pod/sentinel-2   1/1     Running   0          12h
     
    NAME                  TYPE     DATA   AGE
    secret/redis-secret   Opaque   1      13h

  1. 動作確認用Pod作成

    1
    kubectl run -it redis-debug --image=redis:5.0.5-alpine3.10 --restart=Never --rm -- sh
  2. マスターへログイン

    1
    redis-cli -h redis-0.cache-service -a password
  3. マスターにデータ投入

    1
    set key1 "Hello World"
  4. マスターからログアウト

    exit でログアウトします。

  5. スレーブにログイン

    1
    redis-cli -h redis-1.cache-service -a password
  6. スレーブでデータ確認

    1
    2
    get key1
    "Hello World"

  1. 動作確認用Pod作成

    1
    kubectl run -it redis-debug --image=redis:5.0.5-alpine3.10 --restart=Never --rm -- sh
  2. 現在の状態を確認

    1
    2
    redis-cli -h redis-0.cache-service -a password info | grep role
    redis-cli -h redis-1.cache-service -a password info | grep role
  3. マスターを停止

    1
    redis-cli -h redis-0.cache-service -a password debug sleep 30
  4. スレーブが昇格していることを確認

    1
    2
    redis-cli -h redis-0.cache-service -a password info | grep role
    redis-cli -h redis-1.cache-service -a password info | grep role

…と、まぁ、"1回" だけは自動でフェールオーバーしてくれるのですが…2回目を続けて実施すると動かないんですよね。。 おそらく Redis Sentinel が内部的に管理しているものが「IPアドレス」で「ホスト名」ではないことに起因しているように見えます。 また、 sleep ではなく kubectl delete pod/redis-0 のようなことをすると、そもそもIPアドレスが変わってしまって動きがおかしくなります。

これに対する対策は…「ホスト名解決できる実装を待つ」か「IPアドレスがおかしな場合に補正を行うスクリプトを書く」かどちらかになりそうです。 後者(補正するスクリプト)については実際に作っている人がいる(hortonworks/k8s-redis-ha )ので参考にしてみるとよいかも。

今回は「Kubernetes 上 に Redis Sentinel 構成 を構築する方法」についてまとめました。 ポイントは以下の通りです。

  • レプリケーションは StatefulSet で構築、 Headless サービスで特定できるようにする
  • センチネルは StatefulSet ないし Deployment で構築
  • 標準機能だけだと1回しかフェールオーバーできない

参考になったでしょうか? 本記事がお役に立っていると嬉しいです!!

最後に… このブログに興味を持っていただけた方は、 ぜひ 「Facebookページ に いいね!」または 「Twitter の フォロー」 お願いします!!