一、什么是控制器

kubernetes中内建了很多controller(控制器),这些相当于一个状态机,用来控制pod的具体状态和行为。

部分控制器类型如下:
ReplicationController 和 ReplicaSet
Deployment
DaemonSet
StatefulSet
Job/CronJob
HorizontalPodAutoscaler

二、DaemonSet控制器

  1. DaemonSet 确保全部(或者某些)节点上运行一个 Pod 的副本。当有节点加入集群时,会为他们新增一个 Pod。当有节点从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod
  2. DaemonSet 的一些典型用法:
  3. 在每个节点上运行集群存储 DaemonSet,例如 glusterdceph
  4. 在每个节点上运行日志收集 DaemonSet,例如 fluentdlogstash
  5. 在每个节点上运行监控 DaemonSet,例如 Prometheus Node ExporterFlowmillSysdig 代理、collectdDynatrace OneAgentAppDynamics 代理、Datadog 代理、New Relic 代理、Ganglia gmond 或者 Instana 代理。
  6. 一个简单的用法是在所有的节点上都启动一个 DaemonSet,并作为每种类型的 daemon 使用。
  7. 一个稍微复杂的用法是单独对每种 daemon 类型使用一种DaemonSet。这样有多个 DaemonSet,但具有不同的标识,并且对不同硬件类型具有不同的内存、CPU 要求。

备注:DaemonSet 中的 Pod 可以使用 hostPort,从而可以通过节点 IP 访问到 Pod;因为DaemonSet模式下Pod不会被调度到其他节点。使用示例如下:

  1. ports:
  2. - name: httpd
  3. containerPort: 80
  4. #除非绝对必要,否则不要为 Pod 指定 hostPort。 将 Pod 绑定到hostPort时,它会限制 Pod 可以调度的位置数;DaemonSet除外
  5. #一般情况下 containerPort与hostPort值相同
  6. hostPort: 8090 #可以通过宿主机+hostPort的方式访问该Pod。例如:pod在/调度到了k8s-node02 【192.168.153.147】,那么该Pod可以通过192.168.153.147:8090方式进行访问。
  7. protocol: TCP

下面举个栗子:

1.创建DaemonSet

DaemonSet的描述文件和Deployment非常相似,只需要修改Kind,并去掉副本数量的配置即可

当然,我们这里的pod运行的是nginx,作为案例;

  1. [root@k8s-master daemonset]# cat nginx-daemonset.yml
  2. apiVersion: apps/v1
  3. kind: DaemonSet
  4. metadata:
  5. name: nginx-daemonset
  6. labels:
  7. app: nginx
  8. spec:
  9. selector:
  10. matchLabels:
  11. app: nginx
  12. template:
  13. metadata:
  14. labels:
  15. app: nginx
  16. spec:
  17. containers:
  18. - name: nginx
  19. image: nginx
  20. ports:
  21. - name: nginx
  22. containerPort: 80
  23. hostPort: 8090
  24. protocol: TCP

控制器 - 图1

2.测试效果

用宿主机的ip+8090端口,即可访问到:

控制器 - 图2

控制器 - 图3

下面来看DaemonSet的效果;

也可以看到,每个node上,都会有一个DaemonSet的pod

控制器 - 图4

尝试删除,也会重建

  1. [root@k8s-master daemonset]# kubectl delete pod nginx-daemonset-5trrn

控制器 - 图5

三、StatefulSet控制器

本次实验基于k8s-v1.19.0版本

控制器 - 图6

StatefulSet 是用来管理有状态应用的工作负载 API 对象。

StatefulSet 中的 Pod 拥有一个具有黏性的、独一无二的身份标识。这个标识基于 StatefulSet 控制器分配给每个 Pod 的唯一顺序索引。Pod 的名称的形式为- 。例如:web的StatefulSet 拥有两个副本,所以它创建了两个 Pod:web-0和web-1。

和 Deployment 相同的是,StatefulSet 管理了基于相同容器定义的一组 Pod。但和 Deployment 不同的是,StatefulSet 为它们的每个 Pod 维护了一个固定的 ID。这些 Pod 是基于相同的声明来创建的,但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。

【使用场景】StatefulSets 对于需要满足以下一个或多个需求的应用程序很有价值:

  1. 稳定的、唯一的网络标识符,即Pod重新调度后其PodName和HostName不变【当然IP是会变的】
  2. 稳定的、持久的存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC实现
  3. 有序的、优雅的部署和缩放
  4. 有序的、自动的滚动更新
    如上面,稳定意味着 Pod 调度或重调度的整个过程是有持久性的。

