2.1 面临的困难
毫无疑问,生产环境的运维是一项困难且常常“吃力不讨好”的工作。在软件正常发布或者意外停机的时候,运维人员甚至还需要熬夜工作甚至在周末加班。应用程序的开发团队和运维团队之间出现冲突是常有的事情,他们都指责对方没有为客户提供更好的产品体验。
但正如我所说的,这并不是运维团队或者开发团队的过错。我们所面临的挑战在于不经意间设置了一系列阻碍成功的行为方式。尽管面临的每个挑战都不同,各有各的原因,但几乎所有企业都存在几个常见的问题(如图2.1所示),总结如下:
■ 碎片化的变化—在软件开发生命周期中存在不断出现的变化,不仅会对初次部署造成影响,还会影响应用程序后续的稳定性。这种情况会导致部署的软件与部署环境之间存在着不一致性。
■ 有风险的部署—如今,软件部署的环境已经非常复杂,许多组件之间紧耦合、相互关联。因此,部署在复杂网络中的某个组件如果发生了改变,可能会引起系统的其他部分出现连锁反应,从而导致很大的风险。这种部署所造成的恐惧感会产生下游效应,降低后续部署的频率。
■ 认为变化是例外—在过去的几十年里,我们在编写和运行软件的时候,通常都希望软件底层依赖的系统是稳定的。这种想法曾经常常受到质疑,但是现在,随着IT系统变得复杂和高度分布式化,这种对基础设施稳定性的期望已经是完全错误的了。[1]这种想法导致的结果是,基础架构的任何不稳定都会影响到正在运行的应用程序,使得程序难以保持正常运行状态。
■ 生产环境的不稳定性—最后,因为部署到不稳定的环境中通常会带来更多的麻烦,所以限制了生产环境的部署频率。
图2.1 导致软件难以部署和无法在生产环境中保持良好运行状态的原因
我们在后面将进一步讨论这些问题。
2.1.1 碎片化的变化
“在我的机器上是好的”,当运维团队正在努力解决生产环境中的系统问题并向开发团队求助时,经常会听见这种说法。数十家大型企业的专业人士告诉我,从软件准备发布到用户可以使用,中间通常会有6周、8周甚至10周的延期。造成这种延期的主要原因之一是,在软件开发生命周期(SDLC)中通常不断发生变化。这种变化主要会发生在两个方面:
■ 环境发生变化
■ 部署的构件发生变化
如果没有一种机制来为从开发到测试、预发布和生产提供完全相同的环境,那么在一个环境中能够良好运行的软件,很容易在不经意间会依赖于另一个环境中缺少或者不同的东西。一个常见的例子是,所部署软件的依赖包存在差异。例如,开发人员可能会不断更新到Spring框架的最新版本,甚至将其作为自动化构建脚本的一部分。但是生产环境中的服务器控制更加严格,对Spring框架的更新每季度进行一次,而且必须经过完整的审计。因此当系统更新到新版本时,测试无法通过,可能需要回溯到开发人员那里解决,比如改用生产环境要求的依赖项版本。
但是,并不只是环境上的差异会降低部署的频率。即使是正在部署的构件也经常会在软件开发生命周期中发生变化,哪怕我们没有将与环境相关的值硬编码到软件中。(我们都不会这样做,对吧?)属性文件中的配置通常会直接被编译到可部署构件中。例如,Java应用程序的JAR文件会包含一个application.properties文件,如果该文件中的某些配置在开发、测试和生产环境之间有所不同,那么对应开发环境、测试环境、生产环境的JAR文件也必须是不同的。从理论上讲,各个JAR文件之间应该只是在属性文件的内容上有差异,但是任何对可部署构件的重新编译或者重新打包,都有可能而且经常会不经意地引入其他的差异。
这些碎片化的变化不仅会影响初次的部署,也会大大加剧运维的不稳定性。例如,假设你有一个应用程序已经运行在生产环境中,大约有50000个并发用户。虽然这个数字通常波动不大,但你希望系统能够承载更多的用户。于是,在用户验收测试(UAT)阶段,你使用了两倍的流量进行压力测试,而且所有测试都通过了。随后,你将应用程序部署到了生产环境中,在一段时间内运行一切正常。但是,在星期六凌晨2点,系统发生了拥堵。突然出现了超过75000个用户,而且系统正在崩溃。不过等等,你已经在UAT中测试了多达100000个并发用户,那么这究竟是怎么回事呢?
原因就在于环境的不同。用户会通过套接字(socket)连接到系统,套接字连接需要打开文件描述符,而Linux系统有一个配置会限制打开文件描述符的数量。在UAT环境中,/proc/sys/fs/file-max中的值是200000,但是在生产环境服务器上是65535。由于UAT和生产环境之间的差异,在UAT中运行的测试无法测试出生产环境中出现的问题。
更糟糕的还在后面。在诊断出问题并增大/proc/sys/fs/file-max文件的值之后,因为情况紧急,没有运维人员记录下这一次改动的原因和过程。随后,在新配置的一台服务器上,文件描述符的最大值再次被设置为65535。如果软件安装在新的服务器上,同样的问题最终会再次发生。
还记得上面提到需要在开发、测试、预发布和生产环境中修改属性文件,以及这对部署的影响吗?假设最终你已经部署并运行了所有程序,但是基础设施发生了一些变化,例如,服务器名称、URL或者IP地址发生了变化,或者添加了几台新的服务器。如果这些环境配置存在于属性文件中,那么你必须重新打包可部署的构件,并且很可能会引入其他一些意想不到的问题。
虽然这听起来可能有些极端,而且我也希望大多数企业都能够控制这种混乱状况,但是除技术最先进的IT部门外,导致不断变化的因素存在于其他所有部门中。定制化的环境和部署构件显然会给系统带来不确定性,但是根本的问题在于我们接受了这种存在风险的部署行为。
2.1.2 有风险的部署
你们公司一般在什么时候发布软件?是在“休息时间”完成的吗,例如,周六凌晨2点?这种做法之所以普遍,是因为一个简单的事实:部署通常充满危险。在升级期间需要停机或者部署时引起意外停机都是很正常的,而停机的代价是昂贵的。如果你的顾客不能在你的网站上订购比萨,他们就会转向竞争对手的网站,从而直接导致你的收入损失。
为了应对昂贵的停机,许多企业已经创建了大量的工具和流程来减少与发布软件相关的风险。这些努力的核心在于,通过大量的前期工作来减少失败的可能性。在预定部署之前的几个月,我们会每周组织一次例会来讨论“如何提交到下一个环境”,并且将变更控制审批作为防止生产环境发生意外的最后一道防线。就人员和基础设施资源的成本而言,最昂贵的可能就是在一个“与生产环境完全一样”的环境中进行测试。原则上,这些想法听起来合情合理,但在实践中,它们最终会给部署过程本身带来沉重的负担。以部署为例,我们来更深入地了解一下,如何在一个与生产环境完全一样的环境中测试部署过程。
搭建这样一个测试环境需要大量的成本。首先,我们不仅需要两倍的硬件,还要加上两倍的软件,这样仅资金成本一项就增加到了两倍。然后还有保持测试环境与生产环境一致的人工成本,以及大量的需求带来的复杂性,例如,在生成测试数据时,需要清除生产数据中的个人身份信息。
一旦测试环境搭建起来,你必须面对数十个或者数百个希望在产品发布之前进行测试的团队,为它们精心安排和协调对环境的使用。从表面上看,这似乎是一个日程安排的问题,但是大量不同团队和系统的组合,会让这很快变成一个棘手的问题。
以一个简单的情景为例,假设你有两个应用程序,一个用于付款的PoS(比如收银机)程序,一个允许客户下订单并使用PoS程序付款的SO(私人订制)程序。现在,每个团队都准备发布各自程序的一个新版本,并且都必须在预发布环境中进行一次测试。如何协调这两个团队的需求?一种选择是一次测试一个应用程序,尽管按顺序执行测试会延长发布时间,但是如果每个测试都可以通过,那么这个过程相对来说是比较容易处理的。
图2.2显示了以下两个步骤。首先,使用v4版本的SO应用程序与v1版本(旧版本)的PoS应用程序进行测试。如果成功,就将v4版本的SO应用程序部署到生产环境中。现在测试环境和生产环境都在运行v4版本的SO应用程序,并且还在运行v1版本的PoS应用程序,测试环境和生产环境一致。现在可以测试v2版本的PoS应用程序了。当所有测试通过后,就可以将该版本的应用程序部署到生产环境,这样两个应用程序的升级就都完成了,并且测试环境和生产环境保持一致。
但是,如果升级SO系统的测试失败了怎么办?显然,你不能将新版本部署到生产环境中。但是现在在测试环境中应该怎么做?即使PoS系统不依赖于SO系统,你是否会将SO系统恢复到v3版本(这需要花费时间)?是因为依赖的顺序不对吗?SO系统需要v2版本的PoS才能开始测试吗?多久之后才能再次在测试环境中运行SO?
图2.3显示了两个备选方案,即使在这个虚拟场景中,情况也会很快变得复杂起来。在实际环境中,情况将会变得更加棘手。
图2.2 在所有测试通过的情况下,依次测试两个应用程序是很简单的
我的目标不是要解决这个问题,而是要证明即使是一个非常简单的场景也会很快变得异常复杂。我相信你可以想象,当你需要测试更多应用程序,或者同时测试多个应用程序的新版本时,这个过程将变得无法控制。当我们将软件部署到生产环境中时,无法提供保障的环境变成了一个巨大的瓶颈,同时团队面临着尽可能快地交付软件和尽可能好地交付软件的两难选择。最后,没有人能准确地测试生产环境中会出现的场景,而部署成了一件有风险的事情。
事实上,风险已经够大了,大多数企业在一年中都会遇到无法将软件部署到生产环境的情形。对于医疗保险公司来说,这可能是开放登记期间;对于美国的电子商务企业来说,这可能是感恩节和圣诞节之间的一个月;对于航空业来说,感恩节到圣诞节的这段时间也是非常宝贵的。尽管我们已经努力降低风险,但是部署软件依然很困难。
图2.3 一个失败的测试,会增加在预发布环境中进行测试的复杂度
正是由于这种困难,目前在生产环境中运行的软件可能不得不继续运行一段时间。我们可能很清楚应用程序和系统中的bug或者漏洞,正是因为它们的存在,我们才能不断提升客户体验和满足业务需求,但是无法解决它们,直到我们能够安排发布下一个版本。例如,如果一个应用程序存在已知的内存泄漏问题,经常导致系统崩溃,我们可能为了应急,会定期重新启动该应用程序。但是,如果该应用程序的工作负载增加了,可能会比预期更早地导致内存不足,同时意外的崩溃又会导致下一个紧急情况出现。
最后,低频次的发布会导致每次部署包含更多的变更,也会对系统其他部分造成更多的影响。即使部署完成,从直觉上讲,与众多其他系统的关联会更有可能导致一些意外的出现。有风险的部署对于运维的稳定性有直接的影响。
2.1.3 认为变化是例外
多年来,我与很多CIO及他们的员工进行了数十次谈话,他们都表示希望创建一些系统,为业务和客户创造各种价值。但事与愿违的是,由于不断面临紧急情况,不得不将注意力从创新行为上转移开来。我相信,导致员工处于持续“救火”模式的原因,是这些IT企业长期形成的一个普遍心态,认为变化是个例外。
大多数企业已经认识到让开发人员参与初次部署的价值。新推出的产品存在一定程度的不确定性,因此让深入了解产品的开发团队参与进来是非常重要的。但是在某种程度上,维护生产环境中系统的责任已经完全交给了运维团队,关于如何保持系统运行方面的知识,运维团队手里只有一本运维手册。虽然运维手册详细描述了可能的失败场景及其解决方案,但是更加深入思考一下不难了解,手册设定了一个假设,就是失败场景是已知的。但是,绝大多数情况不是这样的!
当一个新部署的应用程序在一段时间内保持稳定时,开发团队就会退出运维工作,这微妙地暗示了一种哲学,即某个时间点标志着变化的结束—从现在开始,一切都将保持稳定。于是,当一些意想不到的事情发生时,每个人都陷入混乱。因此,唯一不变的就是变化,对于云环境上的系统来说,它们都将持续经历不稳定。
2.1.4 生产环境的不稳定性
到目前为止,我所讨论的所有这些因素都毫无疑问地阻碍了软件的良好运行,而且生产环境本身的不稳定性,又进一步增加了部署的难度。将应用程序部署到一个已经不稳定的环境是不明智的,对于大多数企业,有风险的部署仍然是导致系统崩溃的主要原因之一。相对稳定的环境是进行部署的先决条件。
当IT部门把大部分时间都花在“救火”上时,我们几乎没有机会进行部署。考虑到生产环境不多的稳定时间,再结合前面提到的完成复杂测试周期占用的时间,留给部署的时间少之又少。这就形成了一个恶性循环。
正如你所看到的,开发软件只是为客户提供数字体验的一个开始。碎片化的变化,允许有风险的部署,将变化看作一种例外,这些都使得在生产环境中运行软件变得非常困难。如果想进一步了解它们对运维产生的负面影响,我们需要研究那些运转良好的企业,即那些“生于云计算”的公司。当你应用与它们一样的实践和原则时,就会形成一个优化了整个软件交付生命周期的系统,从开发到平稳运维。