返回日报条目

资料节译

哪些要求其实可以放宽?

阅读原文

设计软件时,你没有的需求和你有的需求同样重要。找出哪些方面可以妥协——哪些可以较慢、较陈旧或不够灵活——通常是实现优雅设计的关键。

例如,看看 Unicorn 这类预派生 Web 服务器。由于它们不具备服务器实例之间共享可变状态的功能,因此可以让每个服务器运行在独立进程中,从而免费获得隔离。由于 Unicorn 特意忽略了客户端处理缓慢的问题<sup>1</sup>,它可以直接在 Unix 套接字上排队请求,大幅简化实现。

由于 S3 不支持原子性的多键更新——你无法在单个事务中更新多个数据块——它可以自行决定以何种方式在数据块级别存储数据。这样会快得多,因为读写不必跨越存储分区;扩展也容易得多,因为协调越少,真实数据源就越少。

StatsD 默认使用 UDP,这意味着它无法保证你发送的每一份数据都会进入存储。放弃这项要求,换来的是大幅减少的网络开销;对于一个不断通过网络传送数据的统计聚合服务来说,这是很大的优势。

将数据扇出到边缘缓存这一通用设计方法,通过放弃数据必须始终保持最新的要求,换来了速度和扩展能力的大幅提升。SQLite 放弃并发写入的要求,换来了运维上的简便——它只需一个文件就能运行!接受 高尾延迟<sup>2</sup>,能让 Cursor 更轻松地扩展其分布式 Git 后端。设计良好的系统会把弱点转化为优势。

任何花过时间设计现实世界软件的人(或学过 分布式系统的人)都知道,系统设计是一个不断妥协的过程。如果你试图满足每一项潜在需求——速度、并发、数据保持最新、简单的界面、支持复杂的用户行为——最终连一个能正常工作的系统都设计不出来。

弄清楚哪些需求可以放宽很困难,因为这同时需要商业和技术方面的专业知识。要准确判断某项设计变更会允许数据陈旧到什么程度,在技术上颇具挑战;同样,从商业角度判断用户能否接受这种程度的数据陈旧,也很有难度。

多数情况下,了解技术方面的工程师必须尝试向产品经理解释,由产品经理从商业角度作出判断。但要让这种合作顺利进行非常困难。工程师和产品经理的激励机制不同,彼此也很难检查对方的工作,因此很难 建立信任。缺少信任时,产品经理很可能一点也不肯让步<sup>3</sup>,使设计陷入无解的境地。

这是一个常被低估的原因,可以解释为什么单干开发者或初创公司有时能远远胜过大公司。技术决策和商业决策由同一个人作出,因此他们能够作出大公司无法作出的设计取舍,从而构建出优雅得多、能力强得多的系统。

了解哪些需求并不存在,在使用编码代理时重要得多。前沿模型能力很强,但它们都经历过 RLVR 的淬炼:就像成绩优异的学生一样,它们一心只想让评分器满意。除此之外,与有经验的人类相比,它们几乎没有商业背景知识。这常常表现为它们发现所谓的正确性或一致性“漏洞”,而这些其实只是对假想中未满足需求的担忧。

例如,LLM 随手加入互斥锁或其他并发机制,确保两份半相关的数据保持同步,这种情况并不少见,即使没人要求它这么做。我见过 LLM 在请求处理过程中加入古怪的检查,确保某份已经检查过的数据在处理中途没有发生变化,即使这样也无法成功避免人们担心的 TOCTOU 漏洞<sup>4</sup>。LLM 对边缘情况格外执着,比如“如果我们正在处理当前用户的一项管理员请求时,他失去了管理员权限怎么办?”<sup>5</sup>,或者“如果存储分区 A 中的缓存比存储分区 B 中的缓存早一纳秒更新,导致一些用户看到不同的数据怎么办?”。特别是当它们在循环中运行或被要求“不要犯错”时,就会不断堆叠这类情况,直到把你四行的改动变成一个千行怪物。

这就是为什么我对有些人声称 代码审查已死感到困惑。每当我审查大型 AI 生成改动时,差不多有一半的时间会发现某个不必要的需求让差异变得更复杂。仔细追查后,我总会发现这项需求是 LLM 自己加进去的。

使用缓存时,我经常会在提示词中明确说明,我不介意数据偶尔陈旧,或缓存实例之间短暂不一致。审查 Go 代码时,我首先会检查有没有新增我认为没必要的互斥锁或间接层。

如果放任这类需求在代码库中不受检查地蔓延,它们会随着时间推移让系统僵化。整个系统会变成一团乱麻,不停地检查、复查、加锁和强制保持一致,以至于完全无法扩展。要让系统保持优雅,就必须聚焦于接受陈旧数据或偶发的尾延迟,避免除关键情况之外的一切协调,并且总的来说,弄清楚有哪些需求是你不必满足的。

编辑:这篇文章在 lobste.rs 上引来了一些不错的评论。点赞最高的评论者链接到 这篇文章,把这类针对边缘情况需求的修补称为“托勒密修正”(epicycles)。我觉得这个名字很有意思。

问题是:如果网速很慢的人发出的 HTTP 请求每秒才传来一个数据包,是否就必须让一个服务器进程一直被占用?Unicorn 的回答是“是,所以请在我前面放一个 nginx 或 HAProxy”。 â© 2. 当读取请求命中陈旧的存储节点时,该节点必须先更新自身,然后才能响应。 â© 3. 我也见过反过来的情况:产品经理拼命试图说服工程师,某种程度的数据陈旧是可以接受的,而工程师却寸步不让,坚持要打造“完美”的系统。后果同样严重。 â© 4. 当然,单独做一次检查只是缩短了 TOCTOU 窗口,并没有消除它。 â© 5. 在我参与过的每一个系统中,直接执行管理员请求(或让它失败)都完全没问题:如果某人 10 毫秒前有权执行某项操作,现在仍允许他执行,想来也不至于酿成灾难。 â©