目录

k8s学习笔记——实战篇——手动搭建redis集群

简介:k8s中的redis集群搭建。包含三个主节点,三个从节点。使用statusfulSetheadless-service部署。使用configMap方式配置。使用nfspvpvc挂载存储。使用service暴露统一接口,供集群内其他容器调用。

问题分析

本质上来说,在k8s上部署一个redis集群和部署一个普通应用没有什么太大的区别,但需要注意下面几个问题:

  1. reids是一个有状态应用

    这是部署redis集群时最需要注意的问题,当我们把redis以pod的形式部署在k8s中时,每个pod里缓存的数据都是不一样的,而且pod的IP是会随时变化,这时候如果使用普通的deploymentservice来部署redis-cluster就会出现很多问题,因此需要改用StatefulSet + Headless Service来解决。

  2. 数据持久化

    redis虽然是基于内存的缓存,但还是需要依赖于磁盘进行数据的持久化,以便服务出现问题重启时可以恢复已经缓存的数据。在集群中,我们需要使用共享文件系统 + PV(持久卷)的方式来让整个集群中的所有pod都可以共享同一份持久化储存。

解决方案

借助StatefulSet和Headless Service,集群的部署方案设计如下:

架构

说明:redis-cluster包含三个主节点,三个从结点。使用nfs存储。整个redis集群使用service暴露统一的接口。外部使用ubuntu容器对其进行管理、操作和测试。

环境说明

主机名 系统版本 IP地址 cpu/内存/磁盘 用途 软件版本
k8s-master1 CentOS7.9 192.168.3.40 2核/2GB/30GB k8s master1节点 k8s v1.20.15
k8s-master2 CentOS7.9 192.168.3.41 2核/2GB/30GB k8s master2节点 k8s v1.20.15
k8s-master3 CentOS7.9 192.168.3.42 2核/2GB/30GB k8s master3节点 k8s v1.20.15
k8s-node1 CentOS7.9 192.168.3.43 4核/8GB/30GB k8s node1节点、nfs存储 k8s v1.20.15、nfs v4
k8s-node2 CentOS7.9 192.168.3.44 4核/8GB/30GB k8s node2节点 k8s v1.20.15

实际操作

配置共享文件系统NFS

创建NFS存储主要是为了给Redis提供稳定的后端存储,当Redis的Pod重启或迁移后,依然能获得原先的数据。这里,我们先要创建NFS,然后通过使用PV为Redis挂载一个远程的NFS路径。

安装NFS:

由于硬件资源有限,可以在k8s-node2上搭建。执行如下命令安装NFS和rpcbind:

1
yum -y install nfs-utils rpcbind 

其中,NFS依靠远程过程调用(RPC)在客户端和服务器端路由请求,因此需要安装rpcbind服务。

然后,新增/etc/exports文件,用于设置需要共享的路径:

1
2
3
4
5
6
/usr/local/k8s/redis/pv1 *(rw,all_squash)
/usr/local/k8s/redis/pv2 *(rw,all_squash)
/usr/local/k8s/redis/pv3 *(rw,all_squash)
/usr/local/k8s/redis/pv4 *(rw,all_squash)
/usr/local/k8s/redis/pv5 *(rw,all_squash)
/usr/local/k8s/redis/pv6 *(rw,all_squash)

如上,rw表示读写权限;all_squash 表示客户机上的任何用户访问该共享目录时都映射成服务器上的匿名用户(默认为nfsnobody);*表示任意主机都可以访问该共享目录,也可以填写指定主机地址,同时支持正则。

由于我们打算创建一个6节点的Redis集群,所以共享了6个目录。当然,我们需要在k8s-node2上创建这些路径,并且为每个路径修改权限:

1
chmod 777 /usr/local/k8s/redis/pv*

这一步必不可少,否则挂载时会出现mount.nfs: access denied by server while mounting的权限错误。

启动NFS和rpcbind服务:

1
2
systemctl start rpcbind
systemctl start nfs

