在k8s中部署redis cluster实战
0. 背景
项目需要在k8s上搭建一个redis cluster集群,网上找到的教程例如:
github原版带配置文件
在原版基础上补充详细使用步骤但是无配置文件版
手把手教你一步一步创建的一篇博客
redis运行在容器中时必须选择一种外部存储方案,用来保存redis的持久化文件,否则容器销毁重建后无法读取到redis的持久化文件(随着容器一同销毁了);并且还要保证容器重建后还能读取到之前对应的持久化文件。上面的教程使用的是nfs存储,但是受于条件限制本文只能使用宿主机的本地目录来做存储,与上面的教程有一些不一样的地方。
本文的目的是讲一下使用local pv来作为存储创建redis cluster集群的步骤,以及说明过程中需要注意的问题。
1. k8s的本地存储方案
Kubernetes支持几十种类型的后端存储卷,其中本地存储卷有3种,分别是emptyDir、hostPath、local volume,尤其是local与hostPath这两种存储卷类型看起来都是一个意思。这里讲一下区别。
1.1 区别
- emptyDir类型的Volume在Pod分配到Node上时被创建,Kubernetes会在Node上自动分配一个目录,因此无需指定Node上对应的目录文件。 这个目录的初始内容为空,当Pod从Node上移除时,emptyDir中的数据会被永久删除。
- hostPath类型则是映射node文件系统中指定的文件或者目录到pod里。
- Local volume也是使用node文件系统的文件或目录,但是使用PV和PVC将node节点的本地存储包装成通用PVC接口,容器直接使用PVC而不需要关注PV包装的是node的文件系统还是nfs之类的网络存储。Local PV的定义中需要包含描述节点亲和性(即指定PV使用哪个/哪些Node)的信息,k8s调度pod时则使用该信息将pod调度到该od使用的local pv所在的Node节点。
1.2 使用示例
emptyDir
apiVersion: v1 # 版本号,跟k8s版本有关
kind: Pod # 创建Pod类型,其他还有Deployment、StatefulSet、DaemonSet等等各种
metadata:
name: test-pod
spec:
containers:
- image: busybox # 创建pod使用的镜像
name: test-emptydir
command: [ "sleep", "3600" ] # 这里睡眠等待的原因是:如果pod里面启动的进程执行完,pod就会结束。所以redis之类的程序都要以非后台方式运行
volumeMounts:
- mountPath: /var/log # 容器并不一定存在这个目录,自己试一下,选择一个与系统运行无关的目录。因为pod是先挂载后启动,如果挂载到了系统盘上,pod里面的linux就运行不起来了
name: tmp-volume # 把下面那个叫做tmp-volume的存储卷挂载到容器的/var/log 目录
volumes:
- name: tmp-volume # 创建一个emptyDir类型的存储卷,起名叫做tmp-volume
emptyDir: {}
hostPath
apiVersion: v1
kind: Pod
metadata:
name: test-pod2
spec:
containers:
- image: busybox
name: test-hostpath
command: [ "sleep", "3600" ]
volumeMounts:
- mountPath: /var/log
name: host-volume
volumes:
- name: host-volume # 创建一个hostPath类型的存储卷,起名叫做host-volume
hostPath:
path: /data # 创建存储卷使用的Node目录,你的Node可能没有这个目录,自己找一个可用目录
local volume
# pv和pvc使用同一个StorageClass,就能将pvc自动绑定到pv
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Mi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle # pv的回收策略,这个后面讲
storageClassName: local-storage
local:
path: /mnt/disks/ssd1 # 把本地磁盘/mnt/disks/ssd1上100M空间拿出来作为pv
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- example-node # 选择集群里面kubernetes.io/hostname=example-node这个标签的节点来创建pv
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: example-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
storageClassName: local-storage
2. pv的回收策略
pv的回收策略有三种:Retain、Recycle、Delete,可以在脚本中指定:
persistentVolumeReclaimPolicy: Retain
也可以在pv创建成功后使用命令修改:
sudo kubectl patch pv <your-pv-name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
假设有一个pv叫test-pv,绑定的pvc角坐test-pvc,test-pv使用的local pv
2.1 Retain
- 删除test-pvc后,test-pv得到了保留,但test-pv的状态会一直处于 Released而不是Available,不能被其他PVC申请;
- 为了重新使用test-pv绑定的nfs存储空间,可以删除并重新创建test-pv;
- 删除操作只是删除了test-pv对象,nfs存储空间中的数据并不会被删除。
2.2 Recycle
- 删除test-pvc之后,Kubernetes启动了一个新的Pod角坐recycler-for-test-pv,这个Pod的作用就是清除test-pv的数据。在此过程中test-pv的状态为Released,表示已经解除了与 test-pvc的绑定,不过此时还不可用;
- 当数据清除完毕,test-pv的状态重新变为 Available,此时test-pv可以被新的PVC绑定;
- 同样,也不会删除nfs存储空间中的数据。
2.3 Delete
会删除test-pv在对应存储空间上的数据。NFS目前不支持 Delete,支持Delete的存储空间有AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等(网上看的,没测试过)。
3. Deployment和Statsfulset
前面已经说过,redis有数据持久化需求,并且同一个pod重启后需要读取原来对应的持久化数据,这一点在不使用k8s时很容易实现(只使用docker不使用k8s时也很容易),启动redis cluster每个节点时指定其持久化目录就行了,但是k8s的Deployment的调度对于我们这个需求来说就显得很随机,你无法指定deployment的每个pod使用哪个存储,并且重启后仍然使用那个存储。
Deployment不行,Statefulset可以。官方对Statefulset的优点介绍是:
- 稳点且唯一的网络标识符
- 稳点且持久的存储
- 有序、平滑的部署和扩展
- 有序、平滑的删除和终止
- 有序的滚动更新
看完还是比较迷糊,我们可以简单的理解为原地更新,更新后还是原来那个pod,只更新了需要更新的内容(一般是修改自己写的程序,与容器无关)。
Statefulset和local pv结合,redis cluster的每个pod挂掉后在k8s的调度下重启时都会使用之前自己的持久化文件和节点信息。
4. 创建redis集群
4.1 创建StorageClass
创建StorageClass的目的是deployment中根据StorageClass来自动为每个pod选择一个pv,否则手动为每个pod指定pv又回到了老路上。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: redis-local-storage # StorageClass的name,后面需要声明使用的是这个StorageClass时都是用这个名字
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
4.2 创建PV
创建6个pv,因为redis cluster最低是三主三从的配置,所以最少需要6个pod。后面的pv2~pv5我就不贴出来了。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv1
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: redis-local-storage # 上面创建的StorageClass
local:
path: /usr/local/kubernetes/redis/pv1 # 创建local pv使用的宿主机目录,可以自己指定
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname # k8s node的标签,结合下面的ip,该标签为kubernetes.io/hostname=192.168.0.152
operator: In
values:
- 192.168.0.152 # localpv创建在192.168.0.152这台机器上
4.3 使用configmap创建redis的配置文件redis.conf
# 下面的redis.conf中不能写注释,否则k8s解析时会当作配置文件的一部分,出错
# dir /var/lib/redis使得持久化文件dump.rdb在容器的/var/lib/redis目录下
# cluster-config-file /var/lib/nodes.conf使得集群信息在/var/lib/redis/nodes.conf文件中
# /var/lib/redis目录会挂载pv,所以持久化文件和节点信息能保存下来
kind: ConfigMap
apiVersion: v1
metadata:
name: redis-cluster-configmap # configmap的名字,加上下面的demo-redis就是这个configmap在k8s集群中的唯一标识
namespace: demo-redis
data:
# 这里可以创建多个文件
redis.conf: |
appendonly yes
protected-mode no
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
dir /var/lib/redis
port 6379
4.4 创建headless service
Headless service是StatefulSet实现稳定网络标识的基础,需要提前创建。
apiVersion: v1
kind: Service
metadata:
name: redis-headless-service
namespace: demo-redis
labels:
app: redis
spec:
ports:
- name: redis-port
port: 6379
clusterIP: None
selector:
app: redis
appCluster: redis-cluster
4.5 创建redis节点
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-app
namespace: demo-redis
spec:
serviceName: redis-service
replicas: 6
selector:
matchLabels:
app: redis
appCluster: redis-cluster
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"
command:
- "redis-server" #redis启动命令
args:
- "/etc/redis/redis.conf" #redis-server后面跟的参数,换行代表空格
- "--protected-mode" #允许外网访问
- "no"
resources:
requests: # 每个pod请求的资源
cpu: 2000m # m代表千分之,这里申请2个逻辑核
memory: 4Gi # 内存申请4G大小
limits: # 资源限制
cpu: 2000m
memory: 4Gi
ports:
- name: redis
containerPort: 6379
protocol: "TCP"
- name: cluster
containerPort: 16379
protocol: "TCP"
volumeMounts:
- name: redis-conf # 把下面创建的redis.conf配置文件挂载到容器的/etc/redis目录下
mountPath: /etc/redis
- name: redis-data # 把叫做redis-data的volume挂载到容器的/var/lib/redis目录
mountPath: /var/lib/redis
volumes:
- name: redis-conf # 船舰一个名为redis-conf的volumes
configMap:
name: redis-cluster-configmap # 引用上面创建的configMap卷
items:
- key: redis.conf # configmap里面的redis.conf
path: redis.conf # configmap里面的redis.conf放到volumes中叫做redistribution.conf
volumeClaimTemplates: # pod使用哪个pvc,这里是通过StorageClass自动创建pvc并对应上pv
- metadata:
name: redis-data # pvc创建一个volumes叫做redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: redis-local-storage
resources:
requests:
storage: 5Gi
每个Pod都会得到集群内的一个DNS域名,格式为(service name).$(namespace).svc.cluster.local。可以在pod中ping一下这些域名,是可以解析为pod的ip并ping通的。
4.6 创建一个service,作为redis集群的访问入口
这个service是可以自由发挥的,使用port-forward、NodePort还是ingress你自己选择,我这里只是一个内网访问统一入口。
apiVersion: v1
kind: Service
metadata:
name: redis-access-service
namespace: demo-redis
labels:
app: redis
spec:
ports:
- name: redis-port
protocol: TCP
port: 6379
targetPort: 6379
selector:
app: redis
appCluster: redis-cluster
至此,redis cluster的六个节点都已经创建成功。下面需要创建集群(此时就是6个单节点的redis,并不是一个集群)。
4.7 创建redis cluster集群
我们之前都是通过外部安装redis-trib创建的集群,但是根据这篇文章redis 5.0之后已经内置了redis-trib工具,感兴趣的可以尝试。
专门启动一个Ubuntu/CentOS的容器,可以在该容器中安装Redis-tribe,进而初始化Redis集群,执行:kubectl run -i --tty centos --image=centos --restart=Never /bin/bash
成功后,我们可以进入centos容器中,执行如下命令安装基本的软件环境:
cat >> /etc/yum.repo.d/epel.repo<<'EOF'
[epel]
name=Extra Packages for Enterprise Linux 7 - $basearch
baseurl=https://mirrors.tuna.tsinghua.edu.cn/epel/7/$basearch
#mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-7&arch=$basearch
failovermethod=priority
enabled=1
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
EOF
yum -y install redis-trib.noarch bind-utils-9.9.4-72.el7.x86_64
然后执行如下命令创建集群:
redis-trib create --replicas 1 \
`dig +short redis-app-0.redis-headless-service.demo-redis.svc.cluster.local`:6379 \
`dig +short redis-app-1.redis-headless-service.demo-redis.svc.cluster.local`:6379 \
`dig +short redis-app-2.redis-headless-service.demo-redis.svc.cluster.local`:6379 \
`dig +short redis-app-3.redis-headless-service.demo-redis.svc.cluster.local`:6379 \
`dig +short redis-app-4.redis-headless-service.demo-redis.svc.cluster.local`:6379 \
`dig +short redis-app-5.redis-headless-service.demo-redis.svc.cluster.local`:6379
根据提示一步一步完成。
5. tips
5.1 集群哪怕只有一个节点可访问,也要按照集群配置方式
否则报错例如MOVED 1545 10.244.3.239:6379","data":false
如本文的情况,redis cluster的每个节点都是一个跑在k8s里面的pod,这些pod并不能被外部直接访问,而是通过ingress等方法对外暴露一个访问接口,即只有一个统一的ip:port给外部访问。经由k8s的调度,对这个统一接口的访问会被发送到redis集群的某个节点。这时候对redis的用户来说,看起来这就像是一个单节点的redis。但是,此时无论是直接使用命令行工具redis-cli,还是某种语言的sdk,还是需要按照集群来配置redis的连接信息,才能正确连接,例如
./redis-cli -h {your ip} -p {your port} -c
这里-c就代表这是访问集群,又或者springboot的redis配置文件
spring:
redis:
# 集群配置方式
cluster:
nodes: {your ip1}:{your port1},{your ip2}:{your port2}
password:{your password}
# 对比一下单节点配置方式
host: {your ip}
port: {your port}
password:{your password}