“你的任务是重新编写这个系统。它支持我们所有的业务。哦,对了,它还是用APL编写的。”
我的遗产重写之旅就是从那时开始的。对于不了解APL的人来说,APL是一种起源于20世纪60年代的编程语言,以独特的数学符号和强大的数组操作能力而著称。如今找到熟悉APL的开发人员,就像在现代电脑里找到软盘驱动器一样困难。
该系统已经发展了四十年。它最初只是一个简单的库存管理工具,后来演变成了一个全面的ERP系统。包含超过460个数据库表。代码中融入了无数的业务规则。与业务流程的各个部分都进行了复杂的集成。该系统是制造业务的核心支柱,每年营收超过1000万美元。
我们的任务虽然明确但也很艰巨:使用.NET、PostgreSQL和React来现代化这个系统。
问题在于业务在转型期间不能停止,必须保持运行。不能停顿,不能丢失任何数据,不能影响日常运营。
这不仅是一个技术难题。它教会了我们如何管理复杂度、理解旧有业务流程以及应对组织内部关系。
这就是那个故事,以及我们从中得到的教训。
初始状态:理解传统第一个挑战是理解这个庞大系统真正是如何运作的。代码库在四十年间随着时间的推移自然地发展和增长,一直由单一的开发团队维护。团队成员现在都快六十岁了,正准备退休。
第一次走进代码审查,就像打开了一个时光胶囊。APL的简洁语法使得复杂的业务逻辑可以用寥寥数行表达。读懂了是美妙的;读不懂则是可怕的。而我们大多数人对此都是一头雾水。
原团队在知识传递过程中至关重要。他们了解每一个细微之处,每一个特殊情况,每一个几十年来不断添加的业务规则。但是通过对话所能学到的知识是有限的。文档非常稀少,现有的文档也已经陈旧不堪。真正的知识和经验都保留在原开发者的头脑中。
我们花了几周时间来梳理系统的功能。
- 核心制造过程分布在超过50个相互依赖的表中
- 库存管理涉及系统几乎每个方面
- 经过数十年的发展,自定义报表工具已经满足了特定的业务需求
- 与外部组件的集成点通过错综复杂的存储过程来处理
从基本模式开始的表已经发展到包含数百个列。其中一些列已经不再被使用,但无法删除,因为没有人能确定这些不常用的报告是否仍然需要这些列。
这特别具有挑战性的是,业务流程和技术实现之间存在脱节现象。业务会描述看似简单的流程,但技术实现却发现多年积累的复杂性,这些复杂性因各种特殊情况和特殊需求而产生。
我们需要一个系统的方法来理解这个庞大的系统。我们从绘制业务流程图及其相应的技术实现方案开始。这帮助我们识别了核心业务领域,这些领域后来影响了我们的模块化设计。更重要的是,这使我们真正理解了我们所面临的挑战。
德国产品团队与工程团队之间的冲突管理层希望快速看到成效。他们催促我们从最简单的部件开始。这在产品管理与开发团队之间制造了紧张气氛。
产品经理的看法很直接:向业务展示进展。他们需要看到具体的成果,以证明重写是值得的。业务正在投入大量的资金,他们希望尽快看到回报,而不是等待漫长的过程。
开发团队看到了不一样的现实。我们意识到从边缘功能开始相当于在不稳固的基础上打地基。核心业务逻辑仍然保留在旧系统中,使得每个集成点都更加复杂化。这笔技术债务会随着时间的推移而逐渐累积。
作为技术负责人,我坚决反对这种方法。我的论点很简单:核心制造过程是系统的命脉。每个外围功能都依赖于这个核心过程。推迟它的迁移导致了新旧系统之间错综复杂的依赖关系。我们迁移的每个新功能都需要与遗留的核心进行复杂的同步。我们就是在流沙上打地基。
我认为我们应该先专注于核心领域。是的,这会需要更多时间来展示初步成果。但这将为后续工作奠定坚实的基础。但这意味着业务要等待更久才能看到明显进展,整体迁移不仅会更快,也会更可靠。
双方的目标都没有错。产品管理团队有正当的理由担心展示进度的压力。开发团队也有正当的理由担心技术可持续性。但这种不匹配导致了让步,影响了项目进度。直到今天,我认为如果我们一开始就从核心业务逻辑着手迁移,我们会更早完成迁移。
软件架构:面向未来在探索阶段,我们发现了系统中一些不同的业务领域。这使我们决定采用模块化单体架构。每个模块都是独立的,但可以通过共享事件总线与其他模块进行通信。
关键的架构决定。
- 模块化的单体架构:每个模块代表一个独立的业务领域。这为将来潜在的微服务迁移提供了清晰的路径。
- 异步通信:模块通过使用RabbitMQ的事件进行通信。这减少了模块间的耦合,增强了系统的弹性。
- 具有明确边界的共享数据库:尽管所有模块都使用相同的PostgreSQL数据库,但每个模块有自己的表和模式。这有助于保持逻辑上的分离。
- 云就绪设计:该系统通过容器化技术部署在AWS上。Jenkins流水线可以实现几分钟内部署到多个环境。
双向数据同步比我们最初预期的要复杂得多。以下是为什么我们不能使用现有的变更数据捕获 (CDC) 解决方案,例如 Debezium 的原因:
- 复杂的转换:许多遗留表需要来自多个新表的数据。这不仅仅是CDC工具擅长的一对一映射那么简单。
- 同步过程中包含的业务逻辑:同步过程需要在转换中应用业务规则。这超出了大多数复制工具的能力范围。
- 双向同步的要求:我们需要实现双向同步,并防止出现无限循环。遗留系统仍然是未迁移部分的权威参考。
我们用RabbitMQ构建了一个定制化的消息传输方案。虽然这对我们管用,但教训依然存在:一定要仔细评估现有的工具。即使你不能全盘采用,你也能从它们的处理方式中学到不少有用的套路。
一个关键的技术要点列表- 模块化的单体结构带来回报:模块化的单体结构使系统更易于理解和维护。每个模块都有明确的边界和职责。
- 投资部署自动化:持续集成/持续交付(CI/CD)流水线至关重要。它使我们能够自信地频繁部署,降低每次变更的风险。
- 基于消息的集成:模块之间的异步通信为逐步迁移提供了必要的灵活性。
- 数据同步的复杂性:切勿低估在遗留迁移中数据同步的复杂性。不论是使用现有的工具还是开发自定义解决方案,这都将是主要的挑战。
技术挑战仅仅是其中的一小部分。重写旧系统能否成功,在很大程度上取决于如何处理各个利益相关方。
- 产品管理团队需要看到进展
- 开发团队需要时间把事情做对
- 业务需要保持运行
- 旧团队需要传授知识
在这些相互竞争的需求之间找到合适的平衡可能有点棘手。
我们找到了几种有效的方法:
- 定期召开利益相关者会议,让每个小组都能提出他们的关切
- 透明的项目追踪,使所有各方都能看到
- 清晰地向所有各方传达技术决策及其对企业的影响
- 庆祝技术和业务上的每一个里程碑
- 记录技术和组织知识
我怎么强调也不过分,记录四十年来旧系统运行中积累的知识的重要性。当原团队退休的时候,我们有一套完整的文档,详细说明了每一条业务规则和每一个特殊情况。
四年之后,系统发展得很好。云基础设施提供了可靠性和可扩展性。模块化单体架构(modular monolith architecture)使得系统易于维护。自动化的部署流水线支持快速更新。
但这段经历教会了我们在技术需求与商业压力之间取得平衡的重要教训。在进行遗留代码重写时,成功不仅仅是技术上的卓越。它还需要深入理解业务领域,管理利益相关方的期望,并做出务实的架构选择。
软件架构很关键,但同样重要的还有人力因素。两者都要考虑到。
谢谢大家的阅读!
保持,太棒了!
原发表于 https://www.milanjovanovic.tech,发布于2024年12月28日.
无论你什么时候准备好了,我可以帮你的方式有4种:
- (即将推出) 使用ASP.NET Core构建REST APIs: 您将学习如何利用最新的ASP.NET Core功能和最佳实践构建生产级REST APIs。它包括一个完整功能的UI应用程序,我们将它与REST API进行集成。加入等候名单!
- 实用的Clean Architecture: 加入3,600多位学生,学习我用来构建生产级应用程序的Clean Architecture系统的方法。了解现代软件架构的最佳实践。
- 模块化单体架构: 加入1,600多位工程师,学习如何构建现代系统。您将学习如何在实际场景中应用模块化单体架构的最佳实践。
- Patreon 社区: 加入由1,000多位工程师和软件架构师组成的社区。您还将解锁我在YouTube视频中使用的源代码、未来视频的提前观看权限以及我课程的独家折扣。
- 通过赞助这份通讯,将自己推广给61,000多名订阅者