在k8s-node2上测试一下,执行:

1
mount -t nfs 192.168.56.102:/usr/local/k8s/redis/pv1 /mnt

表示将k8s-node1上的共享目录/usr/local/k8s/redis/pv1映射为k8s-node2的/mnt目录,我们在/mnt中创建文件:

1
touch test

可以在k8s-node1上看到该文件:

1
2
[root@k8s-node01 pv1]# ls
test

创建PV和PVC

每一个Redis Pod都需要一个独立的PV来存储自己的数据,因此可以创建一个pv.yaml文件,包含6个PV:

 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
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv1
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv1"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv2
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv2"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv3
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv3"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv4
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv4"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv5
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv5"

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv6
spec:
  capacity:
    storage: 200M
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.56.102
    path: "/usr/local/k8s/redis/pv6"

如上,可以看到所有PV除了名称和挂载的路径外都基本一致。执行创建即可:

1
2
3
4
5
6
7
[root@k8s-node1 redis]# kubectl create -f pv.yaml 
persistentvolume "nfs-pv1" created
persistentvolume "nfs-pv2" created
persistentvolume "nfs-pv3" created
persistentvolume "nfs-pv4" created
persistentvolume "nfs-pv5" created
persistentvolume "nfs-pv6" created

创建ConfigMap

这里,我们可以直接将Redis的配置文件转化为Configmap,这是一种更方便的配置读取方式。配置文件redis.conf如下:

1
2
3
4
5
6
appendonly yes
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
dir /var/lib/redis
port 6379

创建名为redis-conf的Configmap:

1
kubectl create configmap redis-conf --from-file=redis.conf

查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@k8s-master01 ~]# kubectl describe cm redis-conf
Name:         redis-conf
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
redis.conf:
----
appendonly yes
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
dir /var/lib/redis
port 6379


BinaryData
====

Events:  <none>

如上,redis.conf中的所有配置项都保存到redis-conf这个Configmap中。

创建Headless Service

Headless service是StatefulSet实现稳定网络标识的基础,我们需要提前创建。准备文件headless-service.yml如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
  name: redis-service
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    port: 6379
  clusterIP: None
  selector:
    app: redis # 必须和statufulSet的name一致
    appCluster: redis-cluster

创建:

1
kubectl create -f headless-service.yml

查看:

1
2
3
[root@k8s-master01 ~]# kubectl get svc redis-service
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
redis-service   ClusterIP   None         <none>        6379/TCP   19h

可以看到,服务名称为redis-service,其CLUSTER-IPNone,表示这是一个“无头”服务。

创建StatefulSet

创建好Headless service后,就可以利用StatefulSet创建Redis 集群节点,这也是本文的核心内容。我们先创建redis.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
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-app
spec:
  serviceName: "redis-service"
  replicas: 6
  selector:
    matchLabels:
      app: redis # has to match .spec.template.metadata.labels
  template:
    metadata:
      labels:
        app: redis
        appCluster: redis-cluster
    spec:
      terminationGracePeriodSeconds: 20
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - redis
              topologyKey: kubernetes.io/hostname
      containers:
      - name: redis
        image: "redis"
        imagePullPolicy: IfNotPresent
        command:
          - "redis-server"
        args:
          - "/etc/redis/redis.conf"
          - "--protected-mode"
          - "no"
        resources:
          requests:
            cpu: "100m"
            memory: "100Mi"
        ports:
            - name: redis
              containerPort: 6379
              protocol: "TCP"
            - name: cluster
              containerPort: 16379
              protocol: "TCP"
        volumeMounts:
          - name: "redis-conf"
            mountPath: "/etc/redis"
          - name: "redis-data"
            mountPath: "/var/lib/redis"
      volumes:
      - name: "redis-conf"
        configMap:
          name: "redis-conf"
          items:
            - key: "redis.conf"
              path: "redis.conf"
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteMany" ]
      resources:
        requests:
          storage: 200M