如果应用程序不需要任何稳定的标识符或有序的部署、删除或伸缩,则应该使用由一组无状态的副本控制器提供的工作负载来部署应用程序,比如使用 Deployment 或者 ReplicaSet 可能更适用于无状态应用部署需要。

1.限制

给定 Pod 的存储必须由 PersistentVolume 驱动 基于所请求的 storage class 来提供,或者由管理员预先提供。
删除或者收缩 StatefulSet 并不会删除它关联的存储卷。这样做是为了保证数据安全,它通常比自动清除 StatefulSet 所有相关的资源更有价值。
StatefulSet 当前需要 headless 服务 来负责 Pod 的网络标识。你需要负责创建此服务。
当删除 StatefulSets 时,StatefulSet 不提供任何终止 Pod 的保证。为了实现 StatefulSet 中的 Pod 可以有序和优雅的终止,可以在删除之前将 StatefulSet 缩放为 0。
在默认 Pod 管理策略(OrderedReady) 时使用滚动更新,可能进入需要人工干预才能修复的损坏状态。
有序索引
对于具有 N 个副本的 StatefulSet,StatefulSet 中的每个 Pod 将被分配一个整数序号,从 0 到 N-1,该序号在 StatefulSet 上是唯一的。

StatefulSet 中的每个 Pod 根据 StatefulSet 中的名称和 Pod 的序号来派生出它的主机名。组合主机名的格式为控制器 - 图7-#card=math&code=%28StatefulSet%20%E5%90%8D%E7%A7%B0%29-&id=OOm0c)(序号)。

2.部署和扩缩保证

对于包含 N 个 副本的 StatefulSet,当部署 Pod 时,它们是依次创建的,顺序为 0~(N-1)。
当删除 Pod 时,它们是逆序终止的,顺序为 (N-1)~0。
在将缩放操作应用到 Pod 之前,它前面的所有 Pod 必须是 Running 和 Ready 状态。
在 Pod 终止之前,所有的继任者必须完全关闭。
StatefulSet 不应将 pod.Spec.TerminationGracePeriodSeconds 设置为 0。这种做法是不安全的,要强烈阻止。

3.部署顺序

在下面的 nginx 示例被创建后,会按照 web-0、web-1、web-2 的顺序部署三个 Pod。在 web-0 进入 Running 和 Ready 状态前不会部署 web-1。在 web-1 进入 Running 和 Ready 状态前不会部署 web-2。

如果 web-1 已经处于 Running 和 Ready 状态,而 web-2 尚未部署,在此期间发生了 web-0 运行失败,那么 web-2 将不会被部署,要等到 web-0 部署完成并进入 Running 和 Ready 状态后,才会部署 web-2。

4.收缩顺序

如果想将示例中的 StatefulSet 收缩为 replicas=1,首先被终止的是 web-2。在 web-2 没有被完全停止和删除前,web-1 不会被终止。当 web-2 已被终止和删除;但web-1 尚未被终止,如果在此期间发生 web-0 运行失败,那么就不会终止 web-1,必须等到 web-0 进入 Running 和 Ready 状态后才会终止 web-1。

5.项目实战

提前说明:由于本地动态实战,我在v1.22.2版本中,尝试多次未成功,采用了v1.19.0版本的k8s集群;

控制器 - 图8

Dynamic Provisioning机制工作的核心在于StorageClass的API对象。
StorageClass声明存储插件,用于自动创建PV

控制器 - 图9

当我们k8s业务上来的时候,大量的pvc,此时我们人工创建匹配的话,工作量就会非常大了,需要动态的自动挂载相应的存储,‘
我们需要使用到StorageClass,来对接存储,靠他来自动关联pvc,并创建pv。
Kubernetes支持动态供给的存储插件:
https://kubernetes.io/docs/concepts/storage/storage-classes/
因为NFS不支持动态存储,所以我们需要借用这个存储插件。
nfs动态相关部署可以参考:
https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client/deploy
部署步骤:

5.1部署nfs

  1. 3个节点都下载:
  2. # yum -y install nfs-utils rpcbind
  3. 主节点配置nfs服务端
  4. [root@master pvc-test]# mkdir /opt/container_data
  5. [root@master pvc-test]# chmod 777 -R /opt/container_data
  6. [root@master pvc-test]# cat /etc/exports
  7. /opt/container_data *(rw,no_root_squash,no_all_squash,sync)
  8. [root@master pvc-test]# systemctl start rpcbind && systemctl start nfs

