“列数已达上限”:史上最烂代码库的“绝命”一击

科技   2024-08-07 11:50   浙江  
转自:InfoQ- 核子可乐、Tina

一位资深软件开发者 Jimmy Mille r在他的博客上分享了他早期接触过的一套代码。这套代码库存在着诸如 SQL Server 设计缺陷、缺乏版本管理等一系列问题。很多甚至是“常识”性问题,比如 SQL Server 对于单个表中所能容纳的列数是有上限的...... Jimmy 的经历引发了开发者们的热烈讨论,短短一天内便成为 Hacker News 上最热门的帖子,评论区汇集了 500 多条留言,大家纷纷吐槽自己遇到的各种奇葩问题。


我所见过最糟糕的代码库:

我的编程生涯是从孩提时期开始的,那时候我压根不知道有人在靠编程养家糊口。甚至直到我高中毕业那会,也总是感觉“专业开发”世界跟我自己在业余时间的编程探索根本不是一回事。而当我幸运地找到第一份软件开发工作后,才很快意识到自己的认知有多离谱、但选择又有多正确。我在第一个岗位上就遇到了严苛的考验,现在回想起来,那个代码库仍然是我这辈子接触过的最烦恼、也最美好的项目。虽然它将永远被那家公司的专利墙所锁定,但我还是想跟大家分享一点其间有趣又让人头痛的故事。

数据库的问题永远解决不完

在大型遗留系统当中,数据库所代表的绝不只是存储数据的地方,更是文化的来源与缔造因素。数据库为整个系统的运行设置了约束,是所有代码相碰撞的所在。数据库就像动物大迁徙途中的水坑,只是在我的故事里,这片水坑被污染得很厉害。

大家知道 SQL Server 对于单个表中所能容纳的列数是有上限的吗?我之前就不知道。当时的上限是 1024,现在好像是 4096。相信大多数人既不知道这一点,也不需要了解。可我们不一样,因为我们用于存储客户信息的 merchants 表早早就耗尽了列数配额,所以得添加 merchants2 来解决问题。如果没记错的话,那个表本身就有 500 多列。

于是乎,merchants 和 merchants2 两兄弟就成了公司系统的命脉。一切都会以或明或暗的方式归结于 merchants。而且 merchants 代表的并不只是单 / 双表,而是以大量规范化表的形式存在,且每个表都带有 merchants 的外键。其中在我心里永远占据特殊位置的,就是 SequenceKey。

为了便于理解,以上是我重新整理出的完整 SequenceKey 表。是的,大家没看错,这就是完整的表——只有一个键,对应一个值。如果简单是一种美德,那 SequenceKey 应该就是最完美无瑕的表了,毕竟还有什么能比它更简单吗?

但大家可能会好奇,这种只有单行单列的表,到底能用来做什么?答案很简单:生成 ID。当时我听说 SQL Server 还不支持自动递增 ID,而且是人人都这么说。我后来还搜索了一下想验证这种说法,但最终没有定论。而且在开发实践当中,这个表的作用还不止于此。

SequenceKey 就像是粘合剂,在每一个创建新实体的存储过程当中,我们首先得从 SequenceKey 中获取一个键,然后递增它,再将其作为 N 个不同表的 ID 进行插入。也就是说,所有实体表之间都存在一条隐式连接。如果大家在系统中看到某个 ID,那么各相关表中很可能会存在一个拥有相同 ID 的行。说实话,这设计挺绝的。

数据库可以永远存在,但我们的登录系统却要受到日历的限制。当然,我指的不是用来代表真实日期的日历,而是名叫“日历”的数据库表。其中有哪些内容?就是手动填写的日历。在问及公司老技术 Munch 时,他告诉我日历用尽后我们就没法登录系统了。几年之前发生过这种情况,所以他们让一名实习生把日期又续了五年,以确保这种情况短时间内不会再次出现。那到底是哪些系统在使用日历?天知道……

每天早上 7:15,员工表都会被删除一次,所有对应数据也将完全消失。之后来自 adp 的一个 csv 文件会被上传到表中。在此期间,我们无法登录系统。有时候这个过程会失败,但这还没完。相应数据需要被复制给总部。所以有人会向某位管理员发送一封电子邮件,而他每天都会按下按钮来手动复制数据。

说到这里,很多朋友可能地想:难道就没人出手清理一下这套数据库吗?至少让它更易用一点?嗨嗨,我们公司当然早就想到了,所以保留了一套数据库副本。这套副本中的数据大概比实际应用的版本旧了 10 分钟,而且同步只会单向进行。但好消息是这套数据库是有标准化操作流程的,“仅”需 7 次连接就能完成从商家到相应电话号码的匹配。

