K8s环境中部署各种开发应用服务(四 )Redis!!!

Redis来了

有状态服务的准备工作都做好了
终于终于终于,来到了这一步

这篇我们来在K8s环境中启动一个3主3从的Redis集群服务

开始

直接进入Kubernetes Dashboard 用导入YAML来操作
找到上一篇创建的redis这个命名空间
image-20210717001607436

然后我们一步一步来

配置文件的映射(ConfigMap)

先做一个配置文件的映射(ConfigMap)出来

输入下面的内容去导入

apiVersion: v1
data:
  redis.conf: |-
    port 6379
    cluster-enabled yes
    cluster-require-full-coverage no
    cluster-config-file /data/nodes.conf
    cluster-node-timeout 5000
    cluster-migration-barrier 1
    appendonly yes
    save 900 1
    save 300 10
    save 60 10000
    protected-mode no
  update-node.sh: |-
    #!/bin/sh
    REDIS_NODES="/data/nodes.conf"
    sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/{POD_IP}/"{REDIS_NODES}
    exec "$@"
kind: ConfigMap
metadata:
  name: redis-cluster
  namespace: redis

说明一下

这里创建了一个ConfigMap名叫redis-cluster
这是一个配置文件的映射,用来给后面继续创建任务(DeploymentStatefulSet)来使用的。其中直接输入来创建了两个文件(redis.confupdate-node.sh)

image-20210717001617653

redis.conf
其内容就是redis启动时的一些配置参数,像端口号(port)、节点响应最大时长(cluster-node-timeout)以及是否开启集群功能(cluster-enabled)等等,这些内容和正常部署redis用到的一样。
同时还额外使用了一个配置文件cluster-config-file /data/nodes.conf
此处的/data/nodes.conf内容是通过另一个配置向其写入的当前redis节点的IP地址等信息,用于redis在创建集群时使用的

——特别注意如果没有这个nodes.conf文件去记录podIP 会给后面redis集群运行的时候留下坑
在文章的最后面会现身说法复现一下这个

update-node.sh
这里的内容可以看到就是一个执行脚本,在redis节点启动的时候把当前的IP地址写进/data/nodes.conf里面去,其中还额外使用了个环境变量POD_IP,这个是哪儿来的在后面会出现

下一步,来给控制Redis任务的StatefulSet部署一个Headless Service
关于K8s的Service相关详情可以参考下面这个文章
详解k8s 4种类型Service

配置Headless Service

突如其来的科普

简单介绍一下4中Service分别是干嘛的

ClusterIP的service,就是服务于节点,也就是集群内部节点间通信使用的网络服务,如果不希望当前的应用直接暴露给集群外部,就用这个。这个service有一个Cluster-IP,具体实现原理依靠kubeproxy组件,通过iptables或是ipvs实现

NodePort,用于集群外业务访问,这也是引导外部流量到你的服务的最原始方式。Kubernetes会自动或者由人工手动给节点分配一个对外端口

LoadBalancer类型的service 是可以实现集群外部访问服务的另外一种解决方案,也是暴露服务到 internet 的标准方式。这主要用于云厂商提供的服务中,通过云厂商提供的负载均衡服务来支持,最终会提供给使用LoadBalancer的服务一个专门的对外暴露的IP地址

ExternalName是将服务映射到了DNS 名称,而不是IP地址。这样就可以通过DNS服务返回CNAME记录将外部服务映射到内部服务,实现集群外部访问以及服务之间跨命名空间的访问

最后回到Headless Service

Headless Service 其实也是用于集群内部通信用的服务,这个目的和ClusterIP是一样的
但是我们的ClusterIP工作起来是通过iptables规则将请求转到Real Server,最终落到某个EndPoint(Pod)上,这个过程就是负载均衡

然而Headless Service工作起来自主性更加直接,它主要应用在以下两个场景中

第一种:自主选择权,有时候 client 想自己来决定使用哪个Real Server,可以通过查询DNS来获取 Real Server 的信息。
第二种:Headless Service 的对应的每一个 Endpoints,即每一个Pod,都会有对应的DNS域名,这样Pod之间就可以互相访问。

根据以上网上抄来的内容可得知,这第二种场景,就是我们使用StatefulSet去控制有状态服务节点要用到的

我们通过Headless Service会让服务中的每个Pod都会得到集群内的一个DNS域名

格式为(service name).$(namespace).svc.cluster.local

也就是说在pod中去ping一下这些域名,是可以解析为pod的ip并ping通的

这样在我们集群的中其他应用里去访问这些pod的时候,就可以直接通过这个域名来访问,而不用苦恼于变幻莫测的pod IP
我们在后面部署起Redis的节点之后会进去试一下

