分布式系统开发实战:虚拟化与容器技术,基于容器的持续部署

基于容器的持续部署

随着Docker等容器技术的纷纷涌现及开源发布,软件开发行业对于现代化应用的打包以及部署方式发生了巨大的变化。想象在没有容器等虚拟化技术的年代,程序经常需要手工部署和测试,这种工作极其烦琐且容易出错,特别是服务器数量多的时候,重复性的工作总是令人厌烦。由于开发环境、测试环境以及最终的生产环境的不一致,同样的程序,有可能在不同的环境出现不同问题,所以经常会出现开发人员和测试人员“扯皮”的事。开发机上没有出现问题,部署到测试服务器上就出问题了。

现在就来介绍一下如何基于容器来解决持续部署上面提到的种种问题。

持续部署管道

持续部署管道(Continuous-Deployment Pipeline)是指在每次代码提交时会执行的一系列步骤。管道的目的是执行一系列任务,将一个经过完整测试的功能性服务或应用部署至生产环境。唯一一个手工操作就是向代码仓库执行一次签入操作,之后的所有步骤都是自动完成的。这种流程可以在一定程度上消除人为产生错误的因素,从而增强可靠性。

并且可以让机器完成它们最擅长的工作——运行重复性的过程,而不是创新性思考,从而增加了系统的吞吐量。之所以每次提交都需要通过这个管道,原因就在于“持续”这个词。如果选择延迟这一过程的执行,例如在某个Sprint结束前再运行,那么整个测试与部署过程都不再是持续的了。

延迟测试以及延迟部署程序到生产环境,也就延误了发现系统潜在问题的时机,最终导致的结果是修复这些问题需要投入更多的精力,毕竟在问题发生一个月后再去尝试修复,比起在问题发生一周内进行修复的成本要高得多。与之类似的是,如果在代码提交的几分钟内立即发出Bug的通知,那么定位该Bug所需的时间就显得微不足道了。持续部署的意义不仅在于节省维护与Bug修复的投入,它还能够让我们更快地将新特性发布至生产环境,开发功能并最终交付给用户使用之间的时间越短,我们就能够越快地从中受益。

我们先从一个最小的子集开始,探讨一种持续部署的方案。这个最小子集能够让我们对服务进行测试、构建以及部署,如图12-1所示。这些任务都是必不可少的。缺少了测试,我们就无法保证该服务能够正常运行;缺少了构建,就没有什么东西可部署;而缺少了部署,用户就无法从新的发布中受益。

图12-1 简单的测试、构建、部署流程

测试

传统的软件测试方式是对源代码进行单元测试,这种方式虽然能够带来较高的代码覆盖率,但不见得一定能够保证特性按照预期的方式工作,也无法保证单独的代码单元(方法、函数、类等)的行为符合设计要求。为了对特性进行验证,往往需要进行功能性测试,这种方式偏向于黑盒测试,与代码没有直接的关联。功能性测试的一个问题在于对系统存在依赖。举例来说,Java应用可能需要一个特定的JDK版本,而Web应用可能需要在大量的浏览器上进行测试。在不同的系统条件组合中对相同的测试集需要进行大量重复的测试。

一个令人遗憾的事实是,许多组织的测试并不充分,这无法确保一次新的发布能够在没有人工干预的情况下部署至生产环境。即使这些测试本身是可靠的,但往往没有将这些测试在所有可能出现在生产环境中的相同条件下运行。出现这一问题的原因与我们对基础设施的管理方式有关。以人工方式对基础设施进行设置的代价是非常高的。大多数企业都需要用到多种不同的环境。比方说某个环境需要运行Ubuntu而另一个需要运行Red Hat,或者是某个环境需要JDK8而另一个环境需要JDK7。

这是一种非常费时费力的途径,尤其当这些环境作为静态环境(与之相对的是通过云计算托管的“创建与销毁”途径)时更为明显。即使为了满足各种组合而设置了足够的服务器,仍然会遇到速度与灵活性的问题。

举例来说,如果某个团队决定开发一个新服务,或是使用不同的技术对某个现有的服务进行重构,从请求搭建新环境直至该环境具备完整的可操作性为止也会浪费大量时间。在这段过程中,持续部署过程将陷入停顿。如果在这种环境添加微服务,则浪费的时间将呈指数级增长。在过去,开发者通常只需关注有限的几个应用程序,而如今则需要关注几十个、几百个乃至上千个服务。毕竟,微服务的益处包括为某个用例选择最佳技术的灵活性,以及高速的发布。我们不希望等到整个系统开发完成才去部署,而是希望完成某个属于单一微服务的功能后立即进行发布。