每位销售人员每个月都有一项必须达成的配额,名叫“胜利”。保存这些数据的表格(请注意,不是财务记录,而是特定的销售核算方式)非常复杂。每一天,都会有专人识别出哪些行经过了添加和更新,并将新内容与总部那边的某套系统进行同步。这倒不是什么大问题,可后来有销售人员发现他们可以要求手动更改这些记录。

当时这名销售人员已经完成了绩效目标,之后又在当月签下另外一笔大单。那人家当然希望能把这笔单子推迟到下个月,减轻一下后续的绩效压力。一名实习生负责处理这件事。消息传出之后,接下来的三年间请求数量开始呈现指数级增长。有段时间,我们有 3 名实习生全职负责编写这类 SQL 语句。之所以没有专门开发应用程序处理这项工作,是因为公司觉得难度太大。不过在我离职之后,我已经帮助这些实习生开发了相应的应用程序,只是不确定能不能如期生效。

代码库:部分代码得看硬盘!

说起数据库,就不得不提相应的代码库。当时那套代码库可太夸张了。我入职的时候,所有代码都保存在 Team Foundation Server 当中。很多朋友可能不太熟悉,这是一套微软开发的集中式源代码控制系统。我上手的主代码库一半是 VB,一半是 C#。它运行在 IIS 之上,而且使用会话状态来处理各项事务。这在实践层面意味着什么?就是说如果分别通过路径 A 和路径 B 导航到页面,那页面上显示的内容将会完全不同。

但我们也不能简单说这套代码库就是一半 VB、一半 C#,因为仓库中都签入了当时业已存在的每一种 JavaScript 框架,此外还涉及每位开发者自认为需要保留的自定义更改。更值得注意的还有 knockout、backbone 和 marionette,当然还有少量的 jquery 和 jquery 插件。

而且这套代码库还不是孤立的,在它身旁还有十几项 soap 服务和一些原生 Windows 应用程序。另外值得注意的是发货管理器——据说整个应用程序是由某位开发人员在一个周末之内独力完成的。我们叫他 Gilfoyle,据说这是位开发效率极高的程序员。我从没跟他当面交流过,但又总觉得似乎跟他神交已久,因为我一方面看过他存进仓库的代码、另外也看过他硬盘上的开发成果。

大家还记得 Munch 吗,他在 Gilfoyle 离开公司多年之后,仍然把这位老前辈的硬盘以 RAID 的形式保留在自己的办公桌上。为什么?因为 Gilfoyle 老兄最臭名昭著的习惯,就是不爱签入代码。不仅如此,他还为某位个别用户构建了随机的一次性 Windows 应用程序。于是乎,总会有用户带着只存在于 Gilfoyle 硬盘上的应用程序 bug 报告来找我们,而唯一的解决办法就是翻找这位老兄的技术“遗产”。

解决各种 bug

我的主要工作内容,就是追踪技术团队不想接手的各种 bug。有个特别讨厌的 bug 每隔几个月就会出现:我们发货之后,订单会在发货队列中卡住,且声明状态同时处于已发货和尚未发货的状态。我尝试过一系列解决办法(SQL 脚本、Windows 应用程序等),想要摆脱这种令人崩溃的状态。有人建议我没必要去追根溯源,但我实际控制不住自己的好奇心。

在过程当中,我逐渐理解了 Gilfoyle 当时的开发思路。发货应用会下载整个数据库,之后按日期进行筛选,保留应用程序上线日期之后的全部订单。该应用程序依赖于 SOAP 服务,而非执行任何服务性操作。换句话说,这项服务本身就是一条纯函数,而所有副作用都由客户端来承担。在相应的客户端里,我发现了一个巨大的类层次结构。这里有 120 个类,每个类都包含多种方法,而且继承深度高达 10 层。唯一的问题在于,其中所有的方法都是空的……注意,我可没有夸张——不是大部分是空的,而是全都是空的。

这个问题着实让我困惑了一段时间。最终,我意识到这是为了构建一套可以使用反射的结构。通过反射,Gilfoyle 可以创建一条由管道符(|)分隔的字符串(其结构完全由数据库驱动,但又是纯静态的),再通过套接字发送该字符串。而这一切最终会被发送到 Kewill,也就是与出货商那边会话的服务。那为什么会触发 bug 呢?因为 Kewill 每个月都会重复使用 9 位长的数字,原因是有人禁用了删除旧订单的 cron 作业。

烂摊子

