云计算之k8s系列_第十三回

上一回留了一个问题:Deployment实际上并不足以覆盖所有的应用编排问题。原因在于Deployment对应用做了简单化假设。它认为:一个应用的所有Pod,是完全一样的,没有顺序,也无所谓运行在哪台机器上,需要的时候,Deployment就可以通过Pod模板创建新的Pod;不需要的时候,Deployment就可以删除任意一个Pod。

但是在实际环境中,并不是所有的应用都可以满足这样的要求。尤其是分布式应用,它的多个实例之间,往往有依赖关系,如:主从、主备等。还有就是数据存储应用,它的多个实例,往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据之间的对应关系也会丢失,从而导致应用失败。所以,这种实例之间不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”。

为了解决这种“有状态应用”,k8s项目扩展出了一个编排功能:StatefulSet。


StatefulSet把真实世界的应用状态,抽象为了两种情况:
  

     1.拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点A要先于从节点B启动。而如果你把A和B两个Pod删掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的Pod,必须和原来Pod的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新Pod。
     2.存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间Pod A被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

所以,StatefulSet的核心功能,就是通过某种方式记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态。

在了解原理之前,先看看什么是Headless Service。Service是k8s项目中用来将一组Pod暴露给外接访问的一种机制。比如,一个Deployment有3个Pod,那么我就可以定义一个Service。然后,用户只要能访问这个Service,它就能访问到某个具体的Pod。

那么Service又是如何被访问到的呢?
    1.是以Service的VIP的方式。比如:当我访问10.0.23.1这个Service的IP地址时,10.0.23.1其实就是一个VIP,它会把请求转发到该Service所代理的某一个Pod上。
    2.是以Service的DNS方式。比如:这时候,只要我访问my-svc.my-namespace.svc.cluster.local这条DNS记录,就可以访问到my-svc的Service所代理的某一个Pod。

而在第二种Service DNS的方式下,具体还可以分为两种处理方式:
    1.Normal Service。这种情况下,你访问my-svc.my-namespace.svc.cluster.local解析到的,正式my-svc这个Service的VIP,后面的流程就跟VIP方式一致了。
    2.Headless Service。这种情况下,你访问my-svc.my-namespace.svc.cluster.local解析到的,直接就是my-svc代理的某一个Pod的IP地址。可以看到,这里的区别在于,Headless Service不需要分配一个VIP,而是可以直接以DNS记录的方式解析出被代理Pod的IP地址。

那么这样的设计又有什么作用呢?下面看看 Headless Service 对应的YAML文件:

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,即:这个Service,没有一个VIP作为“头”。这也就是Headless的含义。所以,这个Service被创建后并不会被分配一个VIP,而是会以DNS记录的方式暴露出它所代理的Pod。而它所代理的Pod,采用Label Selector机制选择出来,所有携带了 app=nginx标签的Pod,都会被这个Service代理起来。

当你按照这样的方式创建出一个Headless Service之后,它所代理的所有Pod的IP地址,都会被绑定一个这样格式的DNS记录:

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

这个DNS记录,这是k8s项目Pod分配的唯一的可解析身份。有了这个身份,只要你知道了一个Pod的名字,以及它对应的Service名字,你就可以非常确定地通过这条DNS记录访问到Pod的IP地址。

那么,StatefulSet又是如何使用这个DNS记录来维持Pod的拓扑状态的呢?
现在,看一下StatefulSet的YAML文件:

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-deployemnt的唯一区别,就是多了一个serviceName=nginx字段。这个字段的作用,是告诉StatefulSet控制器,在执行控制循环的时候,请使用nginx这个Headless Service来保证Pod的可解析身份。

所以,当你通过kubectl create创建了上面这个Service和StatefulSet之后,就会看到如下两个对象:

$ 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 的-w参数,实时查看StatefulSet创建两个有状态实例的过程:

$ kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         20s

通过上面这个Pod的创建过程,我们能够看到,StatefulSet给它所管理的所有Pod的名字,进行了编号,规则是:Pod名-编译次数。 而这些编号都是从0开始累加,与StatefulSet的每个Pod实例一一对应,绝不重复。更重要的是,这些Pod的创建,也是严格按照编号顺序进行的。比如,在web-0进入到Running状态、并且成为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的hostname与Pod名字是一致的,都被分配了对应的编号。接下来,我们再试着以DNS的方式,访问一下这个Headless Service:

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh 

通过这条命令,在这个Pod容器里面,我们尝试用nslookup命令,解析一下Pod对应的Headless Service:

$ 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的时候,最后解析到的,正是web-0这个Pod的IP地址;而当访问web-1.nginx的时候,解析到的则是web-1的IP地址。这时候,如果你在另外一个Terminal里把这两个“有状态应用”的Pod删掉:

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


然后,再在当前Terminal里Watch一下这两个Pod状态变化,就会发现:

$ kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         32s

可以看到,当我们把这两个Pod删除之后,k8s会按照原先编号的顺序,创建出了两个新的Pod。并且,k8s依然为它们分配了与原来相同的“网络身份”:web-0.nginx和web-1.nginx。通过这种严格的对应规则,StatefulSet就保证Pod网络标识的稳定性。

比如,如果web-0是一个需要先启动的主节点,web-1是一个后启动的从节点,那么只要这个StatefulSet不被删除,你访问web-0.nginx始终都会落在主节点上,访问web-1.nginx时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。

所以,如果我们再用nslookup命令,查看一下这个新Pod对应的Headless Service的话:

$ 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

可以看出,在这个StatefulSet中,这两个新Pod的“网络标识”(比如:web-0.nginx和web-1.nginx),再次解析到了正确的IP地址(比如:web-0 Pod的IP地址10.244.1.8)。

通过这种方法,k8s就成功地将Pod的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照Pod的“名字+编号”的方式固定了下来。此外,k8s还为每一个Pod提供了一个固定并且唯一的访问入口,即:这个Pod对应的DNS记录。

这些状态,在StatefulSet整个生命周期里都会保持不变,绝不会因为对应Pod的删除或者新建而失效。

不过,相信你也已经注意到了,尽管web-0.nginx这条记录不会变,但它解析到的Pod的IP地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用DNS记录或者hostname的方式,而绝不应该直接访问这些Pod的IP地址。

总结:
首先,我们了解了StatefulSet的基本概念,解释了什么是应用的状态。接着看了StatefulSet如何保证应用实例之间的“拓扑状态”。
StatefulSet这个控制器的主要作用之一:就是使用Pod模板创建Pod的时候,对他们进行编号,并且按照编号顺序逐一完成创建工作。而当StatefulSet的控制循环发现Pod的实际状态与期望状态不一致,需要新建或者删除Pod进行调整,它会严格按照这些Pod编号的顺序,注意完成这些操作。总得来说,StatefulSet就是对Deployment的改良。与此同时,通过Headless Service的方式,StatefulSet为每个Pod创建了一个固定并且稳定的DNS记录,来作为它的访问入口。

实际上,在部署有状态应用的时候,应用的每个实例拥有唯一并且稳定的“网络标识”,是一个非常重要的假设。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值