在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 区别

  1. emptyDir类型的Volume在Pod分配到Node上时被创建,Kubernetes会在Node上自动分配一个目录,因此无需指定Node上对应的目录文件。 这个目录的初始内容为空,当Pod从Node上移除时,emptyDir中的数据会被永久删除。
  2. hostPath类型则是映射node文件系统中指定的文件或者目录到pod里。
  3. 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  -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

假设有一个pv叫test-pv,绑定的pvc角坐test-pvc,test-pv使用的local pv

2.1 Retain

  1. 删除test-pvc后,test-pv得到了保留,但test-pv的状态会一直处于 Released而不是Available,不能被其他PVC申请;
  2. 为了重新使用test-pv绑定的nfs存储空间,可以删除并重新创建test-pv;
  3. 删除操作只是删除了test-pv对象,nfs存储空间中的数据并不会被删除。

2.2 Recycle

  1. 删除test-pvc之后,Kubernetes启动了一个新的Pod角坐recycler-for-test-pv,这个Pod的作用就是清除test-pv的数据。在此过程中test-pv的状态为Released,表示已经解除了与 test-pvc的绑定,不过此时还不可用;
  2. 当数据清除完毕,test-pv的状态重新变为 Available,此时test-pv可以被新的PVC绑定;
  3. 同样,也不会删除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的优点介绍是:

  1. 稳点且唯一的网络标识符
  2. 稳点且持久的存储
  3. 有序、平滑的部署和扩展
  4. 有序、平滑的删除和终止
  5. 有序的滚动更新

  看完还是比较迷糊,我们可以简单的理解为原地更新,更新后还是原来那个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}

你可能感兴趣的:(在k8s中部署redis cluster实战)