如上,总共创建了6个Redis节点(Pod),其中3个将用于master,另外3个分别作为master的slave;Redis的配置通过volume将之前生成的redis-conf这个Configmap,挂载到了容器的/etc/redis/redis.conf;Redis的数据存储路径使用volumeClaimTemplates声明(也就是PVC),其会绑定到我们先前创建的PV上。

这里有一个关键概念——Affinity,请参考官方文档详细了解。其中,podAntiAffinity表示反亲和性,其决定了某个pod不可以和哪些Pod部署在同一拓扑域,可以用于将一个服务的POD分散在不同的主机或者拓扑域中,提高服务本身的稳定性。

而PreferredDuringSchedulingIgnoredDuringExecution 则表示,在调度期间尽量满足亲和性或者反亲和性规则,如果不能满足规则,POD也有可能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

在这里,matchExpressions规定了Redis Pod要尽量不要调度到包含app为redis的Node上,也即是说已经存在Redis的Node上尽量不要再分配Redis Pod了。但是,由于我们只有三个Node,而副本有6个,因此根据PreferredDuringSchedulingIgnoredDuringExecution,这些豌豆不得不得挤一挤,挤挤更健康~

另外,根据StatefulSet的规则,我们生成的Redis的6个Pod的hostname会被依次命名为https://math.jianshu.com/math?formula=(statefulset%E5%90%8D%E7%A7%B0)-(序号),如下图所示:

1
2
3
4
5
6
7
8
[root@k8s-master01 ~]# kubectl get pods -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP                NODE           NOMINATED NODE   READINESS GATES
redis-app-0   1/1     Running   0          42m   172.162.195.8     k8s-master03   <none>           <none>
redis-app-1   1/1     Running   0          42m   172.169.92.76     k8s-master02   <none>           <none>
redis-app-2   1/1     Running   0          42m   172.161.125.30    k8s-node01     <none>           <none>
redis-app-3   1/1     Running   0          42m   172.169.244.196   k8s-master01   <none>           <none>
redis-app-4   1/1     Running   0          42m   172.171.14.220    k8s-node02     <none>           <none>
redis-app-5   1/1     Running   0          44m   172.171.14.219    k8s-node02     <none>           <none>

如上,可以看到这些Pods在部署时是以{0..N-1}的顺序依次创建的。注意,直到redis-app-0状态启动后达到Running状态之后,redis-app-1 才开始启动。

同时,每个Pod都会得到集群内的一个DNS域名,格式为$(podname).$(service name).$(namespace).svc.cluster.local

注意:若Redis Pod迁移或是重启(可以手动删除掉一个Redis Pod来测试),则IP是会改变的,但Pod的域名、SRV records、A record都不会改变。

另外可以发现,我们之前创建的pv都被成功绑定了:

1
2
3
4
5
6
7
8
[root@k8s-master01 ~]# kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                            STORAGECLASS   REASON   AGE
nfs-pv1   200M       RWX            Retain           Bound    default/redis-data-redis-app-1                           19h
nfs-pv2   200M       RWX            Retain           Bound    default/redis-data-redis-app-3                           19h
nfs-pv3   200M       RWX            Retain           Bound    default/redis-data-redis-app-4                           19h
nfs-pv4   200M       RWX            Retain           Bound    default/redis-data-redis-app-2                           19h
nfs-pv5   200M       RWX            Retain           Bound    default/redis-data-redis-app-5                           19h
nfs-pv6   200M       RWX            Retain           Bound    default/redis-data-redis-app-0                           19h

初始化redis集群

在k8s-master01节点中得到6个redis容器的ip地址:

1
2
3
4
5
6
7
8
[root@k8s-master01 ~]# kubectl get po -owide
NAME          READY   STATUS    RESTARTS   AGE    IP                NODE           NOMINATED NODE   READINESS GATES
redis-app-0   1/1     Running   0          16s    172.162.195.8     k8s-master03   <none>           <none>
redis-app-1   1/1     Running   0          12s    172.169.92.76     k8s-master02   <none>           <none>
redis-app-2   1/1     Running   0          10s    172.161.125.30    k8s-node01     <none>           <none>
redis-app-3   1/1     Running   0          8s     172.169.244.196   k8s-master01   <none>           <none>
redis-app-4   1/1     Running   0          6s     172.171.14.220    k8s-node02     <none>           <none>
redis-app-5   1/1     Running   0          2m3s   172.171.14.219    k8s-node02     <none>           <none>

