可靠计算系统设计分析与验证课程笔记 5:从不同开发阶段的验证方法出发,讨论故障仿真、硬件与软件故障注入、实验结果分析、定向故障注入,以及现场数据中的相关失效
故障注入与实验验证
上一篇介绍了 watchdog、heartbeat、控制流检查和应用断言等检测机制。要知道这些机制是否有效,还需要让系统真正面对错误,观察它能否及时发现异常、限制传播并恢复服务
故障注入(Fault Injection)通过人为施加可控制的扰动,研究系统在异常条件下的行为。它既可以验证一个恢复方案,也可以揭示尚未考虑的传播路径,例如一个寄存器比特翻转如何演化为错误地址、内核崩溃或静默的数据损坏
实验结论始终与故障模型、注入位置、触发时刻、工作负载和观察范围有关。把这些条件写清,才能判断结果能够支持什么设计决策
不同开发阶段怎样验证可靠性
系统的开发阶段不同,可观察的信息和适用的实验手段也不同
| 阶段 | 主要方法 | 能得到的信息 | 关键限制 |
|---|---|---|---|
| 早期设计 | 在 CAD、逻辑或系统模型中进行故障仿真 | 故障影响、错误延迟、检测覆盖率、恢复时间分布 | 模型与输入是否足够准确 |
| 原型阶段 | 在可运行原型中进行硬件或软件故障注入 | 实际传播路径、检测与恢复行为、服务中断 | 注入场景能否代表目标故障 |
| 运行阶段 | 收集真实工作负载下的日志与性能数据 | 自然失效特征、失效率、失效间隔、相关性 | 数据完整性与被观测系统的代表性 |
仿真允许在硬件尚未实现时修改设计;原型实验可以观察软硬件之间的实际交互;现场测量则提供真实环境中的证据。三者可以互相校验,例如用现场数据修正故障模型,再用注入实验检验改进后的恢复机制
故障注入常常主动提高异常出现的频率,以便在有限时间内观察足够多的案例。因此,注入实验中的“每分钟崩溃次数”不能直接当作系统在生产环境中的自然失效率
先明确注入的是故障还是错误
沿用故障传播链
改变晶体管电流,是在较低层模拟故障的物理作用;直接修改内存中的一个比特,则通常是在高层放入一个已经损坏的状态,也就是模拟故障所产生的错误
两者都可以纳入广义的故障注入,但实验解释不同。例如寄存器翻转可以研究错误值被使用后的行为,却不直接说明某种物理故障产生该错误值的概率
一次注入需要至少回答以下问题:
| 维度 | 需要明确的内容 | 例子 |
|---|---|---|
| What | 扰动类型、数量和持续时间 | 单比特翻转、持续 stuck-at、消息丢失 |
| Where | 注入层次与具体目标 | 某个寄存器、内核栈、API 返回值 |
| When | 时间或事件触发条件 | 运行后某时刻、执行到某条指令、某次请求到达 |
| Workload | 系统正在处理什么任务 | 文件读写、事务请求、网络通信 |
| Observation | 怎样判定结果与结束实验 | 输出校验、告警、崩溃转储、超时 |
“向程序注入随机错误”仍然不够具体:在全部地址中均匀抽样,在被访问地址中抽样,以及在每次内存访问时抽样,会得到不同的实验分布
设计阶段:分层故障仿真
电气层:保留物理作用细节
电气层仿真可以改变电流、电压或器件参数,研究物理扰动如何影响电路。例如带电粒子引起的瞬态电流脉冲,可能在节点上形成电压变化,再传播到后续逻辑
简单的 stuck-at 模型无法表达所有这类现象。瞬态脉冲的宽度、幅度、发生时刻和传播路径,都可能决定它是否最终被寄存器捕获
这一层细节丰富,但计算成本高,通常适合研究局部器件、电路或关键路径
逻辑层:用抽象故障模型扩大规模
逻辑层通常操作门、信号线和触发器,常见模型包括:
- Stuck-at-0 / Stuck-at-1:在指定持续时间内强制信号为固定值,可用于模拟永久性故障
- 瞬态反相或比特翻转:在某一时刻改变逻辑值,模拟短暂扰动产生的错误
为了让逻辑模型更接近电气行为,可以先在低层进行大量实验,再构造故障字典。字典按输入向量、故障位置和注入时刻记录可能的输出错误模式及其频率
例如,一个四位加法器受到电流脉冲影响后,可能只改变进位,也可能同时改变若干和位。高层仿真可以使用这些模式,而不必每次重新模拟全部晶体管
字典中的概率仍然对应生成它时使用的条件。输入分布、器件参数或时序改变后,需要重新检查模型是否适用
功能层:观察软件与系统行为
功能层把组件抽象成处理器、存储器、通信链路和节点,可以注入
- 内存或寄存器比特翻转
- 消息内容损坏、丢失或延迟
- 节点服务中断
- I/O 错误或异常返回值
这一层能够运行更完整的软件和工作负载,因而适合观察应用相关的传播路径。同样的错误值,可能在一个程序中被覆盖,在另一个程序中成为数组索引,在第三个程序中改变控制流
分层仿真的取舍是:较低层解释扰动怎样形成错误,较高层解释错误怎样影响服务。连接两层时,需要保留对上层结果有决定作用的信息
原型阶段:硬件故障注入
FPGA 中的掩码注入
一种实现方式是把故障注入模块与目标电路一起综合到 FPGA 中,为目标触发器配置对应的注入掩码位
处理器通过扫描链装入掩码,当注入信号有效时,对指定位置的组合逻辑输出进行翻转,并把结果写入触发器。对单个位置,可示意为
其中 表示选择该位置。这样能够精确控制被改变的状态位和捕获时刻,适合研究寄存器状态错误及其后续传播
注入电路也会占用面积、影响布线或时序,因此需要确认加入注入模块后的原型仍能代表待评估设计
插入网络数据通路的注入器
网络硬件注入器可以直接放在数据通路中,按流水线完成
以 FPGA 为核心,可以根据包头、控制信息或特定事件触发注入,修改某些软件接口难以触及的链路控制状态,并统计流量和注入结果
它可以作为透明的链路中间设备工作,而不成为一个普通网络节点。短流水线有助于减少附加延迟,但“透明”仍需通过正常运行对照来验证,尤其要观察吞吐量、延迟和协议时序是否被改变
硬件注入的优势是接近目标层次、时间控制精细;成本则包括专用设备、实现复杂度和可访问位置的限制
软件实现的故障注入
软件实现的故障注入通常记为 SWIFI,也可见 SIFI。它利用操作系统、调试器、插桩或接口拦截来改变目标状态,具有成本较低、易于自动化和便于扩展模型的特点
常见实现方式
| 方法 | 实现思路 | 适合观察的问题 |
|---|---|---|
| 编译期修改 | 修改源代码、字节码或生成代码 | 特定软件缺陷、判断条件或计算逻辑变化 |
| 运行时内存修改 | 改变代码段、数据段、堆或栈 | 状态损坏、指针错误、指令损坏 |
| 寄存器修改 | 在断点或异常处修改寄存器 | 地址、运算结果或控制状态损坏 |
| API / 系统调用拦截 | 改变参数、返回码或返回数据 | 错误处理路径、依赖服务异常 |
| 资源限制 | 限制内存、磁盘空间或带宽 | 资源不足时的降级与恢复 |
| 网络注入 | 丢弃、延迟或修改消息 | 重试、超时、复制和一致性处理 |
软件工具只能改变自己能够访问或拦截的状态。普通用户态工具对内核、设备内部状态和底层链路控制的可见范围,与内核模块或专用硬件不同
API 拦截:制造依赖失败
历史上的 Holodeck 工具使用系统调用和 API 拦截,在 Windows 用户应用与其运行环境之间注入异常,例如文件被占用、磁盘数据错误、网络不可用、注册表项损坏以及异常返回值
这类方法直接检验应用如何处理依赖失败:函数返回错误后是否仍使用无效句柄,写入失败后是否错误地报告成功,内存分配失败后是否仍访问指针
注入模型应与接口语义一致。若接口约定失败时不会修改输出缓冲区,实验却同时随意改写缓冲区,就需要把这一额外扰动明确纳入模型
调试器注入:把触发位置与修改动作结合起来
基于 GDB 的方法可以在函数入口、指令地址或数据访问处设置触发点,暂停执行后读取目标值,再修改内存或寄存器并继续运行
翻转字中第 位的基本操作为
自动化脚本可以组织“启动程序、等待触发、读取状态、执行翻转、继续运行、收集结果”的全过程。没有源代码时,也可能依据二进制符号和反汇编定位,但可获得的语义信息通常更少
暂停与恢复会改变执行时序,特别容易影响并发程序、竞态和实时任务。对于一过性的存储翻转,只修改一次状态与持续强制该位置为错误值,也代表不同的故障持续模型
注入目标与触发时刻怎样选择
随机抽样与热点分析
随机注入可以扩大覆盖范围,但大量选中的位置可能根本不会被当前工作负载访问。代码 profiling 可以找出热点函数和控制流,数据 profiling 可以找出频繁访问的结构,再对这些位置进行有针对性的注入
例如,一项基于 UnixBench 的 Linux 内核分析在 个观测函数中选出了 个核心函数,分布在体系结构相关代码、文件系统、内核公共逻辑和内存管理等子系统。这种目标选择反映的是该工作负载下的执行特征
热点注入提高了观察到错误影响的效率,但实验统计的是被选中位置上的行为。要推断更大范围的风险,需要说明抽样权重;低频但关键的恢复路径、启动路径和边界条件,也需要单独覆盖
时间触发与事件触发
时间触发可以在启动后固定时刻或随机时刻注入,比较容易实现。事件触发则把注入条件绑定到执行状态,例如
- 某条目标指令即将执行
- 某个变量第一次被读取
- 某次系统调用准备返回
- 某个请求已经执行,但响应尚未发出
事件触发有助于在重复实验中命中相近的语义位置。例如测试“写盘成功后、返回确认前崩溃”,用执行事件定位通常比等待固定毫秒数更准确
自动化注入实验怎样组织
实验框架将控制主机与注入目标分开:控制主机执行实验脚本并收集日志,目标节点的进程管理器负责运行注入器、应用、工作负载与监测器