这套代码库还有很多值得吐槽的地方。比如超级高层开发者团队,他们在 5 年之内没有发布任何代码就重写了整个代码库。还有红帽那边的顾问,他们构建了一套数据库来管理其他数据库。反正整个代码库里有太多几近疯狂的东西,种种问题堆积起来迫使技术团队下决心从头开始开发一套彻底告别过去的新方案。

但我认为最重要的进展,还是 Justin 对于商家搜索页面的改进。商家搜索页面是整个应用程序的入口点。每位客服代表都需要与商家通电话,并输入他们的 ID 或者姓名来查找相应信息。过程中,系统会把用户引向一个包含所有信息的庞大页面,这里塞满了密度极高的信息内容,包括我们可能需要的一切信息还有可以访问的所有链接。唯一的问题就是,它的速度太慢了。

Justin 是我们团队中唯一的高级开发人员。他聪明、尖刻,对于业务需求毫不关心。同时他也实事求是、不留情面,而且总能比身边的其他团队成员更快地解决问题。有一天,Justin 实在受不了用户关于商家搜索页面太慢的抱怨,决定出手去修复一下。之后屏幕上的每个框都成了独立的端点,在载时首屏上方的全部内容开始获取,并在后续一个框、一个框的加载过程中添加更多请求。这就让页面的加载时间从几分钟缩短到了不足一秒。

没人知道 Justin 到底是怎么办到的。因为这套代码库没有总体规划,没有适当的总体系统设计,没有符合预期的 API 格式,没有文档化的设计系统,也没有能确保工作连贯稳定的架构审查委员会。整个应用程序完全就是一团糟,所以没人能轻易修复、也没人敢随便尝试。在这样的情况下,每位开发者都只能尽量在自己的理解范围之内小心翼翼地活动。

在这样的前提下,庞大的整体应用程序就成了边缘位置上一个个微小应用的总集。每个人在尝试改进应用程序中的特定部分时,都会不可避免地回避令人眼花缭乱的原有网络,找到适合自己的安全小角落来构建新东西。之后再慢慢更新指向新东西的链接,于是新旧两端开始愈发孤立。

这事听起来可能很乱,但实际体验却相当不错。代码重复的问题消失了,一致性冲突消失了,可扩展性障碍也消失了。新增的代码一定有其明确的用途,同时又会尽可能少地触及其他功能而且易于替换。正是由于跟旧系统耦合起来难度太大,所以我们的代码有着很强的解耦属性。

在之后的职业生涯中,我再也没碰到过这么丑陋的代码库。或者说,后面接触过的哪怕再糟糕的代码库,也没有这么夸张的一致性需求。也许是因为当初那套代码库早就被“严肃”开发者们抛弃了,只有杂牌实习生和新手程序员愿意在泥潭中苦苦挣扎。又或许是因为当时开发者和用户之间没有直接沟通渠道,没有传话的项目主管,没有需求收集,也没有工单。唯一的交流途径就是跑到客服代表面前,询问怎么才能让客户体验更好些。

勾起了大家惨痛的回忆

Jimmy Miller 的这篇博文,完美地展现了软件开发中那些混乱又充满挑战的场面。这往往是理论与实践脱节的结果。同时,大多数公司和项目,在发展多年后都会积累下一些奇葩的流程、混乱的代码和各种“黑科技”。这些过时的系统,只有代码和数据库能诉说它们的过往,而那些曾经的创造者可能早已从公司离开,只留下了这些复杂的遗产。

他的回忆在 Hacker News 上激起了非常多的讨论,一天之内就超过了 500 条。很多人因为文章里的内容回忆起自己所经历的糟糕代码库。这些吐槽可以分为几个方面:不懂 Linux 管理、不擅长数据库设计,还有很多是不知道应用版本管理软件......

网友 emme 分享了一个故事,在他第一份全职工作的第一天,他被要求查看一个已经无法运行数月的报告任务。通过查看错误日志,她发现只需为脚本分配更多内存(只需调整 php.ini)即可解决问题,并告知团队负责人当晚应该可以正常运行。团队负责人震惊不已,开玩笑说如果他能修复这个报告,可能会直接升职,因为几个月来没有人能解决这个问题。虽然是玩笑话,但也能看出他的老板对问题解决速度感到惊讶。后来他才意识到,开发团队中的大多数人都不喜欢 Linux,希望将所有内容重写为 .NET 并迁移到 Windows,因此没有人愿意尝试与任何 Linux 机器相关的工作。