StatefulSet创建完毕后,6个pod已经启动了,但这时候整个redis集群还没有初始化,需要使用官方提供的redis-trib工具。

此时可以在任意一个redis节点上运行对应的工具来初始化整个集群,但这么做显然有些不太合适,考虑到每个节点的职责尽可能的单一,所以最好单独起一个pod来运行整个集群的管理和测试工具。

在这里需要先介绍一下redis-trib,它是官方提供的redis-cluster管理工具,可以实现redis集群的创建、更新等功能,在早期的redis版本中,它是以源码包里redis-trib.rb这个ruby脚本的方式来运作的,现在(我使用的7.0.4)已经被官方集成进redis-cli中。

开始初始化集群,首先在k8s上创建一个ubuntu的pod,用来作为管理节点:

1
kubectl run -i --tty ubuntu --image=ubuntu 

进入ubuntu内部:

1
[root@k8s-master01 ~]# kubectl exec -it ubuntu -- bash

先安装一些工具,包括wget,dnsutils, gcc, make, vim,然后下载和安装redis:

1
2
3
4
wget http://download.redis.io/releases/redis-7.0.4.tar.gz
tar -xvzf redis-7.0.4.tar.gz
cd redis-7.0.4.tar.gz
make

编译完毕后redis-cli会被放置在src目录下,把它放进/usr/local/bin中方便后续操作。

这次部署我们使用0,1,2作为Master节点;3,4,5作为Slave节点,先运行下面的命令来初始化集群的Master节点:

注意:此处必须使用ip地址初始化节点,使用域名会报错,应该是redis不支持。

 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
[root@k8s-master01 /]# kubectl exec -it ubuntu -- bash
root@ubuntu:/# redis-cli --cluster create 172.162.195.8:6379 172.169.92.76:6379 172.161.125.30:6379
>>> Performing hash slots allocation on 3 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: 68c1f049f38e5fc9dbd811d656e4996c1c601fd5 172.162.195.8:6379
   slots:[0-5460] (5461 slots) master
M: e8785fab674971706ab205af459a9535f4f4c589 172.169.92.76:6379
   slots:[5461-10922] (5462 slots) master
M: 764d577a02f76f418df8dae0d912c45b83c59648 172.161.125.30:6379
   slots:[10923-16383] (5461 slots) master
Can I set the above configuration? (type 'yes' to accept): yes
>>> 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 172.162.195.8:6379)
M: 68c1f049f38e5fc9dbd811d656e4996c1c601fd5 172.162.195.8:6379
   slots:[0-5460] (5461 slots) master
M: 764d577a02f76f418df8dae0d912c45b83c59648 172.161.125.30:6379
   slots:[10923-16383] (5461 slots) master