5.2定义一个storage

  1. [root@master pvc-test]# cat storageclass-nfs.yaml
  2. apiVersion: storage.k8s.io/v1
  3. kind: StorageClass
  4. metadata:
  5. name: managed-nfs-storage
  6. provisioner: fuseim.pri/ifs

5.3部署授权

因为storage自动创建pv需要经过kube-apiserver,所以要进行授权

创建1个sa

创建1个clusterrole,并赋予应该具有的权限,比如对于一些基本api资源的增删改查;

创建1个clusterrolebinding,将sa和clusterrole绑定到一起;这样sa就有权限了;

然后pod中再使用这个sa,那么pod再创建的时候,会用到sa,sa具有创建pv的权限,便可以自动创建pv;

  1. [root@master pvc-test]# cat rbac.yaml
  2. apiVersion: v1
  3. kind: ServiceAccount
  4. metadata:
  5. name: nfs-client-provisioner
  6. ---
  7. kind: ClusterRole
  8. apiVersion: rbac.authorization.k8s.io/v1
  9. metadata:
  10. name: nfs-client-provisioner-runner
  11. rules:
  12. - apiGroups: [""]
  13. resources: ["persistentvolumes"]
  14. verbs: ["get", "list", "watch", "create", "delete"]
  15. - apiGroups: [""]
  16. resources: ["persistentvolumeclaims"]
  17. verbs: ["get", "list", "watch", "update"]
  18. - apiGroups: ["storage.k8s.io"]
  19. resources: ["storageclasses"]
  20. verbs: ["get", "list", "watch"]
  21. - apiGroups: [""]
  22. resources: ["events"]
  23. verbs: ["list", "watch", "create", "update", "patch"]
  24. ---
  25. kind: ClusterRoleBinding
  26. apiVersion: rbac.authorization.k8s.io/v1
  27. metadata:
  28. name: run-nfs-client-provisioner
  29. subjects:
  30. - kind: ServiceAccount
  31. name: nfs-client-provisioner
  32. namespace: default
  33. roleRef:
  34. kind: ClusterRole
  35. name: nfs-client-provisioner-runner
  36. apiGroup: rbac.authorization.k8s.io

5.4部署一个自动创建pv的服务

这里自动创建pv的服务由nfs-client-provisioner 完成

  1. [root@master pvc-test]# cat deployment-nfs.yaml
  2. kind: Deployment
  3. apiVersion: apps/v1
  4. metadata:
  5. name: nfs-client-provisioner
  6. spec:
  7. selector:
  8. matchLabels:
  9. app: nfs-client-provisioner
  10. replicas: 1
  11. strategy:
  12. type: Recreate
  13. template:
  14. metadata:
  15. labels:
  16. app: nfs-client-provisioner
  17. spec:
  18. # imagePullSecrets:
  19. # - name: registry-pull-secret
  20. serviceAccount: nfs-client-provisioner
  21. containers:
  22. - name: nfs-client-provisioner
  23. #image: quay.io/external_storage/nfs-client-provisioner:latest
  24. image: lizhenliang/nfs-client-provisioner:v2.0.0
  25. volumeMounts:
  26. - name: nfs-client-root
  27. mountPath: /persistentvolumes
  28. env:
  29. - name: PROVISIONER_NAME
  30. #这个值是定义storage里面的那个值
  31. value: fuseim.pri/ifs
  32. - name: NFS_SERVER
  33. value: 172.17.0.21
  34. - name: NFS_PATH
  35. value: /opt/container_data
  36. volumes:
  37. - name: nfs-client-root
  38. nfs:
  39. server: 172.17.0.21
  40. path: /opt/container_data

创建:

  1. [root@master pvc-test]# kubectl apply -f storageclass-nfs.yaml
  2. [root@master pvc-test]# kubectl apply -f rbac.yaml
  3. [root@master pvc-test]# kubectl apply -f deployment-nfs.yaml

查看创建好的storage:

  1. [root@master storage]# kubectl get sc

控制器 - 图10

nfs-client-provisioner 会以pod运行在k8s中,

  1. [root@master storage]# kubectl get pod
  2. NAME READY STATUS RESTARTS AGE
  3. nfs-client-provisioner-855887f688-hrdwj 1/1 Running 0 77s