每轮先初始化系统并运行负载,命中触发条件后注入,随后校验结果、保存证据并恢复基线,再进入下一轮
目标内核可能崩溃或挂起,因此关键日志和转储最好能送往独立的收集端,并由外部监控判断是否需要重启。每轮结束后,需要清除上一轮遗留的损坏状态,避免把连续污染误认为独立实验
结果中还应区分“没有触发”“注入器执行失败”和“注入成功但没有影响”。否则工具自身的问题可能被统计成系统成功容错
结果分析:注入、激活与失效不能混为一谈
激活是过程属性,结局是结果属性
损坏位置被执行或读取,称为错误被激活。已激活错误仍可能被覆盖、屏蔽或纠正,因此不一定产生可见失效
实验可以分别记录是否激活、是否告警、是否恢复,再记录最终结局:
| 观测结果 | 含义 |
|---|---|
| 未激活 | 在观察窗口内,损坏的指令或数据没有被使用 |
| 已激活但未表现异常 | 错误被使用,但没有观察到目标范围内的异常 |
| 检测并恢复 | 机制发现异常,并在规定条件下恢复服务 |
| 错误输出 | 结果或外部状态偏离要求 |
| 崩溃 | 应用或操作系统终止、panic 或进入不可继续执行的异常路径 |
| 挂起 | 在规定期限内失去有效进展,例如死锁或活锁 |
这些信息不应简单混成一个互斥标签表:一个实验既可能“已激活”,也可能“被检测”,最终又表现为“崩溃”。需要在数据结构中分开记录,或者明确规定最终分类的优先级
Fail-silent 要求组件发生失效时保持静默,避免向外传播错误结果。如果组件继续返回错误数据,就违反了这一假设。判断这种情况需要独立的结果校验,单看进程是否退出无法发现静默数据损坏
覆盖率必须注明条件
设成功施加扰动的实验数为 ,其中激活的数量为 。若选择“已激活错误”作为检测目标,检测覆盖率应写成
其中 只统计既已激活又被检测的事件。类似地,给定检测后恢复成功的比例为
这里 的判定需要明确,例如“恢复后输出正确且在期限内完成”,而不只是“重启成功”
举一个假设实验:成功注入 次, 次被激活,其中 次被检测到,那么激活比例为 ,以已激活错误为目标的检测覆盖率为 。 表示成功注入实验中产生检测结果的比例,回答的是另一个问题
如果只想统计会导致外部错误的事件,则还需要能够识别这类目标,例如通过关闭保护机制的配对实验估计。未激活或被应用自然屏蔽的错误,不能直接算作检测器的保护成果
延迟也需要一个明确的起点
设 为注入时刻, 为激活时刻, 为检测时刻, 为恢复服务时刻,则可以定义
从注入到检测的时间包含等待激活的部分,不能直接当作检测器自身的响应时间。分布式实验还应使用可比较的时钟或因果事件来建立顺序
对观察窗口结束时仍未检测或仍未恢复的实验,应报告超时或删失情况。只对成功完成的样本求平均,会遗漏恢复最慢或根本无法恢复的情况
Linux 内核注入:架构怎样影响错误传播
一项历史实验在 Pentium 4 与 PowerPC G4 平台上的 Linux 2.4.22 内核中,向代码、数据、栈和处理器系统寄存器注入错误,累计超过 次。工作负载和热点分析用于确定可比较的目标范围
崩溃原因分布揭示检测位置
下图分别统计 P4 的 个崩溃样本与 PPC G4 的 个崩溃样本

