使用Kubernetes和Redis Sentinel构建自动故障转移和复制重建的会话存储

你好。

通过与Redis Sentinel结合使用,Redis作为内存数据存储可以实现自动故障转移。

既然如此,我觉得可以轻松地在Kubernetes上创建冗余配置,但还需要一些步骤,所以我总结了一下我尝试的方法。
(只是在我的单节点环境中简单尝试了一下,没有考虑到生产运营)

请留意事项

在我的完全误解中,我曾认为将Redis的slave-read-only参数设置为no会使得在从服务器上的写操作能够同步到整个复制集,但实际上,从服务器上的写操作只限于该从服务器。因此,在本文介绍的配置中,在故障转移时需要在客户端进行一些操作……

我寫完了幾乎所有的內容後才注意到,但既然有了,我還是發表一下。希望這對於使用Redis和Sentinel進行設定的人有所幫助。

关于Redis和Redis Sentinel

请点击以下链接:https://redis.io/

Redis是一种开源的内存数据存储系统,在AWS、GCP、Azure等公司中也提供了托管服务。
(从这个意义上说,我们可能会考虑是否需要自己构建冗余结构,但作为工程师,我们首先想尝试一下,对吧?)

在Redis中,您可以使用标准功能来配置主从复制。Redis的复制允许从节点进行写入。

Redis Sentinel的功能就是使用单独的监视进程来实现自动容错,当主节点发生故障时,没有“将从节点晋升为主节点”的故障转移功能。

完成的组合出现了

以下形成如下。

k8s構成.png

Redis复制通过StatefulSet的顺序保证,将第一个启动的pod配置为主节点。

Redis Sentinel 在一个服务器上启动3个进程也没有问题,但是在部署时放置更加简单,所以我们选择了这种方式。

使用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官方镜像,并包含了下面提到的配置文件和启动脚本。

由于使用本地主机来保存主节点信息,因此需要考虑多节点支持。

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。

在受控机中,我们将在启动脚本中对该文件进行配置添加。

daemonize no
bind 0.0.0.0
port 6379

启动脚本

这是Redis的入口点。
在初始配置中,由于共享存储没有创建主节点信息,因此在这种情况下要将自己的IP写入。

除此之外,它会从已存在的主机信息中读取IP,并以从机的身份参与。

Redis Sentinel默认在主节点下线30秒后进行故障转移判断。
而StatefulSet在检测到Pod异常后立即重新分配新的IP地址给Pod,因此Pod的重新分配可能会比故障转移判断更早进行,导致在共享存储上的主节点信息更新之前,旧的主节点IP地址已经加入到从节点中。

为了避免这种情况,我们在入口点处设置了30秒的等待时间。


#!/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哨兵

接下来是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通知脚本,可以在Sentinel事件发生时启动脚本。

在启动脚本中将监视Redis主节点信息附加到此配置文件。

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,这是一个简单的入口点。

#!/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パラメータ
+switch-master

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

从这个日志中使用正则表达式提取切换后的主IP,并修改共享存储的主信息,除主节点外的Pod将根据此信息作为从节点参与进程。

#!/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(索引为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复制却无法在从站写入后进行同步,所以行不通。

虽然我只完成了一半,但如果Sentinel的使用方法能对你有所帮助,那就太好了。

广告
将在 10 秒后关闭
bannerAds