背景
本次迁移的直接动机非常纯粹 —— 成本考量😄。因为老的 x86_64 数据集群服务器即将到期,购买新的 ARM64 的服务器成本能比续费老服务器便宜一半!所以不打算续费老服务器了,直接购置新的服务器。
我们曾在 《十年后的今天,我们将设备实时数据从 HBase 迁移到 ClickHouse》 这篇文章提到过,我们的数据集群服务器部署的是 CDH 发行版的 HBase 集群,同样也用它部署并管理 Kafka 集群,用作消息队列。而 CDH 发行版早已不再维护开源版本,购买的新服务器根本没办法部署,只能重新部署新版本。在新服务器上部署了 Confluent Kafka 最新版本,所以相关使用 Kafka 的客户端也需要一并升级。
我们在 《最后,我们把设备实时数据存放到了HBase里》 中,曾提到过我们的接入层技术栈:
采集器 <----> 接入服务 <----> Kafka <----> 解析服务 <----> Kafka <----> 存储服务 <----> 数据库
...
解析服务负责数据转换,以监测量为单位,但以设备为单位进行收集。由于会经过一系列的转换,内部采用了类Actor架构(并非AKKA,用的是Vert.x。这个选择没有其他原因,主要是后者更轻量级,性能也不差)。
...
整个接入层围绕 Vert.x 框架开发,使用 Groovy 2 语言编写,使用 Kafka 对微服务解耦,整套管线使用 Kafka 串联。所以本次的目标就是翻新整个基于 Vert.x 框架开发的接入层代码。
既然这次迁移绕不过去,那就不做小修小补了。借助 AI,我们打算把这个运行十几年的接入层老项目,从 Java 8、Groovy 2、Vert.x 2.x、Kafka 1.0 一口气翻新到现代技术栈。用一句话概括,就是“老树发新芽”。
迁移目标
在 AI Vibe Coding 的加持下,个人的效率被成倍放大,所以这次我们不打算像传统项目升级那样小打小闹,只做局部升级和改造,只升级某一部分的依赖或客户端,让它继续能在新的服务端跑。我们打算干一票大的,彻底重构升级整个仓库,包括所有依赖,基础设施。具体如下:
- Java 8 升级到 Java 21 (本来想升级到 Java 25,后来因为 CodeNarc 不支持 Java 25 降级到 Java 21)
- Groovy 2 升级到 Groovy 5
- Vert.x 框架从 2.x 升级到 5.1
- Kafka 从 1.0 升级到 4.3
- 纯 Groovy 代码迁移到 Java + Groovy 混合编程模式,各司其职,其中:
- Java 负责对接 Vert.x API,并启用 VIRTUAL_THREADS 模式,以及数据库逻辑等需要严格的数据类型和性能要求的部分
- Groovy 负责动态代码部分逻辑,以及 POGO 等使用 Groovy 更简单的部分
- 优化整体架构与设计模式,解决遗留的技术债
单从迁移目标来看,这个工作量几乎等于将整个项目重写一遍。因为以前很多老旧的 API 被废弃,比如 Vert.x 以前的 Callback Hell 接口完全删除,改为更加现代化的 Promise 接口,直接导致以前的代码完全无法使用。
事后证明跟我当初推断的一致,整个生产部分代码量原本 1 万多行,这次翻新之后变更行数 1.1 万多行,几乎完全重写。整个过程耗时 3周,大部分时间在线上测试联调。
放在以前这种规模的翻新都是不敢想象的事情,轻易不敢碰。
迁移过程
制定好迁移目标后,就开始稳步推进迁移过程了。整个项目代码比较大,还有很多历史遗留的业务问题,因此迁移的计划需要人工规划。
修复测试
是的你没有看错,在我翻修整个项目之前,第一步先做的事情是修复老代码的测试。
虽然整个项目是一个线上运行了十几年的老旧项目,但我仍然没有第一时间就做迁移,而是去修复测试,磨刀不误砍柴工嘛!
得益于我们团队一直对代码质量有比较高的要求,前期代码对核心部分逻辑一直有测试覆盖。只不过后期这些代码年久失修,慢慢的测试都没办法直接运行,跑 test 直接报错。所以后来的测试也就越来越没人管了,新加功能也就没有任何测试覆盖了。
我的第一步就是先修复这些测试。虽然整体测试覆盖率不是特别高,但是这部分测试代码依旧覆盖了核心逻辑,对迁移过程大有裨益,所以值得花心思先修复测试。而且通过测试,也可以让 AI 了解代码的业务逻辑。
让 AI 修复测试还是非常容易的,也不会对生产代码进行干扰和破坏。为了减小测试外部环境依赖,不污染开发环境,测试中我特别引入了 embedded-postgres (https://github.com/zonkyio/embedded-postgres) 和 testcontainers (https://testcontainers.com/modules/clickhouse/),分别用于跑 PostgreSQL 数据库相关的测试和 ClickHouse 相关的测试。
完成修复测试之后,就开始正式迁移的计划了。
worktree 创建脚手架
我们曾在 《Git Worktree 在 Vibe Coding 的妙用》 一文中分享过 git worktree 的妙用,在这次大规模翻新重构中,我决定依然使用这个强大的功能辅助 AI Vibe Coding 快速准确的完成迁移计划。
在分支中创建一个 worktree,删除原本的代码,创建一个新的 Gradle 工程,并且添加 CodeNarc, SonarLint, PMD, Spotless 等插件,可以让 AI 写出高质量的代码,并且格式化成统一的风格。未来也方便 AI 在开发的过程中随时比对老版本的代码。
1:1 迁移老代码
这个阶段不做任何优化和改造,只是将老代码 1:1 复制到新的工程中,同时保证代码能编译通过,并且测试能跑通。
这个过程只通过 Gradle 升级依赖,将所有的依赖全部升级到最新版本。迁移过程除了修复编译警告、修改因为升级依赖造成的废弃 API 调用之外,不做任何改动。只需要保证能用 Java 21 环境编译并运行,产品环境依旧能正常运行即可。
这一步的意义在于尽快产品环境集成验证,防止闭门造车导致线上环境不兼容的问题。
持续两周的打磨
前面几个阶段都是相对工作量比较低的阶段,整体改动不算太大。AI Vibe Coding 不到一周就干完了,就已经可以上线运行了。我也是在这个阶段将 CDH Kafka 全部废除,产品环境直接切换到 Confluent Kafka 最新版本。同时检查线上日志,看看有没有回归问题等等。
待稳定之后,后续开始了真正花时间与反复迭代的阶段 —— 重构与审查循环。
具体不细表了,大致过程就是我根据功能模块进行分组,一部分一部分的逐步让 AI 进行重构,哪部分使用 Java,哪部分使用 Groovy,如何优化架构,如何改进设计模式等,让 AI 去执行,整个过程按照 TDD 方式推进,一遍重构一边完善测试,保证每次迭代都能跑通测试,直到最后完成全部的重构,并且 lint 检查通过,代码质量达到要求。
每一轮大型迭代之后,我都要经历一轮 AI 审查 + 人工审查 + 线上运行这样的流水线验证,确保每次迭代都能稳定运行,每一轮迭代都经历线上环境真实集成,不引入破坏性更新。这样做的目的更新。这也符合敏捷开发的最佳实践,防止一次性憋个大招,到时候线上真的出现问题了无法快速定位是哪一次的更新引入了回归。
整个产品代码上万行,不可能靠人工一行一行过的。这里我的策略是这样的:
- 多个 AI 交叉 Review —— 俗话说三个臭皮匠顶个诸葛亮
- 人工审查收尾,尤其是需要验证 AI 有没有过度设计
我使用 Claude Code 开发,每一轮迭代之后,分别在当前终端使用 /code-review 指令、新开窗口使用 /code-review 指令,以及 Codex 打开使用 /review 指令同时审查,审查内容包括:
- 是否破坏原分支的逻辑?(这里 worktree 就派上了大用场,AI 不需要通过
git show命令比对代码) - 是否引入新的问题?
- 架构是否合理?是否可以简化?
然后将每个窗口得到的结果互相再让 AI 进行一轮二次审查,得到的结论就会准确许多。经过如此迭代多轮,架构就逐步稳定了。
最后人工审核不能少,因为只有人才知道历史上这些代码是怎么跟业务搭配的,真实业务场景和业务需求是什么,而 AI 是不知道的,这些边界情况往往需要人去检查,看看有没有遗漏。
还有一点是,AI 写代码非常快,特别容易过度设计。举个真实的例子,我们使用 Kafka 解耦整个接入层各个节点服务,也使用 Kafka client 的 seek 手动控制 offset,实现 timer + poll 方式的重试逻辑。遇到应该重试的异常,手动 seek 调整一下 offset 即可,下一轮消费消息的时候天然进行重试,而且由于 Kafka 的持久性,消息也不会丢失,整个代码非常简短清晰,易于维护。而 AI 在重构这段代码的过程中,搞了个非常繁琐的判断,判断了一堆“自认为的边界情况”,在每一轮消费消息的内部进行各种判断,然后内部自己进行各种 fallback 重试,把原本的代码量翻了三倍。我在 Review 的时候发现这里特别复杂,让 AI 重新设计,它才意识到这里过度设计了,改回了跟以前一样的简约设计。
总结
这次大规模翻新老代码,能够这么顺利的走下来,得益于我们的老代码原本就有不错的底子,核心功能有测试覆盖,让 AI 可以快速准确的理解产品代码要解决的业务用例,在重构和 Review 的过程中不至于跑偏,所以后期即使人工审查也不会太费力。从 AI 代码到产品架构可用的过程中,中间还存在不少 gap,依然需要传统软件开发的迭代思路 + linter + 测试保障,才能真正形成可用的产品级架构。
这次整体翻新老项目的任务持续了将近一个月,整体架构更加现代化,也用上了 Java 高版本的各种新特性,代码组织也更加清晰。最重要的是,代码测试覆盖率达到了 80%+,夯实了代码的健壮性。甚至还加上了 GitLab CI 流水线保证每次提交代码都有质量保障。
同时,深感当下 AI 时代,个人效率杠杆将被极大放大,以前像这种规模的重构升级几乎都只是想想而已,从来没想过真正的干一番。
掌握正确的方法论尤为重要。本次的任务也为我们在今后重构大型老项目积累了宝贵的经验,遂写下此文记录整个过程,也分享给喜欢阅读的各位😄。