科普完毕 我们继续

真的开始配置Headless Service了

还是打开YAML导入窗口

输入以下内容

apiVersion: v1
kind: Service
metadata:
  name: redis-headless-service
  namespace: redis
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    port: 6379
  clusterIP: None
  selector:
    app: redis
    appCluster: redis-cluster

---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
  namespace: redis
  labels:
    app: redis
spec:
  ports:
  - name: redis-port
    protocol: "TCP"
    port: 6379
    targetPort: 6379
  selector:
    app: redis
    appCluster: redis-cluster
---

image-20210717001639586

这里注意一下 命名空间用的是redis
并且在 clusterIP: None 这里指定了headless 的特性

这里不光配置了一个Headless Service 还同时创建了一个ClusterIP Service提供给别的应用连接使用,例如SpringBoot

导入执行

StatefulSet来了

到了这一步,就是真正创建我们的Redis任务了

依旧是打开YAML导入窗口
输入以下内容

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-app
  namespace: redis
spec:
  replicas: 6
  selector:
    matchLabels:
      workload.user.cattle.io/workloadselector: statefulSet-redis-redis-app
  serviceName: redis-service
  template:
    metadata:
      labels:
        app: redis
        appCluster: redis-cluster
        workload.user.cattle.io/workloadselector: statefulSet-redis-redis-app
    spec:
      containers:
      - args:
        - /conf/update-node.sh
        - redis-server
        - /conf/redis.conf
        env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: status.podIP
        image: redis:6.2.4-alpine
        imagePullPolicy: IfNotPresent
        name: redis-app
        ports:
        - containerPort: 6379
          name: redis
          protocol: TCP
        - containerPort: 16379
          name: cluster
          protocol: TCP
        volumeMounts:
        - mountPath: /conf
          name: conf
        - mountPath: /data
          name: redis-storage
          subPath: redis/node
      volumes:
      - configMap:
          defaultMode: 493
          name: redis-cluster
          optional: false
        name: conf
  volumeClaimTemplates:
  - apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: redis-storage
      namespace: redis
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 500Mi
      storageClassName: nfs-redis #此处选择用哪个存储类

内容有点多 稍微说明一下

这里创建了一个6节点(replicas: 6)的StatefulSet任务

名字就叫redis-app(redis-app)后面会根据命名自动生成pod的名字(redis-app-0 … redis-app-5)

  • containers: – args: 这里面是容器启动时执行的命令
    我们在 redis-server 命令后带上了参数 – /conf/redis.conf 也就是挂上了前面配置映射里面的配置文件redis.conf

  • 上面还有一个命令 – /conf/update-node.sh 是为了在容器节点(pod)启动时执行一下拿到pod的IP地址,随后要将IP地址写入到 /data/nodes.conf 文件中记录下来,这一部分非常重要 要是缺少了这个,我们的redis集群节点在重生的时候就会与集群失联了

  • image这里 依旧需要注意镜像的选择

  • imagePullPolicy是镜像拉取规则,这里IfNotPresent是说拉过就别再拉了

  • ports里面设置两个端口,分别是redis服务端口和redis集群操作用的端口(6379,16379)

  • volumes下面有两个卷

一个叫conf(name: conf)它引用了之前创建的名叫redis-cluster(name: redis-cluster)的configMap

此处的defaultMode: 493也不能丢掉 这是挂载的映射文件操作权限

动作 权值 解释
read 4 可读
write 2 可写
execute 1 可执行

这里如果没有设置权限的话,默认是不可执行的,也就无法自行上面获取和写入IP地址的命令

另一个卷是redis-storage(name: redis-storage)它就是我们之前费老大劲部署的nfs服务中的存储卷

  • volumeMounts指的是挂载信息

  • name: conf这条的挂载路径是容器内的/conf里面的文件(mountPath: /conf) 这里和前面的containers: – args:里面指定的conf文件路径是一致的

  • name: redis-storage这里挂载的容器内的/data里面的文件

  • subPath: redis/node是指NFS服务器下面那块硬盘路径下的子路径,这里面存储的就是redis集群节点的信息了,其中包括我们前面ConfigMap那里通过执行命令来写入的本节点IP信息以及集群的信息

  • volumeClaimTemplates就是前面提到的PVC模板
    通过它我们就能让StatefulSet自动去创建PV,PVC

下面storageClassName里面指定了我们用到的StorageClass存储类

也就是前面创建的那个 它叫 nfs

