The Wheels We Keep Reinventing

The Wheels We Keep Reinventing

我们不断重复发明的轮子

The industry has a maddening relationship with solved problems. A thing gets figured out, packaged, documented, and shipped, and a decade later half the field is building it again from scratch. The old solution was rarely wrong. Somewhere along the way the proven thing got traded for something that looked cheaper, and it turned out more expensive the moment anyone counted the hours. I watch this happen on repeat and it wears on me.

软件行业与“已解决的问题”之间存在一种令人抓狂的关系。某样东西被研究透彻、封装、记录并发布,十年后,半个行业的人却又在从零开始重新构建它。旧的解决方案很少出错。但在某个环节,人们为了看起来更便宜的东西而放弃了经过验证的方案,结果一旦计算起工时,反而变得更加昂贵。我反复目睹这种情况发生,这让我感到疲惫。

Heroku did rolling deploys, blue/green releases, and health-checked rollbacks over a decade ago. You pushed to a remote and it handled the rest. Now every team running Kubernetes rebuilds that same machinery: readiness probes, deployment strategies, surge and unavailability tuning, rollback logic, and the observability to know when any of it went wrong. Kubernetes promised to hand you this. What it actually hands you is the primitives and a config surface large enough to get every piece of it subtly wrong. You traded a convenience you already had for a customization you now babysit every quarter, and somehow that counts as progress.

十多年前,Heroku 就已经实现了滚动部署、蓝绿发布和带有健康检查的回滚功能。你只需推送到远程仓库,剩下的它全搞定。现在,每个运行 Kubernetes 的团队都在重建同样的机制:就绪探针、部署策略、激增与不可用性调整、回滚逻辑,以及用于监控故障的可观测性。Kubernetes 承诺提供这些功能,但实际上它只给了你一些基础组件和一个庞大的配置界面,大到足以让你在每一个环节都犯下隐蔽的错误。你用原本拥有的便利换取了现在每个季度都要维护的定制化方案,而这居然被视为进步。

The Usual Suspects Auth is the clearest case, and the one that irritates me most. There is no shortage of auth providers. Engineers write the same ad hoc routes anyway, the same users and sessions tables, the same password reset flow, the same JSON payloads, the same token refresh logic. Everyone knows the provider exists. They build it themselves regardless, ship it with at least one hole in it, and then maintain that hole forever.

“惯犯”身份验证(Auth)是最明显的例子,也是最让我恼火的一个。市面上从不缺乏身份验证提供商。但工程师们依然在编写同样的临时路由、同样的用户和会话表、同样的密码重置流程、同样的 JSON 载荷以及同样的令牌刷新逻辑。每个人都知道有现成的提供商,但他们还是选择自己构建,发布时带上至少一个漏洞,然后永远地维护那个漏洞。

Background jobs are the same story. You need to run arbitrary work outside the request cycle, so you wire together a queue, a worker pool, retry handling, dead letter behavior, and a way to see what failed. Sidekiq, Celery, and a dozen managed queues have existed for years. It gets rebuilt regardless, per stack, per team, per service. Once you start looking, the list is long. A few others: Retries, timeouts, backoff, and circuit breaking. Service meshes were supposed to own this so your application code never had to. Most retry logic still lives in the app, hand-written, usually without jitter and often applied to operations that were never safe to run twice.

后台任务也是同样的故事。你需要在一个请求周期之外运行任意任务,于是你拼凑出一个队列、一个工作池、重试处理、死信队列行为,以及查看失败原因的方法。Sidekiq、Celery 和十几种托管队列已经存在多年了,但它依然被反复重建——针对每个技术栈、每个团队、每个服务。一旦你开始留意,就会发现这个清单很长。其他例子还包括:重试、超时、退避和熔断。服务网格(Service Mesh)本应承担这些工作,让你的应用代码无需处理。但大多数重试逻辑仍然存在于应用中,是手写的,通常没有抖动(jitter)处理,且经常被应用于那些根本不适合重复执行的操作上。

Rate limiting. Token bucket and leaky bucket are old, well-described algorithms. Gateways offer rate limiting as a checkbox. It still gets reimplemented as middleware, often with a race condition around the counter. Idempotency keys. Stripe wrote down the canonical pattern years ago and it is not complicated. Teams rebuild it per endpoint anyway, and get the retry-safety edge cases wrong in the process. Feature flags. A managed service exists. Teams build a flags table and an admin page instead, then discover they have reinvented targeting rules and gradual rollout.

速率限制。令牌桶和漏桶是古老且描述详尽的算法。网关提供速率限制功能只需勾选一下即可。但它依然被作为中间件重新实现,且往往在计数器上存在竞态条件。幂等键。Stripe 多年前就写下了标准模式,这并不复杂。但团队依然为每个端点重新构建它,并在过程中搞砸了重试安全性的边界情况。功能开关。现成的托管服务已经存在。但团队却选择构建一个开关表和一个管理页面,然后发现自己重新发明了目标规则和灰度发布。

Caching and invalidation. Redis already exists. What gets hand-rolled is the part that was always hard: when to consider a cached value stale, and how to avoid serving the wrong thing. Config and secrets. Parameter stores, Vault, Consul, and etcd all solve this. Every service still hand-writes its own loading, precedence, and validation, usually discovering the precedence bug in production. Search. Postgres full text search and Elasticsearch both exist. The common path is a LIKE query, then a slow reinvention of relevance ranking once LIKE stops being enough.