比例的分母是各平台的崩溃样本,不能直接比较两平台的总体失效概率。分类粒度也不同:PPC 的 Bad Area 包含空指针等坏地址访问,而 P4 单独列出了 NULL Pointer
这些数据有助于理解错误最终被哪一类机制发现,但一次坏地址异常的根因可能来自更早的指令损坏、返回地址破坏或错误写入,最终异常类型不等于原始故障类型
相同的比特翻转可以产生不同延迟
在一个 PPC 代码注入例子中,一位翻转把 mflr r0 改成了 lhax r0,r8,r0。前者读取链接寄存器,后者按寄存器计算地址并执行半字加载
新的指令仍然合法,但它访问哪里取决于当时的寄存器值。因此,同一条指令上的相同比特翻转,在不同运行状态下产生了约 到 个 CPU 周期的崩溃延迟
栈错误的传播同样受到检测位置影响。较早发现栈越界,可以减少错误状态继续运行的时间;若损坏在较晚的访存阶段才暴露,执行期间可能已经产生更多污染
这些观察说明,错误影响由指令编码、数据布局、运行状态、硬件异常检查与操作系统处理共同决定。历史平台上的结论应结合其内核版本、体系结构和负载理解
定向故障注入:命中因果路径中的窗口
在分布式系统中,某些故障只有发生在特定事件序列中才会暴露问题。例如请求已执行、状态已持久化,但确认尚未送达时崩溃,会检验重试是否导致重复执行
定向故障注入(Targeted Fault Injection,TFI)通过识别因果有序的事件序列,在指定的最后事件执行前施加扰动。它将“何时注入”从墙上时钟时间转化为可解释的执行上下文
一个描述实验的示意配置可以写成
1 | 前置事件:请求到达 -> 事务写入完成 |

