第 6 章 版本化、发布和滚动更新

传统单体应用程序的主要抱怨之一是,随着时间的推移,它们开始变得太大、太笨重,且难以以业务需要的速度进行适当的升级、版本化或修改。许多人可能会说,这是导致更敏捷的开发实践和微服务架构出现的主要关键因素之一。能够快速迭代新代码、解决新问题或修复隐藏问题,以免它们成为重大问题,以及零停机时间升级的承诺,这些都是开发团队在这个不断变化的互联网经济世界中努力追求的目标。实际上,这些问题可以通过适当的流程和程序来解决,无论系统类型是什么,但这通常需要更高的技术和人力资本成本来维护。

采用容器作为应用程序代码的运行时,可以实现隔离和可组合性,这有助于设计可以接近的系统,但仍需要高水平的人工自动化或系统管理才能在可靠的状态保持水平超过大型系统占用空间。随着系统的发展,引入了越来越脆性,系统工程师开始构建复杂的自动化流程,以提供复杂的发布、升级和故障检测机制。服务协调器(如 Apache Mesos、HashiCorp Nomad),甚至基于容器的专用协调器(如 Kubernetes 和 Docker Swarm)都将其演变为更原始的组件,以进入其运行时。现在,系统工程师可以解决更复杂的系统问题,因为表的利害关系已经提升到将应用程序的版本化、发布和部署到系统中。

版本化

本节并不是软件版本控制及其背后的历史记录的入门。关于这个主题有无数的文章和计算机科学课程的书籍。最重要的是选择一个模式,并坚持下去。大多数软件公司和开发人员都认为某种形式的语义版本控制是最有用的,尤其是在微服务体系结构中,编写特定微服务的团队将依赖于组成系统的其他微服务的 API 兼容性。

对于那些刚接触语义版本控制的人来说,基本是它遵循一个三部分的版本号在主要版本次要版本补丁的模式,通常表示以*点表示,*如1(主版本).2(次版本).3(补丁)。这个补丁程序意味着一个增量版本,其中包括 Bug 修复或没有 API 更改的非常轻微的更改。次要版本表示可能具有新的 API 更改但与早期版本向后兼容的更新。对于使用他们可能没有参与开发的其他微服务的开发人员来说,这是一个关键属性。知道我编写的服务是为了与最近升级到 1.4.8 的另一个微服务版本 1.4.7 进行通信,这应该意味着,除非我想利用任何新的 API 功能,否则我可能不需要更改代码。主版本是代码的重大更改增量。在大多数情况下,API 不再在同一代码的主要版本之间兼容。此过程有许多细微的修改,包括"4"版本,用于指示软件在其开发生命周期中的阶段,例如 1.4.7.0 用于 alpha 代码,1.4.7.3 表示发布。最重要的是整个系统的一致性。

发布

事实上,Kubernetes 没有真正的发布控制器,因此没有发布的原生概念。metadata.labels和/或pod.spec.template.metadata.label 通常会添加到部署的规范中。何时包括其中之一非常重要,并且根据 CD 如何用于更新对部署的更改,它可以产生不同的效果。在引入 Kubernetes 的 Helm 时,它的主要概念之一是发布的概念,用于以区分群集中同一 Helm 图表的运行实例。这个概念不使用 Helm也很容易实现,但是,Helm 具备跟踪发布及其历史记录,因此许多 CD 工具将 Helm 集成到其流水线中,成为实际的发行版服务。同样,这里的关键是如何使用版本控制以及版本控制在群集的系统状态中出现的位置的一致性。

如果组织对某些名称的定义达成一致,则发布名称可能非常有用。通常会使用(如 stablecanary)之类的标签,这有助于在添加服务网等工具以做出细粒度路由决策时提供某种操作控制。为不同受众推动大量更改的大型组织也将采用一个也可以表示为ring-0ring-1等的ring体系结构。

本主题需要对 Kubernetes 声明性模型中标签的细节进行一点补充。标签本身是非常自由的,可以是任何键/值对遵循 API 的语法规则。关键不在于内容,而在于每个控制器如何处理标签、标签的更改和标签选择器匹配。Jobs、Deployments、ReplicaSets和DaemonSets支持通过直接映射或基于集的表达式通过标签对基于选择的 pod 进行匹配。请务必了解标签选择器在创建后是不可变的,这意味着如果添加新选择器且 Pod 的标签具有相应的匹配项,则会创建新的副本集,而不是升级到现有副本集。在处理部署时,了解这一点非常重要,我们接下来将讨论这些部署。

滚动更新

