暗无天日

=============>DarkSun的个人博客

读:20条软件工程定律

Milan 在他的newsletter里整理了 20 条软件工程定律。这些定律有些来自 1960 年代,到现在还管用。它们讲的是人在压力下一起造东西时会发生什么,不教你怎么做,只告诉你已经发生了什么、什么行不通。下面逐条解读。

Gall 定律

一个能跑的复杂系统,一定是从一个能跑的简单系统演变来的。

系统在纸面上看着没问题,真正的问题要等真实用户用起来才会暴露。所以每个能跑的复杂系统都是一步步长出来的。想一步到位造出完美系统,基本都失败了。从零重写的系统经常翻车也是这个原因:大功能倒是照搬了,但老系统在多年运行中积累的那些不起眼的小设计(比如某个特殊的缓存策略、某条异常处理路径),没人记得也没人照搬,而它们恰恰是让系统跑起来的关键。

Instagram 前身叫 Burbn,集合了签到、游戏、拍照分享。创始人砍掉所有功能只留照片分享,变成了现在的 Instagram。Google Wave 反其道行之,聊天、邮件、论坛、文档编辑一起上,没人说得清它到底是干嘛的,15 个月就死了。

KISS 原则

保持简单。超出必要的都是负担。

能 50 行脚本解决的问题,别搞 500 行的复杂方案。每多写一行代码就多一分引入 bug 的风险。简单的设计好维护:新人上手快,bug 好找,改一处不会牵动一片。别整那些花里胡哨的代码技巧,也别替未来还不存在的问题预埋复杂度。

有个创业公司需要功能开关(feature flag,不改代码就能开关某个功能的配置项),于是建了一个独立微服务:自己的数据库、缓存、管理界面、WebSocket 通知、A/B 测试支持,花了大量时间,出了问题还很难查。其实一个 JSON 配置文件就够了,一个下午搞定。

Conway 定律

组织的沟通结构会镜像到系统架构里。

你的应用架构基本等同于组织架构图。四个团队做项目,大概率产出四个模块。前端、后端、数据各干各的,做出来的东西也拼不到一块。光重写系统不调组织架构,换汤不换药。

反过来也成立:先选好想要的架构,再按架构组建团队。Amazon 在 2000 年代就把系统拆成小服务、每个服务一个小团队,系统和组织一起变了。这叫逆向 Conway 播种(Inverse Conway's Maneuver)。

三人的团队几乎总是出单体应用,因为拆分的成本比维持整体的成本还高。

Hyrum 定律

用户够多之后,你 API 的每一个可观测行为都会变成某人的依赖,不管接口文档怎么写。

接口文档写着不算数,系统实际表现出来的才算。响应时间、错误信息的文字、JSON 字段顺序、哈希的精确字节,这些你从没觉得重要的细节,总有人依赖着。

SimCity 有个 use-after-free 的 bug,在 Windows 3.x 上没事,因为内存压根不被回收。到了 Windows 95 真正回收内存了,SimCity 就崩。微软专门在 Windows 95 里加了一个特殊内存分配模式,只在 SimCity 运行时激活,让这个 bug 继续工作。

浏览器也是这么干的。Web 开发者依赖的每一个怪癖都变成了平台的一部分,浏览器改不了,改了就崩半个互联网。

CAP 定理

分布式系统只能同时保证一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)中的两个。

网络迟早会断。一旦两个节点联系不上(这就是分区),你要么停写保数据一致,要么继续服务但允许副本间数据不一致。每个分布式数据库都做了选择,只是多数不会主动告诉你。

MongoDB 选了 consistency:分区时某些副本拒绝写入,等整个系统恢复。Cassandra 则选了 availability:副本数据不一致时照样响应查询,之后再修。谁都没错,取舍不同罢了。

Zawinski 定律

每个程序都会膨胀到能收发邮件为止。做不到的会被能做到的替代。

这是 Netscape 早期工程师 Jamie Zawinski 在 1990 年代说的玩笑话。那个年代浏览器、编辑器、日历都在加邮件功能,好像不收发邮件就不完整。说白了就是每个程序都会无限制地膨胀,直到塞满所有能想到的功能。工具做好了、用户喜欢,产品方就想让人留在平台上,于是不断往里塞东西。时间一长,工具变得又慢又臃肿,然后新的竞争者带着精简版出现。

