理解有状态应用StatefulSet,Kubernetes概念

这篇文章主要介绍StatefulSet控制器的设计原理和有状态应用的具体实践。

一. StatefulSet的设计原理

首先我们先来了解下Kubernetes的一个概念:有状态服务与无状态服务。

  • 无状态服务(Stateless Service):该服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的。这种方式适用于服务间相互没有依赖关系,如Web应用,在Deployment控制器停止掉其中的一个Pod不会对其他Pod造成影响。

  • 有状态服务(Stateful Service):服务运行的实例需要在本地存储持久化数据,比如数据库或者多个实例之间有依赖拓扑关系,比如:主从关系、主备关系。如果停止掉依赖中的一个Pod,就会导致数据丢失或者集群崩溃。这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。

其中无状态服务在我们前面文章中使用的Deployment编排对象已经可以满足,因为无状态的应用不需要很多要求,只要保持服务正常运行就可以,Deployment删除掉任意中的Pod也不会影响服务的正常,但面对相对复杂的应用,比如有依赖关系或者需要存储数据,Deployment就无法满足条件了,Kubernetes项目也提供了另一个编排对象StatefulSet。

StatefulSet将有状态应用抽象为两种情况:

  1. 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。

  2. 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。它包含Deployment控制器ReplicaSet的所有功能,增加可以处理Pod的启动顺序,为保留每个Pod的状态设置唯一标识,同时具有以下功能:

  • 稳定的、唯一的网络标识符
  • 稳定的、持久化的存储
  • 有序的、优雅的部署和缩放

二. 有状态服务的拓扑状态

1. 唯一的网络标识

在上面我们提到有状态应用大致抽象为拓扑状态和存储状态,拓扑状态是为应用多实例中有相互依赖的服务而实现的。首先我们先来了解StatefulSet如何保证唯一的网络标识,这就需要涉及到Kubernetes 项目中非常实用的概念:Headless Service

Service资源对象我们在k8s(一) 基本概念与组件原理已经提及到它是k8s项目中用来将一组 Pod 暴露给外界访问的一种机制。Service也分为两种方式分别是:

  • 第一种方式,是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上,相当于前面的负载均衡代理。
  • 第二种方式,就是以 Service 的 DNS 方式。比如:这时候,只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。

而第二种以DNS的方式又分为两种处理方法:

  • Normal Service:正常使用,访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,然后将请求通过VIP地址转发到代理的Pod。
  • Headless Service:无头服务,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

定义Headless Service 资源文件

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx 

Headless Service 在配置中和普通的Service的yaml文件定义基本相同,只是修改了clusterIP 字段为None,这样Headless Service 就不需要分配VIP地址,可以直接通过DNS的方式直接访问到Pod的IP地址。创建Headless Service 后它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录:

<pod-name>.<svc-name>.<namespace>.svc.cluster.local

这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,就可以通过这条 DNS 记录访问到 Pod 的 IP 地址。

定义StatefulSet资源文件

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web spec: serviceName: "nginx" replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.9.1 ports: - containerPort: 80 name: web 

上面这个YAML文件比我们之前部署的nginx-deployment增加了ServiceName:nginx的字段定义,即:告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。

下面我们创建这些资源:

$ kubectl create -f svc.yaml
$ kubectl get service nginx
NAME      TYPE         CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP None <none> 80/TCP 10s $ kubectl create -f statefulset.yaml $ kubectl get statefulset web NAME DESIRED CURRENT AGE web 2 1 19s $ kubectl get pods -l app=nginx NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 12m web-1 1/1 Running 0 10m 

可以看到,Pod的名字后面都以数字顺序为后缀,这个是因为StatefulSet 对它所管理 Pod 名字进行了编号,编号规则是:-,编号是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,不会重复。这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。

当这两个 Pod 都进入了 Running 状态之后,就可以查看到它们各自唯一的“网络身份”了。我们使用kubectl exec命令分别进入到容器中查看它们的 hostname。

$ kubectl exec web-0 -- sh -c 'hostname' 
web-0
$ kubectl exec web-1 -- sh -c 'hostname' 
web-1

然后我们启动一个一次性的pod来验证是否可以通过DNS解析到Pod的IP地址

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh $ nslookup web-0.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 10.244.1.7 $ nslookup web-1.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 10.244.2.7 

