zoukankan      html  css  js  c++  java
  • Kubernetes之deployment

      Kubernetes实现了零停机的升级过程。升级操作可以通过使用ReplicationController或者ReplicaSet实现,但是Kubernetes提供了另一种基于ReplicaSet的资源Deployment,并支持声明的更新应用程序。

     

    1.更新运行pod内的应用程序

      从一个简单的例子开始。有一组pod实例为其他pod或外部客户端提供服务。这些pod是由ReplicationController或ReplicaSet来创建和管理的。客户端(运行在其他pod或外部的客户端程序)通过该Service访问pod。这就是Kubernetes中一个典型应用程序的运行方式(如图9.1 所示)。

      假设pod—开始使用v1版本的镜像运行第一个版本的应用。然后开发了一个新版本的应用打包成镜像,并将其推送到镜像仓库,标记为v2,接下来想用这个新版本替换所有的pod。由于pod在创建之后,不允许直接修改镜像,只能通过删除 原有pod并使用新的镜像创建新的pod替换。

      有以下两种方法可以更新所有pod:

      • 直接删除所有现有的pod,然后创建新的pod。
      • 也可以先创建新的pod,并等待它们成功运行之后,再删除旧的pod。可以先创建所有新的pod,然后一次性删除所有旧的pod,或者按顺序创建新的pod,然后逐渐删除旧的pod。

      这两种方法各有优缺点。第一种方法将会导致应用程序在一定的时间内不可用。 使用第二种方法,你的应用程序需要支持两个版本同时对外提供服务。如果你的应用程序使用数据库存储数据,那么新版本不应该对原有的数据格式或者数据本身进行修改,从而导致之前的版本运行异常。

      如何在Kubernetes中使用上述的两种更新方法呢?首先,来看看如何进行手动操作;一旦了解了手动流程中涉及的要点之后,将继续讨论如何让Kubernetes自动执行更新操作。

     

    1.1 删除旧版本pod,使用新版本pod替换

      ReplicationController会将原有pod实例替换成新版本的pod 并且ReplicationController内的pod模板一旦发生了更新之后,ReplicationController会使用更新后的pod模板来创建新的实例。

      如果有一个ReplicationController来管理一组v1版本的pod,可以直接通过将pod模板修改为v2版本的镜像,然后删除旧的pod实例。ReplicationController会检测到当前没有pod匹配它的标签选择器,便会创建新的实例。整个过程如图9.2所示。

      如果可以接受从删除旧的pod到启动新pod之间短暂的服务不可用,那这将是更新一组pod的最简单方式。

     

    1.2 先创建新pod在删除旧版本pod

      如果短暂的服务不可用完全不能接受,并且应用程序支持多个版本同时对外服务,那么可以先创建新的pod再删除原有的pod。这会需要更多的硬件资源,因为将在短时间内同时运行两倍数量的pod。

      与前面的方法相比较,这个方法稍微复杂一点,可以结合ReplicationController和Service的概念进行使用。

      从旧版本立即切换到新版本

      pod通常通过Service来暴露。在运行新版本的pod之前,Service只将流量切换到初始版本的pod。一旦新版本的pod被创建并且正常运行之后,就可以修改服务的标签选择器并将Service的流量切换到新的pod。这就是所谓的蓝绿部署。在切换之后,一旦确定了新版本的功能运行正常,就可以通过删除旧的ReplicationController来删除旧版本的pod。

      注意:可以使用kubectl set selector命令来修改Service的pod选择器。

      执行滚动升级操作

      还可以执行滚动升级操作来逐步替代原有的pod,而不是同时创建所有新的pod并一并删除所有旧的pod。可以通过逐步对旧版本的ReplicationController进行缩容并对新版本的进行扩容,来实现上述操作。在这个过程中,希望服务的pod选择器同时包含新旧两个版本的pod,因此它将请求切换到这两组pod,如图9.4所示。

      手动执行滚动升级操作非常烦琐,而且容易出错。根据副本数量的不同,需要以正确的顺序运行十几条甚至更多的命令来执行整个升级过程。但是实际上Kubernetes可以实现仅仅通过一个命令来执行滚动升级。在下一节会介绍如何执行这个操作。

    2.使用ReplicationController实现自动的滚动升级

      不用手动地创建Replicationcontroller来执行滚动升级,可以直接使用kubectl来执行。使用kubectl执行升级会使整个升级过程更容易。虽然这是一种相对过时的升级方式,但是还是会先介绍一下,因为它是第一种实现自动滚动升级的方式,并且允许在不引入太多额外概念的情况下了解整个过程。

    2.1 运行第一个版本的应用

      在更新一个应用之前,需要先部署好一个应用。使用一个NodeJS应用作为基础,并稍微修改版本作为应用的初始版本。它就是一个简单的web应用程序,在HTTP响应中返回pod的主机名。

      创建v1版本的应用

      你将修改这个应用,让它在响应中返回版本号,以便区分应用构建的不同版本。 原作者已经构建并将应用镜像推送到DockerHub的luksa/kubia:v1中。下面的代码清单就是应用的源代码。

    #代码9.1 v1版本的应用:v1/app.js
    const http = require('http');
    const os = require('os');
     
    console.log("Kubia server starting...");
     
    var handler = function(request, response) {
      console.log("Received request from " + request.connection.remoteAddress);
      response.writeHead(200);
      response.end("This is v1 running in pod " + os.hostname() + "
    ");
    };
     
    var www = http.createServer(handler);
    www.listen(8080);
    #docker
    FROM node:7
    ADD app.js /app.js
    ENTRYPOINT ["node", "app.js"]
     

      使用单个YAML文件运行应用并通过Service暴露

      通过创建一个ReplicationController来运行应用程序,并创建LoadBalancer服务将应用程序对外暴露。接下来,不是分别创建这两个资源,而是为它们创建一个YAML文件,并使用一个kubectl create 命令调用 KubernetesAPI。YAML manifest可以使用包含三个横杠(---)的行来分隔多个对象,如下面的代码清单所示。

    #单个YAML文件同时包含一个RC和一个Service:kubia-rc-and-service-v1.yaml
    apiVersion: v1
    kind: ReplicationController
    metadata:
      name: kubia-v1
    spec:
      replicas: 3
      template:
        metadata:
          name: kubia
          labels:
            app: kubia
        spec:
          containers:
          - image: luksa/kubia:v1          #使用ReplicationController来创建pod并运行镜像
            name: nodejs
    ---                                      #YAML文件可以包含多种资源定义,并通过三个横杠(---)来分行
    apiVersion: v1
    kind: Service
    metadata:
      name: kubia
    spec:
      type: LoadBalancer
      selector:
        app: kubia
      ports:
      - port: 80
        targetPort: 8080

      这个YAML定义了一个名为kubia-v1的ReplicationController和一个名为kubia的Service。将此YAML发布到Kubernetes之后,三个v1 pod和负载均衡器都会开始工作。如下面的代码清单所示,可以通过查找服务的外部IP并使用curl命令来访问服务。

    #代码9.3 查找到Service IP并使用curl循环调用接口服务
    $ kubectl get svc kubia
    NAME      CLUSTER-IP     EXTERNAL-IP       PORT(S)         AGE
    kubia     10.3.246.195   130.211.109.222   80:32143/TCP    5m
    $ while true; do curl http://130.211.109.222; done
    This is v1 running in pod kubia-v1-qr192
    This is v1 running in pod kubia-v1-kbtsk
    This is v1 running in pod kubia-v1-qr192
    This is v1 running in pod kubia-v1-2321o
    ...

    2.2 使用kubectl来执行滚动升级

      接下来创建V2版本的应用,修改之前的应用程序,使得其请求返回“This is V2”:

    response.end("This is v2 running in pod " + os.hostname() + "
    ");

      这个新的镜像己经推到了DockerHub的luksa/kubia:v2中,也可以用v1版本的DockerFile自己构建。

      保持curl循环运行的状态下打开另一个终端,开始启动滚动升级。运行kubectl rolling-update命令来执行升级操作。指定需要替换的ReplicationController,以及为新的ReplicationController指定一个名称,并指定想要替换的新镜像。下面的代码清单显示了滚动升级的完整命令。

    #代码9.4 使用 kubectl 开始ReplicationController的滚动升级
    $ kubectl rolling-update kubia-v1 kubia-v2 --image=luksa/kubia:v2
    Created kubia-v2
    Scaling up kubia-v2 from 0 to 3, scaling down kubia-v1 from 3 to 0 (keep 3
         pods available, don't exceed 4 pods)
    ...

      使用kubiav2版本应用来替换运行着kubia-v1的ReplicationController,将新的复制控制器命名为kubia-v2,并使用luksa/kubia:v2作为容器镜像。

      当运行该命令时,一个名为kubia-v2的新ReplicationController会立即被创建。此时系统的状态如图9.5所示。

      新的ReplicationController的pod模板引用了luksa/kubia:v2镜像,并且其初始期望的副本数被设置为0,如下面的代码清单所示。

    #代码9.5 滚动升级过程中创建的新的ReplicationController描述
    $ kubectl describe rc kubia-v2
    Name:       kubia-v2
    Namespace:  default
    Image(s):   luksa/kubia:v2                                           
    Selector:   app=kubia,deployment=757d16a0f02f6a5c387f2b5edb62b155
    Labels:     app=kubia
    Replicas:   0 current / 0 desired                                    
    ...

      了解滚动升级前kubectl所执行的操作

      kubectl通过复制kubia-v1的ReplicationController并在其pod模板中改变镜像版本。如果仔细观察控制器的标签选择器,会发现它也被做了修改。它不仅包含一个简单的app=kubia标签,而且还包含一个额外的deployment标签,为了由这个ReplicationController管理,pod必须具备这个标签。

      尽可能避免使用新的和旧的ReplicadonController来管理同一组pod。但是即使新创建的pod添加了除额外的deployment标签以外,还有app=kubia标签,这是否也意味着它们会被第一个ReplicationController的选择器选中?因为它的标签内也含有app=kubia。

      其实在滚动升级过程中,第一个ReplicationController的选择器也会被修改:

    $ kubectl describe rc kubia-v1
    Name:       kubia-v1
    Namespace:  default
    Image(s):   luksa/kubia:v1
    Selector:   app=kubia,deployment=3ddd307978b502a5b975ed4045ae4964-orig

      但这是不是意味着第一个控制器现在选择不到pod呢?因为之前由它创建的三个pod只包含app=kubia标签。事实并不是这样,因为在修改 ReplicationController的选择器之前,kubectl修改了当前pod的标签:

    $ kubectl get po --show-labels
    NAME            READY  STATUS   RESTARTS  AGE  LABELS
    kubia-v1-m33mv  1/1    Running  0         2m   app=kubia,deployment=3ddd...
    kubia-v1-nmzw9  1/1    Running  0         2m   app=kubia,deployment=3ddd...
    kubia-v1-cdtey  1/1    Running  0         2m   app=kubia,deployment=3ddd...

      如果觉得上述内容描述得太过复杂,看图9.6,其中显示了pod、标签、两个Replicationcontroller,以及它们的pod标签选择器。

      kubectl在开始伸缩服务前,都会这么做。设想手动执行滚动升级,并且在升级过程中出现了问题,这可能使ReplicationController删除所有正在为生产级别的用户提供服务的pod!

      通过伸缩两个ReplicationController将旧pod替换成新的pod

      设置完所有这些后,kubectl开始替换pod。首先将新的Controller扩展为1, 新的Controller因此创建第一个v2pod,kubectl将旧的ReplicationController缩小1.以下两行代码是kubectl输出的:

    Scaling kubia-v2 up to 1 
    Scaling kubia-v1 down to 2

      由于Service针对的只是所有带有app=kubia标签的pod,所以应该可以看到一一每进行几次循环访问,就将curl请求的流量切换到新的v2 pod:

    This is v2 running in pod kubia-v2-nmzw9   
    This is v1 running in pod kubia-v1-kbtsk  
    This is v1 running in pod kubia-v1-2321o   
    This is v2 running in pod kubia-v2-nmzw9   

      随着kubectl继续滚动升级,开始看到越来越多的请求被切换到v2Pod。因为在升级过程中,v1 pod不断被删除,并被替换为运行新镜像的pod。最终,最初的ReplicationController被伸缩到0,导致最后一个v1 pod被删除,也意味着服务现在只通过v2 pod提供。此时,kubectl将删除原始的ReplicationController完成升级过程,如下面的代码清单所示。

    #代码 9.6 kubectl执行滚动升级的最终步骤
    Scaling kubia-v2 up to 2
    Scaling kubia-v1 down to 1
    Scaling kubia-v2 up to 3
    Scaling kubia-v1 down to 0
    Update succeeded. Deleting kubia-v1
    replicationcontroller "kubia-v1" rolling updated to "kubia-v2"

      现在只剩下kubia-v2的ReplicationController和三个v2 pod。在整个升级过程中,每次发出的请求都有相应的响应,通过一次滚动升级,服务一直保持可用状态。

     

    2.3 为什么 kubectl rolling-update 已经过时

      在本节的开头,提到了一种比通过kubectl rolling-update更好的方式进行升级。那这个过程有什么不合理的地方呢?

      首先,这个过程会直接修改创建的对象。直接更新pod和ReplicationController的标签并不符合之前创建时的预期。还有更重要的一点是,kubectl只是执行滚动升级过程中所有这些步骤的客户端。

      当触发滚动更新时,可以使用--V选项打开详细的日志并能看到这一点:

    $ kubectl rolling-update kubia-v1 kubia-v2 --image=luksa/kubia:v2 --v 6

      提示:使用--v 6选项会提高日志级别使得所有kubectl发起的到API服务器的请求都会被输出。

      使用这个选项,kubectl会输出所有发送至Kubernetes API服务器的HTTP请求。会看到一个PUT请求:

    /api/v1/namespaces/default/replicationcontrollers/kubia-v1

      它是表示kubia-v1 ReplicationController资源的RESTful URL。这些请求减少 了ReplicationController的副本数,这表明伸缩的请求是由kubectl客户端执行的,而不是由Kubernetes master执行的。

      提示:使用详细日志模式运行其他kubectl命令,将会看到kubectl和API服务器之前的更多通信细节。

      但是为什么由客户端执行升级过程,而不是服务端执行是不好的呢?在上述的例子中,升级过程看起来很顺利,但是如果在kubectl执行升级时失去了网络连接,升级进程将会中断。pod和ReplicationController最终会处于中间状态。

      这样的升级不符合预期的原因还有一个。强调过Kubernetes是如何通过不断地收敛达到期望的系统状态的。这就是pod的部署方式,以及pod的伸缩方式。直接使用期望副本数来伸缩pod而不是通过手动地删除一个pod或者增加一个pod。

      同样,只需要在pod定义中更改所期望的镜像tag,并让Kubernetes用运行新镜像的pod替换旧的pod。正是这一点推动了一种称为Deployment的新资源的引入,这种资源正是现在Kubernetes中部署应用程序的首选方式。

     

    3.使用Deployment声明式地升级应用

      Deployment是一种更高阶资源,用于部署应用程序并以声明的方式升级应用,而不是通过ReplicationController或ReplicaSet进行部署,它们都被认为是更底层的概念。

      当创建一个Deployment时,ReplicaSet资源也会随之创建(最终会有更多的资源被创建)。ReplicaSet是新一代的ReplicationController,并推荐使用它替代ReplicationController来复制和管理pod。在使用Deployment时,实际的pod 是由Deployment的Replicaset创建和管理的,而不是由Deployment直接创建和管理的(如图9.8所示)。

      你可能想知道为什么要在ReplicationController或ReplicaSet上引入另一个对象来使整个过程变得更复杂,因为它们己经足够保证一组pod的实例正常运行了。如上面滚动升级示例所示,在升级应用程序时,需要引入一个额外的ReplicationController,并协调两个Controller,使它们再根据彼此不断修改,而不会造成干扰。所以需要另一个资源用来协调。Deployment资源就是用来负责处理这个问题的(不是Deployment资源本身,而是在Kubernetes控制层上运行的控制器进程)。

      使用Deployment可以更容易地更新应用程序,因为可以直接定义单个Deployment资源所需达到的状态,并让Kubernetes处理中间的状态,接下来将会介绍整个过程。

     

    3.1 创建一个Deployment

      创建Deployment与创建ReplicationController并没有任何区别。Deployment也是由标签选择器、期望副数和pod模板组成的。此外,它还包含另一个字段,指定一个部署策略,该策略定义在修改Deployment资源时应该如何执行更新。

      创建一个Deployment Manifest

      看一下如何使用本章前面的kubia-v1 ReplicationController示例,并对其稍作修改,使其描述一个Deployment,而不是一个ReplicationController。这只需要三个简单的更改,下面的代码清单显示了修改后的YAML。

    #代码 9.7 Deployment 定义:kubia-deployment-v1.yaml
    apiVersion: apps/v1beta1                  #Deployment属于apps API组,版本为V1beta1
    kind: Deployment                              #需要将原有kind从ReplicationController修改为Deployment
    metadata:                                    
      name: kubia                                     #Deployment的名称中不再需要包含版本号
    spec:
      replicas: 3
      template:
        metadata:
          name: kubia
          labels:
            app: kubia
        spec:
          containers:
          - image: luksa/kubia:v1
            name: nodejs

      因为之前的ReplicationController只维护和管理了一个特定版本的pod,并需要命名为kubia-v1。另一方面,一个Deployment资源高于版本本身。Deployment可以同时管理多个版本的pod,所以在命名时不需要指定应用的版本号。

      创建Deployment资源

      在创建这个Deployment之前,请确保删除仍在运行的任何ReplicationController和pod,但是暂时保留kubia Service。可以使用--all选项来删除所有的ReplicationController:

    $ kubectl delete rc --all

      此时己经可以创建一个Deployment了:

    $ kubectl create -f kubia-deployment-v1.yaml --record
    deployment kubia" created

      注意:确保在创建时使用了--record选项。这个选项会记录历史版本号,在之后的操作中非常有用。

      展示Deployment滚动过程中的状态

      可以直接使用kubectl get deployment和kubectl describe deployment命令来查看Deployment的详细信息,但是还有另外一个命令,专门用于查看部署状态:

    $ kubectl rollout status deployment kubia
    deployment kubia successfully rolled out

      通过上述命令查看到,Deployment己经完成了滚动升级,可以看到三个pod副本己经正常创建和运行了:

    $ kubectl get po
    NAME                     READY     STATUS    RESTARTS   AGE
    kubia-1506449474-otnnh   1/1       Running   0          14s
    kubia-1506449474-vmn7s   1/1       Running   0          14s
    kubia-1506449474-xis6m   1/1       Running   0          14s

      了解Deployment如何创建Replicaset以及pod

      注意这些pod的命名,之前当使用ReplicationController创建pod时,它们的名称是由Controller的名称加上一个运行时生成的随机字符串(例如kubia-v1-m33mv)组成的。现在由Deployment创建的三个pod名称中均包含一个额外的数字。那是什么呢?

      这个数字实际上对应Deployment和ReplicaSet中的pod模板的哈希值。如前所述,Deployment不能直接管理pod。相反,它创建了ReplicaSet来管理pod。所以看看Deployment创建的ReplicaSet是什么样子的。

    $ kubectl get replicasets
    NAME               DESIRED   CURRENT   AGE
    kubia-1506449474   3         3         10s

      ReplicaSet的名称中也包含了其pod模板的哈希值。之后的篇幅也会介绍, Deployment会创建多个ReplicaSet,用来对应和管理一个版本的pod模板。像这样使用pod模板的哈希值,可以让Deployment始终对给定版本的pod模板创建相同的(或使用己有的)ReplicaSet。

      通过Service访问pod

      ReplicaSet创建了二个副本并成功运行以后,因为新的pod的标签和Service的标签选择器相匹配,因此可以直接通过之前创建的Service来访问它们。

      至此可能还没有从根本上解释,为什么更推荐使用Deployment而不是直接使用ReplicationController。另一方面看,创建一个Deployment的难度和成本也并没有比ReplicationController更高。后面将针对这个Deployment做一些操作,并从根本上了解Deployment的优点和强大之处。接下来会介绍如何通过Deployment资源升级应用,并对比通过ReplicationController升级应用的区别,你就会明白这一点。

     

    3.2 升级Deployment

      前面提到,当使用ReplicationController部署应用时,必须通过运行ku-bectl rolling-update显式地告诉Kubernetes来执行更新,甚至必须为新的ReplicationController指定名称来替换旧的资源。Kubernetes会将所有原来的pod替换为新的pod,并在结束后删除原有的ReplicationController。在整个过程中必须保持终端处于打幵状态,让kubectl完成滚动升级。

      接下来更新Deployment的方式和上述的流程相比,只需修改Deployment资源中定义的pod模板,Kubernetes会自动将实际的系统状态收敛为资源中定义的状态。类似于将ReplicationController或ReplicaSet扩容或者缩容,升级需要做的就是在部署的pod模板中修改镜像的tag, Kubernetes会收敛系统,匹配期望的状态。

      不同的Deployment升级策略

      实际上,如何达到新的系统状态的过程是由Deployment的升级策略决定的, 默认策略是执行滚动更新(策略名为RollingUpdate)。另一种策略为Recreate, 它会一次性删除所有旧版本的pod, 然后创建新的pod, 整个行为类似于修改ReplicationController的pod模板,然后删除所有的pod。

      Recreate策略在删除旧的pod之后才幵始创建新的pod。如果你的应用程序不支持多个版本同时对外提供服务,需要在启动新版本之前完全停用旧版本,那么需要使用这种策略。但是使用这种策略的话,会导致应用程序出现短暂的不可用。

      RollingUpdate策略会渐进地删除旧的pod,与此同时创建新的pod,使应用程序在整个升级过程中都处于可用状态,并确保其处理请求的能力没有因为升级而有所影响。这就是Deployment默认使用的升级策略。升级过程中pod数量可以在期望副本数的一定区间内浮动,并且其上限和下限是可配置的。如果应用能够支持多个版本同时对外提供服务,则推荐使用这个策略来升级应用。

      演示如何减慢滚动升级速度

      在接下来的练习中,将使用Rollingupate策略,但是需要略微减慢滚动升级的速度,以便观察升级过程确实是以滚动的方式执行的。可以通过在Deployment上设置minReatySconds属性来实现。将在本章末尾解释这个属性的作用。现在,使用kubectl pateh命令将其设置为10秒。

    $ kubectl patch deployment kubia -p '{"spec": {"minReadySeconds": 10}}'
    "kubia" patched

      提示:kubectl patch对于修改单个或者少量资源属性非常有用,不需要再通过编辑器编辑。

      使用patch命令更改Deployment的自有属性,并不会导致pod的任何更新, 因为pod模板并没有被修改。更改其他Deployment的属性,比如所需的副本数或部署策略,也不会触发滚动升级,现有运行中的pod也不会受其影响。

      触发滚动升级

      如果想要跟踪更新过程中应用的运行状况,需要先在另一个终端中再次运行curl循环,以查看请求的返回情况(需要将IP替换为Service实际暴露IP)

    $ while true; do curl http://130.211.109.222; done

      要触发滚动升级,需要将pod镜像修改为luksa/kubia:v2。和直接编辑Deployment资源的YAML文件或使用patch命令更改镜像有所不同,将使用kubectl set image命令来更改任何包含容器资源的镜像(ReplicationController、 ReplicaSet、Deployment等)。将使用这个命令来修改Deployment:

    $ kubectl set image deployment kubia nodejs=luksa/kubia:v2
    deployment "kubia" image updated

      当执行完这个命令,kubia Deployment的pod模板内的镜像会被更改为luksa/kubia:v2(从:v1更改而来)。如9.9所示。

      如果循环执行了curl命令,将看到一开始请求只是切换到v1 pod;然后越来越多的请求切换到v2 pod中,最后所有的v1 pod都被删除,请求全部切换到v2 pod。这和kubectl的滚动更新过程非常相似。

      Deployment的优点

      回顾一下刚才的过程,通过更改Deployment资源中的pod模板,应用程序己经被升级为一个更新的版本——仅仅通过更改一个字段而己!

      这个升级过程是由运行在Kubernetes上的一个控制器处理和完成的,而不再是运行kubectl rolling-update命令,它的升级是由kubectl客户端执行的。让Kubernetes的控制器接管使得整个升级过程变得更加简单可靠。

      注意:如果Deployment中的pod模板引用了一个ConfigMap(或Secret),那么更改ConfigMap资源本身将不会触发升级操作。如果真的需要修改应用程序的配置并想触发更新的话,可以通过创建一个新的ConfigMap并修改pod模板引用新的ConfigMap。

      Deployment背后完成的整个升级过程和执行kubectl rolling-update命令非常相似。一个新的ReplicaSet会被创建然后慢慢扩容,同时之前版本的Replicaset会慢慢缩容至0(初始状态和最终状态如图9.10所示)

      可以通过下面的命令列出所有新旧ReplicaSet

    $ kubectl get rs
    NAME               DESIRED   CURRENT   AGE
    kubia-1506449474   0         0         24m
    kubia-1581357123   3         3         23m

      与ReplicationController类似,所有新的pod现在都由新的ReplicaSet管理。与 以前不同的是,旧的ReplicaSet仍然会被保留,而旧的ReplicationController会在滚动升级过程结束后被删除。之后马上会介绍这个被保留的旧ReplicaSet的用处。

      因为并没有直接创建ReplicaSet,所以这里的ReplicaSet本身并不需要用户去关心和维护。所有操作都是在Deployment资源上完成的,底层的ReplicaSet只是实现的细节。和处理与维护多个ReplicationController相比,管理单个Deployment对象要容易得多。

      尽管这种差异在滚动升级中可能不太明显,但是如果在滚动升级过程中出错或遇到问题,就可以明显看出两种方案的差异。下面来模拟一个错误。

     

    3.3 回滚Deployment

      现阶段,应用使用v2版本的镜像运行,接下来会先准备v3版本的镜像。

      创建v3版本的应用程序

      在v3版本中,将引入一个bug,使你的应用程序只能正确地处理前四个请求。 第五个请求之后的所有请求将返回一个内部服务器错误(HTTP状态代码500)。你将通过在处理程序函数的开头添加if语句来模拟这个bug。下面的代码清单显示了修改后的代码,所有需要更改的地方都用粗体显示。

    #代码 9.8 v3版本的应用(运行出错版本): v3/app.js
    const http = require('http');
    const os = require('os');
     
    var requestCount = 0;
     
    console.log("Kubia server starting...");
     
    var handler = function(request, response) {
      console.log("Received request from " + request.connection.remoteAddress);
      if (++requestCount >= 5) {
        response.writeHead(500);
        response.end("Some internal error has occurred! This is pod " + os.hostname() + "
    ");
        return;
      }
      response.writeHead(200);
      response.end("This is v3 running in pod " + os.hostname() + "
    ");
    };
     
    var www = http.createServer(handler);
    www.listen(8080);

      在第5个请求和所有后续请求中,返回一个状态码为500的错误, 错误消息“Some internal error has occurred...”。

      部署v3版本

    $ kubectl set image deployment kubia nodejs=luksa/kubia:v3
    deployment "kubia" image updated

      可以通过运行kubectl rollout status来观察整个升级过程:

    $ kubectl rollout status deployment kubia
    Waiting for rollout to finish: 1 out of 3 new replicas have been updated...
    Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
    Waiting for rollout to finish: 1 old replicas are pending termination...
    deployment "kubia" successfully rolled out

      新的版本己经开始运行。接下来的代码清单会显示,在发送几个请求之后,客户端开始收到服务端返回的错误。

    #代码 9.9访问出错的v3版本
    $ while true; do curl http://130.211.109.222; done
    This is v3 running in pod kubia-1914148340-lalmx
    This is v3 running in pod kubia-1914148340-bz35w
    This is v3 running in pod kubia-1914148340-w0voh
    ...
    This is v3 running in pod kubia-1914148340-w0voh
    Some internal error has occurred! This is pod kubia-1914148340-bz35w
    This is v3 running in pod kubia-1914148340-w0voh
    Some internal error has occurred! This is pod kubia-1914148340-lalmx
    This is v3 running in pod kubia-1914148340-w0voh
    Some internal error has occurred! This is pod kubia-1914148340-lalmx
    Some internal error has occurred! This is pod kubia-1914148340-bz35w
    Some internal error has occurred! This is pod kubia-1914148340-w0voh

      回滚升级

      不能让用户感知到升级导致的内部服务器错误,因此需要快速处理。在后面,将看到如何自动停止出错版本的滚动升级,但是现在先看一下如何手动停止。比较好的是,Deployment可以非常容易地回滚到先前部署的版本,它可以让Kubernetes取消最后一次部署的Deployment:

    $ kubectl rollout undo deployment kubia
    deployment "kubia" rolled back

      Deployment会被回滚到上一个版本。

      提示:undo命令也可以在滚动升级过程中运行,并直接停止滚动升级。在升级过程中已创建的pod会被删除并被老版本的pod替代。

      显示Deployment的滚动升级历史

      回滚升级之所以可以这么快地完成,是因为Deployment始终保持着升级的版本历史记录。之后也会看到,历史版本号会被保存在ReplicaSet中。滚动升级成功后,老版本的ReplicaSet也不会被删掉,这也使得回滚操作可以回滚到任何一个历史版本,而不仅仅是上一个版本。可以使用kubectl rollout history来显示升级的版本:

    $ kubectl rollout history deployment kubia
    deployments "kubia":
    REVISION    CHANGE-CAUSE
    2           kubectl set image deployment kubia nodejs=luksa/kubia:v2
    3           kubectl set image deployment kubia nodejs=luksa/kubia:v3

      还记得创建Deployment时的--record参数吗?如果不给定这个参数,版本历史中的CHANGE-CAUSE这一栏会为空。这也会使用户很难辨别每次的版本做了哪些修改。

      回滚到一个特定的Deployment版本

      通过在undo命令中指定一个特定的版本号,便可以回滚到那个特定的版本。 例如,如果想回滚到第一个版本,可以执行下述命令:

    $ kubectl rollout undo deployment kubia --to-revision=1

      还记得第一次修改Deployment时留下的ReplicaSet吗?这个ReplicaSet便表示Deployment的第一次修改版本。由Deployment创建的所有ReplicaSet表不完整的修改版本历史,如图9.11所示。每个ReplicaSet都用特定的版本号来保存Deployment的完整信息,所以不应该手动删除ReplicaSet。如果这么做便会丢失Deployment的历史版本记录而导致无法回滚。

      旧版本的ReplicaSet过多会导致ReplicaSet列表过于混乱,可以通过指定Deployment的revisionHistoryLimit属性来限制历史版本数量。默认值是2,所以正常情况下在版本列表里只有当前版本和上一个版本(以及只保留了当前和上一个ReplicaSet),所有再早之前的ReplicaSet都会被删除。

      注意:extensions/v1betal版本Deployment的revisionHisto-ryLimit没有值,在apps/v1beta2版本中,这个默认值是10

     

    3.4 控制滚动升级速率

      当执行如kubectl rollout status命令来观察升级到v3的过程时,会看到第一个pod被新创建,等到它运行时,一个旧的pod会被删除,然后又一个新的pod被创建,直到再没有新的pod可以更新。创建新pod和删除旧pod的方式可以通过配置滚动更新策略内的两个属性。

      介绍滚动升级策略的maxSurge和maxUnavailable属性

      在Deployment的滚动升级期间,有两个属性会决定一次替换多少个pod:maxSurge和maxUnavailable。可以通过Deployment的strategy字段下rollingUpdate的子属性来配置,如下面的代码清单所示。

    #代码9.10 为rollingUpdate策略指定参数
    spec:
      strategy:
        rollingUpdate:
          maxSurge: 1 
          maxUnavailable: 0 
        type: RollingUpdate
    表9.2 控制滚动升级速率的属性
    属性 含义
    maxSurge

    决定了Deployment配置中期望的副本数之外,最多允许超出的pod实例的数量。默认值为25%,所以pod实例最多可以比期望数量多25%。

    如果期望副本数被设置为4,那么在滚动升级期间,不会运行超过5个pod实例。当把百分数转换成绝对值时,会将数字四舍五入。这个值

    也可以不是百分数而是绝对值(例如,可以允许最多多出一个或两个pod)。

    maxUnavailable

    决定了在滚动升级期间,相对于期望副本数能够允许有多少pod实例处于不可用状态。 默认值也是25%,所以可用pod实例的数量不能低于

    期望副本数的75%。百分数转换成绝对值时这个数字也会四舍五入。如果期望副本数设置为4,并且百分比为25%,那么只能有一个pod处于不

    可用状态。在整个发布过程中,总是保持至少有三个pod实例处于可用状态来提供服务。与maxSurge—样,也可以指定绝对值而不是百分比。

      由于在之前场景中,设置的期望副本数为3,上述的两个属性都设置为25%,maxSurge允许最多pod数量达到4,同时maxUnavailable不允许出现任何不可用的pod(也就是说三个pod必须一直处于可运行状态),如图9.12所示。

      了解maxUnavailable属性

      extensions/v1beta1版本的Deployment使用不一样的默认值,maxSurge和maxUnavailable会被设置为1,而不是25%。对于三个副本的情况,maxSurge还是和之前一样,但是maxUnavailable是不一样的(是1而不是0),这使得滚动升级的过程稍有不同,如图9.13所示。

      在这种情况下,一个副本处于不可用状态,如果期望副本数为3,则只需要两个副本处于可用状态。这就是为什么上述滚动升级过程中会立即删除一个pod并创建两个新的pod。这确保了两个pod是可用的并且不超过pod允许的最大数量〔在这种情况下,最大数量是4,三个加上maxSurges的一个。一旦两个新的pod处于可用状态,剩下的两个旧的pod就会被删除了。

      这里相对有点难以理解,主要是maxUnavailable这个属性,它表示最大不可用pod数量。但是如果仔细查看前一个图的第二列,即使maxUnavailable被设置为1,可以看到两个pod处于不可用的状态。

      重要的是要知道maxUnavailable是相对于期望副本数而言的。如果replica的数量设置为3,maxUnavailable设置为1,则更新过程中必须保持至少两个(3-1)pod始终处于可用状态,而不可用pod数量可以超过一个。

     

    3.5 暂停滚动升级

      在经历了v3版本应用的糟糕体验之后,假设你现在已经修复了这个错误,并推送了第四个版本的镜像。但是你还是有点担心像之前那样将其滚动升级到所有的pod上。你需要的是在现有的v2pod之外运行一个v4pod,并查看一小部分用户请求的处理情况。如果一旦确定符合预期,就可以用新的pod替换所有旧的pod。

      可以通过直接运行一个额外的pod或通过一个额外的Deployment、ReplicationController或ReplicaSet来实现上述的需求。但是通过Deployment自身的一个选项,便可以让部署过程暂停,方便用户在继续完成滚动升级之前来验证新的版本的行为是否都符合预期。

      暂停滚动升级

      可以直接将镜像修改为luksa/kubia:v4(镜像已准备好)并触发滚动更新,然后立马〔在几秒之内)暂停滚动更新:

    $ kubectl set image deployment kubia nodejs=luksa/kubia:v4
    deployment "kubia" image updated 
    $ kubectl rollout pause deployment kubia
    deployment "kubia" paused

      一个新的pod会被创建,与此同时所有旧的pod还在运行。一旦新的pod成功运行,服务的一部分请求将被切换到新的pod。这样相当于运行了一个金丝雀版本。金丝雀发布是一种可以将应用程序的出错版本和其影响到的用户的风险化为最小的技术。与其直接向每个用户发布新版本,不如用新版本替换一个或一小部分的pod。 通过这种方式,在升级的初期只有少数用户会访问新版本。验证新版本是否正常工作之后,可以将剩余的pod继续升级或者回滚到上一个的版本。

      恢复滚动升级

      在上述例子中,通过暂停滚动升级过程,只有一小部分客户端请求会切换到V4 pod,而大多数请求依然仍只会切换到v3pod。一旦确认新版本能够正常工作,就可以恢复滚动升级,用新版本pod替换所有旧版本的pod:

    $ kubectl rollout resume deployment kubia
    deployment "kubia" resumed

      在滚动升级过程中,想要在一个确切的位置暂停滚动升级目前还无法做到,以后可能会有一种新的升级策略来自动完成上面的需求。但目前想要进行金丝雀发布的正确方式是,使用两个不同的Deployment并同时调整它们对应的pod数量。

      使用暂停功能来停止滚动升级

      暂停部署还可以用于阻止更新Deployment而自动触发的滚动升级过程,用户可以对Deployment进行多次更改,并在完成所有更改后才恢复滚动升级。一旦更改完毕,则恢复并启动滚动更新过程。

      注意:如果部署被暂停,那么在恢复部署之前,撤销命令不会撤销它。

     

    3.6 阻止出错版本的滚动升级

      现在讨论Deployment资源的另一个属性。在<升级Deployment>节开始时在Deployment中设置的minReadySeconds属性吗?使用它来减慢滚动升级速率,使用这个参数之后确实执行了滚动更新,并且没有一次性替换所有的pod。minReadySeconds的主要功能是避免部署出错版本的应用,而不只是单纯地减慢部署的速度。

      了解minReadySeconds的用处

      minReadySeconds属性指定新创建的pod至少要成功运行多久之后,才能将其视为可用。在pod可用之前,滚动升级的过程不会继续(还记得maaxUnavailable属性吗?)。当所有容器的就绪探针返回成功时,pod就被标记为就绪状态。如果一个新的pod运行出错,就绪探针返回失败,如果一个新的pod运行出错,并且在minReadySeconds时间内它的就绪探针出现了失败,那么新版本的滚动升级将被阻止。

      使用这个属性可以通过让Kubernetes在pod就绪之后继续等待10秒,然后继续执行滚动升级,来减缓滚动升级的过程。通常情况下需要将minReadySeconds设置为更高的值,以确保pod在它们真正开始接收实际流量之后可以持续保持就绪状态。

      当然在将pod部署到生产环境之前,需要在测试和预发布环境中对pod进行测试。但使用minReadySeconds就像一个安全气囊,保证了即使不小心将bug发布到生产环境的情况下,也不会导致更大规模的问题。

      使用正确配置的就绪探针和适当的minReadySeconds值,Kubernetes将预先阻止发布部署带有bug的v3版本。下面会展示如何实现。

      配置就绪探针来阻止全部v3版本的滚动部署

      再一次部署v3版本,但这1次会为pod配置正确的就绪探针。由于当前部署的是v4版本,所以在开始之前再次回滚到v2版本,来模拟假设是第一次升级到v3。当然也可以直接从v4升级到v3,但是后续假设都是先回滚到了v2版本。

      与之前只更新pod模板中的镜像不同的是,还将同时为容器添加就绪探针。之前因为就绪探针一直未被定义,所以容器和pod都处于就绪状态,即使应用程序本身并没有真正就绪甚至是正在返回错误。Kubernetes是无法知道应用本身是否出现了故障,也不会将未就绪信息暴露给客户端。

      同时更改镜像并添加就绪探针,则可以使用kubectl apply命令。使用下 面的 YAML 来更新 Deployment (将它另存为 kubian-deployment-v3-with-readinesscheck.yaml),如下面的代码清单所示。

    #代码 9.11 Deployment 包含一个就绪探针:kubia-deployment-v3 with-readinesscheck.yaml
    apiVersion: apps/v1beta1
    kind: Deployment
    metadata:
      name: kubia
    spec:
      replicas: 3
      minReadySeconds: 10                     #设置minReadySeconds的值为10
      strategy:
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 0                      #设置maxUnavailable的值为0来确保升级过程中pod被挨个替换
        type: RollingUpdate
      template:
        metadata:
          name: kubia
          labels:
            app: kubia
        spec:
          containers:
          - image: luksa/kubia:v3
            name: nodejs
            readinessProbe:
              periodSeconds: 1                    #定义一个就绪探针并每隔一秒钟执行一次
              httpGet:
                path: /
                port: 8080

      使用kubectl apply升级Deployment

      可以使用如下方式直接使用kubectl apply来升级Deployment:

    $ kubectl apply -f kubia-deployment-v3-with-readinesscheck.yaml
    deployment "kubia" configured

      apply命令可以用YAML文件中声明的字段来更新Deployment。不仅更新镜像, 而且还添加了就绪探针,以及在YAML中添加或修改的其他声明。如果新的YAML也包含replicas字段,当它与现有Deployment中的数量不一致时,那么apply操作也会对Deployment进行扩容。

      提示:使用kubectl apply更新Deployment时如果不期望副本数被更改,则不用在YAML文件中添加repLicas这个字段。

      运行apply命令会自动开始滚动升级过程,可以再一次运行rollout status命令来查看升级过程:

    $ kubectl rollout status deployment kubia
    Waiting for rollout to finish: 1 out of 3 new replicas have been updated...

      因为升级状态显示一个新的pod己经创建,一小部分流量应该也会切换到这个pod。可以通过下面的命令看到:

    $ while true; do curl http://130.211.109.222; done
    This is v2 running in pod kubia-1765119474-jvslk
    This is v2 running in pod kubia-1765119474-jvslk
    This is v2 running in pod kubia-1765119474-xk5g3
    This is v2 running in pod kubia-1765119474-pmb26
    This is v2 running in pod kubia-1765119474-pmb26
    This is v2 running in pod kubia-1765119474-xk5g3
    ...

      结果显示并没有请求被切换到v3pod,为什么呢?先列出所有pod:

    $ kubectl get po
    NAME                     READY     STATUS    RESTARTS   AGE
    kubia-1163142519-7ws0i   0/1       Running   0          30s
    kubia-1765119474-jvslk   1/1       Running   0          9m
    kubia-1765119474-pmb26   1/1       Running   0          9m
    kubia-1765119474-xk5g3   1/1       Running   0          8m

      可以看到,有一个pod并没有处于就绪状态

      就绪探针如何阻止出错版本的滚动升级

      当新的pod启动时,就绪探针会每隔一秒发起请求(在pod spec中,就绪探针的间隔被设置为1秒)。在就绪探针发起第五个请求的时候会出现失败,因为应用从第五个请求开始一直返回HTTP状态码500。

      因此,pod会从Service的endpoint中移除(参见图9.14)。当执行curl循环请求服务时,pod已经被标记为未就绪。这就解释了为什么curl发出的请求不会切换到新的pod。这正是符合预期的,因为你不希望客户端流量会切换到一个无法正常工作的pod。

      rollout status命令显示只有一个新副本启动,之后滚动升级过程没有再继续下去,因为新的pod—直处于不可用状态。即使变为就绪状态之后,也至少需要保持10秒,才是真正可用的。在这之前滚动升级过程将不再创建任何新的pod,因为当前maxUnavailable属性设置为O,所以也不会删除任何原始的pod。

      实际上部署过程自动被阻止是一件好事。如果继续用新的pod替换旧的pod, 那么最终服务将处于完全不能工作的状态,就和当时没有使用就绪探针的情况下滚动升级v3版本时出现的结果一样。但是添加了就绪探针之后,升级出错程序而对用户造成的影响面不会过大,对比之前替换所有pod的方式,只有一小部分用户受到了影响。

      提示:如果只定义就绪探针没有正确设置minReadySeconds,—旦有一次就绪探针调用成功,便会认为新的pod已经处于可用状态。因此最好适当地设置minReadySeconds的值。

      为滚动升级配置deadline

      默认情况下,在10分钟内不能完成滚动升级的话,将被视为失败。如果运行 kubectl describe deployment 命令,将会显示一条 ProgressDeadline-Exceeded的记录,如下面的代码清单所示。

    #代码9.12 使用kubectl describe 查看Deployment的详细情况
    $ kubectl describe deploy kubia
    Name:                   kubia
    ...
    Conditions:
      Type          Status  Reason
      ----          ------  ------
      Available     True    MinimumReplicasAvailable
      Progressing   False   ProgressDeadlineExceeded           #Deployment完成滚动升级的时间过久

      判定Deployment滚动升级失败的超时时间,可以通过设定Deployment spec中的progressDeadlineSeconds来指定。

      注意:extensions/v1betal版本不会设置默认的deadline。

      取消出错版本的滚动升级

      因为滚动升级过程不再继续,所以只能通过rollout undo命令来取消滚动升级:

    $ kubectl rollout undo deployment kubia
    deployment "kubia" rolled back

    4.修改Deployment或其他资源的不同方式

    表9.1在Kubernetes中修改资源
    方法 作用
    kubectl edit

    使用默认编辑器打开资源配置。修改保存并退出编辑器,资源对象会被更新

    例子:kubectl edit deployment kubia

    kubectl patch

    修改单个资源属性

    例子:kubectl patch deployment kubia -p'{"spec": {"template": {"spec": {"containers":

    [{"name": "nodejs", "image": "luksa/kubia:v2"}]}}}}’

    Kubectl apply

    通过一个完整的YAML或JSON文件,应用其中新的值来修改对象。如果YAML/JSON中指定的对象不存在,

    则会被创建。该文件需要包含资源的完整定义(不能像kubectl patch那样只包含想要更新的字段)

    例子:kubectl apply -f kubia-deployment-v2.yaml

    Kubectl replace

    将原有对象替换为YAML/JSON文件中定义的新对象。与apply命令相反,

    运行这个命令前要求对象必须存在,否则会打印错误

    例子:kubectl replace -f kubia-deployment-v2.yaml。

    kubectl setimage

    修改Pod、ReplicationController、Deployment、DemonSet、Job或ReplicaSet内的镜像

    例子:kubectl set image deployment kubia nodejs=luksa/kubia:v2

    作者:小家电维修

    相见有时,后会无期。

  • 相关阅读:
    第一次开发分享的经验教训
    开发人员的 Linux 命令学习清单
    代码质量基本准则
    职业发展思考(二)
    软件调试的基本技巧
    多数据源的动态配置与加载使用兼框架交互的问题调试
    对象与并发:概述
    编程模式: 回调
    创新式开发探索(一) —— 开篇
    Linux 命令学习示例: tr
  • 原文地址:https://www.cnblogs.com/lizexiong/p/14797228.html
Copyright © 2011-2022 走看看