单体系统如何实现动态演进扩展
单体架构是一种软件设计方法,其中应用程序的所有组件都集成为一个不可分割的单元。在这种架构中,整个应用程序(包括用户界面、业务逻辑和数据访问层)作为单一实体进行开发、部署和维护。
什么是单体?
- 单一存储库 — 将源代码共同定位在单个存储库中
- 单一应用程序 — 在单一应用程序中提供所有功能
- 单一服务——所有功能都存在于单一后端服务中
- 单一数据库 — 所有数据库表都存在于单一模式中
以下是一些关键投资,尽早考虑这些投资,以便做好准备,将未来的重构成本保持在最低水平。
- 工程思维——从前端到后端了解需求,但反向执行;从后到前构建。
- #领域驱动设计——从域和聚合之间专门构建的实体开始。
- #模块化 代码——在准备重构代码或远程托管共享包时分离您的关注点。
- 数据库表结构——每个#限定上下文 有单独的数据表结构。
- 解耦服务部署——定期确保您可以独立部署每个组件。
不断成长的团队
创业起初,如果最小可行产品(MVP)获得关注,那么初创公司可能会获得资金,并且将会出现增长期。毫无疑问,这意味着更多的人将加入团队,并且需要分享最初工程师的愿景。
早期,当团队规模较小(2-15 人)时,流程通常比较松散,因为协调惯例的开销是可控的。然而,随着团队规模扩大,一个团队处理所有事情的效率会降低,因此会决定拆分团队,组建更小、更灵活的团队,分头完成公司目标。
在任何公司生命周期的早期,都倾向于从整体结构开始,保持简单(#KISS 原则),然后通过提供最低限度(#MVP )来保持每个决策的精益( #YAGNI 思维方式) ;在此过程中降低质量。
然而,这是一种无知的错觉,因为你的视野只能看到地平线,而你无法看到角落。需要一些远见才能意识到即将发生的事情,并做好准备,积极应对未来的挑战,否则将面临一连串无休止的被动行为。
接下来的挑战是,如果你不偿还技术债务而让复杂性增长的时间越长,你就会开始走向生产力断崖式下降的境地。
在这个阶段,需要投资的关键技术是针对原有的单体架构。如果你早点采取这些措施,那么成本会更低;但如果你拖延,那么债务就会随着公司的发展速度成倍增长。
关键技术投资
- 远程发布——尽管短期内开销会增加,但开始将共享代码发布到远程存储库。
- 解耦部署——将可独立部署的后端服务重构到自己的存储库中。
- 组织设计——你现在有机会利用低成本主动设计团队以获得成功,否则你将因让组织结构决定你将交付的软件类型而受苦。(参考)
- 文档 — 花时间创建具有周到信息架构的 wiki,开始构建写作的组织能力。部落知识无法扩展,您将看到团队凝聚力的丧失。
七种技术单体
初创公司遇到的七种最常见的技术整体模式。
经典单体
- (A) 单一代码库
- (B)单体应用程序
- (C) 单体服务
- (D)单体数据库
高级单体
- (E) 分布式单体架构
- (F)云原生单体架构
- (G) GraphQL 单体
单一代码库
第一种单体结构是大型的单一的 monorepo。如果有一个大型团队在其中工作,那么一个包含整个代码库的单一仓库最终将变得难以管理。
虽然在早期阶段共置代码有很多好处,但最终会达到一个门槛,这些好处就会对你不利,因为有太多的问题混杂在一起。
- 耦合问题——如果没有分布式思维,共享代码将位于同一位置,并且直接引用时缺乏版本控制,意味着任何依赖关系都会被强制升级。
- 治理挑战——代码库的表面积越大,治理活动就越慢。
- 实现多样性——随着工作负载的范围和多样性的扩大,将会出现各种变化,从而开始给共享空间带来关注(想想从产品工程代码库中出现的数据工程代码)
如果您从单一存储库开始,则没有硬性规定何时应该进行转换。您的答案将取决于您的具体情况,因为您总是会在降低未来生产力与近期优先事项之间进行权衡。
单体应用
大多数应用程序应该是可维护的。
可维护性定义为能够在合理的时间内管理变更的功能,其中包括构建、打包、部署、自动化和手动测试。
当应用程序较小时,整个交付周期很快。然而,当您添加更多功能时,应用程序的范围可能会从几分钟延长到几个小时,在您意识到之前,您会在多个团队之间进行高度协调的情况下一次等待数天。
因此,需要关注的指标是交付周期需要多长时间以及需要多少团队参与。随着交付周期的延长,团队会因为变更数量过多而承担更多风险,从而进行“大爆炸”发布。这形成了一个良性循环,即部署时间膨胀和需要人力测试一切。
投资一项将应用程序拆分为可独立部署单元的策略将允许团队独立工作和部署。在尽量减少不良用户体验并促进独立部署的同时,制定一组粗略的功能。
单体服务
单体服务代表了一种场景,即后端 API 服务是在没有领域驱动设计等基础策略的情况下构建的。这可能会导致代码库包含各种问题,并逐渐发展成为“#泥球架构”。
由于边界不明确,利用代码会受到系统稳定性问题的惩罚,从而逐渐导致速度变慢——改变系统某一部分的行为可能会在系统的另一部分引发无法预料的问题。
这使得未来的解耦工作变得更加困难,因为这些后端架构的基础存在缺陷。我寻找的典型气味包括以下内容。
- 具有形容词字段的实体
- 具有动词字段的实体
- 具有大量可疑字段的实体
- 代码中缺乏层次
- 访问模式不一致
不幸的是,这些挑战本质上通常更具结构性,而且纠正这些挑战的成本最高。您不仅需要重新绘制域,而且现在还必须重构代码并执行数据迁移。
建议是专门派一个团队来盘点所有后端系统,优先考虑系统中损害业务运营或阻碍未来修复业务机会的部分。
- 部署WET 策略(将所有内容写入两次)以将您的功能提取到其自己的服务中。
- 使用Strangler Fig 模式重构Ball of Mud中的引用以使用新提取的服务。
- 通过删除原始代码来完成#DRY(不要重复自己) 。
值得注意的是,并非所有技术债务都值得偿还。降低其余工作的优先级,只有在其上的工作才比其他工作更具商业价值。
单体式数据库
最常见的出错的单体模式之一是共享单体数据库和单一巨型模式。
最早犯的捷径是:让员工自行决定关注点。然而,不可避免地,会有新员工或现有团队成员必须做出决定,是按时完成任务还是遵守最初的关注点分离协议?这种反模式的问题包括潜在的安全性、隐私性和昂贵的重构。
单独的凭证和表结构隔离是一种解决单体系统挑战并为未来迁移到更模块化架构做准备的方法。
为应用程序的不同组件或功能模块创建单独的数据库凭证(如用户名和密码)。这样可以实现:
- 访问控制和安全性 - 通过授予特定的权限和特权,只有授权的实体才能访问和修改各自模块的数据。
- 职责分离 - 不同的团队或组件只能访问和管理自己的数据,促进更好的代码组织和可维护性。
通过在前期投入少量工作设置单独的凭证和数据表隔离,可以为未来更容易地交付和测试单个组件,降低引入错误的风险,并为向模块化架构平滑过渡奠定模式。
云原生单体式架构
在现代云原生环境中,单个开发人员使用单个命令启动整个技术堆栈从未如此简单。这些一体化的奇迹可以部署应用程序、服务及其所有必需的基础设施。
挑战在于它们被过度使用来代替传统计算技术。结果是基础设施泥团大团圆,将问题空间推给了基础设施团队。
当数据的依赖关系或流跳出一个上下文并创建与另一个上下文的紧密基础设施耦合时,我们会遇到问题:
您开始看到新型分布式单体的出现:
- 它不仅基于软件域耦合,
- 而且还基于云基础设施级别。
建议制定严格的规则,不要利用框架来获取任何旨在利用或共享的核心组件。一旦出现这些要求,您就应该投资将代码与基础架构分离。
GraphQL 联合单体
这种联合或聚合类型的单体架构是 GraphQL 所特有的,因为该技术具有遍历整个 API 的能力。
当联合发展到无法治理的地步时,挑战就来了。随着时间的推移,当您添加更多 API 时,您必须管理这些更改,以确保您没有引入不希望与GraphQL 的其他部分结合的内容。
为了有效地进行治理,发布到GraphQL 的每个人都必须审查所有内容 - 对组织造成高度的认知拖累。
建议如果希望利用联合,请避免将所有内容都放入单一的超级图中。相反,应围绕每个联合图的目的划定意图边界。这些边界可以按应用程序、应用程序功能、安全隔离范围等来划定。
总结
天下没有免费的午餐,尤其是当您的公司取得成功并扩大规模时。当复杂性增加时,您应该准备好重组你的单体系统,以便您的团队不会因单体应用的惯性而陷入困境。
要点:
- 单体系统在初期技术创业公司中的作用和优势(优化速度)
- 随着公司发展和规模扩大,单体系统面临的挑战(技术债务、缺乏灵活性等)
设计系统的组织被限制于产生这些组织的通信结构的副本的设计 — 梅尔文·E·康威
虽然#康威定律 通常是在组织设计的框架内讨论的,但我相信它是双向的。系统的整体设计可能会在组织中造成通信流和变革的瓶颈。