缓存与失效。Redis 已经存在。人们手写的部分往往是那些一直很难处理的问题:何时认为缓存值过期,以及如何避免提供错误的数据。配置与密钥。参数存储、Vault、Consul 和 etcd 都解决了这个问题。但每个服务依然手写自己的加载、优先级和验证逻辑,通常直到生产环境才发现优先级错误。搜索。Postgres 全文搜索和 Elasticsearch 都存在。常见的路径是先用 LIKE 查询,等 LIKE 不够用时,再缓慢地重新发明相关性排序。

State machines and workflows. Temporal and Step Functions exist to model long-running stateful work. The default is a status enum column and a growing pile of conditionals that no one can reason about after six months. Multi-tenancy isolation. The tenant_id filter on every query is a wheel that has been built thousands of times, and the version with the security hole has been built almost as many.

状态机与工作流。Temporal 和 Step Functions 已经存在,用于建模长时间运行的有状态任务。但默认做法通常是一个状态枚举列,加上一堆不断增长的条件判断,六个月后没人能理清其中的逻辑。多租户隔离。每个查询中的 tenant_id 过滤器是一个被构建过成千上万次的轮子,而带有安全漏洞的版本也几乎被构建了同样多次。

The Pattern The pattern underneath all of these is the same, and it is not subtle. A mature solution exists. The ecosystem swears a platform or provider already covers it. The engineer rebuilds it anyway, because the abstraction leaked, or did not fit the exact shape of the problem, or solved an adjacent problem and left a gap sitting right where the real work was. So you get half a solution you did not want and a second half you had to write yourself, which is the worst of both.

模式:所有这些现象背后的模式都是一样的,而且非常明显。成熟的解决方案已经存在。生态系统声称某个平台或提供商已经涵盖了它。但工程师依然选择重建,因为抽象层发生了泄漏,或者它不完全符合问题的具体形态,又或者它解决的是邻近的问题,却在真正需要解决的地方留下了空白。于是,你得到了一半你不想要的解决方案,而另一半你不得不自己编写,这结合了两者最糟糕的部分。

Keeping It Simple and Extensible None of this is an argument for overengineering the first version. Keep it simple is still the right default, and I tell people that constantly. The point is narrower: do not engineer yourself into a box while you keep it simple. A small system and a system with every future decision already foreclosed can look identical on the first day. Building the core product on standards and known protocols is what keeps the door open. If your auth speaks OIDC, swapping in a provider later is a configuration change. If your messaging speaks a documented protocol, adding an external system or a plugin is an integration rather than a rewrite.

保持简单与可扩展性:以上这些并不是为了论证第一版应该过度设计。保持简单仍然是正确的默认原则,我经常这样告诉别人。我的观点更具体:在保持简单的同时,不要把自己困在死胡同里。一个小型系统和一个已经预设了所有未来决策的系统,在第一天看起来可能是一模一样的。基于标准和已知协议构建核心产品,才能为你留有余地。如果你的身份验证使用 OIDC,那么以后更换提供商只是一个配置变更。如果你的消息传递使用文档化的协议,那么添加外部系统或插件就是集成,而不是重写。

The trap is the opposite: hardcoded vendor dependencies, bespoke config formats, homegrown encodings, and undocumented patterns that no one wrote a test for. That version feels simple on day one and turns out to be the box, because everything you might want to add later has to be pried in against the grain.

陷阱恰恰相反:硬编码的供应商依赖、定制的配置格式、自制的编码方式,以及无人编写测试的未文档化模式。那种版本在第一天感觉很简单,但最终会变成一个盒子,因为你以后想添加的任何东西都必须违背系统架构强行塞进去。

Own It Without Rebuilding It There is a real reason people avoid the managed version. A managed service is an external dependency that can fail on someone else’s schedule, and it takes ownership of a piece of your system out of your hands. Most teams want agency over the things they run. That instinct is correct. When your auth provider has an outage, the outage is yours to explain and not yours to fix, and that is a genuinely bad position to be in. The mistake is treating that instinct as a mandate to build from scratch. Those are not the only two options. The middle ground is a subsystem you can run locally and own end to end, without writing so much glue that you have effectively rebuilt it. Keycloak runs on your infrastructure and speaks standard protocols. Postgres, Redis, and NATS run locally and…

拥有它,而不是重建它:人们回避托管版本是有正当理由的。托管服务是一种外部依赖,它可能会在别人的时间表上发生故障,并且它将你系统一部分的所有权从你手中夺走了。大多数团队希望对自己运行的东西拥有控制权。这种直觉是正确的。当你的身份验证提供商发生故障时,你需要向用户解释,却无法亲自修复,这确实是一个非常糟糕的处境。但错误在于将这种直觉视为“必须从零开始构建”的指令。这并不是仅有的两个选择。中间地带是使用可以在本地运行并端到端拥有的子系统,而无需编写过多的胶水代码,以至于实际上又重新构建了一遍。Keycloak 可以在你的基础设施上运行并使用标准协议。Postgres、Redis 和 NATS 可以在本地运行,并且……