容器技术的出现可以轻松地处理各种测试问题,因为测试和最后需要部署到生产环境的将是同一个容器,里面包含的系统运行所需要的运行时与依赖都是相同的。这样开发者在测试过程中选择应用所需的组件,通过团队所用的持续部署工具构建并运行容器,让这一容器执行所需的各种测试。当代码通过全部测试之后,就可以进入下一阶段的工作了。测试所用的容器应当在注册中心(可选择私有或公有)进行注册,以便之后重用。在测试执行结束之后,就可以销毁该容器,使服务器回到原来的状态。如此一来,就可以使用同一台服务器(或服务集群)对全部服务进行测试了。

图12-2所示的流程已经开始显得有些复杂了。

图12-2 持续集成中的测试

构建

当执行完所有测试后,就可以开始创建容器,并最终将其部署至生产环境中了。由于我们很可能会将其部署至一个与构建所用不同的服务器中,因此同样应当将其注册在注册中心,如图12-3所示。

图12-3 持续集成中的构建

当完成测试并构建好新的发布后,就可以准备将其部署至生产服务器中了。我们所要做的就是拉取对应的镜像并运行容器,如图12-4所示。

图12-4 拉取镜像并运行容器

部署

当容器上传至注册中心后,就可以在每次签入之后部署我们的微服务,并以前所未有的速度将新的特性交付给用户。

但目前所定义的流程还远远谈不上一个完整的持续部署管道。它还遗漏了许多步骤、需要考虑的内容以及必需的路径。让我们依次找出这些问题并逐个解决。

蓝-绿部署

整个管道中最危险的步骤可能就是部署了。如果我们获取了某个新的发布并开始运行,容器就会以新的发布取代旧的发布。也就是说,在过程中会出现一定的宕机时间。容器需要停止旧的发布并启动新的发布,同时我们的服务也需要进行初始化。虽然这一过程可能只需几分钟、几秒甚至是几微秒,但还是造成了宕机时间。如果实施了微服务与持续部署实践,那么发布的次数会比之前更频繁。最终,我们可能会在一天之内进行多次部署。无论决定采用怎样的发布频率,对用户的干扰都是我们应当避免的。

应对这一问题的解决方案是蓝-绿部署。简单地说,这个过程将部署一个新发布,使其与旧发布并行运行。可将某个版本称为“蓝”,另一个版本称为“绿”。由于两者是并行运行的,因此不会产生宕机时间(至少不会由于部署流程引起宕机)。并行运行两个版本的方式为我们带来了一些新的可能性,但同时也造成了一些新的挑战。

在实践蓝-绿部署时要考虑的第一件事就是,如何将用户的请求从旧的发布重定向至新的发布?在此之前的部署方式中,我们只是简单地将旧的发布替换为新的发布,因此它们将在相同的服务器与端口上运行。而蓝-绿部署将并行运行两个版本,每个版本将使用自己的端口。

有可能我们已经使用了某些代理服务(NGINX、HAProxy等),那么我们可能会面对一个新的挑战,即这些代理不能是静态的了。在每次新发布中,代理的配置需要进行持续变更。如果在集群中进行部署,那么该过程将变得更为复杂。不仅端口需要变更,IP地址也需要变更。为了有效地使用集群,我们需要将服务部署在当时最适合的服务器上。决定最适合服务器的条件包括可用的内存、磁盘和CPU的类型等。通过这种方式,我们能够以最佳的方式分布服务,并极大地优化可用资源的利用率。而这又造成了新的问题,最紧迫的问题是如何找到所部署的服务的IP地址与端口号。对这个问题的答案是使用服务发现。

服务发现包括3个部分。首先需要通过一个服务注册中心保存服务的信息。其次需要某个进程对新的服务进行注册,并撤销已中止的服务。最后需要通过某种方式获取服务的信息。举例来说,当部署一个新的发布时,注册进程需要在服务注册中心保存IP地址与端口信息。随后,代理可发现这些信息,并通过信息对本身进行重新配置。常见的服务注册中心包括etcd、Consul和ZooKeeper。可以使用Registrator来注册和撤销服务,以及用confd和Consul Template来实现服务发现与创建模板。

现在,我们已经找到了一种保存及获取服务信息的机制,可利用该机制对代理进行重新配置,唯一一个还未解答的问题就是要部署哪个版本(颜色)。当我们进行手工部署时,自然知道之前部署的是哪个颜色。如果之前部署了绿色,那么现在当然要部署蓝色。如果一切都是自动化执行的,就需要将这一信息保存起来,让部署流程能够访问它。由于我们在流程中已经建立了服务发现功能,所以可以将部署颜色与服务IP地址和端口信息一同保存起来,以便在必要时获取该信息。

