经过七年的发展,gilt.com已经从一个使用Ruby on Rails开发的创业公司成长为使用Scala微服务架构的主流电子商务平台。Gilt的限时抢购商业模式的基础是:在短时间内会涌入大量的客户访问,以竞买某些限量的奢侈品。通过使用微服务架构,它为我们的服务提供了可伸缩性、性能以及可靠性的结合,同时也为我们的开发团队带来了自治性、自主性以及灵活性。团队可以自由地选择编程语言、框架、数据库以及构建系统,为核心的网站功能、移动应用、个性化算法、实时数据源以及通知功能创建服务。
随着软件服务的大爆发,随之出现的问题是如何部署与运行我们所创建的代码。在Gilt发展的早期,它们的软件运行在“裸机”之上,部署在一个、随后升级为两个传统的数据中心之内。团队需要申请新的硬件设置,随后通过自定义脚本和Capistrano的混合使用进行部署。这种方式虽然能够运作,但也带来了很大的困难。对虚拟化技术的首次尝试表现为在每日的访问高峰时性能表现不佳,大量的服务最终在同一个物理环境内相互交织在一起,导致了性能方面的异常,并且有时会由于缺乏资源的分离性而导致停机。在访问高峰时,对于某个服务进行大规模的部署会占用“每次请求的多个线程”,由此造成了整个网站的瘫痪。
我们曾经尝试了多种技术,并且创建了各种工具,通过在生产环境中进行测试和持续部署等手段试图减轻这方面的问题。但经过研究发现,我们用于在数据中心的硬件上分配并自动设置服务的方式已经被证实是一种非常耗时、并且非常困难的方式。
因此我们开始考虑将微服务基础设施大规模地转移到云环境中,主要是指Amazon Web Services(AWS)环境,并且提炼出不可变部署(Immutable Deployment)这一概念,它是由我们之前在函数式编程中所使用的不可变变量的经验所启发的。我们将一套原本基于自身的数据中心打造的工具“ION-Cannon”升级为一套即将开源的工具“ION-Roller”,促使我们开发ION-Roller这套工具的动力在于:
在本文中,我们将谈论ION-Roller中的一些核心概念,以及相关的技术和实现途径,并且简要地叙述它的使用方式。
我们首先将微服务世界想象成为构建在独立的、不可变的环境之上的HTTP终结点,通过REST API的方式进行访问。
Gilt使用HTTP作为系统间通信的传输封装,对于用户来说,HTTP终结点表现为一个主机名与端口的组合(在它之上还有一个发现层)。我们的想法是,当组织中的软件与配置随着时间而演变时,将这个终结点作为一个组织级的概念进行使用。
虽然这个概念很简单,但可以实现许多实用的目标。通过对代理与/或网络配置的合理应用,我们就能够提供以下特性:
在运维规模非常小的情况下,在部署过程中要求得到不受限制的灵活性看起来是一种合理的诉求。但随着规模的增加,这种方式很快就变得难以管理、监控及理解了。
声明式的配置在描述及管理部署方面是一种实用的工具,它能够以一种结构化的风格进行管理。从配置的角度来说,它能完成的任务比起代码来说更为有限,但也因此能够对请求进行分析与报告。
软件发布包括了各种权衡:灵活性与简便性、以及速度与安全性。
大多数生产环境的发展方向是转向新的部署流程,这种流程能够在特性开发或bug修复完成之后在更短的时间之内让用户看到这些变更。这也表示对生产环境进行变更的机率更高。由于每次发布都内含着风险,因此这一策略也有它的不利之处。为了抵消这种不利情况,团队需要采取一些能够缓解每次发布中的风险的发布流程,让每次发布都成为一个日常的、安全的操作。
风险体现在多个方面:异常的数量和严重度、受影响用户的数量及比例、以及从故障中恢复的时间长短。
发布流程对于异常的数量以及异常的严重度的影响并没有开发流程那么高,但频繁的发布通常能够使找到某个异常发生原因的速度更快(因为每个发布中的变量数量减少了)。
我们所能做的就是减少受影响的用户数量,以及减少修复的时间。在生产环境上进行测试、部分发布以及金丝雀部署(canary release)等方式正适用于这一场景。如果能够在没有任何生产环境中访问的情况下对新的发布进行测试,那么就能够将影响降至最低(甚至为零),但这种方式能够找到的异常数量也是最少的。由于许多异常情况只在常规的访问中才会出现,因此一台金丝雀机器(运行新软件的一个单一实例)是很有价值的工具,通过它可以了解新的发布是否适于使用。目前为止,对用户的影响依然是很小的,这取决于你如何对服务进行分片。保持渐进式的发布过程也是通过将受影响的比例最小化,将bug对用户的影响降至最低。
从故障中恢复的时间是不可变软件方式的一个主要优点,因为旧版本的软件依然“可用”。一旦发现异常情况,可以以最小的风险在最短的时间内回到之前的旧版本。我们稍后将进一步对不可变部署展开讨论。
我们希望持续地监控某个终结点背后系统的期望运行状态以及实际运行状态的差别。如果这种差别确实存在,那么我们就应当(逐渐地)让实际状态向期望状态靠拢。我们持续地对运行环境进行大量的测试(包括运行的软件、负载均衡器设置、DNS等等),如果其中任何一项不能够满足需求,就将它替换掉。
举例来说,假设我们知道访问应当由某个特定版本的软件进行处理,却发现没有任何一台运行这个版本的服务器是可用的(可以出于任何原因,包括某人无意中移除了这些服务器),那么就应当请求进行部署。
“不可变部署”的思想是通过一个已定义的、易于理解的配置部署软件,而且这个配置不会随着时间而改变。软件与配置都是发布中的自然组成,你不能通过修改数据中的某些地方随后重启的方式对软件进行变更,而是重新生成该软件的一个副本,让其中包含经过更新的细节内容。
这种思想能够产生更易于预测的环境,并且当某个新发布被证实“有问题”的情况下,能够回滚至软件的老版本而无需进行重启。(即使能够找到老版本的软件,在有些情况下要完全启动它也是一件费时费力的任务,因为它可能需要进行加载缓存等工作。)
在一个小规模的环境中,可能只存在少量的运行服务器,因此可能只有在发布完全结束之后,才能够发现新的发布中出现的异常情况。在传统的非不可变发布场景中,所有旧版本的软件都已经被替换了。
不可变部署方式在我们目前的服务中的实现还存在着一点不足之处。因为所有的环境都是按需搭建的,因此搭建时间取决于某些因素,例如启动新EC2实例的时间、以及下载适当的Docker镜像的时间。与之相比,一些其它部署工具要求存在一个机器池,不要求在软件安装之前搭建机器。因此我们选择为可回滚性牺牲了一些低延迟特性,因此在软件发布后的初次启动会有较高的延迟。
在微服务环境中,将工具与编程语言的选择从部署环境中抽象出来是一种非常实用的概念。它允许个别团队在开发软件时具有更多的自治性。虽然这方面的选择已经有许多,从特定于操作系统的包管理系统 —— 例如RPM,到特定于云提供商的平台 —— 例如Packer,但我们最终还是决定用Docker实现这一目标。Docker在选择部署环境与策略方面提供了很高的灵活性,并且十分符合我们的需求。
当我们在寻找能够方便地管理基于不可变web服务器的基础设施时,所找到的选择是非常有限的。大多数现有的软件都是为了便于软件更新进行优化的,但这不是我们所感兴趣的更新方式。我们也希望能够管理web服务器流量的整个生命周期,但没有发现任何一种对这一任务进行优化的选择。
另一点需要考虑的是,到底是要求开发者学习并理解某种现有的部署工具集所带来的价值更高,还是说通过一种无冲突的、简化的工具,能够满足我们将某个特定版本的软件部署到生产环境中的方式能够带来更高的生产力。我们相信,开发者应当能够简单地发布软件,而无需理解复杂的底层机制(同时也保留进行自定义的可能性,这在一些高级场景中是必需的)。
许多工具采取的都是以机器为中心的视角,配置是在每一台机器的基础上完成的。我们则采取了系统状态这一更广泛的视角(比如我们需要某个软件的四个拷贝以提供服务),而并不太关心个别机器的情况。我们利用了AWS所提供的高层概念,例如将机器运行情况检查时总是失败的那些机器进行替换,以及当HTTP终结点的访问量激增的情况下动态地扩展它的能力等等。
CodeDeploy是一套由Amazon提供的软件发布管理系统,但它无法满足我们支持不可变基础设施的需求。虽然很容易搭建脚本,使用CodeDeploy发布软件,但你需要预先搭建你的环境。此外,CodeDeploy本身没有内置的部署Docker镜像的功能。
Elastic Beanstalk提供了创建环境的各种功能,它允许你运行Docker镜像,并且创建大量的支持性系统,例如EC2实例、负载均衡器、自动扩展组(根据访问量改变服务器的数量)。它还允许对日志文件的访问,并且可以对这些系统进行一定程度的管理工作。
但是,它对于“不可变部署”概念的支持非常有限,对于某部分软件的多次发布将为同一个用户可见的终结点带来大量的访问量,然后逐渐地转移访问量。对于这一点,它唯一支持的能力就是“切换CNAMEs”,这是一种非常粗糙的转移访问量的方式,因为所有的访问量都会瞬间转移到新的环境中。此外,出于DNS的本质,它在可靠性方面也存在问题,因为DNS查找结果在DNS进行变更后依然会被缓存一段时间,并且某些糟糕的客户端会忽略DNS TTL值,导致访问量依然在很长的一段时间内会被发送给旧的环境。
此外,Elastic Beanstalk没有提供某种高层次的结构,以帮助你理解在某个终结点的背后有哪些东西运行在生产环境上。“有哪些运行正在运行,它们的配置情况又是怎样的”这一问题并不容易解答,需要某些高层次的系统辅助。
我们决定利用Elastic Beanstalk作为一种实用的方法以部署Docker软件,但需要有层次地进行适当的管理,并且对它的层次进行控制,以便为用户提供一个完整的工作流。
ION-Roller是一套服务(包含API、web应用和命令行界面),它利用了Amazon的Elastic Beanstalk及其底层的CloudFormation框架的功能,将Docker镜像部署到EC2实例中。
启动部署软件过程的全部工序如下:
如果想了解使用AWS帐户安装ION-Roller的细节与完整的文档,敬请期待即将开源的这个项目https://github.com/gilt/ionroller。
你可以通过内置的命令行工具简单地启动部署过程:
ionroller release <SERVICE_NAME> <VERSION>
这个工具能够在发布过程中提供即时的反馈信息:
[INFO] NewRollout(ReleaseVersion(0.0.17)) [INFO] Deployment started. [INFO] Added environment: e-k3bybwxy2f [INFO] createEnvironment is starting. [INFO] Using elasticbeanstalk-us-east-1-830967614603 as Amazon S3 storage bucket for environment data. [INFO] Waiting for environment to become healthy. [INFO] Created security group named: sg-682b430c [INFO] Created load balancer named: awseb-e-k-AWSEBLoa-A4GOD7JFELTF
你也可以选择以编程的方式触发一次部署过程,ION-Roller提供了一套REST API,可以对你的配置和发布过程进行完全控制。
在系统的背后,ION-Roller会触发一个Elastic Beanstalk部署流程,充分地利用了它的功能,包括创建一个负载均衡器、安全以及自动扩展组、设置CloudWatch监控,并从某个Docker registry中获取特定的Docker镜像。
一旦ION-Roller检测到部署成功之后,它就能够安全地逐渐将访问从老版本转移到新版本的服务。
访问的重定向是通过对EC2实例的修改实现的,它能够通过负载均衡器对HTTP请求进行响应。随着发布的流程推进,新部署的实例会逐渐加入负载均衡器的配置中,而原先部署的实例会逐步被删除。这一流程的启动时机是可配置的。
渐进式的访问重定向使你能够对最近的发布进行监控、快速地检测到失败、并在必要时进行回滚。
由于在发布过程中,老的环境依然可访问,老版本的软件也依然在运行中,因此我们可以安全地将软件回滚到之前的某个老版本。无用的旧实例在一段时间(可配置)之后才会被移除,由于这段延迟的存在,因此我们在发布全部完成之后的一段时间内仍然可以选择回滚。
我们的目标是持续地监控终结点的运行情况,并且在检测到异常的情况下自动地回滚到老的版本。我们将使用Amazon的CloudWatch警告功能发出一个即将进行回滚操作的信号。
进行手动回滚非常简单,运行以下脚本即可:
ionroller release <SERVICE_NAME> <PREVIOUS_VERSION>
如果旧的实例仍然可用,那么只需几秒钟的时间即可将访问全部转回老版本上。当然,前提是你没有进行任何使老版本的软件不可用的操作(例如更新数据存储系统的schema等等)。如果你希望依靠这种能力以回滚软件,那么牢记这一点是非常重要的,无论你使用的是哪一种部署系统。
ION-Roller通过对访问转移流程的配置,支持金丝雀发布的概念。当有新的版本部署到一些初始的实例之后,该流程就会停止,可以对生产环境中的访问进行一些发布测试。在一段可配置的时间之后,发布流程将会继续推进。
对于某些用例,你希望能够对新的软件进行测试(或演示),而又不希望产生实际的访问,那么我们需要ION-Roller搭建一个分离的HTTP终结点,在生产环境的终结点更新之前,可以通过它处理请求。
ION-Roller能够查看某个终结点背后的环境与软件随着时间推移产生的变化,在对环境进行监控或进行变更的过程中,它将记录与环境生命周期或部署活动相关的事件。可以通过这一功能实现系统的审计、监控以及报表功能。ION-Trail是一个支持性的服务,为所有被记录的部署活动提供了一个事件源。
希望读者在阅读本文后能够对于我们将微服务架构部署到AWS的思想有所了解。ION-Roller允许我们将整个DevOps组织实现去中央化,并且通过声明式的部署让工程师更易于掌握。ION-Roller允许我们进行分阶段的发布与热回滚,并且支持诸如“金丝雀发布”与“在生产环境中测试”等特性。如果想了解更多的信息,请关注Gilt Tech博客(http://tech.gilt.com),我们很快就会在博客上宣布ION-Roller开源项目的公开发布。
Natalia Bartol曾在IBM担任Eclipse支持工程师,从事改善开发者工具的工作,随后在Zend Technologies担任Eclipse开发人员及团队主管。如今她在Gilt担任软件工程师,专注于微服务的部署以及提高开发者的生产力。Natalia拥有波兹南科技大学软件工程专业的理科硕士证书。
Gary Coady是来自Gilt Groupe的一位高级软件工程师,他的工作是自动化部署、编写构建工具以及传授Scala技术。他之前曾在Google担任网站可靠性工程师,将大量时间用于剖析、管理以及处理大规模环境中的运维异常。Gary拥有都柏林大学圣三一学院计算机科学专业的学士学位(Mod)。
Adrian Trenaman在位于爱尔兰都柏林的gilt.com担任工程师高级副总裁。他拥有爱尔兰国立梅努斯大学计算机科学专业的博士学位,和爱尔兰管理学院的商业开发专科文凭,以及都柏林大学圣三一学院计算机科学专业的学士学位(Mod. Hons)。
查看英文原文:Deploying Microservices to AWS at Gilt: Introducing ION-Roller