下面说说Pod的升级和回滚问题。
当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有Pod,然后下载新版本镜像并创建新的Pod。如果集群规模比较大,则这个工作变成了一个挑战,而且先全部停止然后逐步升级的方式会导致较长时间的服务不可用。Kubernetes提供了滚动升级功能来解决上述问题。
如果Pod是通过Deployment创建的,则用户可以在运行时修改Deployment的Pod定义(spec.template)或镜像名称,并应用到Deployment对象上,系统即可完成Deployment的rollout动作,rollout可被视为Deployment的自动更新或者自动部署动作。如果在更新过程中发生了错误,则还可以通过回滚操作恢复Pod的版本。
以Deployment nginx为例:


已运行的Pod副本数量有3个:

现在Pod镜像需要被更新为Nginx:1.9.1,我们可以通过kubectl set image命令为Deployment设置新的镜像名称:

另一种更新的方法是使用kubectl edit命令修改Deployment的配置,将spec.template.spec.containers[0].image从Nginx:1.7.9更改为Nginx:1.9.1:

镜像名(或Pod定义)一旦发生了修改,则将触发系统完成Deployment所有运行Pod的滚动升级操作。可以使用kubectl rollout status命令查看Deployment的更新过程:


查看当前运行的Pod,名称已经更新了:

查看Pod使用的镜像,已经更新为Nginx:1.9.1了:

那么,Deployment是如何完成Pod更新的呢?
我们可以使用kubectl describe deployments/nginx-deployment命令仔细观察Deployment的更新过程。初始创建Deployment时,系统创建了一个ReplicaSet(nginx-deployment-4087004473),并按用户的需求创建了3个Pod副本。更新Deployment时,系统创建了一个新的ReplicaSet(nginx-deployment-3599678771),并将其副本数量扩展到1,然后将旧的ReplicaSet缩减为2。之后,系统继续按照相同的更新策略对新旧两个ReplicaSet进行逐个调整。最后,新的ReplicaSet运行了3个新版本的Pod副本,旧的ReplicaSet副本数量则缩减为0,如图3.11所示。

图3.11 Pod的滚动升级示意图
下面列出Deployment nginx-deployment的详细事件信息:


运行kubectl get rs命令,查看两个ReplicaSet的最终状态:

在整个升级过程中,系统会保证至少有两个Pod可用,并且最多同时运行4个Pod,这是Deployment通过复杂的算法完成的。Deployment需要确保在整个更新过程中只有一定数量的Pod可能处于不可用状态。在默认情况下,Deployment确保可用的Pod总数量至少为所需的副本数量(DESIRED)减1,也就是最多1个不可用(maxUnavailable=1)。Deployment还需要确保在整个更新过程中Pod的总数量不会超过所需的副本数量太多。在默认情况下,Deployment确保Pod的总数量最多比所需的Pod数量多1个,也就是最多1个浪涌值(maxSurge=1)。Kubernetes从1.6版本开始,maxUnavailable和maxSurge的默认值将从1、1更新为所需副本数量的25%、25%。
这样,在升级过程中,Deployment就能够保证服务不中断,并且副本数量始终维持为用户指定的数量(DESIRED)。
在Deployment的定义中,可以通过spec.strategy指定Pod更新的策略,目前支持两种策略:Recreate(重建)和RollingUpdate(滚动更新),默认值为RollingUpdate。在前面的例子中使用的就是RollingUpdate策略。
◎ Recreate:设置spec.strategy.type=Recreate,表示Deployment在更新Pod时,会先“杀掉”所有正在运行的Pod,然后创建新的Pod。
◎ RollingUpdate:设置spec.strategy.type=RollingUpdate,表示Deployment会以滚动更新的方式来逐个更新Pod。同时,可以通过设置spec.strategy.rollingUpdate下的两个参数(maxUnavailable和maxSurge)来控制滚动更新的过程。
对滚动更新时两个主要参数的说明如下。
◎ spec.strategy.rollingUpdate.maxUnavailable:用于指定Deployment在更新过程中不可用状态的Pod数量的上限。该maxUnavailable的数值可以是绝对值(例如5)或Pod期望的副本数量的百分比(例如10%),如果被设置为百分比,那么系统会先以向下取整的方式计算出绝对值(整数)。而当另一个参数maxSurge被设置为0时,maxUnavailable则必须被设置为绝对数值大于0(从Kubernetes 1.6开始,maxUnavailable的默认值从1改为25%)。举例来说,当maxUnavailable被设置为30%时,旧的ReplicaSet可以在滚动更新开始时立即将副本数量缩小到所需副本总数量的70%。一旦新的Pod创建并准备好,旧的ReplicaSet就会进一步缩容,新的ReplicaSet又继续扩容,整个过程中系统在任意时刻都可以确保可用状态的Pod总数量至少占Pod期望副本总数量的70%。
◎ spec.strategy.rollingUpdate.maxSurge:用于指定在Deployment更新Pod的过程中Pod总数量超过Pod期望副本数量部分的最大值。该maxSurge的数值可以是绝对值(例如5)或Pod期望副本数量的百分比(例如10%)。如果设置为百分比,那么系统会先按照向上取整的方式计算出绝对数值(整数)。从Kubernetes 1.6开始,maxSurge的默认值从1改为25%。举例来说,当maxSurge的值被设置为30%时,新的ReplicaSet可以在滚动更新开始时立即进行副本数量扩容,只需保证新旧ReplicaSet的Pod副本数量之和不超过期望副本数量的130%即可。一旦旧的Pod被“杀掉”,新的ReplicaSet就会进一步扩容。在整个过程中系统在任意时刻都能确保新旧ReplicaSet的Pod副本总数量之和不超过所需副本数量的130%。
这里需要注意多重更新(Rollover)的情况。如果Deployment的上一次更新正在进行,此时用户再次发起Deployment的更新操作,那么Deployment会为每一次更新都创建一个ReplicaSet,而每次在新的ReplicaSet创建成功后,会逐个增加Pod副本数量,同时将之前正在扩容的ReplicaSet停止扩容(更新),并将其加入旧版本ReplicaSet列表中,然后开始缩容至0的操作。
例如,假设我们创建一个Deployment,这个Deployment开始创建5个Nginx:1.7.9的Pod副本,在这个创建Pod动作尚未完成时,我们又将Deployment进行更新,在副本数量不变的情况下将Pod模板中的镜像修改为Nginx:1.9.1,又假设此时Deployment已经创建了3个Nginx:1.7.9的Pod副本,则Deployment会立即“杀掉”已创建的3个Nginx:1.7.9 Pod,并开始创建Nginx:1.9.1 Pod。Deployment不会在等待Nginx:1.7.9的Pod创建到5个之后再进行更新操作。
还需要注意更新Deployment的标签选择器(Label Selector)的情况。通常来说,不鼓励更新Deployment的标签选择器,因为这样会导致Deployment选择的Pod列表发生变化,也可能与其他控制器发生冲突。如果一定要更新标签选择器,那么请务必谨慎,确保不会出现其他问题。关于Deployment标签选择器的更新的注意事项如下。
(1)添加选择器标签时,必须同步修改Deployment配置的Pod的标签,为Pod添加新的标签,否则Deployment的更新会报验证错误而失败:

添加标签选择器是无法向后兼容的,这意味着新的标签选择器不会匹配和使用旧选择器创建的ReplicaSets和Pod,因此添加选择器将会导致所有旧版本的ReplicaSets和由旧ReplicaSets创建的Pod处于孤立状态(不会被系统自动删除,也不受新的ReplicaSet控制)。
为标签选择器和Pod模板添加新的标签(使用kubectl edit deployment命令)后,效果如下:

可以看到新ReplicaSet(nginx-deployment-3661742516)创建的3个新Pod:

(2)更新标签选择器,即更改选择器中标签的键或者值,也会产生与添加选择器标签类似的效果。
(3)删除标签选择器,即从Deployment的标签选择器中删除一个或者多个标签,该Deployment的ReplicaSet和Pod不会受到任何影响。但需要注意的是,被删除的标签仍会存在于现有的Pod和ReplicaSets上。
Deployment会自动创建并控制对应的ReplicaSet,给它们增加一个名为pod-template-hash的标签。切记,这个标签是不能被手动修改的。
在什么情况下会触发Deployment的rollout行为呢?只有Pod模板定义部分(Deployment的.spec.template)的属性发生改变时才会触发Deployment的rollout行为,对于其他的比如修改Pod的副本数量(spec.replicas)的值,则不会触发rollout行为。
对于用RC(Replication Controller)来控制Pod的滚动升级,Kubernetes之前提供了对应的kubectl rolling-update命令来实现类似的功能。该命令通过创建一个新RC,然后自动控制旧RC中的Pod副本数量逐渐减少到0,新RC中的Pod副本数量从0逐步增加到目标值,来完成Pod的升级。此命令在kuberntes的1.17版本中被标记为DEPRECATED,在1.18中不再提供支持。
如果在Deployment升级过程中出现意外,比如写错新镜像的名称、新镜像还没被放入镜像仓库里、新镜像的配置文件发生不兼容性改变、新镜像的启动参数不对,以及因可能更复杂的依赖关系而导致升级失败等,就需要回退到之前的旧版本,这时就可以用到Deployment的回滚功能了。
假设在更新Deployment镜像时,将容器镜像名误设置成Nginx:1.91(一个不存在的镜像):

则这时Deployment的部署过程会卡住:

所以需要运行Ctrl C命令来终止这个查看命令。
查看ReplicaSet,可以看到新建的ReplicaSet(nginx-deployment-3660254150):