Netscape 从浏览器膨胀成套装(邮件、新闻、网页编辑器),Firefox 来收拾残局,精简后火了,然后又加插件和开发工具链。Slack 诞生时说要杀死邮件,现在有语音、视频、机器人、应用商店。

Brooks 定律

给延期的项目加人,只会让它更延期。

软件工作拆不了。新人进来要上手,老手得停下手头的活来带。项目已经延期了,加人只会添乱。Brooks 说得好:九个女人也不能一个月生出孩子。

有人带过一个八人团队,一直延期。第一反应是招两个人。结果招人期间走了两个,团队变成六个人,沟通反而顺畅了,产出反而更高了。

Ringelmann 效应

团队越大,人均产出越低。

很多人一起拉绳子,每个人反而不用那么大力。协作摩擦是一方面,"总觉得别人会做"是另一方面。

GitHub 做过大规模统计:2-5 人团队的工程师平均每月写 1850 行代码,10 人团队降到 1200 行,50 人以上只剩 450 行。人均掉了 75%。小团队出活快,Amazon 的"两个披萨"规则也是这个道理。

Price 定律

一半的工作是由人数的平方根完成的。

100 人的团队,大约 10 个人干了一半的重要工作。16 人的团队,4 个人承担主要产出。所有创意领域都这样。核心的人走了,团队产出能力大幅缩水。剩下的人也没闲着,干的是让团队运转起来的支撑活。

Musk 接管 Twitter 后裁员约 50%,网站照样跑。Price 定律提前预测到了。但裁掉的是那些不怎么显眼却不可或缺的能力:内容审核团队没了,SRE(站点可靠性工程)人手不够了,出了故障没人能深入排查根因,只能头疼医头。核心人员撑住了日常运转,但碰到下一个复杂问题就抓瞎了。Twitter 后来悄悄请回了一些被裁的人。

Dunning-Kruger 效应

你对一件事知道得越少,越自信。

做一件事需要的本事,和判断自己做得好不好的本事,是同一件本事。不会的人看不出自己哪做错了,觉得自己挺厉害。会的人看得到自己所有的不足,觉得自己还不够好。

新工程师被问到工期,张口就来一个精确数字。老工程师给的是范围,口头禅是"看情况"。新人不是在瞎说,他们只是还不知道自己不知道什么。

刚接触新技术的人总是特别兴奋。AI 领域正在上演这一幕:喊"AI 什么都能做"最响的,通常不是天天在用 AI 的人。

Hofstadter 定律

做事总是比你预期的久,就算你已经考虑了 Hofstadter 定律也一样。

你估 4 周,想起自己总是太乐观,翻倍到 8 周,最后花了 16 周。下次学乖了估 16 周,结果花了 32 周,因为总有你不知道的意外冒出来。

柏林勃兰登堡机场就是个典型。软件集成涉及 75000 个传感器和 50000 个灯具控制,原计划 18 个月完工,后来意识到不可能,延长到 30 个月。最终花了 7 年,造价 70 亿欧元,超支 2.5 倍,晚了 9 年。

Parkinson 定律

工作会膨胀到填满分配给它的时间。

给开发者两周做一个两天能完成的任务,他会花两周。不是说他想偷懒,而是在两周里会做计划、试方案、加没人要求的花哨细节(gold-plating)。截止日期就定一天,他大概率一天能交。

Goodhart 定律

一个指标一旦变成考核目标,就不再是好指标了。

拿 bug 数、事故数、测试覆盖率、团队速度来考核人,大家就会想办法让数字好看,而不是把事做好。数字上去了,质量没动。

2000 年代初有团队按代码行数发奖金,后来有人按 PR 数考核。结果开发者开始复制粘贴而不是提取共享逻辑,有人几乎每次提交都开一个 PR。现在流行按 AI token 消耗量(tokenmaxxing)来衡量,消耗越多越"高效"。

Gilb 定律

任何你需要量化的东西,总能找到某种方式来测量,而且比不量强。

Gilb 定律和 Goodhart 定律一体两面。Goodhart 说指标被滥用会失真,Gilb 说没指标更糟。重要的东西就该想办法去量,量不了就没法优化。DORA 指标里的部署频率和变更交付时间就是开发生产力的可用度量,不完美,但比代码行数和 token 消耗靠谱。