accessModes的模式有三种

  • ReadWriteOnce:可读科写,但支持被单个node挂载
  • ReadOnlyMany:可以以读的方式被多个node挂载
  • ReadWriteMany:可以以读写的方式被多个node挂载

我们用的是ReadWriteOnce,也就是一个redis的节点(node)自己独享一个PV,PVC

  • resources下面还可以指定一下初始分配的存储空间大小以及上限

最后,导入执行

image-20210717001717423

结果一直在 In Progress
出问题了

坑坑坑

这里出现了一个前期部署K3s集群的时候就留下来的坑

我们点开redis-app来看看

image-20210717001727482

发现这第一个节点redis-app-0就没部署成功

继续点进去看

image-20210717001737945

可以看到提示错误在 PersistentVolumeClaims 这里

一路追杀到PVC目录下面发现果然第一个PVC就没成功

image-20210717001746193

继续点开 redis-storage-redis-app-0 然后查看最近事件

image-20210717001754567

说是等待nfs-client-provisioner或者管理员来创建volume

这就很不科学,因为我们的nfs-client-provisioner就是用来自动创建PVC和PV的

既然没成功肯定是nfs-client-provisioner这里有问题

所以我们继续一路追杀到了 nfs-client-provisioner 这边
从左侧菜单里的Deployments找到我们在上一篇文章中出创建的这个nfs-client-provisioner

image-20210717001802117

能看到它倒是活的好好儿的,可丫为什么不干活儿呢

image-20210717001809796

点击右侧三个点儿,打开菜单去找日志

image-20210717001819180

selfLink was empty, can’t make reference

破了案了

直接贴上来一个stackoverflow上面网友们就这个问题的挣扎与迷思

Kubernetes nfs provider selfLink was empty

image-20210717001834368

也就是说,错误中提到的SelfLinks功能在Kubernetes v1.20之后的版本中默认已经被关闭了

查看一下我们Kubernetes Dashboard首页上能看到的版本

image-20210717001844488

可以看到当前版本是 v1.21.2+k3s1

我们的nfs-client-provisioner工作时要使用SelfLink,然而SelfLink默认就已经关了,因此正是这个原因导致报错

问题找到就好解决了

如果是kubernetes的集群,我们只要找到对应的配置文件加上相关的配置就好

vi /etc/kubernetes/manifests/kube-apiserver.yaml

找到这里

spec:
  containers:
  - command:
    - kube-apiserver

在下面添加一行

–feature-gates=RemoveSelfLink=false

spec:
  containers:
  - command:
    - kube-apiserver
- --feature-gates=RemoveSelfLink=false

然后应用一下就可以了

kubectl apply -f /etc/kubernetes/manifests/kube-apiserver.yaml

然而这并不适用于我们的K3s集群

我们要做的是下面两种方式

第一种 从一开始就配置好
回到我们遥远的第二篇文章
4台RaspberryPi4B搭建K8s(K3s)容器集群(二)部署K3s集群

使用K3S官方脚本进行安装这里
启动Master节点的时候就颇具前瞻性的加入这个启动参数
像这样

curl -sfL http://rancher-mirror.cnrancher.com/k3s/k3s-install.sh | INSTALL_K3S_EXEC=" --kube-apiserver-arg=feature-gates=RemoveSelfLink=false" INSTALL_K3S_MIRROR=cn K3S_NODE_NAME=pi1 sh -

–kube-apiserver-arg=feature-gates=RemoveSelfLink=false

也就是说如果确定要使用NFS服务的话,从一开始直接加上就好

但是一开始我们图省心,用了AutoK3s来部署K3s集群
这里其实也是有个地方可以添加最初的参数的

image-20210717001854258

创建新的集群时,点开K3s OptionsMaster下添加一个Custom Args就好了

第二种

对于我们一路摸着石头跑过来的情况,并不能未卜先知的预料到这里的问题
那么在集群已经运行起来的情况下,就需要向修改K8s的yaml文件一样来修改我们K3s的yaml配置文件

只不过这里并不是像K8s那样跑一段yaml把配置注进去就行
而是需要从K3s服务的启动配置上下手(k3s.service)

具体做法如下

直接来到K3s的Master节点主机(树莓派集群的零号机)上去编辑k3s.service文件

vim /etc/systemd/system/multi-user.target.wants/k3s.service

image-20210717001905598

找到 ExecStart 的内容,加上下面两行

‘–kube-apiserver-arg’ /
‘feature-gates=RemoveSelfLink=false’ /

image-20210717001916191

随后记得daemon-reload

systemctl daemon-reload

再重启k3s.service

systemctl restart k3s.service

这样就可以了

回到 dashboard 下 找到刚才pending 的StatefulSet重新部署