在完成了以上工作后,管道将变为图12-5中所显示的状态。由于步骤的数量增加了,因此将这些步骤划分为预部署、部署以及部署后3个组。

图12-5 蓝-绿部署

运行预集成以及集成后测试

虽然测试的运行至关重要,但它无法验证要部署至生产环境中的服务是否真的能够按预期运行。服务在生产环境上无法正常工作的原因是多种多样的,许多环节都有可能产生错误,可能是没有正确地安装数据库或是防火墙阻碍了对服务的访问。即使代码按预期工作,也不代表已验证了部署的服务得到了正确的配置。即便搭建了一个预发布服务器以部署我们的服务,并且进行了又一轮测试,也无法完全确信在生产环境中总是能够得到相同的结果。为了区分不同类型的测试,Viktor Farcic将其称为“预部署(Pre-Deployment)”测试,这些测试的相同点是在构建与部署服务之前运行。

蓝-绿部署流程为我们展现了一种新的机会。由于旧发布与新发布是并行运行的,我们可以对新发布进行测试,随后再对代理进行重新配置,以指向新的发布。通过这种方式,我们可以放心地将新发布部署至生产环境并进行测试,而代理仍会将我们的用户重定向至旧的发布。

Viktor Farcic将这一阶段的测试称为“预集成(Pre-Integration)”测试。

它意味着我们在将新发布与代理服务集成之前(在代理进行重新配置之前)所需运行的测试。这些测试可以让我们忽略预发布环境(这种环境与生产环境永远做不到完全一致),使用对代理重新配置之后用户将使用完全相同的配置对新发布进行测试。

最后,当我们重新配置代理之后,还需要再进行一轮测试。这一轮测试称为“集成后(Post-Integration)”测试,如图12-6所示。这一过程应当能够快速完成,因为唯一需要验证的就是代理是否确实正确地配置了。通常来说,只需对80(HTTP)与443(HTTPS)端口进行几次请求作为测试。

图12-6 预集成以及集成后测试

回滚与清理

如果在整个流程中有任何一部分出错,整个环境就应当保持与该流程尚未初始化之前相同的状态,即状态回滚。即使整个过程如计划般一样顺利执行,也仍然有一些清理工作需要处理。我们需要停止旧的发布,并删除其注册信息。

回滚与清理如图12-7所示。

图12-7 回滚与清理

决定每个步骤的执行环境

决定每个步骤的执行环境是至关重要的。按照一般的规则来说,尽量不要在生产服务器中执行。这表示除了部署相关的任务,其他任务都应当在一个专属于持续部署的独立的集群中执行。在图12-8中(此图请下载源文件查看原图,见资源包文件),我们将这些任务标记为黄色,并将需要在生产环境中执行的任务标记为蓝色。请注意,即使是蓝色的任务也不应当直接在生产环境中执行,而是通过工具的API执行。举例来说,如果使用Docker Swarm进行容器的部署,那么无须直接访问主节点所在的服务,而是创建DOCKER_HOST变量,将最终的目标地址发送到本地Docker客户端。

图12-8 决定每个步骤的执行环境

完成整个持续部署流

现在我们已经能够可靠地将每次签入部署至生产环境中了,但我们的工作只完成了一半。另一半工作是对部署进行监控,并根据实时数据与历史数据进行相应的操作。由于我们的最终目标是将代码签入后的一切操作实现自动化,因此人为的交互将会降至最低。创建一个具备自恢复能力的系统是一个很大的挑战,它需要我们进行持续的调整。我们不仅希望系统能够从故障中恢复(响应式恢复),同时也希望尽可能第一时间防止这些故障出现(预防性恢复)。

如果某个服务进程出于某种原因中止了运行,系统应当再次将其初始化。如果产生故障的原因是某个节点变得不可靠,那么初始化过程应当在另一个正常的服务器中运行。响应式恢复的要点在于通过工具进行数据收集、持续的监控服务,并在发生故障时采取行动。预防性恢复则要复杂许多,它需要将历史数据记录在数据库中,对各种模式进行评估,以预测未来是否会发生某些异常情况。预防性恢复可能会发现访问量处于不断上升的情况,需要在几个小时之内对系统进行扩展。也可能每个周一早上是访问量的峰值,系统在这段时间需要扩展,随后在访问量恢复正常之后收缩成原来的规模。

本文给大家讲解的内容是分布式系统开发实战: 虚拟化与容器技术,基于容器的持续部署

  1. 下篇文章给大家讲解的是分布式系统开发实战: 虚拟化与容器技术,容器技术与微服务架构;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
(0)

相关推荐