许多企业正在转向平台工程,以扩充他们的开发团队并提升开发者体验,这有助于提高工程师的工作效率。然而,平台工程通常止步于持续集成 / 持续部署(CI/CD)管道。随着系统变得日益庞大和复杂,我们需要将平台工程的概念提升到更高层次——到代码层面——通过构建平台和抽象来减轻认知负担,简化和加速软件开发过程,并使得平台的维护和升级变得更加容易。这有助于减少跨公司级别的任务,例如修复臭名昭著的 Log4J 安全漏洞。在这个过程中,我们也应致力于减少每个微服务的资源占用,以降低云服务的成本。让我们从“平台”的概念转向“平台即运行时”。
复杂软件系统对开发者和组织的影响庞大且复杂的系统可能会对公司的创新能力和快速适应能力造成阻碍。这些系统通常要求开发者处理大量信息和问题,导致认知过载。作为一名工程经理,我亲身经历了这种情况。新功能开发,无论规模大小,都可能会因为需要解决众多横向问题而进展缓慢,例如网络协议、合规性和与核心业务需求并存的各种非功能性需求。对于像 Wix 这样的开放平台来说,情况尤其如此。Wix 作为一个开放平台,向第三方开发者提供了大量 API,要求所有服务都需要以相同的方式工作。我们有许多关于如何构建服务的指导原则,以及规模化和形成生态系统的最佳实践。
例如,软件平台可能规定每次数据库的修改操作都必须触发一个领域事件。开发者需要记住在每次数据库操作时定义并实现领域事件消息,这无疑增加了认知负担,消耗了更多的时间,并增加了功能的复杂性。系统中还有许多其他要求,例如系统可能需要支持多种语言和货币、确保遵守 GDPR 法规、处理“删除”通知、实现乐观锁或在每个数据库模型中除了记录最后更新时间外,还应包含版本控制字段等,以及与 IAM 或他们所属生态系统中的其他组件集成。随着系统复杂性的提升,这个不断增长的考虑事项和"最佳实践"清单,可能会严重阻碍产品发布周期。
为什么软件会变得复杂软件系统在初始阶段往往规模较小,但随着它们不断发展壮大,逐渐演变成拥有复杂依赖关系的庞大系统,这使得理解一个组件的变动如何影响到其他组件变得更加具有挑战性。
软件系统变得越来越庞大,并分布在众多的服务器和云组件上。管理和维护这些分布式系统带来了额外的复杂性。每个组件和功能都有需要遵循自己的最佳实践,并要求具备专业的知识。例如,要发送一个领域事件,你需要了解如何使用 Kafka,包括它的 API、至少一次传递保证等最佳实践。
这一规律同样适用于像 MySQL 或 MongoDB 这样的数据库、像 Elastic Search 这样的搜索引擎,甚至是你集成的其他内部服务,例如你的功能标志系统。基本上,你需要深入理解并学习如何最有效地使用每一个组件。
另外,软件开发过程中缺乏统一的跨团队开发标准和实践也会显著增加系统的复杂性。例如,在定义数据库模式主键时,一个开发者可能将其定义为 UUID,而另一个将其定义为 Long。同样,对于 GDPR 等法规的遵循也可能存在差异,一个开发者可能实现了“删除”和“获取我的资料”的功能,而另一个在业务压力下,为了快速发布只实现了“获取我的资料”功能。即使对于同一个功能也可能有不同的实现,例如,一个开发者将“删除”实现为硬删除,另一个实现为“软删除”,第三个则进行了数据匿名化而实际上没有删除记录。虽然这些可能都是有效的解决方案,但当有人问你如何实现删除功能时,如果答案总是“视情况而定”,这将不利于构建一个可预测和一致的系统。
要确保所有开发者完全实现系统的所有非功能性需求是不可能得。即使是像输入验证这样简单的事情,不同的开发者也会有不同的实现。例如,一些开发者不允许字符串中有空值,而其他开发者则允许空值,导致整个系统的实现不一致。
通常,要让所有开发者对最佳实践和非功能性需求达成一致,首先需要进行文档化、建立代码质量规则(lint 规则),以及进行教育和培训。然而,在一个复杂的现实世界中,我们无法构建完美的系统。当开发者需要实现新功能时,可能会面临以下权衡。
在许多情况下,我们通常会在三个主要支柱之间进行权衡:
代码 - 在决定如何构建系统时,例如是选择单体应用还是微服务,我们会面临几个可能影响我们决策的问题。理解现有的代码基础和业务领域有多容易、能否对 API 做出重大变更以及这将对系统产生什么影响、重构代码和测试有多容易,以及如何扩展组织,以便让多个团队可以在相互依赖最小化的情况下独立开发各自的功能模块。
部署 - 对于这一支柱,我们做出与发布生命周期相关的权衡,即多个团队是否可以在任意时候将他们新版本代码发布到生产环境。部署过程有多容易和快速?每次部署的风险是什么(部署的代码越多,出现错误的机会就越大)?另一个需要考虑的事情是保持向后兼容性和 API 变动。例如,在单体应用中,重构和改变(内部)API 比较容易,因为你拥有整个代码库的控制权,但在微服务环境中,改变 API 可能会由于其分布式的特性而引发一系列意外的连锁反应。
运行 - 对于这一支柱,我们考虑的是系统的运维。性能要求是什么?系统的各个部分扩展性如何?在生产环境中运行时,系统监控的易用性如何?如果发生事故,我们能否迅速定位到故障部分,并找到相应的负责人?
尽管文档是明确软件开发愿景和推荐最佳实践的关键步骤,但开发者在具体实现上仍拥有相当大的自由度。不同的团队会有不同的内部库,并以各自的方式遵循指南和实现系统约定。
实现的多样性给整个系统带来了日益增长的技术债务,因为跨系统的任何一个变更都需要多个团队进行协调和修改,一些问题在不同的实现中都需要进行修复,而基本上它们做的是同样的事情。Log4J 漏洞几乎要求所有团队都进行紧急修复,而确保 100% 的代码库得到修复是一项艰巨的任务。
标准化复杂的环境要求对编码实践进行标准化。
确立标准和整合技术栈确实至关重要,但仅仅停留在文档化层面是不够的。正如我之前提到的,过多的文档可能会导致开发者面临信息过载的问题。
解决方案在于将这些标准、指南和最佳实践编码化,我们需要提供一个编码平台,它将自动处理大部分系统的横向关注点,并让开发者在遵守指南的前提下轻松编码,基本上为快速产品特性开发铺就了一条黄金路径。
例如,对于 PII 字段的加密,平台应该自动处理这些字段的加密和解密,而不需要开发者学习、理解甚至是使用加密库。例如,只需在字段上加个 @PII 注解,平台就会在字段被写入数据库和从数据库中读取时自动加密和解密字段,这样开发者甚至不需要在写代码时考虑这个问题。
由于开发这样一个强大的平台的成本非常高,我们需要尽可能限制我们的软件栈。过度允许偏离标准平台会增加系统的复杂性和维护成本,因此任何偏离标准的决定都应该经过严格的评估,我们需要考虑它们所引入的额外复杂性。
标准化的目的是为了解决规模化挑战。微服务是另一种可尝试用于解决规模化问题的解决方案,但是随着微服务数量的增长,你将开始面临管理大规模微服务环境的复杂性。
在分布式系统中,网络问题可能导致请求失败。由于请求需要在多个服务之间通过网络进行传输,与单体应用中的进程间方法调用相比,这无疑会增加性能开销。监控系统变得更加困难,因为调用分布在多个服务中。安全性成为一个更大的问题,因为每增加一个微服务,攻击面就变大一些。除此之外,我们别忘了还有人为因素:跨多个团队和服务维持标准、质量和协议的一致性变得更加困难。
这些缺陷显而易见,但在大型系统中,我们更需警惕的是那些不那么明显的成本和可维护性问题。让我来解释一下:
在开发微服务时,我们通常会采用一些流行的框架,例如 Spring。除此之外,还有内部库和其他依赖项,例如日志库和 JDBC 驱动程序,这些都是构建微服务时必须包含的。这意味着在微服务中实际运行的代码超过 90% 实际上来自框架和库。微服务中实际编写的业务代码只占 10% 左右,甚至更少,具体取决于微服务的大小。在许多情况下,业务逻辑代码所占比例甚至不到 1%。
这些代码被复制并在生产环境中被部署了成百上千次,每次增加新微服务都会增加系统足迹。这不仅导致云成本上升,还导致保持不同框架和库版本的一致性变得更加困难。
在 Wix,我们的微服务超过了 4000 个,这无疑给我们带来了一些挑战。为了应对这些挑战,我们正在努力采取措施来缓解这些问题。我们通过构建平台即运行时(Platform as a Runtime,PaaR)来解决这个问题。
为了深入分析问题领域,我们专注于开发者编写代码的方式,并在三个关键支柱上做出技术栈选择:代码、部署和运行时。我们将解决方案分为两部分:平台和运行时。
平台:开发者体验自动巡航平台专注于开发者体验,将最佳实践、契约、合规性和集成编码到我们生产环境的代码中间件组件中。我们可以将它看作一个高度定制化的框架,专为公司的特定需求量身定制。它负责处理非功能性需求,减少冗余的样板代码,并有效降低开发者的认知负担。当开发者在平台上进行他们的开发工作时,会发现一切流程都能"如预期般"顺畅进行。
我们内部将这个项目叫作 “Nile”,它的目标是简化软件开发,为专注于开发者体验的开发者提供最大的价值。
这种方法超越了传统框架和平台工程的界限,我们将平台工程从 CI/CD 层面带到了代码层面。虽然许多公司提供了供开发者使用的框架,但他们没有这样一个将框架与组织的运维实践无缝集成的平台。
例如,以 遵守 GDPR 合规性为例。为了满足 GDPR 删除数据的要求,你通常需要订阅一个 Kafka 主题并监听“删除我的资料”请求。一个基础框架或许能够让你轻松地订阅到这些主题,但开发者仍然需要编写处理消息和执行数据删除的逻辑代码。然而,一个强大的平台将自动订阅主题,处理消息,并触发从数据库中删除数据的过程——这一切都无需开发者进行额外的干预,开发者所要做的仅仅是在代码中标注出个人识别信息(PII)字段,平台将自动完成剩余的工作。
运行时:优化服务资源占用和部署策略PaaR 的运行时组件专注于优化服务资源占用和部署策略。与传统的将整个平台和框架与代码构件捆绑在一起不同,运行时负责管理平台代码并所有的网络通信(入站和出站)。这消除了每次部署微服务都需要重新打包平台的需求,使平台与“产品”构件能够拥有独立的发布周期。每个部署的构件只需简单地连接到运行时,结果就是服务占用更小。我们可以将其视为运行时依赖,而不是构建时依赖。
通过减小构件的大小,PaaR 可在单个节点上实现更高的服务密度。由于不需要与所有框架和常用库捆绑在一起,客户端(即微服务)的资源占用显著减少了。一台运行时主机可以更高效地为多个客户端服务提供支持,从而构建出一个高效的虚拟单体应用。
为了支持多样化的编程语言,我们启动了一项叫作 “SingleRuntime” 的 “平台即运行时” 计划,它利用了 gRPC 协议,通过本地网络(localhost)与客户端服务通信。这种方法将使我们能够在保持统一平台架构的同时,实现多语言开发的能力。
尽管 PaaR 仍在进行中,但我们已经在使用 Nile 方面取得了显著的成效。平台为开发者带来了很多价值,成功地将内部开发者的开发速度提高了 50% 至 80%。他们现在可以专注于构建产品的业务逻辑,而不是花费大量时间编写样板代码和实现所有非功能性需求,不仅开发者体验得到了改善,他们需要编写和测试的代码量也减少了,产品发布速度也比以往更快了。
平台所带来的影响是如此的深远,以至于我们决定将我们所有的遗留服务——数量多达数百个——在接下来的一年内逐步迁移到 Nile 平台中,这一举措无疑是值得的。
采用标准平台另一个常被低估的好处是,它对产品质量的提升。产品开发者从重复实现非功能性需求中解放了出来,因为这些工作现在可以由平台完成,并由平台团队根据最佳实践以最有效的方式来实现。此外,任何新加入平台的功能都会自动在所有基于该平台构建的服务中得到应用和激活,节省了整个公司层面的重复劳动。
支持数据局部性就是一个典型的例子。在转向 Nile 之前,我们只有少数几个服务支持数据局部性,但在我们将数据局部性支持集成到 Nile 平台之后,之前不支持数据局部性的数百个服务在一夜之间都获得了支持,而所有这些都没有要求产品开发者进行任何额外的开发工作。在用新平台重新编译后,它们就能“免费”获得数据局部性支持。这为公司减少了数百个人周的工作量,如果没有统一平台而想要获得数据局部性支持,这将是一笔巨大的成本。
你应该构建自己的 PaaR 吗?开发一个平台即运行时(PaaR)解决方案是一项艰巨的任务,尤其适合那些面临大规模扩展挑战的组织。如果你的微服务架构规模较小,微服务数量仅在低百位数,那么采用更具成本效益的扩展方案可能更为合适。你可以从以下几个方面着手:实施标准库、严格管理第三方依赖,以及建立一套规范来确保标准得以执行。在 Wix,我们采取了一种措施,构建了一个“普遍可用”执行器,它会强制要求所有团队至少每两周将他们的组件与最新的库和框架一起部署到生产环境中。
当你的服务数量到了数千个,就可以考虑构建一个平台。
对于即将踏上 PaaR 之旅的组织,我的建议是首先集中精力打造平台。专注于通过提升平台工程的抽象层次——不仅仅是基础设施,还有软件层本身——来自动化和简化开发和集成过程。
专注于业务逻辑对我们来说非常有效,因为我们平台团队服务的客户正是我们自己的产品开发者。
构建平台的过程包括对数万行代码进行严格的审查。我们对每一行代码都进行质疑,我们会问:“这行代码真的应该放在这里吗?”平台的设计目标是在代码库中将核心业务逻辑与其他所有功能区分开来,将后者集成到平台中,而不是让产品服务自身承担。正如乔布斯曾经说过的:“编写最快的代码行,永不崩溃的代码,不需要维护的代码,就是你从未写过的代码。”尽管听起来有些理想化,但我们的 KPI 正是代码行数,我们的目标是尽可能减少产品开发者必须编写的与业务逻辑无关的代码行数。
我们深刻地认识到,拥有正确的心态对于平台团队来说至关重要。我们必须培养一个以价值为导向的平台团队。虽然这可以成为一个独立的讨论主题,但我必须强调我们发现的一个关键点:平台团队最关键的 KPI 是“开发者采用率”。如果开发者不使用你的平台,说明它可能没有提供真正或足够的价值。这种以用户为中心的思维方式对于平台团队来说至关重要,与开发者的紧密合作对于平台的广泛采用至关重要,因为开发者始终参与到平台特性和能力的定义中,并帮助设定解决实际问题的具体需求。
我想分享的最后一件事是:实现这些目标绝非易事。除了技术挑战,我们还需要考虑人为因素。开发者可能对那些抽象且不易观察到的功能持保留态度。要赢得开发者的信任和支持,需要持续的沟通和不断提供关于平台内部运作的信息。这种透明度有助于揭开那些看似什么的“魔法”面纱,赋予开发者调试问题和做出有效贡献的能力。
查看英文原文:
https://www.infoq.com/articles/platform-runtime-engineering/