0. 背景
??項(xiàng)目需要在k8s上搭建一個(gè)redis cluster集群剪芍,網(wǎng)上找到的教程例如:
??github原版帶配置文件
??在原版基礎(chǔ)上補(bǔ)充詳細(xì)使用步驟但是無配置文件版
??手把手教你一步一步創(chuàng)建的一篇博客
??redis運(yùn)行在容器中時(shí)必須選擇一種外部存儲(chǔ)方案,用來保存redis的持久化文件裁替,否則容器銷毀重建后無法讀取到redis的持久化文件(隨著容器一同銷毀了)腐巢;并且還要保證容器重建后還能讀取到之前對(duì)應(yīng)的持久化文件左冬。上面的教程使用的是nfs存儲(chǔ)斤彼,但是受于條件限制本文只能使用宿主機(jī)的本地目錄來做存儲(chǔ)葵陵,與上面的教程有一些不一樣的地方液荸。
??本文的目的是講一下使用local pv來作為存儲(chǔ)創(chuàng)建redis cluster集群的步驟,以及說明過程中需要注意的問題脱篙。
1. k8s的本地存儲(chǔ)方案
??Kubernetes支持幾十種類型的后端存儲(chǔ)卷娇钱,其中本地存儲(chǔ)卷有3種,分別是emptyDir绊困、hostPath文搂、local volume,尤其是local與hostPath這兩種存儲(chǔ)卷類型看起來都是一個(gè)意思秤朗。這里講一下區(qū)別煤蹭。
1.1 區(qū)別
- emptyDir類型的Volume在Pod分配到Node上時(shí)被創(chuàng)建,Kubernetes會(huì)在Node上自動(dòng)分配一個(gè)目錄取视,因此無需指定Node上對(duì)應(yīng)的目錄文件硝皂。 這個(gè)目錄的初始內(nèi)容為空,當(dāng)Pod從Node上移除時(shí)作谭,emptyDir中的數(shù)據(jù)會(huì)被永久刪除稽物。
- hostPath類型則是映射node文件系統(tǒng)中指定的文件或者目錄到pod里。
- Local volume也是使用node文件系統(tǒng)的文件或目錄折欠,但是使用PV和PVC將node節(jié)點(diǎn)的本地存儲(chǔ)包裝成通用PVC接口贝或,容器直接使用PVC而不需要關(guān)注PV包裝的是node的文件系統(tǒng)還是nfs之類的網(wǎng)絡(luò)存儲(chǔ)。Local PV的定義中需要包含描述節(jié)點(diǎn)親和性(即指定PV使用哪個(gè)/哪些Node)的信息锐秦,k8s調(diào)度pod時(shí)則使用該信息將pod調(diào)度到該od使用的local pv所在的Node節(jié)點(diǎn)咪奖。
1.2 使用示例
emptyDir
apiVersion: v1 # 版本號(hào),跟k8s版本有關(guān)
kind: Pod # 創(chuàng)建Pod類型酱床,其他還有Deployment赡艰、StatefulSet、DaemonSet等等各種
metadata:
name: test-pod
spec:
containers:
- image: busybox # 創(chuàng)建pod使用的鏡像
name: test-emptydir
command: [ "sleep", "3600" ] # 這里睡眠等待的原因是:如果pod里面啟動(dòng)的進(jìn)程執(zhí)行完斤葱,pod就會(huì)結(jié)束慷垮。所以redis之類的程序都要以非后臺(tái)方式運(yùn)行
volumeMounts:
- mountPath: /var/log # 容器并不一定存在這個(gè)目錄揖闸,自己試一下,選擇一個(gè)與系統(tǒng)運(yùn)行無關(guān)的目錄料身。因?yàn)閜od是先掛載后啟動(dòng)汤纸,如果掛載到了系統(tǒng)盤上,pod里面的linux就運(yùn)行不起來了
name: tmp-volume # 把下面那個(gè)叫做tmp-volume的存儲(chǔ)卷掛載到容器的/var/log 目錄
volumes:
- name: tmp-volume # 創(chuàng)建一個(gè)emptyDir類型的存儲(chǔ)卷芹血,起名叫做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 # 創(chuàng)建一個(gè)hostPath類型的存儲(chǔ)卷贮泞,起名叫做host-volume
hostPath:
path: /data # 創(chuàng)建存儲(chǔ)卷使用的Node目錄,你的Node可能沒有這個(gè)目錄幔烛,自己找一個(gè)可用目錄
local volume
# pv和pvc使用同一個(gè)StorageClass啃擦,就能將pvc自動(dòng)綁定到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的回收策略,這個(gè)后面講
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這個(gè)標(biāo)簽的節(jié)點(diǎn)來創(chuàng)建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創(chuàng)建成功后使用命令修改:
sudo kubectl patch pv <your-pv-name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
假設(shè)有一個(gè)pv叫test-pv狡恬,綁定的pvc角坐test-pvc珠叔,test-pv使用的local pv
2.1 Retain
- 刪除test-pvc后,test-pv得到了保留弟劲,但test-pv的狀態(tài)會(huì)一直處于 Released而不是Available祷安,不能被其他PVC申請(qǐng);
- 為了重新使用test-pv綁定的nfs存儲(chǔ)空間兔乞,可以刪除并重新創(chuàng)建test-pv汇鞭;
- 刪除操作只是刪除了test-pv對(duì)象,nfs存儲(chǔ)空間中的數(shù)據(jù)并不會(huì)被刪除庸追。
2.2 Recycle
- 刪除test-pvc之后虱咧,Kubernetes啟動(dòng)了一個(gè)新的Pod角坐recycler-for-test-pv,這個(gè)Pod的作用就是清除test-pv的數(shù)據(jù)锚国。在此過程中test-pv的狀態(tài)為Released腕巡,表示已經(jīng)解除了與 test-pvc的綁定,不過此時(shí)還不可用血筑;
- 當(dāng)數(shù)據(jù)清除完畢绘沉,test-pv的狀態(tài)重新變?yōu)?Available,此時(shí)test-pv可以被新的PVC綁定豺总;
- 同樣车伞,也不會(huì)刪除nfs存儲(chǔ)空間中的數(shù)據(jù)。
2.3 Delete
會(huì)刪除test-pv在對(duì)應(yīng)存儲(chǔ)空間上的數(shù)據(jù)喻喳。NFS目前不支持 Delete另玖,支持Delete的存儲(chǔ)空間有AWS EBS、GCE PD、Azure Disk谦去、OpenStack Cinder Volume 等(網(wǎng)上看的慷丽,沒測(cè)試過)。
3. Deployment和Statsfulset
??前面已經(jīng)說過鳄哭,redis有數(shù)據(jù)持久化需求要糊,并且同一個(gè)pod重啟后需要讀取原來對(duì)應(yīng)的持久化數(shù)據(jù),這一點(diǎn)在不使用k8s時(shí)很容易實(shí)現(xiàn)(只使用docker不使用k8s時(shí)也很容易)妆丘,啟動(dòng)redis cluster每個(gè)節(jié)點(diǎn)時(shí)指定其持久化目錄就行了锄俄,但是k8s的Deployment的調(diào)度對(duì)于我們這個(gè)需求來說就顯得很隨機(jī),你無法指定deployment的每個(gè)pod使用哪個(gè)存儲(chǔ)勺拣,并且重啟后仍然使用那個(gè)存儲(chǔ)奶赠。
??Deployment不行,Statefulset可以药有。官方對(duì)Statefulset的優(yōu)點(diǎn)介紹是:
- 穩(wěn)點(diǎn)且唯一的網(wǎng)絡(luò)標(biāo)識(shí)符
- 穩(wěn)點(diǎn)且持久的存儲(chǔ)
- 有序毅戈、平滑的部署和擴(kuò)展
- 有序、平滑的刪除和終止
- 有序的滾動(dòng)更新
??看完還是比較迷糊塑猖,我們可以簡(jiǎn)單的理解為原地更新竹祷,更新后還是原來那個(gè)pod谈跛,只更新了需要更新的內(nèi)容(一般是修改自己寫的程序羊苟,與容器無關(guān))。
??Statefulset和local pv結(jié)合感憾,redis cluster的每個(gè)pod掛掉后在k8s的調(diào)度下重啟時(shí)都會(huì)使用之前自己的持久化文件和節(jié)點(diǎn)信息蜡励。
4. 創(chuàng)建redis集群
4.1 創(chuàng)建StorageClass
??創(chuàng)建StorageClass的目的是deployment中根據(jù)StorageClass來自動(dòng)為每個(gè)pod選擇一個(gè)pv,否則手動(dòng)為每個(gè)pod指定pv又回到了老路上阻桅。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: redis-local-storage # StorageClass的name凉倚,后面需要聲明使用的是這個(gè)StorageClass時(shí)都是用這個(gè)名字
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
4.2 創(chuàng)建PV
??創(chuàng)建6個(gè)pv,因?yàn)閞edis cluster最低是三主三從的配置嫂沉,所以最少需要6個(gè)pod稽寒。后面的pv2~pv5我就不貼出來了。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv1
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: redis-local-storage # 上面創(chuàng)建的StorageClass
local:
path: /usr/local/kubernetes/redis/pv1 # 創(chuàng)建local pv使用的宿主機(jī)目錄趟章,可以自己指定
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname # k8s node的標(biāo)簽杏糙,結(jié)合下面的ip,該標(biāo)簽為kubernetes.io/hostname=192.168.0.152
operator: In
values:
- 192.168.0.152 # localpv創(chuàng)建在192.168.0.152這臺(tái)機(jī)器上
4.3 使用configmap創(chuàng)建redis的配置文件redis.conf
# 下面的redis.conf中不能寫注釋蚓土,否則k8s解析時(shí)會(huì)當(dāng)作配置文件的一部分宏侍,出錯(cuò)
# dir /var/lib/redis使得持久化文件dump.rdb在容器的/var/lib/redis目錄下
# cluster-config-file /var/lib/nodes.conf使得集群信息在/var/lib/redis/nodes.conf文件中
# /var/lib/redis目錄會(huì)掛載pv,所以持久化文件和節(jié)點(diǎn)信息能保存下來
kind: ConfigMap
apiVersion: v1
metadata:
name: redis-cluster-configmap # configmap的名字蜀漆,加上下面的demo-redis就是這個(gè)configmap在k8s集群中的唯一標(biāo)識(shí)
namespace: demo-redis
data:
# 這里可以創(chuàng)建多個(gè)文件
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 創(chuàng)建headless service
??Headless service是StatefulSet實(shí)現(xiàn)穩(wěn)定網(wǎng)絡(luò)標(biāo)識(shí)的基礎(chǔ)谅河,需要提前創(chuàng)建。
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 創(chuàng)建redis節(jié)點(diǎn)
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啟動(dòng)命令
args:
- "/etc/redis/redis.conf" #redis-server后面跟的參數(shù),換行代表空格
- "--protected-mode" #允許外網(wǎng)訪問
- "no"
resources:
requests: # 每個(gè)pod請(qǐng)求的資源
cpu: 2000m # m代表千分之,這里申請(qǐng)2個(gè)邏輯核
memory: 4Gi # 內(nèi)存申請(qǐng)4G大小
limits: # 資源限制
cpu: 2000m
memory: 4Gi
ports:
- name: redis
containerPort: 6379
protocol: "TCP"
- name: cluster
containerPort: 16379
protocol: "TCP"
volumeMounts:
- name: redis-conf # 把下面創(chuàng)建的redis.conf配置文件掛載到容器的/etc/redis目錄下
mountPath: /etc/redis
- name: redis-data # 把叫做redis-data的volume掛載到容器的/var/lib/redis目錄
mountPath: /var/lib/redis
volumes:
- name: redis-conf # 船艦一個(gè)名為redis-conf的volumes
configMap:
name: redis-cluster-configmap # 引用上面創(chuàng)建的configMap卷
items:
- key: redis.conf # configmap里面的redis.conf
path: redis.conf # configmap里面的redis.conf放到volumes中叫做redistribution.conf
volumeClaimTemplates: # pod使用哪個(gè)pvc绷耍,這里是通過StorageClass自動(dòng)創(chuàng)建pvc并對(duì)應(yīng)上pv
- metadata:
name: redis-data # pvc創(chuàng)建一個(gè)volumes叫做redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: redis-local-storage
resources:
requests:
storage: 5Gi
??每個(gè)Pod都會(huì)得到集群內(nèi)的一個(gè)DNS域名吐限,格式為(service name).$(namespace).svc.cluster.local∠翘欤可以在pod中ping一下這些域名毯盈,是可以解析為pod的ip并ping通的。
4.6 創(chuàng)建一個(gè)service病袄,作為redis集群的訪問入口
??這個(gè)service是可以自由發(fā)揮的搂赋,使用port-forward、NodePort還是ingress你自己選擇益缠,我這里只是一個(gè)內(nèi)網(wǎng)訪問統(tǒng)一入口脑奠。
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的六個(gè)節(jié)點(diǎn)都已經(jīng)創(chuàng)建成功幅慌。下面需要?jiǎng)?chuàng)建集群(此時(shí)就是6個(gè)單節(jié)點(diǎn)的redis宋欺,并不是一個(gè)集群)。
4.7 創(chuàng)建redis cluster集群
??我們之前都是通過外部安裝redis-trib創(chuàng)建的集群胰伍,但是根據(jù)這篇文章redis 5.0之后已經(jīng)內(nèi)置了redis-trib工具齿诞,感興趣的可以嘗試。
??專門啟動(dòng)一個(gè)Ubuntu/CentOS的容器骂租,可以在該容器中安裝Redis-tribe祷杈,進(jìn)而初始化Redis集群,執(zhí)行:
kubectl run -i --tty centos --image=centos --restart=Never /bin/bash
成功后渗饮,我們可以進(jìn)入centos容器中但汞,執(zhí)行如下命令安裝基本的軟件環(huán)境:
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
然后執(zhí)行如下命令創(chuàng)建集群:
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
根據(jù)提示一步一步完成。
5. tips
5.1 集群哪怕只有一個(gè)節(jié)點(diǎn)可訪問互站,也要按照集群配置方式
??否則報(bào)錯(cuò)例如MOVED 1545 10.244.3.239:6379","data":false
??如本文的情況私蕾,redis cluster的每個(gè)節(jié)點(diǎn)都是一個(gè)跑在k8s里面的pod,這些pod并不能被外部直接訪問胡桃,而是通過ingress等方法對(duì)外暴露一個(gè)訪問接口踩叭,即只有一個(gè)統(tǒng)一的ip:port給外部訪問。經(jīng)由k8s的調(diào)度翠胰,對(duì)這個(gè)統(tǒng)一接口的訪問會(huì)被發(fā)送到redis集群的某個(gè)節(jié)點(diǎn)容贝。這時(shí)候?qū)edis的用戶來說,看起來這就像是一個(gè)單節(jié)點(diǎn)的redis亡容。但是嗤疯,此時(shí)無論是直接使用命令行工具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}
# 對(duì)比一下單節(jié)點(diǎn)配置方式
host: {your ip}
port: {your port}
password:{your password}