1. 程式人生 > 其它 >在k8s中部署redis cluster實戰

在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 <your-pv-name> -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}