在 Kubernetes 中引入Deployment 控制器之前,控制 Kubernetes 控制器进程如何更新应用程序的唯一机制是使用命令行接口 (CLI) 命令kubectl rolling-update 对要更新的replicaController执行特定命令。对于声明性 CD 模型来说,这非常困难,因为这不是原始清单状态的一部分。必须小心地确保正确的更新了清单、正确的进行了版本化,以免意外地将系统回滚,并在不再需要时进行存档。Deployment 控制器添加了使用特定策略自动执行此更新过程的能力,然后允许系统根据对部署spec.template的更改读取声明性新状态。最后一个事实经常被 Kubernetes 的早期用户误解,当他们更改Deployment元数据字段中的标签、重新应用清单并且未触发任何更新时,会感到沮丧。Deployment控制器能够确定对规范的更改,并将根据规范定义的策略采取措施更新部署。 Kubernetes部署支持两种策略:rollingUpdaterecreate,而前者为默认策略。

如果指定了滚动更新,则部署将创建新的副本集以扩展到所需的副本数,并且旧副本集将根据 maxUnavailblemaxSurge 的特定值向下缩放为零。从本质上讲,这两个值将阻止 Kubernetes 在足够数量的较新的 pod 已联之前删除较旧的 pod,并且在删除一定数量的旧 pod 之前不会创建新的 pod。值得一些好处是,部署控制器将保留更新的历史记录,并且通过 CLI,您可以将部署回滚到以前的版本。

recreate 策略对于某些工作负载来说是有效的策略,这些工作负载可以处理副本集中的 Pod 完全中断,而服务很少或根本没有降级。在这种策略中,Deployment控制器将创建新的ReplicaSet,并删除以前的ReplicaSet,然后再使新pods上线。位于基于队列的系统后面的服务是能够处理此类中断的服务的一个示例,因为消息将在等待新pods上线时排队,并且一旦新pods上线,消息处理就会立即恢复。

全部放在一起

在单个服务部署中,一有几个关键区域受到版本控制、发布和滚动更新管理的影响。让我们研究一个部署示例,然后分解与最佳实践相关的特定领域:

  1# Web Deployment
  2apiVersion: apps/v1
  3kind: Deployment
  4metadata:
  5  name: gb-web-deploy
  6  labels:
  7    app: guest-book
  8    appver: 1.6.9
  9    environment: production
 10    release: guest-book-stable
 11    release number: 34e57f01
 12spec:
 13  strategy:
 14    type: rollingUpdate
 15    rollingUpdate:
 16      maxUnavailbale: 3
 17      maxSurge: 2
 18  selector:
 19    matchLabels:
 20      app: gb-web
 21      ver: 1.5.8
 22    matchExpressions:
 23      - {key: environment, operator: In, values: [production]}
 24  template:
 25    metadata:
 26      labels:
 27        app: gb-web
 28        ver: 1.5.8
 29        environment: production
 30    spec:
 31      containers:
 32      - name: gb-web-cont
 33        image: evillgenius/gb-web:v1.5.5
 34        env:
 35        - name: GB_DB_HOST
 36          value: gb-mysql
 37        - name: GB_DB_PASSWORD
 38          valueFrom:
 39            secretKeyRef:
 40              name: mysql-pass
 41              key: password
 42        resources:
 43          limits:
 44            memory: "128Mi"
 45            cpu: "500m"
 46        ports:
 47        - containerPort: 80
 48---
 49# DB Deployment
 50apiVersion: apps/v1
 51kind: Deployment
 52metadata:
 53  name: gb-mysql
 54  labels:
 55    app: guest-book
 56    appver: 1.6.9
 57    environment: production
 58    release: guest-book-stable
 59    release number: 34e57f01
 60spec:
 61  selector:
 62    matchLabels:
 63      app: gb-db
 64      tier: backend
 65  strategy:
 66    type: Recreate
 67  template:
 68    metadata:
 69      labels:
 70        app: gb-db
 71        tier: backend
 72        ver: 1.5.9
 73        environment: production
 74    spec:
 75      containers:
 76      - image: mysql:5.6
 77        name: mysql
 78        env:
 79        - name: MYSQL_PASSWORD
 80          valueFrom:
 81            secretKeyRef:
 82              name: mysql-pass
 83              key: password
 84        ports:
 85        - containerPort: 3306
 86          name: mysql
 87        volumeMounts:
 88        - name: mysql-persistent-storage
 89          mountPath: /var/lib/mysql
 90      volumes:
 91      - name: mysql-persistent-storage
 92        persistentVolumeClaim:
 93          claimName: mysql-pv-claim
 94---
 95# DB Backup Job
 96apiVersion: batch/v1
 97kind: Job
 98metadata:
 99  name: db-backup
