在K8S上搭建Redis叢集

Lumin發表於2019-03-25

今天讓我們試著在k8s裡部署一個redis叢集,瞭解更多k8s的細節和特性。

環境:minikube v0.30 (kubernetes 1.10)

注:redis-cluster相關的背景知識和細節在此不做贅述,可以參考之前的文章稍作回顧

問題分析

本質上來說,在k8s上部署一個redis叢集和部署一個普通應用沒有什麼太大的區別,但需要注意下面幾個問題:

  1. REDIS是一個有狀態應用

    這是部署redis叢集時我們最需要注意的問題,當我們把redis以pod的形式部署在k8s中時,每個pod裡快取的資料都是不一樣的,而且pod的IP是會隨時變化,這時候如果使用普通的deployment和service來部署redis-cluster就會出現很多問題,因此需要改用StatefulSet + Headless Service來解決

  2. 資料持久化

    redis雖然是基於記憶體的快取,但還是需要依賴於磁碟進行資料的持久化,以便服務出現問題重啟時可以恢復已經快取的資料。在叢集中,我們需要使用共享檔案系統 + PV(持久卷)的方式來讓整個叢集中的所有pod都可以共享同一份持久化儲存

概念介紹

在開始之前先來詳細介紹一下幾個概念和原理

Headless Service

簡單的說,Headless Service就是沒有指定Cluster IP的Service,相應的,在k8s的dns對映裡,Headless Service的解析結果不是一個Cluster IP,而是它所關聯的所有Pod的IP列表

StatefulSet

StatefulSet是k8s中專門用於解決有狀態應用部署的一種資源,總的來說可以認為它是Deployment/RC的一個變種,它有以下幾個特性:

  1. StatefulSet管理的每個Pod都有唯一的文件/網路標識,並且按照數字規律生成,而不是像Deployment中那樣名稱和IP都是隨機的(比如StatefulSet名字為redis,那麼pod名就是redis-0, redis-1 ...)

  2. StatefulSet中ReplicaSet的啟停順序是嚴格受控的,操作第N個pod一定要等前N-1個執行完才可以

  3. StatefulSet中的Pod採用穩定的持久化儲存,並且對應的PV不會隨著Pod的刪除而被銷燬

另外需要說明的是,StatefulSet必須要配合Headless Service使用,它會在Headless Service提供的DNS對映上再加一層,最終形成精確到每個pod的域名對映,格式如下:

$(podname).$(headless service name)
複製程式碼

有了這個對映,就可以在配置叢集時使用域名替代IP,實現有狀態應用叢集的管理

方案

藉助StatefulSet和Headless Service,叢集的部署方案設計如下(圖片來自參考文章):

在K8S上搭建Redis叢集

配置步驟大概羅列如下:

  1. 配置共享檔案系統NFS
  2. 建立PV和PVC
  3. 建立ConfigMap
  4. 建立Headless Service
  5. 建立StatefulSet
  6. 初始化redis叢集

實際操作

由於使用的是minikube的單node環境,為了簡化複雜度,這次先不配置PV和PVC,直接通過普通Volume的方式來掛載資料

建立ConfigMap

先建立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
複製程式碼

然後kubectl create configmap redis-conf --from-file=redis.conf來建立ConfigMap

建立HeadlessService

apiVersion: v1
kind: Service
metadata:
  name: redis-service
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    port: 6379
  clusterIP: None
  selector:
    app: redis
    appCluster: redis-cluster
複製程式碼

建立StatefulSet

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: redis-app
spec:
  serviceName: "redis-service"
  replicas: 6
  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: "registry.cn-qingdao.aliyuncs.com/gold-faas/gold-redis:1.0"
        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"
      - name: "redis-data"
        emptyDir: {} 
複製程式碼

初始化redis叢集

StatefulSet建立完畢後,可以看到6個pod已經啟動了,但這時候整個redis叢集還沒有初始化,需要使用官方提供的redis-trib工具。

我們當然可以在任意一個redis節點上執行對應的工具來初始化整個叢集,但這麼做顯然有些不太合適,我們希望每個節點的職責儘可能地單一,所以最好單獨起一個pod來執行整個叢集的管理工具。

在這裡需要先介紹一下redis-trib,它是官方提供的redis-cluster管理工具,可以實現redis叢集的建立、更新等功能,在早期的redis版本中,它是以原始碼包裡redis-trib.rb這個ruby指令碼的方式來運作的(pip上也可以拉到python版本,但我執行失敗),現在(我使用的5.0.3)已經被官方整合進redis-cli中。

開始初始化叢集,首先在k8s上建立一個ubuntu的pod,用來作為管理節點:

kubectl run -i --tty redis-cluster-manager --image=ubuntu --restart=Never /bin/bash
複製程式碼

進入pod內部先安裝一些工具,包括wget,dnsutils,然後下載和安裝redis:

wget http://download.redis.io/releases/redis-5.0.3.tar.gz
tar -xvzf redis-5.0.3.tar.gz
cd redis-5.0.3.tar.gz && make
複製程式碼

編譯完畢後redis-cli會被放置在src目錄下,把它放進/usr/local/bin中方便後續操作

接下來要獲取已經建立好的6個節點的host ip,可以通過nslookup結合StatefulSet的域名規則來查詢,舉個例子,要查詢redis-app-0這個pod的ip,執行如下命令:

root@redis-cluster-manager:/# nslookup redis-app-0.redis-service
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	redis-app-0.redis-service.gold.svc.cluster.local
Address: 172.17.0.10
複製程式碼

172.17.0.10就是對應的ip。這次部署我們使用0,1,2作為Master節點;3,4,5作為Slave節點,先執行下面的命令來初始化叢集的Master節點:

redis-cli --cluster create 172.17.0.10:6379 172.17.0.11:6379 172.17.0.12:6379
複製程式碼

在K8S上搭建Redis叢集

然後給他們分別附加對應的Slave節點,這裡的cluster-master-id在上一步建立的時候會給出:

redis-cli --cluster add-node 172.17.0.13:6379 172.17.0.10:6379 --cluster-slave --cluster-master-id adf443a4d33c4db2c0d4669d61915ae6faa96b46
複製程式碼
redis-cli --cluster add-node 172.17.0.14:6379 172.17.0.11:6379 --cluster-slave --cluster-master-id 6e5adcb56a871a3d78343a38fcdec67be7ae98f8
複製程式碼
redis-cli --cluster add-node 172.17.0.16:6379 172.17.0.12:6379 --cluster-slave --cluster-master-id c061e37c5052c22f056fff2a014a9f63c3f47ca0
複製程式碼

叢集初始化後,隨意進入一個節點檢查一下叢集資訊:

在K8S上搭建Redis叢集

至此,叢集初始化完畢,我們進入一個節點來試試,注意在叢集模式下redis-cli必須加上-c引數才能夠訪問其他節點上的資料:

在K8S上搭建Redis叢集

建立Service

現在進入redis叢集中的任意一個節點都可以直接進行操作了,但是為了能夠對叢集其他的服務提供訪問,還需要建立一個service來實現服務發現和負載均衡(注意這裡的service和我們之前建立的headless service不是一個東西)

yaml檔案如下:

apiVersion: v1
kind: Service
metadata:
  name: gold-redis
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    protocol: "TCP"
    port: 6379
    targetPort: 6379
  selector:
    app: redis
    appCluster: redis-cluster
複製程式碼

部署完做個測試:

在K8S上搭建Redis叢集

很nice,到這裡所有的工作就完畢了~

參考文章:

相關文章