4、部署有状态服务,测试自动创建pv
部署yaml文件参考:https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/
我们部署一个nginx服务,让其html下面自动挂载数据卷,

  1. [root@master pvc-test]# cat nginx.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: nginx
  6. labels:
  7. app: nginx
  8. spec:
  9. ports:
  10. - port: 80
  11. name: web
  12. clusterIP: None
  13. selector:
  14. app: nginx
  15. ---
  16. apiVersion: apps/v1
  17. kind: StatefulSet
  18. metadata:
  19. name: web
  20. spec:
  21. serviceName: "nginx"
  22. replicas: 2
  23. selector:
  24. matchLabels:
  25. app: nginx
  26. template:
  27. metadata:
  28. labels:
  29. app: nginx
  30. spec:
  31. containers:
  32. - name: nginx
  33. image: nginx
  34. ports:
  35. - containerPort: 80
  36. name: web
  37. volumeMounts:
  38. - name: www
  39. mountPath: /usr/share/nginx/html
  40. volumeClaimTemplates:
  41. - metadata:
  42. name: www
  43. spec:
  44. accessModes: [ "ReadWriteOnce" ]
  45. storageClassName: "managed-nfs-storage"
  46. resources:
  47. requests:
  48. storage: 1Gi
  49. [root@master pvc-test]# kubectl apply -f nginx.yaml

5.5有序创建的特性

控制器 - 图11

控制器 - 图12

5.6数据持久化特性

进入其中一个容器,创建一个文件:

  1. [root@master pvc-test]# kubectl exec -it web-0 /bin/sh
  2. # cd /usr/share/nginx/html
  3. # touch 1.txt

控制器 - 图13

直接在web-1的目录下,创建一个文件:

  1. [root@master pvc-test]# touch /opt/container_data/default-www-web-1-pvc-5efd9492-c13d-414b-8df3-68b0c37961dd/2.txt

控制器 - 图14

控制器 - 图15

而且,删除一个pod web-0,数据仍然存在,不会丢失。保证了数据持久化;

控制器 - 图16

测试

  1. [root@master container_data]# pwd
  2. /opt/container_data
  3. [root@master container_data]# kubectl get pod -o wide

控制器 - 图17

  1. ##1.
  2. [root@master container_data]# echo "youngfit-1" >> default-www-web-0-pvc-fd4bf916-fe84-4b20-80b4-27442effb584/index.html
  3. [root@master container_data]# curl 10.244.104.8
  4. youngfit-1
  5. ##2.
  6. [root@master container_data]# kubectl exec -it web-1 /bin/sh
  7. # cd /usr/share/nginx/html
  8. # echo youngfit-2 >> index.html
  9. [root@master container_data]# ls default-www-web-1-pvc-5efd9492-c13d-414b-8df3-68b0c37961dd/
  10. 2.txt index.html
  11. [root@master container_data]# curl 10.244.166.138
  12. youngfit-2

5.7 验证解析

每个 Pod 都拥有一个基于其顺序索引的稳定的主机名

控制器 - 图18

使用 kubectl run 运行一个提供 nslookup 命令的容器,该命令来自于 dnsutils 包。通过对 Pod 的主机名执行 nslookup,你可以检查他们在集群内部的 DNS 地址

  1. [root@master pvc-test]# cat pod.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. name: testnginx
  6. spec:
  7. containers:
  8. - name: testnginx
  9. image: daocloud.io/library/nginx:1.12.0-alpine
  10. [root@master pvc-test]# kubectl apply -f pod.yaml
  11. [root@master pvc-test]# kubectl exec -it testnginx /bin/sh

控制器 - 图19

控制器 - 图20

重启pod会发现,pod中的ip已经发生变化,但是pod的名称并没有发生变化;这就是为什么不要在其他应用中使用 StatefulSet 中的 Pod 的 IP 地址进行连接,这点很重要

控制器 - 图21

  1. [root@master pvc-test]# kubectl delete pod -l app=nginx
  2. pod "web-0" deleted
  3. pod "web-1" deleted
  4. [root@master pvc-test]# kubectl get pod -o wide

控制器 - 图22

控制器 - 图23

控制器 - 图24

5.8 写入稳定的存储

将 Pod 的主机名写入它们的index.html文件并验证 NGINX web 服务器使用该主机名提供服务

  1. [root@k8s-master sts]# kubectl exec -it web-0 /bin/sh
  2. # cd /usr/share/nginx/html
  3. # echo youngfit-1 > index.html
  4. [root@k8s-master sts]# kubectl exec -it web-1 /bin/sh
  5. # cd /usr/share/nginx/html
  6. # echo youngfit-2 > index.html
  7. [root@k8s-master sts]# ls /opt/container_data/default-www-web-0-pvc-ae99bd8d-a337-458d-a178-928cf4602713/
  8. index.html
  9. [root@k8s-master sts]# ls /opt/container_data/default-www-web-1-pvc-afac76ea-9faf-41ac-b03d-7ffc9e277029/
  10. index.html

