在传统企业甚至互联网企业中往往存在大量的遗留代码,这些遗留代码大多都能够正常工作,有的可能还运行着关键业务或者持有核心数据。但是,大部分遗留代码通常经常存在技术陈旧、代码复杂、难以修改等特点。随着时间的推移,遗留代码的维护和管理的成本越来越大。在全面转型微服务的今天,这些遗留代码该如何处理呢?Tomasz Kania-Orzeł 为我们阐述了升级遗留代码的最佳实践,我相信,这篇文章对于拥有大量遗留代码的企业 / 组织很有借鉴意义。
“我在 Ruby on Rails 上有一款可以追溯到 2011 年的应用,在过去五年来没有添加任何新功能。而且它现在运行死慢死慢的,随着我们用户群不断地增长,它已经几乎不能提供服务了。您能帮我们解决这个问题吗?”
这是我在 Monterail 的客户那儿遇到的最常见的情景之一。这种难以维护且存在安全漏洞的遗留代码,对于必须使用它的企业,以及必须处理它的开发人员(比如我们)来说,真不啻噩梦一场。在我担任软件工程师十年左右的时间里,有过很多机会让我得以观察一些开发人员为了更新 Web 应用程序中的遗留代码而进行的技术转变,这些技术转变有成功,也有失败。例如,这可能意味着,从某个框架的版本 2 升级为版本 6,或者从 Ruby 变更为 Python,或者从单体应用转变为微服务架构,或者从手工构建更改为持续交付。为了完成一次无痛(或至少不那么痛苦)更新,你必须确定进行更改是否有必要,确定最合适自己的方法并承诺长期践行。
糟糕的性能是做出技术变更的原因。另一个原因就是你所使用的技术的普及程度逐渐或突然下降了。毕竟,如果市场上能够支持你工作的开发人员越来越少,那么你的技术存在被封闭的风险就越来越大。有些人早在 2010 年就用 Backbone 构建了他们的应用程序,如今却在努力解决“模型 - 视图 - 控制器”的问题,而其他人都在使用基于组件的框架,如 React 或 Vue 等。如果你选择的框架正在失去积极的支持,那么风险就会更大。还记得 AngularJS 吗?2018 年 7 月,它就进入了长期支持阶段,这意味着 Google 不会再合并新的功能或修补程序,哪怕是一个微小的突破性改动。
译注:“模型 - 视图 - 控制器”(Model–view–controller,MVC),是是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 最早由 Trygve Reenskaug 在 1978 年提出,是 Xerox PARC 在 20 世纪 80 年代为程序语言 Smalltalk 发明的一种软件架构。MVC 的目的是实现一种动态的程式设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,MVC 通过对复杂度的简化,使程序结构更加直观。
当你的技术在人力或机器成本方面过于低效、过于昂贵,这不仅是发起技术变更的一个好机会,而且也可能是你在技术变得不可挽回之前修复它的最后机会。你永远不会想达到这样的地步:创建一个新功能是完全不可行的。欧洲电子商务公司 Zalando 正努力快速扩展其单体 PHP 应用程序,却 无法以快速或高效的方式提供新功能。这促使他们在 2016 年从单体架构应用转向微服务,使不同的团队能够以更快的速度交付功能。
一旦确定你正在开发的产品需要升级,那么就是时候对变更方向做出明智的决定了。代码一旦编写就会成为遗留代码, 没有人能保证你当时编写所用的技术在未来不会失去支持或变得过时。(安息吧,AngularJS。)因此,变更应该支持未来的灵活性。以下是一些选项:
第一个也是最明显的选项,就是大爆炸式的重写:从头开始更改代码库,并在一次转换中切换所有用户。但是,完全重写是非常耗时的,而且必然也会产生相当巨大的成本。你也有可能最终得到的是一款在几个月甚至几年内都不适合发布的应用程序,并且在这个过程结束之前,你都无法看到最终结果。另外,应用程序越大,开发者在重写过程中提供维护和添加新功能的难度就越大。如果有文档和技术知识的话,向大型代码库添加新功能就像在公园里散步一样简单。若没有这些的话,要做到这些,真的很难。
第二个选项是在现有代码库中添加使用新技术构建的新功能。理想情况下,你不应该触及旧技术,而应该将所有新功能分离开来。但不幸的是,这样的原始结果相当罕见:新功能几乎总是需要与旧功能集成。这也需要一个详细的计划,因为事情很复杂,没有周密计划的话很难做好。在复杂的架构中创建新的组件,需要大量关于遗留应用程序中组件运行情况的信息。你需要一个广阔的视角,从新旧两个角度看待这项技术。
使用单体应用程序,你可以在单独部署的新代码库中创建新功能,同时使用与新代码和遗留代码交互的单个数据库来存储数据。这个解决方案看起来很简单,但它能否取得长期成功,要取决于你的承诺是否坚如磐石。(特别是当你的整体系统受到机器性能或并发性问题的影响时,就需要考虑这些问题。)例如,如果你的单体应用程序开始获得更多的用户,那么,单个数据库可能会成为瓶颈。(另一方面,云端中的数据库可以扩展。)由于原始代码库的长期脆弱性,特别是考虑到潜在的安全漏洞或 bug,这种架构并不能持久。实际上,让过时的代码保持原样就意味着你在等待它最终永远失败。
重写整个代码库是一个极端的想法,而且往往考虑不周。在旧代码库上添加新功能更可行,但会带来严重的副作用,例如,如果你的遗留代码是基于较旧的框架版本,那么就会出现安全问题。那么, 还有什么事情是你可以做的,而且不那么昂贵,或者不那么危险的? 你还有其他选择吗?
我推荐一种混合方法。这一选项,就像大爆炸式重写一样,也需要对整个旧代码进行变更。但与第一种选项不同的是,重写应该是在一段时间内展开(比如说几年),以最大限度地减少技术债务和财务成本。这种渐进式方法将基于你的愿景,这一点至关重要。你需要知道首先要改变什么:有些功能是核心的,对业务至关重要,而其他功能则更多的是扮演辅助角色。有了明确的目标,在多个层面上工作就会变得更容易。例如,你是否仍然计划在这个转换过程中发布一项新功能?如果是这样的话,你就需要在计划中考虑到这一点。如果没有清晰的路线图,你的代码最终将会变得一团糟,这将会使开发者更难理解。
对我来说,这种方法关乎未来和可扩展性:而且它最容易使用微服务来实现。假设你的遗留代码库是新的微服务生态系统中的一个元素。当然,它太过于庞大,过于复杂,不可能是真正的微服务,但它可以像微服务一样与新功能进行通信。为了处理这种安排,你需要创建 API 或“桥”这样的接口,以允许遗留代码与新技术进行通讯。当你以单独的微服务形式添加新功能时,它们将会一点一点地吞噬遗留代码的业务逻辑。虽然你可以通过向遗留的单体应用程序添加新功能来实现类似的功能,但此举可能会造成技术债务,并失去灵活性。
几乎所有的解决方案都有副作用,包括这种方法。但是我们需要知道如何将副作用最小化。对于 Web 应用的前端,反向代理可以缓和这一变更带来的副作用。使用这种方法,你甚至可以在不触及遗留软件的情况下,替换 Web 应用中为单个 URL 提供服务的逻辑。但这种技术有其自身的要求,例如,如果你有用户登录的话,就应该维护页面之间的状态。我们始终需要存储状态,但在这个解决方案中,我们需要在两个应用之间移动或共享状态。这很难维护,但你还是可以得到更具弹性的基础设施。
更复杂的更改需要对基础设施进行改进,比如,创建一个前端服务器层,你可以从其中呈现来自不同源的应用程序片段,如上面的微服务示例。基于 XML 的标记语言 ESI(Edge Sides Includes)可能适合这项任务,而 Varish 或 Nginx 然后,为了确保你的应用在用户群增长时能够保持性能,请创建负载均衡器和基于上下文的独立数据库,这些数据库在微服务或宏服务中单独使用。
创造一个能够支持这种安排的灵活环境也是一个挑战。转移到微服务时,你只需在基础设施上投资一次,但你将需要进一步支持这种架构的维护。尽管如此,它可能仍然比重写所有代码要便宜得多。
如果你的主要目标是创建一个易于维护的生态系统(而不是关注性能第一,维护第二),你还需要在开发过程中确定系统的关键元素,并创建路线图来对它们进行更改。在这个过程中引入一些持续集成和部署的魔法,其中,CI 和 CD 流程可以在没有 QA 或开发人员的帮助就能自动进行,然后你最终将得到一个结构清晰、易于修改和调整的成熟软件。
当然,这种混合方法并不是世界上唯一可行或正在使用的选项。但是,代码库的增量更改最终导致了完全重写,你得以能够使用工作代码,从而使业务保持安全,同时,微服务使不同团队能够独立地交付新的和不同的功能,提供了一个为长期使用而设计的过程和架构。
你可能会看着我的首选解决方案,然后心想,“嗯,这有点过于工程化了”,或者“我没有一个团队能胜任这种工作”,或者“这对我的平台来说太过于复杂”,或者甚至“这不是纯粹的微服务架构!”我并不反对你这些想法,但我确实认为, 升级你的技术将会迫使你作长远考虑。
我提供的并不是快速解决方案。相反,混合方法为你提供了基于工作基础之上的新技术。通过逐步转向微服务,增量更改允许你轻松地更新应用程序,并利用最新的框架,所有这些都不会迫使你在可靠性上作出妥协。那么,你准备好重新构建你的软件了吗?
Tomasz Kania-Orzel,是一名经验丰富的软件和 DevOps 工程师,最喜欢研究前端技术。目前在 Monterail 担任技术主管,领导一支由 75 名开发人员组成的团队。