我们不断重造的车轮
摘要
一篇论述软件工程师反复重造已被充分解决的基础设施(如身份验证、后台任务、速率限制和功能开关)的文章,用自定义代码换取经过验证的解决方案,却不得不自行维护和调试这些代码。
<p><a href="https://lobste.rs/s/nw7rzu/wheels_we_keep_reinventing">评论</a></p>
查看缓存全文
缓存时间: 2026/07/31 18:50
# 我们不停重造的车轮
来源:https://blainsmith.com/articles/reinventing-the-wheel/
\[主页 (https://blainsmith.com/)\] \[随笔 (https://blainsmith.com/essays)\] \[文章 (https://blainsmith.com/articles)\] \[项目 (https://blainsmith.com/projects)\] \[现在 (https://blainsmith.com/now)\] \[简历 (https://blainsmith.com/resume)\] \[演讲 (https://blainsmith.com/talks)\] \[简历 (https://blainsmith.com/resume)\] \[演讲 (https://blainsmith.com/talks)\]
2026年7月31日
这个行业与已解决问题之间有着一种令人抓狂的关系。某个东西被研究明白、打包、写好文档、发布出去,十年后,半个领域的人又在从头把它造一遍。旧方案很少是错的。只是不知从何时起,那个被验证过的东西被换成了一个看起来更便宜的东西,而一旦有人算算投入的工时,就会发现它其实更贵。我看着这一幕反复上演,实在让人疲惫。
Heroku 在十多年前就实现了滚动部署、蓝绿发布和带健康检查的回滚。你推送到远程仓库,剩下的事情它全包了。而现在每个跑 Kubernetes 的团队都在重新搭建同样的机制:就绪探针、部署策略、峰值和不可用性调优、回滚逻辑,以及用来判断哪一步出了问题的可观测性。Kubernetes 承诺把这些都交给你。它实际交给你的是一堆原语和一块大到足以让每一个细节都微妙出错的可配置面。你用一个早已拥有的便利,换了一个每个季度都要你亲自照看的定制方案,而这居然算作进步。
## 常见嫌疑对象
认证是最明显的例子,也是最让我恼火的。认证提供商并不缺。工程师们还是会写出同样的临时路由、同样的 `users` 和 `sessions` 表、同样的密码重置流程、同样的 JSON 载荷、同样的令牌刷新逻辑。每个人都知道那些提供商存在。他们还是自己造,发布时至少带一个漏洞,然后永远维护着那个漏洞。
后台任务也是同样的故事。你需要在请求周期之外运行任意工作,于是你得把队列、工作池、重试处理、死信行为,以及查看什么失败了的途径全部拼起来。Sidekiq、Celery 和一大堆托管队列已经存在了好多年。它仍然被反复重造,每个技术栈、每个团队、每个服务各造一遍。
一旦你开始留意,这个列表会很长。还有几个:
重试、超时、退避和熔断。服务网格本该负责这些,这样你的应用代码就永远不用碰它们。然而大多数重试逻辑仍然手写在应用里,通常没有抖动,而且经常被用在那些从来就不适合跑两次的操作上。
速率限制。令牌桶和漏桶是很古老、描述得很清楚的算法。网关把速率限制做成一个勾选框。它还是被当作中间件重新实现,而且常常在计数器周围带着一个竞态条件。
幂等键。Stripe 很多年前就写下了规范模式,这并不复杂。团队还是会为每个端点重新实现它,并且在过程中把重试安全性的边界情况搞错。
特性开关。托管服务已经存在。团队却自己建一个标志表和后台管理页面,然后发现自己重新发明了定向规则和逐步发布。
缓存和失效。Redis 已经存在了。被手写出来的往往是那个一直很难的部分:什么时候该认为缓存值已过期,以及如何避免返回错误的内容。
配置和密钥。参数存储、Vault、Consul 和 etcd 都能解决这个问题。每个服务仍然手写自己的加载、优先级和校验逻辑,通常还要在生产环境里才发现优先级那个 bug。
搜索。Postgres 全文搜索和 Elasticsearch 都存在。常见的路径是从一个 `LIKE` 查询开始,然后在 `LIKE` 不够用之后,慢慢重新发明相关度排序。
状态机和工作流。Temporal 和 Step Functions 就是用来建模长时间运行的有状态工作的。默认做法却是一个 `status` 枚举列和一堆不断增多的条件判断,六个月后谁也理不清。
多租户隔离。给每个查询加 `tenant_id` 过滤条件是一个被造了成千上万次的车轮,而带安全漏洞的版本也几乎被造了同样多次。
## 背后的模式
所有这些背后的模式都一样,而且并不隐晦。一个成熟的解决方案存在。生态系统宣称某个平台或提供商已经覆盖了它。工程师还是重新实现了它,因为抽象泄漏了,或者和问题的确切形状不匹配,或者解决了某个相邻问题,却在真正要做的工作所在的位置留下了一道缝隙。于是你得到一个你并不想要的半个方案,外加你必须自己写的另外半个,这是最糟的两者兼得。
## 保持简单与可扩展
这些都不是反对在第一个版本就过度设计的论据。保持简单仍然是正确的默认选择,我也经常这样告诉别人。关键点更窄:在保持简单的同时,不要把自己设计进一个盒子里。小系统和一个所有未来决策都已被锁死的系统,在第一天看起来可能一模一样。
把核心产品建立在标准协议和已知协议之上,才是把门留开。如果你的认证走 OIDC,之后换一个提供商只是配置变更。如果你的消息传递讲一种有文档的协议,加一个外部系统或插件是一次集成,而不是一次重写。陷阱则在反面:硬编码的供应商依赖、特制的配置格式、自造的编码,以及没人写过测试的未文档化模式。这种版本第一天感觉很简洁,最后却成了那个盒子,因为你之后想加的任何东西都得逆着纹理硬撬进去。
## 拥有它,而不重造它
人们避开托管版本有一个真实原因。托管服务是一个外部依赖,可能按别人的时间表出故障,而且它把你系统中一块东西的所有权从你手里拿走。大多数团队希望对自己运行的东西拥有掌控权。这种直觉是对的。当你的认证提供商宕机时,解释工作是你的,修复却不由你来做,这确实是一个糟糕的处境。
错误在于把这种直觉当成从零开始建造的命令。那并不是仅有的两个选项。中间地带是一个你可以在本地运行并端到端拥有的子系统,同时不必写那么多胶水代码以至于实际上等于重造了它。Keycloak 运行在你自己的基础设施上,讲标准协议。Postgres、Redis 和 NATS 可以在本地运行,覆盖了队列、缓存和消息服务所卖的很大一部分能力。你保留了所有权,又不用承担一个特制实现的维护负担。
当有人把方法写下来之后,自己运行的成本会更低。很多这类样板代码之所以被重造,是因为没有一份大家公认的文档可循,于是每个工程师都从第一性原理重新发现同样的形状。一份 IETF 风格的规范,甚至只是一份范围清晰的指南,说明如何构建覆盖了大多数系统实际所需的百分之九十用例的全文搜索或速率限制,这会把一个数周的重新发现变成一下午的实现。你仍然要写代码,但你不用每次都重新推导设计。Stripe 关于幂等键的文章就是证明它有效的例子:写一次,任何读过的人都能构建出一个正确的版本,而不是一个微妙地坏掉的版本。
## 关注投入程度
值得关注的数字是比率。认证、队列、速率限制和部署是理所当然的。它们是对任何严肃系统的期望,而且它们不是产品。花在它们上面的精力不应超过花在让产品真正属于你的那件事上的精力。当你发现自己深陷一个手写任务运行器好几周时,有用的问题不是它能不能用,而是为什么这项工作在争夺你的注意力时胜过了核心产品。
这是可以衡量的,而且大多数团队已经在衡量了,这正是让我来气的地方。如果你的组织用冲刺点和 T 恤尺码来运作,数据就摆在那里。拉出估算,把花在认证、部署管道、队列管道和速率限制上的量加起来,对比只有你的产品才能提供的功能。如果被重造的基础设施在这场对比中胜出,那么你自己的估算就在告诉你:团队这季度花在了重造已经存在的东西上,而没人去读那个数字。
有时重造是合理的。被验证过的东西确实不合适,或者采用它的运营成本超过了小规模定制版本的成本。这种情况真实存在,也值得辩护。问题在于,大多数重造并不是这种情况。它是一种不加检查车轮是否已经在转就随手达成的默认选择,账单稍后才会到来——体现在维护上,体现在那个成熟版本早已修掉的 bug 上,体现在某个工程师用多年的时间重新解决一个在他入行之前就已经被解决的问题上。
在建造下一块基础设施之前,停下来检查一下:它到底是真正未解决,还是仅仅因为你懒得去找答案所在的地方,所以感觉上像是未解决。大多数时候它已经被解决了。自己跑一跑那个被验证过的版本,然后回到你真正该做的那部分工作吧。
相似文章
Andrew Quinn 语录
Andrew Quinn 探讨了为触及知识前沿而“重新发明轮子”的必要性,并反驳了因可能重复现有解决方案而产生的负罪感。
@freeCodeCamp:软件可靠性看似是现代挑战,但工程师们解决这类问题已有很长历史。……
本文在制造业与现代软件工程的可靠性之间建立类比,重点介绍了冗余、根本原因分析和可观测性等原则,以帮助构建弹性系统。
软件关乎人,而非代码(2020)
一篇论述软件成功更多取决于理解人及其需求,而非编写完美代码的文章,并以被遗弃的、未解决实际问题的代码库为例。
我决定回归手写代码
作者在重构一个 Kubernetes 仪表盘工具时反思道,虽然借助 AI 进行“氛围编程”(vibe-coding)能加速功能开发,但在缺乏人工监督的情况下,往往会导致架构臃肿和技术债务。
平台工程仍然重要
一篇观点文章,认为即使有了AI编码工具,平台工程和代码复用仍然有价值,因为token需要成本,而复用能带来杠杆作用。文中引用了Martin Fowler关于重构的类似论点。