控制器 - 图25

  1. [root@k8s-master sts]# curl 10.244.4.5
  2. youngfit-1
  3. [root@k8s-master sts]# curl 10.244.1.4
  4. youngfit-2
  5. 再次删除
  6. [root@k8s-master sts]# kubectl delete pod -l app=nginx
  7. pod "web-0" deleted
  8. pod "web-1" deleted
  9. [root@k8s-master sts]# kubectl apply -f nginx.yaml
  10. [root@k8s-master sts]# kubectl get pod
  11. NAME READY STATUS RESTARTS AGE
  12. nfs-client-provisioner-56cc44bd5-2hgxc 1/1 Running 0 27m
  13. testnginx 1/1 Running 0 6m20s
  14. web-0 1/1 Running 0 13s
  15. web-1 1/1 Running 0 6s

再次查看

控制器 - 图26

控制器 - 图27

5.9 扩容/缩容 StatefulSet

扩容/缩容StatefulSet 指增加或减少它的副本数。这通过更新replicas字段完成。你可以使用kubectl scale 或者kubectl patch来扩容/缩容一个 StatefulSet。

控制器 - 图28

控制器 - 图29

控制器 - 图30

控制器 - 图31

控制器 - 图32

四、基于k8s集群的redis-cluster集群

StatefulSet、pv的动态存储

1.提前准备好nfs存储

  1. [root@k8s-master ~]# yum -y install nfs-utils nfs-common rpcbind
  2. [root@k8s-master ~]# mkdir /data/nfs
  3. [root@k8s-master ~]# chmod -R 777 /data/nfs
  4. [root@k8s-master ~]# vim /etc/exports
  5. /data/nfs *(rw,no_root_squash,sync,no_all_squash)
  6. [root@k8s-master ~]# systemctl start rpcbind nfs
  7. [root@k8s-master ~]# systemctl status nfs
  8. 其余节点下载nfs客户端,确保可以挂载
  9. # yum -y install nfs-utils nfs-common
  10. 下载好之后,可以尝试用挂载命令试试,能否正常挂载

控制器 - 图33

2.制作动态存储

制作动态存储storageclass与nfs的共享目录相关联,这里我是用的helm工具。

  1. [root@k8s-master ~]# helm install stable/nfs-client-provisioner --set nfs.server=192.168.153.148 --set nfs.path=/data/nfs
  2. [root@k8s-master ~]# helm list

控制器 - 图34

  1. [root@k8s-master ~]# kubectl get storageclass

控制器 - 图35

3.redis配置文件ConfigMap

  1. #将redis配置文件内容
  2. [root@k8s-master ~]# mkdir redis-ha
  3. [root@k8s-master redis-ha]# pwd
  4. /root/redis-ha
  5. [root@k8s-master redis-ha]# cat redis.conf
  6. appendonly yes
  7. cluster-enabled yes
  8. cluster-config-file /var/lib/redis/nodes.conf
  9. cluster-node-timeout 5000
  10. dir /var/lib/redis
  11. port 6379
  12. [root@k8s-master redis-ha]# kubectl create configmap redis-conf --from-file=redis.conf

查看创建的configmap

控制器 - 图36

4.创建Headless service

  1. [root@k8s-master redis-ha]# vim headless-service.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: redis-service
  6. labels:
  7. app: redis
  8. spec:
  9. ports:
  10. - name: redis-port
  11. port: 6379
  12. clusterIP: None
  13. selector:
  14. app: redis
  15. appCluster: redis-cluster
  16. [root@k8s-master redis-ha]# kubectl create -f headless-service.yml

控制器 - 图37

可以看到,服务名称为redis-service,其CLUSTER-IP为None,表示这是一个“无头”服务。

5.statefulSet运行redis实例

