Golang Failpoint 的设计与实现

文章介绍了Failpoint项目,它是Golang实现的一种错误注入工具,用于在代码中无侵入地注入故障,以便在自动化测试中模拟各种异常情况,提高系统的容错性和稳定性。文章还讨论了现有工具gofail的局限性,并提出了Failpoint的设计准则,包括代码定义、无侵入、无性能影响等,并展示了如何在Golang代码中使用failpoint进行错误模拟。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于一个大型复杂的系统来说,通常包含多个模块或多个组件构成,模拟各个子系统的故障是测试中必不可少的环节,并且这些故障模拟必须做到无侵入地集成到自动化测试系统中,通过在自动化测试中自动激活这些故障点来模拟故障,并观测最终结果是否符合预期结果来判断系统的正确性和稳定性。如果在一个分布式系统中需要专门请一位同事来插拔网线来模拟网络异常,一个存储系统中需要通过破坏硬盘来模拟磁盘损坏,昂贵的测试成本会让测试成为一场灾难,并且难以模拟一些需要精细化控制的的测试。所以我们需要一些自动化的方式来进行确定性的故障测试。

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天读点书学堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值