那是我批准的一段代码。另外三位资深工程师也点了头。静态分析器通过了。Fuzzer也没发现问题。所有我们信任、专门用来捕捉这类Bug的工具,全都给出了绿灯。
结果还是炸了。
一个lambda捕获了一个引用。这个引用活得比它的作用域还久。在某个层层嵌套的回调链里,我们开始读取一块已经不属于我们的内存。监控系统在19分钟之后才发出警报。
在交易系统里,19分钟非常、非常漫长。
找到根因后,我在停车场坐了半小时。不是因为愤怒,而是因为我真的不知道——我们还能做什么更多的事。所有最佳实践都遵循了。现代C++特性全用上了。我们一直很小心。
但这并不重要。
六个月后,我们用Rust重写了整个系统。性能测试的结果,直接颠覆了我的认知。
没人告诉你的生产事故真相
最糟糕的不是Bug本身,是事后的那种安静。
事故发生后,Slack频道突然沉寂下来。没人想留下任何一句可能会被写进复盘文档的话。我看着在线状态一个个消失——大家突然都“有事要忙”。
我留了下来。
花了九个小时,顺着回调链一层层追踪指针,在我们为了“让C++看起来更安全”而构建的抽象里,寻找所有权是如何悄悄溜走的。
Bug藏在一个异步处理器里。外层作用域已经结束,内层的lambda还握着一个早就不存在的引用。就六行代码。我们所有的工具,全都看不见。
那一刻,我意识到一件令人不适的事:我们不是在C++上失败。我们是在C++上成功了。 当一门语言允许你持有一个“幽灵引用”,这就是成功的样子。
我们一直在骗自己的那个谎言
我们真的以为自己什么都做对了。
- 智能指针无处不在
- RAII干净得可以裱起来
- 每次提交都跑静态分析
- Code review动不动就三天,只因为“内存安全不能妥协”
我们曾是那种会嘲笑“内存Bug公司”的团队。我甚至在2023年做过一次分享,主题是:
安全是纪律问题,不是语言问题。
台下掌声热烈。那时候我对自己感觉很好。那些PPT我现在还留着,偶尔打开看看,提醒自己什么叫自信的无知。
真相是这样的:我们一直在防御一个永远不睡觉的敌人。每一个C++函数,都是一次与未定义行为的无声谈判。你可以赢一千次。你只需要输一次。
我们输了那一次。代价是230万美元,以及三位工程师决定“到此为止”。
所谓的“安全税”,到底是谁在交
大家总说Rust慢,说你要为安全付出性能代价,说借用检查器不免费。
这是我们重写交易撮合引擎后的真实数据:
p99 延迟(微秒)
C++(之前) ████████████████████████ 47.2 μs
Rust(之后) ███████████████████ 38.1 μs
↓
快了 19.3%
我花了两周时间试图找出基准测试的问题。没有问题。
Rust更快,是因为我们不再恐惧地写代码。
- 不再给永远不会是null的指针写防御性判断
- 不再因为所有权不清晰而做多余的数据拷贝
- 不再在运行时验证编译器早就能证明的事实
// 旧的C++生产代码
void process(Order* o) {
if (!o) return; // 理论上永远不为空,但谁敢信
auto copy = *o; // 所有权不清晰,先拷贝一份
if (copy.qty <= 0) return;
// 真正的逻辑从这里才开始
}
// Rust版本
fn process(o: &Order) {
// 编译器保证引用有效
// 从第一行就开始干正事
}
安全税确实存在。只是我们之前,一直把钱交给了错误的语言。
那些离开的工程师
宣布重写的那周,Marcus给整个工程团队发了一封邮件。标题是:
C++ IS NOT THE PROBLEM
十四段文字。十五年的经验。一封为自己整个职业生涯辩护的长信。他在周四辞职了。
我不怪他。当编译器开始比你更擅长你的工作时,人心里真的会断点什么。你花十年培养的直觉,突然变成可以忽略的警告。
那封邮件我留着。偶尔会重读一遍,提醒自己:“正确”和“善良”,不是一回事。
那个让我失眠的部分
上个月,一位初级工程师加入了团队。23岁,只有8个月的Rust经验。她第二周就提交了生产代码——默认就是内存安全的代码。
放在C++时代,我至少会拉三个资深工程师盯着review。她不知道什么是悬空指针,从没花九个小时追踪use-after-free,看到裸指针也不会条件反射地紧张。因为她从没被它们伤害过。
我有点嫉妒她。也真心庆幸:她不必像我一样学这些东西。
老实说一句
你该不该把C++项目重写成Rust?我不再说“可能不用”,也不打算再保持所谓的理性。
我现在真正相信的是:你留在C++的每一天,都在选择继续支付安全税。 静态分析器、代码评审、防御式编程、心理负担——你用工程师的时间、注意力和精力在交这笔税。
对于这类系统级语言的选择与性能讨论,欢迎在云栈社区与更多开发者交流。