创建好Headless service后,就可以利用StatefulSet创建Redis 集群节点,这也是本文的核心内容。我们先创建redis.yml文件:

  1. [root@k8s-master redis-ha]# cat redis.yaml
  2. apiVersion: apps/v1
  3. kind: StatefulSet
  4. metadata:
  5. name: redis-app
  6. spec:
  7. serviceName: "redis-service"
  8. replicas: 6
  9. selector:
  10. matchLabels:
  11. app: redis
  12. appCluster: redis-cluster
  13. template:
  14. metadata:
  15. labels:
  16. app: redis
  17. appCluster: redis-cluster
  18. spec:
  19. terminationGracePeriodSeconds: 20
  20. affinity:
  21. podAntiAffinity:
  22. preferredDuringSchedulingIgnoredDuringExecution:
  23. - weight: 100
  24. podAffinityTerm:
  25. labelSelector:
  26. matchExpressions:
  27. - key: app
  28. operator: In
  29. values:
  30. - redis
  31. topologyKey: kubernetes.io/hostname
  32. containers:
  33. - name: redis
  34. image: redis:
  35. command:
  36. - "redis-server"
  37. args:
  38. - "/etc/redis/redis.conf"
  39. - "--protected-mode"
  40. - "no"
  41. resources:
  42. requests:
  43. cpu: "100m"
  44. memory: "100Mi"
  45. ports:
  46. - name: redis
  47. containerPort: 6379
  48. protocol: "TCP"
  49. - name: cluster
  50. containerPort: 16379
  51. protocol: "TCP"
  52. volumeMounts:
  53. - name: "redis-conf"
  54. mountPath: "/etc/redis"
  55. - name: "redis-data"
  56. mountPath: "/var/lib/redis"
  57. volumes:
  58. - name: "redis-conf"
  59. configMap:
  60. name: "redis-conf"
  61. items:
  62. - key: "redis.conf"
  63. path: "redis.conf"
  64. volumeClaimTemplates:
  65. - metadata:
  66. name: redis-data
  67. spec:
  68. accessModes: [ "ReadWriteMany" ]
  69. storageClassName: "nfs-client"
  70. resources:
  71. requests:
  72. storage: 200M
  1. [root@k8s-master redis-ha]# kubectl apply -f redis.yaml

5.查看

  1. [root@k8s-master redis-ha]# kubectl get pod

控制器 - 图38

  1. [root@k8s-master redis-ha]# kubectl get statefulSet
  2. NAME READY AGE
  3. redis-app 6/6 4h34m

6.验证唯一访问标识可用性

如上,总共创建了6个Redis节点(Pod),其中3个将用于master,另外3个分别作为master的slave;Redis的配置通过volume将之前生成的redis-conf这个Configmap,挂载到了容器的/etc/redis/redis.conf;Redis的数据存储路径使用volumeClaimTemplates声明(也就是PVC),其会绑定到我们先前创建的PV上。
这里有一个关键概念——Affinity,请参考官方文档详细了解。其中,podAntiAffinity表示反亲和性,其决定了某个pod不可以和哪些Pod部署在同一拓扑域,可以用于将一个服务的POD分散在不同的主机或者拓扑域中,提高服务本身的稳定性。
而PreferredDuringSchedulingIgnoredDuringExecution 则表示,在调度期间尽量满足亲和性或者反亲和性规则,如果不能满足规则,POD也有可能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。
在这里,matchExpressions规定了Redis Pod要尽量不要调度到包含app为redis的Node上,也即是说已经存在Redis的Node上尽量不要再分配Redis Pod了。但是,由于我们只有三个Node,而副本有6个,因此根据PreferredDuringSchedulingIgnoredDuringExecution,这些豌豆不得不得挤一挤,挤挤更健康~
另外,根据StatefulSet的规则,我们生成的Redis的6个Pod的hostname会被依次命名为 控制器 - 图39-#card=math&code=%28statefulset%E5%90%8D%E7%A7%B0%29-&id=wV7u4)(序号) 如下图所示:

如上,可以看到这些Pods在部署时是以{0…N-1}的顺序依次创建的。注意,直到redis-app-0状态启动后达到Running状态之后,redis-app-1 才开始启动。
同时,每个Pod都会得到集群内的一个DNS域名,格式为控制器 - 图40.#card=math&code=%28podname%29.&id=Od57l)(service name).$(namespace).svc.cluster.local ,也即是

  1. redis-app-0.redis-service.default.svc.cluster.local
  2. redis-app-1.redis-service.default.svc.cluster.local
  3. ...以此类推...
  1. # 创建1个pod,测试完即可删除此pod
  2. [root@k8s-master redis-ha]# cat busybox.yaml
  3. ---
  4. apiVersion: v1
  5. kind: Pod
  6. metadata:
  7. name: busybox
  8. spec:
  9. containers:
  10. - name: busybox
  11. image: daocloud.io/library/busybox
  12. stdin: true
  13. tty: true
  14. [root@k8s-master redis-ha]# kubectl apply -f busybox.yaml
  15. [root@k8s-master redis-ha]# kubectl exec -it busybox /bin/sh
  16. / # ping redis-app-0.redis-service.default.svc.cluster.local

控制器 - 图41

控制器 - 图42

  1. / # ping redis-app-1.redis-service.default.svc.cluster.local