另一位网友 Twirrim 分享了他十五年前遇到的糟糕的“数据库设计”问题。当时他的一位客户,一家类似黄页的网站运营商,经常向他求助。这家网站允许用户根据行业搜索公司,但运行速度却非常缓慢。经过深入调查,Twirrim 发现问题根源在于数据库查询的低效。客户将所有公司的行业信息都存储在一个文本字段中,并用分号分隔。每次有用户进行搜索,系统都需要遍历整个数据库,逐条记录地查找包含搜索关键词的内容:SELECT * FROM companies WHERE categories LIKE "%PEST%"

想象一下,如果用户要搜索“杀虫公司”,系统就必须在每个公司的信息中查找“PEST”这个词。对于小规模的网站来说,这种方法或许可行。但对于这位客户的网站来说,庞大的数据量和每个公司可能所属的多个行业,使得这种搜索方式变得极为低效。一旦同时访问的用户增多,整个系统就会不堪重负。

Twirrim 多次建议客户重新设计数据库,以提高查询效率。然而,客户却拿谷歌作为对比,认为谷歌也能处理海量数据,自己的网站也应该可以。他们根本不喜欢“通过合理的数据库设计”这个答案,唯一愿意做的是购买当时最强大的服务器,其内存足以容纳整个数据库。

而更多人则分享了他们遇到的代码库管理问题,比如:

“我刚入职的第一家公司就接手了一个非常复杂的 VB 应用。这个应用部署在全国数十家客户处,每家客户的需求各不相同。更复杂的是,整个系统由大量以随机四个大写字母命名的全局变量控制。有一段时间,应用出现了一些诡异的 bug,这些 bug 在 Visual Studio 的调试模式下却无法重现。为了解决这些问题,公司采取了一个看似‘激进’的方案:在每个客户现场安装 Visual Studio,并指导用户直接在现场调试。至于如何说服用户并解决许可证问题,我不得而知。


更糟糕的是,接下来发生的事情完全超出了我的想象。公司完全没有版本控制的概念。所有的代码都堆放在局域网的共享磁盘上,被随意复制到各个文件夹中。这些文件夹的命名更是毫无规律可言,V1、V2、V2.3、V2a、V2_customer_name、V2_customer_name_fix……不一而足。一旦某个客户反馈问题,程序员就会奔赴现场,直接在客户的电脑上修改代码。如果问题波及到其他客户,我们就得像救火队员一样,挨个客户去修改。而对于一些小问题,往往就直接在现场改了事,然后随手丢到共享文件夹的某个角落。


结果可想而知,代码版本完全失控。每个客户的系统都成了一个‘特例’,有的甚至还在使用多年以前的旧版本。”

“天啊,这简直就是我接手一家拥有 35 到 40 年历史代码库的公司时经历的。文件散布各处,毫无一致性,同时还支持了成千上万个客户的定制化需求,甚至不知道其中的一些客户是否还在使用该系统。公司花了五年时间,解雇了长期担任‘首席’程序员的人,才做出了一些有意义的改变。”

实际上,软件版本控制系统的起源可以追溯到 20 世纪 60 年代早期,它源于 20 世纪早期用于管理大型组织文档、蓝图等的手动流程。CVS(Concurrent Versions System)诞生于 1985 年,由荷兰阿姆斯特丹自由大学的 Dick Grune 教授实现。CVS 成功地为后来的版本控制系统确立了标准,包括提交(commit)、检入(checkin)、检出(checkout)、里程碑(tag 或标签)、分支(branch)等。再后来 Linus 于 1991 年推出了 Git......

软件开发行业已经非常成熟了,那为什么有些企业不愿意应用这些最佳实践或新的软件文化呢?可能这位公司高层的回答是一个典型:“软件能用就行,没必要为了改变而改变。”

我曾经加入过一家用 Apache Camel 和几台性能不足的机器做 ETL 的公司。导入整个数据集并运行一系列 NLP 模型需要 3 到 6 个月(估计的,因为它太慢了,没有人会重新处理数据来修复 bug 或发布改进)。我设计了一个简单的架构,使用 Kafka、HBase 和 MapReduce 实现 Lambda 架构。CTO 以一种非常傲慢的语气告诉我,仅仅因为某个技术很新,并不意味着我们就要用它。

参考链接:

https://jimmyhmiller.github.io/ugliest-beautiful-codebase

https://news.ycombinator.com/item?id=41146239&p=1


推荐阅读  点击标题可跳转

1、图解 SQL 的执行顺序,优雅

2、MySQL 9.0“创新版”已支持向量,为何甲骨文却“偷偷摸摸”地宣布?

3、Tabby,一个 5 万星标的终端工具

数据分析与开发
「数据分析与开发」分享数据分析与开发相关技术文章、教程、工具
 最新文章