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}