控制器 - 图43

控制器 - 图44

可以看到, redis-app-0的IP为10.244.1.33;redis-app-1的IP为10.244.2.20。当然,若Redis Pod迁移或是重启(我们可以手动删除掉一个Redis Pod来测试),IP是会改变的,但是Pod的域名、SRV records、A record都不会改变。

另外可以发现,我们之前创建的pv都被成功绑定了:

控制器 - 图45

控制器 - 图46

7.集群初始化

创建好6个Redis Pod后,我们还需要利用常用的Redis-tribe工具进行集群的初始化

创建Ubuntu容器
由于Redis集群必须在所有节点启动后才能进行初始化,而如果将初始化逻辑写入Statefulset中,则是一件非常复杂而且低效的行为。这里,哥不得不赞许一下原项目作者的思路,值得学习。也就是说,我们可以在K8S上创建一个额外的容器,专门用于进行K8S集群内部某些服务的管理控制。
这里,我们专门启动一个Ubuntu的容器,可以在该容器中安装Redis-tribe,进而初始化Redis集群,执行:

  1. [root@k8s-master redis-ha]# kubectl run -it ubuntu --image=ubuntu --restart=Never /bin/bash

控制器 - 图47

先试试ubuntu这个pod是否能与redis所有pod进行通信

  1. [root@k8s-master redis-ha]# kubectl exec -it ubuntu /bin/bash
  2. root@ubuntu:/# nslookup redis-app-1.redis-service.default.svc.cluster.local
  3. Server: 10.96.0.10
  4. Address: 10.96.0.10#53
  5. Name: redis-app-1.redis-service.default.svc.cluster.local
  6. Address: 10.244.2.20

控制器 - 图48

我们使用阿里云的Ubuntu源,执行:

  1. [root@k8s-master redis-ha]# kubectl exec -it ubuntu /bin/bash
  2. root@ubuntu:/# cat > /etc/apt/sources.list << EOF
  3. deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
  4. deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
  5. deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
  6. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
  7. deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
  8. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
  9. deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
  10. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
  11. deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
  12. deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
  13. EOF
  1. 成功后,原项目要求执行如下命令安装基本的软件环境:
  2. root@ubuntu:/# apt-get update
  3. root@ubuntu:/# apt-get install -y vim wget python2.7 python-pip redis-tools dnsutils

初始化集群
首先,我们需要安装redis-trib

  1. root@ubuntu:/# pip install redis-trib==0.5.1

然后,创建只有Master节点的集群:

  1. root@ubuntu:/# redis-trib.py create \
  2. `dig +short redis-app-0.redis-service.default.svc.cluster.local`:6379 \
  3. `dig +short redis-app-1.redis-service.default.svc.cluster.local`:6379 \
  4. `dig +short redis-app-2.redis-service.default.svc.cluster.local`:6379

其次,为每个Master添加Slave

  1. root@ubuntu:/# redis-trib.py replicate \
  2. --master-addr `dig +short redis-app-0.redis-service.default.svc.cluster.local`:6379 \
  3. --slave-addr `dig +short redis-app-3.redis-service.default.svc.cluster.local`:6379
  4. root@ubuntu:/# redis-trib.py replicate \
  5. --master-addr `dig +short redis-app-1.redis-service.default.svc.cluster.local`:6379 \
  6. --slave-addr `dig +short redis-app-4.redis-service.default.svc.cluster.local`:6379
  7. root@ubuntu:/# redis-trib.py replicate \
  8. --master-addr `dig +short redis-app-2.redis-service.default.svc.cluster.local`:6379 \
  9. --slave-addr `dig +short redis-app-5.redis-service.default.svc.cluster.local`:6379

至此,我们的Redis集群就真正创建完毕了,连到任意一个Redis Pod中检验一下:

  1. [root@k8s-master redis-ha]# kubectl exec -it ubuntu /bin/bash
  2. root@ubuntu:/# exec attach failed: error on attach stdin: read escape sequence
  3. command terminated with exit code 126
  4. [root@k8s-master redis-ha]# kubectl exec -it redis-app-5 /bin/bash
  5. root@redis-app-5:/data# /usr/local/bin/redis-cli -c
  6. 127.0.0.1:6379> CLUSTER NODES

控制器 - 图49

  1. 127.0.0.1:6379> CLUSTER info

控制器 - 图50

另外,还可以在NFS上查看Redis挂载的数据:

控制器 - 图51

控制器 - 图52

8.创建用于访问Service