M: e8785fab674971706ab205af459a9535f4f4c589 172.169.92.76:6379
   slots:[5461-10922] (5462 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

然后给他们分别附加对应的Slave节点,这里的cluster-master-id在上一步创建的时候会给出:

1
2
3
4
5
redis-cli --cluster add-node 172.169.244.196:6379 172.162.195.8:6379 --cluster-slave --cluster-master-id 68c1f049f38e5fc9dbd811d656e4996c1c601fd5

redis-cli --cluster add-node 172.171.14.220:6379 172.169.92.76:6379 --cluster-slave --cluster-master-id e8785fab674971706ab205af459a9535f4f4c589

redis-cli --cluster add-node 172.171.14.219:6379 172.161.125.30:6379 --cluster-slave --cluster-master-id 764d577a02f76f418df8dae0d912c45b83c59648

集群初始化后,随意进入一个节点检查一下集群信息:

注意:在集群模式下redis-cli必须加上-c参数才能够访问其他节点上的数据

 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
root@ubuntu:/# redis-cli -c -h 172.171.14.219

172.171.14.219:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:11
cluster_my_epoch:11
cluster_stats_messages_ping_sent:8784
cluster_stats_messages_pong_sent:8855
cluster_stats_messages_fail_sent:5
cluster_stats_messages_auth-req_sent:5
cluster_stats_messages_sent:17649
cluster_stats_messages_ping_received:8850
cluster_stats_messages_pong_received:8771
cluster_stats_messages_fail_received:9
cluster_stats_messages_auth-req_received:2
cluster_stats_messages_auth-ack_received:2
cluster_stats_messages_received:17634
total_cluster_links_buffer_limit_exceeded:0

172.171.14.219:6379> cluster nodes
49fea41ccac3a46830733754fc30174e461b6205 172.171.14.220:6379@16379 master - 0 1661140623557 10 connected 10923-16383
e8785fab674971706ab205af459a9535f4f4c589 172.169.92.76:6379@16379 slave db6661951f323315e49401bda75e94842dd24bea 0 1661140623000 11 connected
764d577a02f76f418df8dae0d912c45b83c59648 172.161.125.30:6379@16379 slave 49fea41ccac3a46830733754fc30174e461b6205 0 1661140622639 10 connected
db6661951f323315e49401bda75e94842dd24bea 172.171.14.212:6379@16379 myself,master - 0 1661140622000 11 connected 5461-10922
91d67c7e7c5bb7848d23cf377718ed8f914e9800 172.169.244.196:6379@16379 slave 68c1f049f38e5fc9dbd811d656e4996c1c601fd5 0 1661140623559 9 connected
68c1f049f38e5fc9dbd811d656e4996c1c601fd5 172.162.195.8:6379@16379 master - 0 1661140623661 9 connected 0-5460

至此,集群初始化完毕。测试一下:

1
2
3
4
5
6
7
172.171.14.219:6379> set name ning
OK
172.171.14.219:6379> exit
root@ubuntu:/# redis-cli -c -h 172.169.92.76 
172.169.92.76:6379> get name
-> Redirected to slot [5798] located at 172.171.14.219:6379
"ning"

创建Service

现在进入redis集群中的任意一个节点都可以直接进行操作了,但是为了能够对集群其他的服务提供访问,还需要建立一个service来实现服务发现和负载均衡(注意这里的service和我们之前创建的headless service不是一个东西)

yaml文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
  name: redis-svc
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    protocol: "TCP"
    port: 6379
    targetPort: 6379
  selector:
    app: redis
    appCluster: redis-cluster

部署完做个测试:

1
2
3
4
5
root@ubuntu:/# redis-cli -c -h redis-svc    
redis-svc:6379> get name
-> Redirected to slot [5798] located at 172.171.14.219:6379
"ning"
172.171.14.219:6379>

测试

搭建好的redis集群应当可以扩容缩容、故障检测和自动恢复。

扩容和缩容

注意:使用这种方式只能实现pod的扩容和缩容,redis集群需要重新配置。很不方便。无法满足需要。

缩容到5个statusfulSet

/k8s-%D0%BF%D1%8E%D0%B8redis%D0%BF/img/image-20220822125224715.png

扩容到6个statusfulSet

1
kubectl scale sts redis-app --relicas=6

故障检测和自动恢复

手动删除5个redis容器,然后观察后续容器变化情况。

image-20220822123837070

虽然恢复后的redis容器的ip地址和之前的不一致,但是该集群仍然可以使用redis-svc接口进行统一调用。

1
2
3
4
5
6
[root@k8s-master01 ~]# kubectl exec -it ubuntu -- bash
root@ubuntu:/# redis-cli -c -h redis-svc
redis-svc:6379> get name
-> Redirected to slot [5798] located at 172.171.14.219:6379
"ning"
172.171.14.219:6379>