本文介绍怎样以容器化方式部署Redis集群。
Redis集群模式说明
Redis支持以集群模式运行,在该模式下,Redis将所有存储空间分为16384个哈希槽,集群中的每个Master节点负责N个哈希槽(一个数据分片),当用户写入一条数据时,Redis计算其哈希槽,然后将数据写在负责该哈希槽的节点上。且每个Master节点可以添加一个或多个Slave节点,当某个Master节点不可用时,其Slave节点自动代替Master节点继续工作。
由此可见,在Redis集群模式下,我们可获得更高的吞吐量,和一定程度的可用性。需要注意的是,在集群模式下,Redis仍不能保证数据零丢失,详见Redis官方文档。
基本原理
本文以Redis官方镜像6.0.8版本作为示例,创建一个6个节点的Redis集群,其中3个Master节点,3个Slave节点。因为每个节点有自己的状态和标识,所以使用Statefulset来创建Pod,此外需要为每个节点挂载一个云盘,用以持久化节点数据。建议选择较新Redis版本,如果Redis版本低于5.0,则初始化集群所用的命令可能会有所不同。
开始构建
-
创建一个Configmap对象,用来存储和管理Redis集群的配置。
kubectl create -f - <<<' apiVersion: v1 kind: ConfigMap metadata: name: redis-cluster data: redis.conf: | bind 0.0.0.0 port 6379 cluster-announce-bus-port 16379 cluster-enabled yes appendonly yes cluster-node-timeout 5000 dir /data cluster-config-file /data/nodes.conf requirepass pass123 masterauth pass123 '
字段dir为Redis节点数据的持久化目录,所以Pod的
/data
目录应当是一个PVC目录。字段cluster-config-file是Redis集群的节点信息,由Redis节点自动生成和修改,该位置也应当是一个持久化的目录,以便节点宕机恢复后能够找到集群中的其他节点,并继续工作。 -
创建Statefulset资源需要依赖Service对象,我们提前创建一个headless类型的Service。
kubectl create -f - <<<' apiVersion: v1 kind: Service metadata: name: redis-cluster-svc spec: clusterIP: None selector: app: redis-cluster '
-
创建Statefulset时引用前面的Service,将Configmap挂载到每一个Pod的
/config
,并且为每一个Pod创建一个PVC,挂载到/data
。kubectl create -f - <<<' apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster spec: serviceName: redis-cluster-svc replicas: 6 template: metadata: labels: app: redis-cluster spec: nodeName: virtual-node-eci-0 terminationGracePeriodSeconds: 10 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - redis-cluster topologyKey: kubernetes.io/hostname weight: 100 containers: - name: redis image: redis:6.0.8 command: ["redis-server", "/config/redis.conf"] ports: - name: redis containerPort: 6379 protocol: TCP - name: election containerPort: 16379 protocol: TCP volumeMounts: - name: redis-conf mountPath: /config - name: pvc-essd-redis-data mountPath: /data volumes: - name: redis-conf configMap: name: redis-cluster items: - key: redis.conf path: redis.conf volumeClaimTemplates: - metadata: name: pvc-essd-redis-data spec: accessModes: [ "ReadWriteOnce" ] storageClassName: alicloud-disk-essd resources: requests: storage: 20Gi '
接下来等待Statefulset中的所有Pod达到Ready状态。
kubectl get statefulset redis-cluster -o wide NAME READY AGE CONTAINERS IMAGES redis-cluster 6/6 77m redis redis:6.0.8
-
初始化集群,目前Redis还不支持以hostname方式初始化集群,所以先获取每个节点的IP地址。
kubectl get pods -l app=redis-cluster -o wide
输出类似以下信息。
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cluster-0 1/1 Running 0 83m 192.168.0.245 virtual-node-eci-0 <none> <none> redis-cluster-1 1/1 Running 0 83m 192.168.0.246 virtual-node-eci-0 <none> <none> redis-cluster-2 1/1 Running 0 81m 192.168.0.247 virtual-node-eci-0 <none> <none> redis-cluster-3 1/1 Running 0 81m 192.168.0.248 virtual-node-eci-0 <none> <none> redis-cluster-4 1/1 Running 0 80m 192.168.0.249 virtual-node-eci-0 <none> <none> redis-cluster-5 1/1 Running 0 80m 192.168.0.250 virtual-node-eci-0 <none> <none>
登录到其中一个Redis节点。
kubectl exec -ti redis-cluster-0 bash
执行初始化命令,共有6个节点,当选项
--cluster-replicas
指定为1时,表示为每个Master节点分配一个Slave节点,这样集群中刚好3个Master节点和3个Slave节点。redis-cli -a pass123 --cluster create \ 192.168.0.245:6379 \ 192.168.0.246:6379 \ 192.168.0.247:6379 \ 192.168.0.248:6379 \ 192.168.0.249:6379 \ 192.168.0.250:6379 \ --cluster-replicas 1
输出类似以下信息表示初始化成功。
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.
-
使用Redis,现在进入集群中的任意一个Pod中都可以访问Redis服务,前面我们创建了一个headless类型的Service,kubernetes集群会为该服务分配一个DNS A记录,格式为
my-svc.my-namespace.svc.cluster-domain.example
,每次访问该服务名时,将随机解析到其中一个Redis节点上。我们进入redis-cluster-0节点中,尝试登录到Redis。
redis-cli -a pass123 -c -h redis-cluster-svc.default.svc.cluster.local -p 6379 192.168.0.248> set k1 v1 OK 192.168.0.248> get k1 "v1"
扩容
我们目前的部署方式不支持动态扩容集群,主要的问题是每次新增节点都需要为集群中的所有节点重新分配哈希槽,我们可以在Redis镜像中添加脚本,以便每个Pod启动时自动完成该操作,但是当集群中的数据非常多时,连续的重新分片会导致扩容操作非常缓慢,并且有可能在重新分片期间因为耗尽Redis集群带宽而导致依赖此服务的所有客户端超时。另一个问题是没有好的策略确定新启动的Pod应该作为Master节点还是Slave节点。所以我们接下来以手动分片演示集群扩容。
-
修改Statefulset副本,现在我们添加一个Master节点和一个Slave节点,首先增加Statefulset的副本数,使其从6增加到8。
kubectl scale statefulsets redis-cluster --replicas=8
-
获取新节点IP,等待所有Pod达到Ready状态后,我们获取新节点的IP地址。
kubectl get pods -l app=redis-cluster -o wide
输出类似以下信息。
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cluster-0 1/1 Running 0 83m 192.168.0.245 virtual-node-eci-0 <none> <none> redis-cluster-1 1/1 Running 0 83m 192.168.0.246 virtual-node-eci-0 <none> <none> redis-cluster-2 1/1 Running 0 81m 192.168.0.247 virtual-node-eci-0 <none> <none> redis-cluster-3 1/1 Running 0 81m 192.168.0.248 virtual-node-eci-0 <none> <none> redis-cluster-4 1/1 Running 0 80m 192.168.0.249 virtual-node-eci-0 <none> <none> redis-cluster-5 1/1 Running 0 80m 192.168.0.250 virtual-node-eci-0 <none> <none> redis-cluster-6 1/1 Running 0 90m 192.168.0.251 virtual-node-eci-0 <none> <none> redis-cluster-7 1/1 Running 0 90m 192.168.0.252 virtual-node-eci-0 <none> <none>
-
添加Master节点,现在我们登录到其中一个节点,执行以下命令将redis-cluster-6添加为集群的Master节点。
redis-cli -a pass123 --cluster add-node 192.168.0.251:6379 192.168.0.245:6379
说明其中
192.168.0.245:6379
为任意一个旧节点IP,连接成功后,redis-cli将自动获取其他所有节点。查看redis-cluster-6节点的ID。
redis-cli -a pass123 -c cluster nodes | grep 192.168.0.251:6379 | awk '{print $1}' 2748a28ec5db88aeb6b3326d06f6e56ee0dfdab5
-
重新分配哈希槽,现在共有4个Master节点,共同分担16384个哈希槽,平均每个节点分担的哈希槽数量为
16384 / 4 = 4096
。现在我们从之前的三个Master分出4096个哈希槽给新节点。执行以下命令开始分配哈希槽,其中192.168.0.245:6379
为任意一个旧节点IP,连接成功后,redis-cli将自动获取其他所有节点。redis-cli -a pass123 --cluster reshard 192.168.0.245:6379
以上命令是交互式的,根据提示依次输入:准备为新节点分配的哈希槽数量(4096)、新节点的ID(2748a28ec5db88aeb6b3326d06f6e56ee0dfdab5)、从所有节点平均抽取哈希槽给新节点(all)、确认分配(yes)。
How many slots do you want to move (from 1 to 16384)? 4096 What is the receiving node ID? 2748a28ec5db88aeb6b3326d06f6e56ee0dfdab5 Source node #1: all Do you want to proceed with the proposed reshard plan (yes/no)? yes
接下来等待分配完毕,在此期间Redis集群可以正常提供服务。
-
添加Slave节点,将redis-cluster-7添加为redis-cluster-6的Slave节点。
redis-cli -a pass123 \ ---cluster add-node 192.168.0.252:6379 192.168.0.245:6379 \ --cluster-master-id 2748a28ec5db88aeb6b3326d06f6e56ee0dfdab5
说明1. 其中
192.168.0.245:6379
为任意一个旧节点IP,连接成功后,redis-cli将自动获取其他所有节点。2.
2748a28ec5db88aeb6b3326d06f6e56ee0dfdab5
为该Slave节点要跟随的Master节点ID。现在可以看到有4个Master节点,并且每个Master有一个副本节点。
redis-cli -a pass123 -c cluster nodes
缩容
集群缩容稍微麻烦,首先Statefulset只能以和创建Pod时相反的顺序逐个删除Pod,也就是说我们要删除一个Master节和一个Slave节点,就只能删除redis-cluster-7和redis-cluster-6。其次,删除这两个节点之前必须先把该节点上所有哈希槽移动到其他节点,否则将会有数据丢失,并且Redis集群将拒绝服务。
-
重新分配哈希槽,为了避免剩下的三个Master节点出现负载倾斜的情况,我们需要将redis-cluster-6的哈希槽平均分配给redis-cluster-0、redis-cluster-2和redis-cluster-4,所以这需要执行三次重新分片的操作,其操作方法与扩容时执行的分片操作基本相同,但接受节点ID变为其它剩下三个节点中的一个,来源节点为redis-cluster-6的节点ID。
redis-cli -a pass123 --cluster reshard 192.168.0.245:6379
说明其中
192.168.0.245:6379
为任意一个旧节点IP,连接成功后,redis-cli将自动获取其他所有节点。 -
修改Statefulset副本,分片完成后将Statefulset的副本数从8改为6。
kubectl scale statefulsets redis-cluster --replicas=6
删除集群
删除集群时需要删除Statefulset,Service,Configmap。
kubectl delete statefulset redis-cluster
kubectl delete svc redis-cluster-svc
kubectl delete cm redis-cluster
需要注意的是,Statefulset删除后,相关的PVC并不会被自动删除,需要单独删除PVC,PVC删除后,相应的云盘和数据也将被删除。
kubectl delete pvc -l app=redis-cluster