前面我们创建了用于实现StatefulSet的Headless Service,但该Service没有Cluster Ip,因此不能用于外界访问。所以,我们还需要创建一个Service,专用于为Redis集群提供访问和负载均衡:

  1. [root@k8s-master redis-ha]# vim redis-access-service.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: redis-access-service
  6. labels:
  7. app: redis
  8. spec:
  9. ports:
  10. - name: redis-port
  11. protocol: "TCP"
  12. port: 6379
  13. targetPort: 6379
  14. selector:
  15. app: redis
  16. appCluster: redis-cluster
  17. [root@k8s-master redis-ha]# kubectl apply -f redis-access-service.yaml

如上,该Service名称为 redis-access-service,在K8S集群中暴露6379端口,并且会对labels nameapp: redisappCluster: redis-cluster的pod进行负载均衡。

创建后查看:

控制器 - 图53

9.测试主从切换

在K8S上搭建完好Redis集群后,我们最关心的就是其原有的高可用机制是否正常。这里,我们可以任意挑选一个Master的Pod来测试集群的主从切换机制,如redis-app-0

说明:一般前3个为主节点,后三个为从节点。不过还是进去确认一下好

控制器 - 图54

控制器 - 图55

控制器 - 图56

控制器 - 图57

控制器 - 图58

控制器 - 图59

如上可以看到,redis-app-0、redis-app-1、redis-app-2为master

redis-app-3、redis-app-4、redis-app-5为slave。

而且可以确定:redis-app-0的从节点的ip是10.244.2.21是redis-app3。

接着,我们手动删除redis-app-0主节点:

  1. [root@k8s-master redis-ha]# kubectl delete pod redis-app-0

我们再进入redis-app-0内部查看:

  1. [root@k8s-master redis-ha]# kubectl exec -it redis-app-0 /bin/bash
  2. root@redis-app-0:/data# /usr/local/bin/redis-cli -c

控制器 - 图60

我们再进入redis-app-3内部查看:

  1. [root@k8s-master redis-ha]# kubectl exec -it redis-app-3 /bin/bash
  2. root@redis-app-3:/data# /usr/local/bin/redis-cli -c

控制器 - 图61

控制器 - 图62

疑问拓展:

  1. 六、疑问
  2. 至此,大家可能会疑惑,那为什么没有使用稳定的标志,Redis Pod也能正常进行故障转移呢?这涉及了Redis本身的机制。因为,Redis集群中每个节点都有自己的NodeId(保存在自动生成的nodes.conf中),并且该NodeId不会随着IP的变化和变化,这其实也是一种固定的网络标志。也就是说,就算某个Redis Pod重启了,该Pod依然会加载保存的NodeId来维持自己的身份。我们可以在NFS上查看redis-app-1nodes.conf文件:
  3. [root@k8s-node2 ~]# cat /usr/local/k8s/redis/pv1/nodes.conf 96689f2018089173e528d3a71c4ef10af68ee462 192.168.169.209:6379@16379 slave d884c4971de9748f99b10d14678d864187a9e5d3 0 1526460952651 4 connected237d46046d9b75a6822f02523ab894928e2300e6 192.168.169.200:6379@16379 slave c15f378a604ee5b200f06cc23e9371cbc04f4559 0 1526460952651 1 connected
  4. c15f378a604ee5b200f06cc23e9371cbc04f4559 192.168.169.197:6379@16379 master - 0 1526460952651 1 connected 10923-16383d884c4971de9748f99b10d14678d864187a9e5d3 192.168.169.205:6379@16379 master - 0 1526460952651 4 connected 5462-10922c3b4ae23c80ffe31b7b34ef29dd6f8d73beaf85f 192.168.169.198:6379@16379 myself,slave c8a8f70b4c29333de6039c47b2f3453ed11fb5c2 0 1526460952565 3 connected
  5. c8a8f70b4c29333de6039c47b2f3453ed11fb5c2 192.168.169.201:6379@16379 master - 0 1526460952651 6 connected 0-5461vars currentEpoch 6 lastVoteEpoch 4
  6. 如上,第一列为NodeId,稳定不变;第二列为IP和端口信息,可能会改变。
  7. 这里,我们介绍NodeId的两种使用场景:
  8. 当某个Slave Pod断线重连后IP改变,但是Master发现其NodeId依旧, 就认为该Slave还是之前的Slave
  9. 当某个Master Pod下线后,集群在其Slave中选举重新的Master。待旧Master上线后,集群发现其NodeId依旧,会让旧Master变成新Masterslave
  10. 对于这两种场景,大家有兴趣的话还可以自行测试,注意要观察Redis的日志