Knuth 的优化原则

过早优化是万恶之源。

性能优化经常做早了、做错了地方。团队优化了永远不会成为热点的代码路径,引入了用不上的复杂度,花时间解决根本不会遇到的规模问题。先写出能跑的代码,再查性能。瓶颈在哪,就改哪。没有就继续往前走。

有人在创业公司花了大量时间搭 Kubernetes,为了扛百万用户。问题是他连 10 个用户都还没有。基础设施做好了,产品功能还没做完。

Amdahl 定律

并行加速的上限取决于串行部分的比例。

10% 的工作必须串行做,那无论多少台机器,最多快 10 倍。50% 必须串行,最多快 2 倍。

人也一样。一个审批组要过问每个技术决策,不管你有多少工程师,速度都卡在审批组。加人只是让排队等着审批的队伍更长。

Web 流量水平扩展,加应用服务器就行,直到每个请求都要打同一个共享数据库或认证服务。之后再加机器没用。

AI 让编码变快了,但思考、检查、修错、协作这些步骤没法并行。所以有人效率提升 10 倍,有人只有 1.2 倍。

Murphy 定律

可能出错的事一定会出错。

空指针、竞态条件、网络断开,用户量够大的时候,一定会在最糟糕的时刻出现。所以代码要写防御性的:检查空值、处理异常、验证输入。运维那边也准备好:监控、回滚方案、应急预案,都不能少。

2024 年 7 月 19 日,CrowdStrike 改了 Falcon Sensor 的配置,850 万台 Windows 蓝屏。修复得人逐台登录,因为机器起不来,远程不了。偏偏那边是周五上午,而且IT还 不在岗。航空公司、医院、银行全中招。能出错的都出了。

Postel 定律

发送要保守,接收要宽容。

自己返回数据时,格式严格按规范来。但收别人的数据时,即使格式不标准只要能安全解读就别直接丢。

浏览器就是活例子。网上大部分 HTML 写得都不规范,但浏览器照样渲染。严格按规范来的话,半个互联网打不开。

但太宽容也有代价:所有人都容忍乱七八糟的输入,问题永远不会被修正,只会越积越多。安全敏感的代码里,容忍输入等于给攻击者开门。宽容不等于放任,得有判断。

Sturgeon 定律

90% 的东西都是垃圾。

造的大部分东西没人用,写的大部分代码质量不行,启动的大部分项目没能交付预期价值。这没什么,创造的常态就是这样。假装一切都好、给每个项目同样的资源,只会把事情搞复杂。要紧的是找到那 10%,砍掉其余的。

WordPress 插件目录大约有 57000 个插件,超过 34000 个两年没更新过,将近 19% 零安装。少数维护良好的插件撑起了公开网站 40% 以上的份额。

Cunningham 定律

在网上得到正确答案最快的方法,是贴一个错误的答案。

论坛提问经常没人理。贴一个明显错误的东西,马上就有人跳出来纠正。路过问题可能懒得停,看到错误忍不住要改。遇到难题别问"该怎么做",提一个你明知道不太行的方案,正确答案可能不请自来。

Wiki 和后来的 Wikipedia 赌的就是这个:人们改错的速度远快于从零写新文章。事实证明这个赌赌对了,Wikipedia 就是靠这个机制撑起来的。

这些定律之间的关系

20 条不用全记住,前五六条能解决大部分问题,剩下的等新问题冒出来再查。关键是要知道什么时候哪条适用。

定律之间会打架。Knuth 说别过早优化,Amdahl 说找到瓶颈修掉它。两条都对,但适用时机不同:项目早期听 Knuth,性能真出问题了听 Amdahl。

还有一点:这是作者个人的清单,他还出了本书涵盖了 56 条定律。你自己的清单会不一样。哪些定律让你吃过亏,哪些对你就是最重要的。下次遇到项目事故、团队重组、系统重写,花一分钟记下来:哪条定律帮了你?哪条给了教训?记久了,你自己的清单比谁的都好用。

框架、平台、部署模式从 Brooks 1975 年写书到现在变了很多,但这些定律没变,因为人性从不改变。

软件工程 : 项目管理 : 软件架构