可以看到我们解析web-0.nginx这个DNS已经正确的返回了Pod的IP地址,现在我们再打开一个终端来删除这两个Pod,看看会发生什么样的变化?

$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted 

查看Statefulset重新创建的Pod是否可以正常解析?

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh $ nslookup web-0.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 10.244.1.8 $ nslookup web-1.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 10.244.2.8 

可以发现重新创建的pod也是按照原先的顺序来创建的,虽然Pod的IP地址有变化,但是用之前的DNS解析还是没有变化的,保持了原先的唯一网络标识,StatefulSet 也就保证了 Pod 网络标识的稳定性。至此Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。

三. 有状态服务的存储状态

下面我们来继续探究StatefulSet对存储状态的管理机制,在前面我们创建Pod需要使用存储的时候,只需要在资源文件中添加spec.volumes字段声明使用volume就可以,比如设置为hostpath或者emptyDir 。但实际环境中开发人员并不清楚我们那些Volume可以使用,所以存储我们就需要使用Kubernetes的另一个资源对象PVC(Persistent Volume Claim)的功能。

  • PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户存储的一种声明,PVC 和 Pod 比较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接声明使用 PVC 即可,不会暴露后端存储的信息。
  • PV 的全称是:PersistentVolume(持久化卷),是对底层的共享存储的一种抽象,PV 由运维人员进行创建和配置,它和具体的底层的共享存储技术的实现方式有关,比如 Ceph、GlusterFS、NFS 等,都是通过插件机制完成与共享存储的对接。

PVC消耗的是PV资源,PV消耗的是后端共享存储。当我们创建一个PVC时,kubernetes 就会自动为PVC绑定一个符合条件的 PV,这里我们PV我们后续会在持久化存储文章中详细讲解,在这个例子中不做过多描述,这里我们已经提前创建完成。

继续以上面拓扑状态的应用为例,我们在资源文件中声明使用PVC:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteOnce  # Volume 的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。
      resources:
        requests:
          storage: 1Gi  #Volume 大小至少是 1 GiB。

我们为这个 StatefulSet 额外添加了一个volumeClaimTemplates字段。它跟 Deployment 里 Pod 模板(PodTemplate)的作用类似。也就是说,凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates这个模板字段。更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。其中这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。

我们来创建这个资源对象:

$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s 

可以看到PVC的命名方式为<PVC名字>-<StatefulSet名字>-<Pod编号>,PVC的状态已经显示为Bound,说明已经绑定到符合条件的PV,现在我们测试验证下数据是否会丢失 ?

分别往Pod的Volume目录中写入内容为hostname的index.html文件,然后分别访问pod的Nginx进程,查看返回信息是否正确

$ for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
$ for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done
hello web-0
hello web-1

然后我们手动删除这两个Pod

$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted 

被删除之后,这两个 Pod 会被按照编号的顺序被重新创建出来。然后我们在新创建的容器里通过访问“http://localhost”的方式去访问 web-0 里的 Nginx 服务:

# 在被重新创建出来的Pod容器里访问http://localhost
$ kubectl exec -it web-0 -- curl localhost
hello web-0

可以发现请求依然会返回:“hello web-0”,也就是说,原先与:“web-0” 的 Pod 绑定的 PV,在这个 Pod 被重新创建之后,依然和新的“ web-0 ” Pod 绑定在了一起。对于 Pod web-1 来说,也是完全一样的情况。

这是因为我们删除pod后,之前绑定的PV和PVC并不会删除,数据仍然保存在后端的远程存储如Ceph中,StatefulSet发现Pod消失后,会自动创建一个新的Pod,名字编号也是和之前相同,而这个新的Pod声明使用的还是之前的PVC名字,所以在这个新的 Pod 创建出来之后,Kubernetes 会为它查找之前绑定的PVC ,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。这样新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。

总结:

  1. StatefulSet 的控制器直接管理的是 Pod。通过在 Pod 的名字里加上事先约定好的编号,保证应用拓扑状态的服务稳定。
  2. Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录,生成唯一的网络标识。
  3. StatefulSet 为每一个 Pod 分配并创建一个同样编号的 PVC。保证了每一个 Pod 都拥有一个独立的 Volume,保证数据不会丢失。

猜你喜欢

转载自www.cnblogs.com/ynog/p/12771306.html