无法重启服务器时的软件工程
太空软件无法优雅地失败。了解工程师如何为一个 bug 就可能毁掉十亿美元任务、重启要耗时数小时的系统编写代码。

凌晨 3 点,你的 Web 服务器崩溃了。Kubernetes 把它重启,用户看到一个短暂的报错页面,没有人会因此被开除。现在想象一下,你的服务器在火星轨道上运行,重启要花 45 分钟。这期间没有姿态控制,没有热管理,也没法通信。那个「用户」是一艘价值 25 亿美元的航天器,而且根本没有 Kubernetes,只有你的代码,和它运行的那颗抗辐射 CPU。
太空软件面临的约束,会让普通软件工程显得轻松得不值一提。你没法部署热修复,没法 SSH 上去看日志,负载升高时也没法临时加机器。代码在发射前必须every line都正确,因为一旦升空,软件就得自己扛上几年甚至几十年。它运行的硬件正被辐射慢慢损伤,通信链路还有好几分钟的延迟,带宽只有每秒几千比特。
决定一切的几项约束
辐射。太空中,高能粒子不断轰击电子设备。一个粒子就可能翻转内存里的一个比特(即单粒子翻转,SEU),破坏寄存器,或者让处理器死锁。这种情况并不罕见,低地球轨道卫星每天都会经历成千上万次比特翻转。太空级硬件使用抗辐射元器件,它们更慢、更贵,而且性能落后消费级硬件好几代。火星车「毅力号」用的处理器是 RAD750,大致相当于一颗 1998 年左右、主频 200 MHz 的 PowerPC。
通信延迟。无线电信号到火星的单程时间是 4 到 24 分钟,取决于轨道位置;到木星则是 33 到 54 分钟。这不只是延迟的问题,它意味着航天器遇到任何故障,都必须在至少一个往返时间内自主处理,因为地面控制中心连问题都看不到,更别提发指令了。旅行者号探测器在距地球超过 22 光时的地方遇到异常时,软件几乎要独自做近两天的决策。
无法现场维护。坏掉的元器件没法换,内存没法加,硬盘也没法拔插。如果主计算机挂了,备份又不好使,任务就结束了。所有故障模式都必须在发射前就用软件预判并处理。
太空代码有何不同
太空软件采用的一些技术,放在普通软件开发里,都会被看成离谱的过度设计。
三模冗余(TMR)。关键计算在三个独立处理器上各运行一次,由表决器比较三个结果,采用多数的答案。如果其中一个处理器因辐射算出了错误结果,另外两个会把它投票否决。有些系统会跑五份副本(五模冗余),以求更保险。
内存擦洗。后台进程持续读取内存,用纠错码(ECC)校验,在单比特错误积累成无法纠正的多比特错误之前把它修掉。这个过程一直在运行,内存中的每个字节每秒都会被检查和修正好几次。
看门狗定时器。这是一种硬件定时器,必须由软件定期复位。如果软件挂起(可能是辐射导致的死锁),定时器超时就会触发硬件重启。因此软件必须按照「随时可能被意外重启、任何计算都可能被打断并从头再来」来设计。
// Simplified space software patterns
// Watchdog — must be kicked regularly or hardware resets
void main_loop(void) {
while (1) {
kick_watchdog(); // Reset the timer — we're alive
// All operations must complete within watchdog timeout
read_sensors();
kick_watchdog();
run_attitude_control();
kick_watchdog();
check_thermal_limits();
kick_watchdog();
process_ground_commands();
kick_watchdog();
}
}
// Critical value stored with redundancy
typedef struct {
int32_t value_a; // Primary copy
int32_t value_b; // Redundant copy
int32_t value_c; // Third copy for voting
} RedundantInt;
int32_t read_redundant(RedundantInt *r) {
// Majority vote — tolerates one corrupted copy
if (r->value_a == r->value_b) return r->value_a;
if (r->value_a == r->value_c) return r->value_a;
if (r->value_b == r->value_c) return r->value_b;
// All three differ — flag anomaly
raise_anomaly(MEMORY_CORRUPTION);
return r->value_a; // Best guess
}
测试本身就是产品
NASA 喷气推进实验室估计,太空软件的测试会占去总开发工作量的 60% 到 80%。注意,这是成本的 60%,不是时间的 60%。测试比开发还贵,因为测试要证明的正确性,远超普通软件测试所能达到的程度。
每一条代码路径都必须被测试。这里说的不是 Web 开发者口中那种「代码覆盖率很高」,而是字面意义上:每个函数里的每一条路径,包括错误路径、超时路径,以及处理硬件故障的路径。100% 分支覆盖只是起点,不是目标。
除了单元测试,太空软件还要做硬件在环测试(在真实的飞行硬件上运行真实软件,模拟太空环境)、长时间压力测试(连续运行数月,找出与时序相关的 bug),以及故障注入测试(故意破坏内存、杀掉处理器、切断通信链路,验证软件能否恢复)。
形式化验证正越来越多地用于最关键的组件。它不是测试代码对特定输入是否有效,而是从数学上证明代码对所有可能的输入都成立。这种方法昂贵且缓慢,但对于控制航天器姿态(方向)或管理推进系统的代码来说,这笔投入是值得的。
总会出问题的地方
尽管如此严格,太空软件仍然会失败。这些失败很有启发意义,因为它们暴露了即便是最谨慎的工程也有其边界。
- 火星气候探测器(1999)坠毁,原因是一个团队用英制单位,另一个团队用公制单位。软件本身没有问题,错的是需求。再多的测试也抓不住错误的规格说明。
- 阿丽亚娜 5 号 501 次飞行(1996)爆炸,是因为一个 64 位浮点数被转换成了 16 位整数,结果溢出。这段代码是从阿丽亚娜 4 号复用过来的,在 4 号上这个数值从未超出 16 位范围。代码在阿丽亚娜 4 号上是正确的,在阿丽亚娜 5 号上却造成了灾难性的错误。
- 火星极地着陆器(1999)很可能是坠毁了:展开着陆腿时,传感器的振动被误判为已经触地,于是发动机在距地面 40 米时关闭。这是一个与时序相关的传感器解读问题,测试没能发现,因为测试没有完全复现振动特征。
- 哈勃望远镜最初的镜面缺陷(1990)不是软件 bug,镜面被打磨成了错误的形状,原因是一台校准有误的测试仪器。测试工具本身就有 bug。
这些事故的共同点是:代码完全符合规格说明,但规格说明与现实不符。这是最难预防的一类 bug,因为它存在于模型与真实世界之间的缝隙里。
地面软件可以学到什么
我们大多数人并不写太空软件。但其中一些做法可以直接用在地面上构建可靠的系统。
为重启而设计。太空软件假定随时可能被重启,并且必须恢复到一个已知的良好状态。Web 服务也应该具备这一特性。如果你的服务器崩溃后重启,它能否在无需人工干预的情况下恢复?它是从中断处继续处理,还是丢掉了进行中的工作?太空工程师视为强制要求的防御性工程模式,在 Web 开发中往往是可选项,但其实不该如此。
测试故障模式,而不只是正常路径。太空软件测试会专门注入故障:杀掉进程、破坏内存、断开网络连接、让每一次系统调用都返回错误码。大多数 Web 应用测试关注的是功能能不能用,而不是系统能否优雅地处理故障。
关键数据要有冗余。如果你的应用存储的数据重建成本很高,比如财务记录、用户内容、配置状态,就应该对其做冗余存储,并定期校验。数据库复制是最明显的例子,但应用层面的冗余(校验和、数据校验、定期完整性检查)能发现复制会扩散的数据损坏。
为关键进程设置看门狗。如果某个进程绝对不能挂起,就要监控它。不只是看「进程在不在运行」,还要看「进程有没有在推进工作」。那些能验证系统是否真正在工作的健康检查(是否在处理请求、是否在更新状态、是否满足时限),能够捕获进程级监控漏掉的故障。
新一代太空软件
商业航天行业(SpaceX、Rocket Lab、Planet)正在挑战其中一些传统。SpaceX 在其 Falcon 9 的飞行计算机上使用 Linux 和 C++,按传统航空航天标准来看,这简直是离经叛道。Planet Labs 运营着数百颗小卫星,把它们当作分布式系统来管理,而不是定制硬件:一颗卫星坏了,星座会自动补偿。
这一转变反映出一个更宽泛的问题:你真正需要多高的可靠性?一台价值 25 亿美元的火星车,值得花五年做测试。而一颗价值 50 万美元、只是数百颗星座之一的通信卫星,则不需要那么多。工程实践应当匹配风险画像,而不是盲目沿用为另一个时代制定的传统。
但无论预算多少,核心教训始终成立:部署之后无法现场接触的软件,必须比可以接触的软件更可靠。无论你是在发射航天器、部署物联网设备群,还是运行边缘计算网络,原则都是一样的:如果你没法 SSH 上去修复,软件就必须自己扛下来。这种思路如果运用得当,会让所有软件都变得更好。


