让我描述一下 2018 年 6 月我走进 Talentera 时看到的技术栈:一个面向政府部委和 MENA 地区企业客户的多租户 B2B 招聘 SaaS,跑着 AOL Server 4.x 上的 TCL/TK 核心,旁边是 PHP 7 用于较新的功能,Node.js 用于某些 API 层,以及一个用于工作流编排的 Camunda BPMN 引擎。这不是原型。这是生产环境。这在为真实政府处理真实的招聘决策。而且是,TCL 8.4 和 8.6,2018 年。

我知道你在想什么。让我打断你。

TCL/TK 不是笑话

大多数工程师看到 TCL 出现在生产技术栈里的第一反应是”哦,他们还没来得及迁移”。这个框架是错的,而且会导致坏的决策。

TCL——Tool Command Language——诞生于 1988 年,被设计用来做一件事:脚本化和粘合异构系统。AOL Server,也就是 Talentera 的 Web 运行时,使用 TCL 作为其原生扩展语言。AOL Server 本身是真正令人印象深刻的基础设施:event-driven、多线程(带线程池,不是每请求一个线程)、内置高效连接池。Naviserver 这个分叉版本至今仍在积极开发,因为这个模型确实有效。当 Google 的 Web 基础设施团队还在做第一性原理工作的 2000 年代初期,AOL Server 架构获得了好评。这不是由于衰退造成的遗留——这是由于持久造成的遗留,那是不同的东西。

TCL 8.4 和 8.6 不是你 2018 年从零开始会选的版本号,但它们是意味着你的运行时稳定的版本号。没有破坏性变更。没有意外弃用。你到来之前写的每一行 TCL 代码还是按照写时的方式运行。对于一个面向政府、为数千名候选人运行招聘轮次的多租户 SaaS 来说,无聊的稳定性是竞争优势。

TCL 语言本身——有一种奇特的美感。一切都是字符串。命令只是列表。执行模型是出奇地可组合的。对于 Talentera 正在做的那种请求处理——组装查询、构建响应、在紧密的服务端循环里操作结构化数据——它说实话是可以的。不是很好,但可以。而”可以、稳定、已经写好了”打败”更好但需要重写”,差不多是在有企业 SLA 要维护的情况下每次都会发生的结果。

“遗留技术栈”实际上的成本

诚实的数字:遗留技术栈的真实成本不在运行时。在运营层面。

AOL Server + TCL 没有丰富的现代可观测性故事。当你的核心是一个跑在 2003 年代服务器模型里的线程池内的 TCL 命名空间时,加入分布式追踪需要解决一些开源生态系统已经为 Go、Java 和 Node.js 解决了但没有为这个解决的问题。那是工程时间,不会用在功能上。当我加入时,团队花在”我们如何获取 TCL 在生产里做什么的可见性”上的认知开销,比花在”TCL 接下来应该做什么”上的不成比例地多。

这才是需要解决的真实问题。不是”重写 TCL”——那是个可能会害死公司的多年项目——而是”降低运营 TCL 的认知开销,让团队能专注于构建产品”。

我们落地的答案:不迁移 TCL 核心。而是隔离它。

DDD + microservices 作为包装层,而非替代层

我主导的架构方式是在 TCL 核心周围分层领域驱动设计,而不是取代它。Camunda BPMN 成为那些在纯 TCL 里管理起来过于复杂的流程的工作流编排器——多阶段审批链、不同政府客户配置的条件分支、文档处理 pipeline。Camunda 说 Java,这意味着 Java 8 服务拥有工作流定义层,TCL 作为执行特定任务实现的工作类型之一。

Keycloak 进来做身份和访问管理——跨多租户场景的 OAuth2 和 OpenID Connect。2018 年这是正确的决定。Keycloak 之前,租户身份是以一种……手工艺品式的方式处理的。自定义 session token、散布在请求处理器各处的每租户 auth 逻辑、有限的联合支持。Keycloak 把这些集中了,给了我们符合标准的 auth,让新服务(Kotlin microservices、Node.js API 层)能参与同一套身份模型,不需要理解 TCL 会话内部。