image-20210717001923425

这下成功了

image-20210717001932344

点进去能看到 自动部署了6个redis的节点
序号也是固定顺序的 0 – 5

image-20210717001942359

点击左侧菜单的PersistentVolumes
可以看到NFS服务已经自动创建了6个PV出来

image-20210717001953585

再来到PersistentVolumeClaims这里
可以看到对应的也自动创建了6个PVC出来

image-20210717002002344

同时我们来到NFS服务器(贰号机)下面的NFS路径下
可以看到对应的文件已经在这里出现了

image-20210717002009401

然后


突如其来的科普回顾

既然已经看得到PV创建出来的文件了
在这里停下思考一下,这些文件的删除操作是在什么时候进行的呢?

——回收策略 Reclaim Policy

这就是上一篇文章我们创建StorageClass存储类时的配置内容了

image-20210717002016427

这里我们用了Retain

首先我们要知道 PVPVC 是一一绑定的,我们在上面的Dashboard中也可以看到绿色的Bound字样,这就说明PV与对应的PVC已经互相绑定好了

同时PV的删除操作并不是说删就删的,其中一个前提条件就是需要先”解绑”
也就是当PVC被删除的时候,它对应的PV会由绑定状态(Bound)变为释放状态(release)
然后PV才可以被删除

至于PVC被删除之后,它对应的PV要不要删,怎么删
这就是由 StorageClass的回收策略 Reclaim Policy 来控制了

这里的配置一共有两种方式

DELETE 这是默认方式,这会导致当删除PVC时,由StorageClass自动生成的PV也会直接被删除,而这个PV可能已经保存了用户不想删除的数据,比如Redis的数据,然而说没就没了。

Retain 因此我们采用这种方式。当PVC被删除的时候,对应的PV被标记为released. 此时该PV已经被解绑释放,同时之前的数据依旧保存在该PV上,只是该PV不可用,需要手动来处理这些数据并删除该PV。这样一来重要数据的回收工作就由管理员来手动进行了。

实际上我们StatefulSet下面的节点(pod)在重生(重新生一个)的时候,相当于新来的克隆人工厂的办公室去捡起了前面死掉的那个节点(pod)丢掉的工牌(固定序号)继续用,也就顺理成章的继续使用前任一直在用的PVCPV继续干活了。


科普之后我们继续,还差一步

Redis的6个节点已经部署成功,最后我们要来到Redis里面用这6个节点组建好Redis集群就可以用了

我们从 Dashboard 上直接用它的命令行操作 按钮在上面

image-20210717002026518

接着来查看一下所有redis节点的IP是什么

kubectl get pods -n redis -l app=redis -o jsonpath='{range.items[*]}{.status.podIP}:6379 {end}'

我们的命令中用命名空间(-n redis)和标签(app=redis)定位到了redis的节点,同时用jsonpath遍历了所有节点的IP(.status.podIP)并且在后面手动加上端口号(:6379)

最后得到了这样的格式

image-20210717002037073

10.42.3.202:6379 10.42.1.162:6379 10.42.2.106:6379 10.42.3.204:6379 10.42.1.164:6379 10.42.1.165:6379

我们用以上的IP+端口+空格的格式用在redis–cluster create命令中就可以了

直接钻进其中一个节点去执行redis命令

kubectl exec -it redis-app-0  -n redis -- redis-cli --cluster create --cluster-replicas 1 $(kubectl get pods -n redis -l app=redis -o jsonpath='{range.items[*]}{.status.podIP}:6379 {end}')

说明一下 参数中的

–cluster-replicas 1

是redis集群的策略 这里是比例
1指的是 1:1来分配主从节点6节点 分配出来就是3主3从了

image-20210717002049228
中间输入一次yes

最后可以看到redis集群建立成功了

再次进入节点的redis环境下去查看一下集群是否正常工作

kubectl exec -it redis-app-0  -n redis -- redis-cli -c 

注意操作redis集群的时候 最后有个-c

进入redis之后 查看一下集群

cluster nodes

image-20210717002059501

能看到6个节点已经meet过彼此并且开始工作了

set值试一下

image-20210717002107984

一切正常,槽儿(slot)的分配也很顺利

——存档点

横线的内的这里是坑的开始

坑坑坑 ——现身说法

看上去一切顺利

这里引出一个思考,我们既然是通过节点IP(容器IP)建立的redis集群,那么节点(pod)死掉又重生的时候,它的节点IP肯定也随之改变了,那么redis集群还会找到它嘛?

这里就是前面反复提到的ConfigMap里面的那条命令起到了至关重要的作用