中央控制器(CC)下发命令,本地控制器(LC)跟踪目标进程并执行注入,全局状态记录器(BK)反馈进展。ZooKeeper 可以承载命令、同步状态和进程信息,帮助确认前置事件是否完成
“观察到目标事件”和“在目标事件之前完成注入”之间仍可能存在竞态,因此需要由本地拦截点或执行控制来落实时序。确定性的触发描述,也应配合实际事件日志验证
怎样判断实验结论是否可信
有限样本带来统计不确定性
假设实验独立、来自同一个明确的抽样分布,将某种失效视为概率为 的二项事件。 次实验中出现 次失效,比例估计为
没有观察到失效也不意味着 。若 ,单侧 置信上界可由
得到
例如 次独立实验零失效,对应上界约为 。这个界限只针对约定的实验分布,无法覆盖完全没有测试过的故障类别或状态
抽样偏差与工具扰动需要分别处理
增加样本数可以降低随机误差,却无法自动修复偏差。例如只选择容易检测的指令、只测试轻负载,或者把没有触发的实验算成成功,都会使结果偏乐观
工具本身也可能影响结果:断点改变调度顺序,插桩增加执行时间,日志写入改变 I/O 压力,监测进程消耗原本紧张的资源。可以分别进行无注入运行、仅开启工具运行和实际注入运行,观察这些差异
可复现的记录应包含软件与配置版本、负载、随机种子、注入参数、实际触发事件、原始日志、输出判据和恢复动作。统计汇总适合寻找规律,具体案例分析适合解释错误怎样一步步传播
运行阶段:用现场数据修正模型
从日志报告整理为失效事件
现场测量通常先清洗和合并日志,再识别事件、提取指标,必要时建立模型并求解。一个故障可能同时触发应用超时、驱动告警和节点重启,不能把每条日志都当作一次独立失效
分析时应区分重复报告、同一故障引起的传播事件,以及真正独立的故障,并考虑监测中断或日志丢失带来的缺口
相关失效会削弱冗余
冗余模型经常从组件独立失效开始,但共享电源、网络、存储、软件缺陷和运维操作都可能同时影响多个副本
历史上对 DEC 和 Tandem 系统的分析发现,一部分问题涉及相关失效,其比例在被研究数据中约为 到 。这个范围反映特定现场样本,不能直接作为所有系统的参数
对于两个副本,独立模型使用
而存在共同原因时,需要直接估计联合失效概率或建立共因模型。只验证“一个节点失效时另一个能接管”,不足以验证它们依赖的共享资源同时异常时的行为
从事件聚合到失效预测
一种历史分析方法先合并重复报告,再把较短时间内的记录组成错误组,将更长时间内的错误组组成事件,最后寻找事件间共有的症状
例如,可以用 分钟组织错误组、用 小时组织事件,并把至少出现在一半错误组中的错误类型作为症状,再合并具有共同症状的相关事件。这些时间窗和阈值是分析方法的参数,需要结合系统调整
预测可以基于这些症状、性能异常和状态转移关系,但发现相关性不等于已经识别因果。现场数据最直接的价值,是帮助发现实验模型遗漏的故障类型、共同原因和实际触发条件
把验证结果反馈到设计
故障注入得到的不只是一个覆盖率数字,还应包括未被检测的传播路径、恢复失败的位置、服务中断的分布,以及哪些错误已经越过了保护边界
若结果错误但没有告警,需要补充检测;若检测及时却无法接管,需要检查恢复状态和切换协议;若副本同时受到影响,需要重新考虑故障隔离范围。