再查看创建的Pod,会发现新的ReplicaSet创建的1个Pod被卡在镜像拉取过程中。

为了解决上面这个问题,我们需要回滚到之前稳定版本的Deployment。首先,用kubectl rollout history命令检查这个Deployment部署的历史记录:


我们将Deployment回滚到之前的版本时,只有Deployment的Pod模板部分会被修改,在默认情况下,所有Deployment的发布历史记录都被保留在系统中(可以配置历史记录数量),以便于我们随时进行回滚操作。注意,在创建Deployment时使用--record参数,就可以在CHANGE-CAUSE列看到每个版本使用的命令了。
如果需要查看特定版本的详细信息,则可以加上--revision=<N>参数:

现在我们决定撤销本次发布并回滚到上一个部署版本:

当然,也可以使用--to-revision参数指定回滚到的部署版本号:

这样,该Deployment就回滚到之前的稳定版本了,可以从Deployment的事件信息中查看到回滚到版本2的操作过程:


对于一次复杂的Deployment配置修改,为了避免频繁触发Deployment的更新操作,可以先暂停Deployment的更新操作,然后进行配置修改,再恢复Deployment,一次性触发完整的更新操作,就可以避免不必要的Deployment更新操作了。
以之前创建的Nginx为例:

通过kubectl rollout pause命令暂停Deployment的更新操作:

然后修改Deployment的镜像信息:

查看Deployment的历史记录,发现并没有触发新的Deployment部署操作:

在暂停Deployment部署之后,可以根据需要进行任意次数的配置更新。例如,再次更新容器的资源限制:

最后,恢复这个Deployment的部署操作:

可以看到一个新的ReplicaSet被创建出来了:


查看Deployment的事件信息,可以看到Deployment完成了更新:

注意,在恢复暂停的Deployment之前,无法回滚该Deployment。
Kubernetes从1.6版本开始,对DaemonSet和StatefulSet的更新策略也引入类似于Deployment的滚动升级,通过不同的策略自动完成应用的版本升级。
目前DaemonSet的升级策略(updateStrategy)包括两种:OnDelete和RollingUpdate。
(1)OnDelete:DaemonSet的默认升级策略,与1.5及之前版本的Kubernetes保持一致。当使用OnDelete作为升级策略时,在创建好新的DaemonSet配置之后,新的Pod并不会被自动创建,直到用户手动删除旧版本的Pod,才触发新建操作,即只有手工删除了DaemonSet创建的Pod副本,新的Pod副本才会被创建出来。如果不设置updateStrategy的值,则在Kubernetes 1.6之后的版本中会被作为updateStrategy的默认设置。
(2)RollingUpdate:从Kubernetes 1.6版本开始引入。当使用RollingUpdate作为升级策略对DaemonSet进行更新时,旧版本的Pod将被自动“杀掉”,然后自动创建新版本的DaemonSet Pod。整个过程与普通Deployment的滚动升级一样是可控的。不过有两点不同于普通Pod的滚动升级:一是目前Kubernetes还不支持查看和管理DaemonSet的更新历史记录;二是DaemonSet的回滚(Rollback)并不能如同Deployment一样直接通过kubectl rollback命令来实现,必须通过再次提交旧版本配置的方式实现。
下面是DaemonSet采用RollingUpdate升级策略的YAML定义:

Kubernetes从1.6版本开始,针对StatefulSet的更新策略逐渐向Deployment和DaemonSet的更新策略看齐;1.7版本之后,StatefulSet又增加了updateStrategy字段给予用户更强的StatefulSet升级控制能力,并实现了RollingUpdate、OnDelete和Partitioned这几种策略,以保证StatefulSet中各Pod有序地、逐个地更新,并且能够保留更新历史,也能回滚到某个历史版本。如果用户未设置updateStrategy字段,则系统默认使用RollingUpdate策略。
当updateStrategy的值被设置为RollingUpdate时,StatefulSet Controller会删除并创建StatefulSet相关的每个Pod对象,其处理顺序与StatefulSet终止Pod的顺序一致,即从序号最大的Pod开始重建,每次更新一个Pod。注意,如果StatefulSet的Pod Management Policy被设置为OrderedReady,则可能在更新过程中发生一些意外,从而导致StatefulSet陷入奔溃状态,此时需要用户手动修复。
当updateStrategy的值被设置为OnDelete时,StatefulSet Controller并不会自动更新StatefulSet中的Pod实例,而是需要用户手动删除这些Pod并触发StatefulSet Controller创建新的Pod实例来弥补,因此这其实是一种手动升级模式。
updateStrategy也支持特殊的分区升级策略(Partitioned),在这种模式下,用户指定一个序号,StatefulSet中序号大于等于此序号的Pod实例会全部被升级,小于此序号的Pod实例则保留旧版本不变,即使这些Pod被删除、重建,也仍然保持原来的旧版本。这种分区升级策略通常用于按计划分步骤的系统升级过程中。