心安理得地复制代码
强韧和反脆弱性的系统不必像脆弱的系统一样,后者必须精确地理解这个世界,因而它们不需要预测,这让生活变得简单许多。
要看看冗余是一种多么缺乏预测性,或者更确切地说,预测性更低的行为模式,让我们借用一下第 2 章的说法:如果你把多余的现金存入银行(再加上储藏在地下室的贸易品,如猪肉和豆泥罐头,以及金条),你并不需要精确地知道哪些事件可能会陷你于困境。这些事件可能是一场战争、一场革命、一场地震、一次经济衰退、一场疫情、一次恐怖袭击,或者新泽西州的分裂等任何事情,但你并不需要作太多的预测。
《反脆弱》 — [美] 纳西姆·尼古拉斯·塔勒布
重复
几年前我刚到一个 Golang 团队里工作,主要业务是对接一些供应商,完成虚拟产品的线上发货。
当时节奏很紧凑,很多工作是在对接五花八门的供应商 HTTP API,代码大概是这样的:
1 | func Payment(request *Request) (result *Result) { |
优化
关于代码的扩展性和可读性,可能不同人观点不同,但像重复地记录日志这种工作,只会增加我的工作量和出错率,这我可忍不了,立马写了个工具类:
1 | type HttpCall struct { |
简洁、对称的代码。
可惜,QA 给我提了一个 Bug,说我没有记 ProviderErrorCode
和 ProviderErrorMessage
。这些是供应商的错误码和错误信息,需要先解析各不相同的供应商回复内容,才能拿到数据,这也导致记录的位置比较隐晦,新人经常漏记。
解决这一个 Bug 我只需加两句代码,但我可不想以后还要检查有没有漏记日志,我想做有趣的事,想解决困难的问题,想顺便展示一下我高超的编程水平。我需要写几个抽象:
1 | type PaymentResponse interface { |
我还需要写一个流程来使用这个抽象:
1 | type PaymentFlow struct { |
我还在接下来的几个新供应商里都用了这些代码,抽象开始增加,流程代码更加通用和复杂,也就是——跟框架一样。一次合作中,同事认为我增加了不必要的复杂度,被激怒的我批评他“甘愿写屎一样的代码”。
下个供应商由VISA提供 API,这次的 API 有些复杂:
- 有 Message Level Encryption(PM:加解密失败都要有日志)
- 提供 Check Transaction Status API(PM:要记录 transaction id,下次重试用)
- 错误码有的放在 Http Status Code,有的放在 Body 里(PM:判断分支按照这个表格走)
那一周我都在加班,带着我修修补补的框架。
到了测试阶段,QA 过来找我:“为什么会改动其他供应商的文件?因为 go 接口变了?这不在本次测试范围内,有风险”。
我麻了。
冗余
没多久又要接新的供应商。
这次我先复制了以前的代码,然后根据需求修改、简化。整个过程迅速、简单,写出来的代码 Bug 少、隔离性强,也没有人说我写代码很绕了。
我开始心安理得地复制代码。
那么,代价呢?
Wiki 上说,复制代码的代价是它增加了项目的长期维护成本,当时在低风险和低维护成本之间,我毫不犹豫地选择了前者,但我没意识到,冗余是有长期风险的。
TransactionV2
团队一直用一个自研、简单的 ORM 库,数据库事务用一个简单函数实现:
1 | func Transaction(db string, fn func(o *Orm) error) (err error) { |
当时我参与开发了一个重要项目,中途一个主力同事休假了,收尾工作由我负责。
项目上线,一个离线服务没有响应任何请求,也没有崩溃。
气氛有些紧张,我赶紧查 goroutine 都在做什么:
- 导出函数调用栈,发现很多 goroutine 都卡在获取数据库连接了,很可能是某个地方没有释放连接
- 尝试用常见 panic 错误搜索日志,定位到一个空指针 Bug 导致 panic
- 查看调用链,发现实现事务的
Transaction
函数处理 panic 时不Rollback
事务
这是一次顺利的问题排查,我有些自豪,开完会的 Leader 刚坐下来,我就过去跟他说了这些问题,并就Transaction
的问题提了一个自然的 Hotfix 修复方案:“只需在 Transaction
里只需增加一行 Rollback
代码”。
为了严谨,我补充了一句:“看上去不会增加额外问题,但理论上影响范围包含所有服务”。
Leader 有些迟疑。
我紧张地又补了句:“也可以增加一个带 Rollback
的 TransactionV2
,只替换本次导致 panic
的几处 Transaction
调用,影响范围小,也明确”。
Leader 选择了第二种方案,我增加了 TransactionV2
函数。
救火
一天晚上我洗完澡,看到有 Leader 的未接来电,我赶紧回拨,Leader 说我负责的模块有告警,让我帮忙看一下怎么回事。
打开电脑,发现群里不少同事在排查问题,资损有些严重。在与同事确认问题跟我负责的模块无关后,我开始从容地排查问题。
检查了一会日志,我发现有些 goroutine 像是消失了,没有继续写日志。刚好群里发过调用栈 Dump,打开一看,又是数据库连接没有释放的问题。于是我熟练地找到 panic 日志,发到群里解释,panic 问题很快被修复了。
1%的责任
隔天早上,有同事夸我能力强,这么快定位到这个隐秘的 Bug。我谦虚地告诉他们,之所以定位快是因为我遇过这个 Bug,当时还写了TransactionV2
来解决问题。
“你既然知道这个 Bug,为什么不直接修正原来的 Transaction
函数呢?”
我紧张地解释这个修复方案是 Leader 确定下来的。
Leader 也知道了,问了我一样的问题。
不久,一位关系不错的同事在他给我的 Peer Feedback 上写道:
他重构了一些有隐藏 bug 的方法,并给出了 v2 版本,但是只在群里同步了这个问题,但有时候很容易被群消息淹没,我觉得可以把这些上升给上级,让上级在组内向下推动,避免一些问题的发生。
回顾
冗余有低风险、简单、灵活等好处,但是会提高长期维护成本和风险,项目的健康发展需要持续维护代码质量。