RabbitMQ 做异步消息。Redis 做缓存。新的移动功能——Ionic 以及后来的 Flutter——和 Node.js API 网关通信,API 网关在移动客户端的期望和基于 TCL 的后端现实之间做翻译。这些都不需要触碰 TCL 核心。TCL 核心继续运行。新服务像城市围绕一条河流生长一样在它周围生长起来。

那一周我在同一个 sprint 里发布了 Kotlin 和 TCL

从 TCL 8.4 切换到 Kotlin 再切换回来,有一种特定的认知割裂感。在 TCL 里你在想字符串操作、命名空间管理,确保你的 proc 参数列表是对的。在 Kotlin 里你在想 coroutines、数据类和 null 安全。这两种范式差异大到几乎是正交的。

让我惊讶的是,这实际上是没问题的。架构强制执行的严格关注点分离——TCL 拥有请求处理层,Kotlin 拥有新的领域服务,Camunda 拥有流程定义——意味着上下文切换是干净的。当我在 TCL 里时,我在想 TCL 的问题。当我在 Kotlin 里时,我在想 Kotlin 的问题。上下文没有渗漏,因为边界是真实的。

这是明确有界架构被低估的论据之一:它让多技术团队在运营上是可处理的。如果你在构建一个 monolith 并试图引入一门新语言,你会到处遇到边界混乱——PHP 在哪里结束,Node.js 在哪里开始?当你把 DDD 做好了,边界是一个 deploy 边界,不只是一个命名空间。当你的团队在同一个 sprint 里在 30 年历史的脚本语言和现代 JVM 语言之间切换时,这种清晰度很重要。

迁移实际上是什么样子

在 Talentera 的时间结束时,我已经清楚地看到迁移路径是什么样的,尽管我们没有完成它。答案不是”重写 TCL”。答案是:

  1. 识别 TCL 实现真正在造成问题的领域。不是”这个很老”,而是”这个在积极限制我们”——那些缺乏更丰富类型系统或有限可观测性故事正在消耗真实工程时间的地方。
  2. 为那个领域定义一个新的服务边界。让新服务拥有那个领域的数据。
  3. 让 Camunda 介导过渡——当新服务上线时,工作流引擎可以把任务路由到新服务,同时 TCL 实现并行处理同样的任务,直到你有信心为止。
  4. 当你把那个领域的流量从 TCL 实现里排干之后,退役它。

这是一个以季度而非年为单位的迁移时间线。而且它不需要你一次性重写一切。关键洞见是:“迁移”不是”替换”——它是”所有权的系统性重新分配”。TCL 不需要消失。它只需要停止拥有它不擅长拥有的领域。

接手遗留技术栈时没人告诉你的事

当你加入一家跑着这样技术栈的公司时,会有一种冲动:通过提议完全重写来留下自己的印记。抵制这种冲动是你会做的最重要的架构决策之一。对一个生产中的多租户 SaaS 进行完整重写是一个赌公司生死的项目。它几乎从不按计划完成。新的东西几乎从不复现旧东西处理过的所有边缘情况。而且在重写期间,你不是在构建新产品——你在构建一个对现有产品的替换。你的竞争对手没有在原地等你。

更有价值的技能是学会用善意的眼光读一个技术栈。TCL on AOL Server 不是技术债,因为它老。它在特定地方是技术债,在那些地方它的运营模型创造了真实的摩擦。理解那些地方在哪。具体修那些地方。把其他的留着不动。

我在那个环境里发布了 Kotlin microservices。我发布了 BPMN 工作流编排。我发布了跨租户的 OAuth2 联合身份。TCL 核心整个时间都在运行。

这就是那份工作。