update-node.sh: |-
    #!/bin/sh
    REDIS_NODES="/data/nodes.conf"
    sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/{POD_IP}/"{REDIS_NODES}
    exec "$@"

我们接下来本着求真务实的科研态度,主动跳入坑中来现身说法

假装回到前面部署StatefulSet 尝试把命令行下面的

 - /conf/update-node.sh

这一行给注释掉

image-20210717002118472

接下来我们假装回到 存档点 那里

就是刚才往组建好的redis集群里面set数据这里

然后我们接着测试集群的工作是否正常

我们看到刚才set数据的时候 located到了 10.42.1.162 这个节点上

查看一下pod信息得知这个节点是redis-app-2

image-20210717002130067

现在直接干掉它

立刻这个节点就terminating随即creating container最后Running起来了

新人来旧人去,就只在一瞬间 然而节点的IP地址确实改变了

image-20210717002136271

我们继续回到刚才打开的Redis的终端上

image-20210717002144130

发现已经被崩出来了

原因是我们刚才在测试set数据的时候 就被located到了 10.42.1.162 上面了

现在 10.42.1.162 死了 自然就崩出来了

那么重新找个节点进入redis集群去看看呢

image-20210717002151892

不对劲…

get 一下刚才的数据

image-20210717002200807

虽然是正常工作

可目前的状况就是 6个节点挂掉了1个 剩下的5个顶上来继续干活

也就是说 重生的节点 失联了

我们来到NFS服务器硬盘内能看到之前创建的6个PVC对应存储文件夹

image-20210717002207693

但是随便打开看一下,里面什么也没有

这就问题大了
也就是说,我们的redis节点只要重生,就处于掉线状态没人管

这可不行

我们回到前面部署StatefulSet那里

假装没有掉进坑中

配置文件上 正常的去加载节点写IP的命令内容

 - /conf/update-node.sh

image-20210717002214451

随后继续假装回到 存档点 那里

横线内的这里是坑的结束

我们继续先查看一下redis cluster的情况

image-20210717002222843

一切正常

接着干掉其中一个 10.42.1.218
它是redis-app-5这个节点

wp_editor_md_a4dd3d4dcc565fb1ec32dcaaa4762238.jpg

然后它重生了 IP变成了 10.42.3.7

image-20210717003618787

我们再去看一下redis集群的信息

image-20210717003454065

可以见到集群信息已经更新了

最后去NFS服务器硬盘里看一眼

image-20210717002250783

可以看到自动创建的PVC目录下已经有文件了,打开看一下

image-20210717002258673

可看到 myself 这一行的IP 已经被更新了

此时再另外几个节点目录下,这一条的IP也同样被更新了

这样一来就实现了节点重生后,集群信息的同步

好了,K8s集群内搭建6节点(3主3从)Redis集群的工作到此结束

这时候在开发的时候,就可以通过本章科普环节提到的
Headless Service给每个Pod都提供的集群内DNS域名来在集群内部访问到Redis节点了

格式为(service name).$(namespace).svc.cluster.local

我们可以直接访问到指定节点

redis-app-0.redis-service.svc.cluster.local:6379

也可以直接访问Headless Service来实现访问到Redis节点(经过负载)

redis-headless-service.redis.svc.cluster.local:6379

还可去去访问集群IP的那个Service

redis-service.redis.svc.cluster.local:6379

redis-service.redis.svc.cluster.local

redis-service.redis.svc.cluster.local

以SpringBoot为例,我们可以把这里的配置写成这样

# Redis 集群
spring.redis.password=
spring.redis.cluster.nodes=redis-app-0.redis-service.svc.cluster.local:6379,redis-app-1.redis-service.svc.cluster.local:6379,redis-app-2.redis-service.svc.cluster.local:6379,redis-app-3.redis-service.svc.cluster.local:6379,redis-app-4.redis-service.svc.cluster.local:6379,redis-service.svc.cluster.local:6379
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-idle=16
spring.redis.lettuce.pool.max-active=32
spring.redis.lettuce.pool.min-idle=8

或者这样直接使用redis-service的集群内部域名而不是用节点域名

# Redis 集群
spring.redis.password=
spring.redis.cluster.nodes=redis-service.redis.svc.cluster.local:6379
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-idle=16
spring.redis.lettuce.pool.max-active=32
spring.redis.lettuce.pool.min-idle=8

这样当SpringBoot应用打成镜像部署到K8s集群内的时候就可以通过集群解析成节点IP访问到redis了

具体在 K8s 下 Pod 与 Service 的 DNS 的用法可参考官网
Pod 与 Service 的 DNS

结束

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注