100  labels:
101    app: guest-book
102    appver: 1.6.9
103    environment: production
104    release: guest-book-stable
105    release number: 34e57f01
106  annotations:
107    "helm.sh/hook": pre-upgrade
108    "helm.sh/hook": pre-delete
109    "helm.sh/hook": pre-rollback
110    "helm.sh/hook-delete-policy": hook-succeeded
111spec:
112  template:
113    metadata:
114      labels:
115        app: gb-db-backup
116        tier: backend
117        ver: 1.6.1
118        environment: production
119    spec:
120      containers:
121      - name: mysqldump
122        image: evillgenius/mysqldump:v1
123        env:
124        - name: DB_NAME
125          value: gbdb1
126        - name: GB_DB_HOST
127          value: gb-mysql
128        - name: GB_DB_PASSWORD
129          valueFrom:
130            secretKeyRef:
131              name: mysql-pass
132              key: password
133        volumeMounts:
134          - mountPath: /mysqldump
135            name: mysqldump
136      volumes:
137        - name: mysqldump
138          hostPath:
139            path: /home/bck/mysqldump
140      restartPolicy: Never
141  backoffLimit: 3

第一次检查时,事情可能看起来有点不对经。部署如何使用版本标记,部署使用的容器镜像具有不同的版本标记?如果一个改变,另一个不改变,会发生什么?在这个示例中,release是什么意思,如果它发生更改,将对系统产生什么影响?如果更改了某个标签,何时会触发对部署的更新?通过查看版本化、发布和滚动更新的一些最佳实践,我们可以找到这些问题的答案。

版本化、发布和滚动更新最佳实践

有效的 CI/CD 和提供减少或零停机时间部署的能力都依赖于使用一致的版本控制和发布管理实践。下面提到的最佳实践可帮助定义一致的参数,以帮助 DevOps 团队提供顺利的软件部署:

  • 对整个应用程序使用语义版本控制,它与组成整个应用程序的容器版本和 Pods 部署版本不同。这允许组成应用程序和作为整个应用程序的容器的独立生命周期。这一开始可能会变得相当混乱,但如果采用有原则的分层方法来处理一个更改另一个时,您可以轻松地跟踪它。在前面的示例中,容器本身当前处于 v1.5.5 ;但是,pod 指定的是 1.5.8 ,这可能意味着对 pod 规范进行了更改,例如新的 ConfigMaps、附加Secret或更新的副本数,但使用的特定容器没有更改其版本。应用程序本身、整个留言簿应用程序及其所有服务都是 1.6.9,这可能意味着操作在方式上所做的更改不仅仅是这个特定的服务,比如构成整个应用程序的其他服务。
  • 在部署元数据中使用发布版本和版本/编号标签来跟踪来自 CI/CD 流水线的版本。发行名称和发行号应与 CI/CD 工具记录中的实际版本一致。这允许通过 CI/CD 过程到群集中进行可追溯性,并允许更轻松地进行回滚标识。在前面的示例中,版本号直接来自创建清单的 CD 流水线的release ID。
  • 如果 Helm 用于将服务打包到 Kubernetes 中,需要特别注意将需要回滚或升级的服务打包到同一个 Helm 图表中。Helm 允许轻松回滚应用程序的所有组件,以使状态恢复到升级前的状态。由于 Helm 实际上是在传递扁平化的 YAML 配置之前处理模板和所有 Helm 指令,所以使用生命周期钩子可以对特定模板的应用程序进行适当的排序。操作人员可以使用正确的 Helm 生命周期钩子,以确保升级和回滚正确进行。规范的上一个Job示例使用 Helm 生命周期钩子来确保模板在回滚、升级或删除 Helm 版本之前运行数据库备份。Job还可确保 在作业成功运行后删除 ,在 Kubernetes 中的 TTL 控制器从 alpha 中出来之前,需要手动清理。
  • 就一个对组织的操作节奏有意义的版本命名达成一致。简单的stablecanaryalpha状态对于大多数情况都相当充足。

总结

Kubernetes 允许在大大小小的公司中采用更复杂、更敏捷开发流程。许多通常需要大量人力和技术资本,而现在,自动化的能力已经得到普及,甚至让初创公司也可以相对轻松地利用这种云模式。Kubernetes真正的生命性本质在规划正确的使用标签和使用原生 Kubernetes 控制器功能时非常突出。通过在部署到 Kubernetes 的应用程序的声明性属性中正确的标识操作和开发状态,组织可以将工具和自动化结合起来,从而更轻松地管理升级、发布和回滚的复杂过程。