对于一个大型复杂的系统来说,通常包含多个模块或多个组件构成,模拟各个子系统的故障是测试中必不可少的环节,并且这些故障模拟必须做到无侵入地集成到自动化测试系统中,通过在自动化测试中自动激活这些故障点来模拟故障,并观测最终结果是否符合预期结果来判断系统的正确性和稳定性。如果在一个分布式系统中需要专门请一位同事来插拔网线来模拟网络异常,一个存储系统中需要通过破坏硬盘来模拟磁盘损坏,昂贵的测试成本会让测试成为一场灾难,并且难以模拟一些需要精细化控制的的测试。所以我们需要一些自动化的方式来进行确定性的故障测试。
Failpoint 项目 就是为此而生,它是 FreeBSD failpoints 的 Golang 实现,允许在代码中注入错误或异常行为, 并由环境变量或代码动态激活来触发这些异常行为。Failpoint 能用于各种复杂系统中模拟错误处理来提高系统的容错性、正确性和稳定性,比如:
- 微服务中某个服务出现随机延迟、某个服务不可用。
- 存储系统磁盘 I/O 延迟增加、I/O 吞吐量过低、落盘时间长。
- 调度系统中出现热点,某个调度指令失败。
- 充值系统中模拟第三方重复请求充值成功回调接口。
- 游戏开发中模拟玩家网络不稳定、掉帧、延迟过大等,以及各种异常输入(外挂请求)情况下系统是否正确工作。
- ……
为什么要重复造轮子?
etcd 团队在 2016 年开发了 gofail 极大地简化了错误注入,为 Golang 生态做出了巨大贡献。我们在 2018 年已经引入了 gofail 进行错误注入测试,但是我们在使用中发现了一些功能性以及便利性的问题,所以我们决定造一个更好的「轮子」。
如何使用 gofail
-
使用注释在程序中注入一个 failpoint:
// gofail: var FailIfImportedChunk int // if merger, ok := scp.merger.(*ChunkCheckpointMerger); ok && merger.Checksum.SumKVS() >= uint64(FailIfImportedChunk) { // rc.checkpointsWg.Done() // rc.checkpointsWg.Wait() // panic("forcing failure due to FailIfImportedChunk") // } // goto RETURN1 // gofail: RETURN1: // gofail: var FailIfStatusBecomes int // if merger, ok := scp.merger.(*StatusCheckpointMerger); ok && merger.EngineID >= 0 && int(merger.Status) == FailIfStatusBecomes { // rc.checkpointsWg.Done() // rc.checkpointsWg.Wait() // panic("forcing failure due to FailIfStatusBecomes") // } // goto RETURN2 // gofail: RETURN2:
-
使用
gofail enable
命令将注释转换为代码:if vFailIfImportedChunk, __fpErr := __fp_FailIfImportedChunk.Acquire(); __fpErr == nil { defer __fp_FailIfImportedChunk.Release(); FailIfImportedChunk, __fpTypeOK := vFailIfImportedChunk.(int); if !__fpTypeOK { goto __badTypeFailIfImportedChunk} if merger, ok := scp.merger.(*ChunkCheckpointMerger); ok && merger.Checksum.SumKVS() >= uint64(FailIfImportedChunk) { rc.checkpointsWg.Done() rc.checkpointsWg.Wait() panic("forcing failure due to FailIfImportedChunk") } goto RETURN1; __badTypeFailIfImportedChunk: __fp_FailIfImportedChunk.BadType(vFailIfImportedChunk, "int"); }; /* gofail-label */ RETURN1: if vFailIfStatusBecomes, __fpErr := __fp_FailIfStatusBecomes.Acquire(); __fpErr == nil { defer __fp_FailIfStatusBecomes.Release(); FailIfStatusBecomes, __fpTypeOK := vFailIfStatusBecomes.(int); if !__fpTypeOK { goto __badTypeFailIfStatusBecomes} if merger, ok := scp.merger.(*StatusCheckpointMerger); ok && merger.EngineID >= 0 && int(merger.Status) == FailIfStatusBecomes { rc.checkpointsWg.Done() rc.checkpointsWg.Wait() panic("forcing failure due to FailIfStatusBecomes") } goto RETURN2; __badTypeFailIfStatusBecomes: __fp_FailIfStatusBecomes.BadType(vFailIfStatusBecomes, "int"); }; /* gofail-label */ RETURN2:
gofail 使用中遇到的问题
- 使用注释的方式在代码中注入 failpoint,代码容易出错,并且没有编译器检测。
- 只能全局生效,大型项目为了缩短自动化测试的时间会引入并行测试,不同并行任务之间会存在干扰。
- 需要写一些 hack 代码来避免一些不必要的错误日志,比如如上代码,必须要写
// goto RETURN2
和// gofail