在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}