golang实现代码故障注入与测试的failpoint插件使用

Golang实现代码故障注入与测试的failpoint插件使用

快速开始(使用failpoint-ctl)

  1. 从源码构建failpoint-ctl
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
  1. 在你的程序中注入故障点,例如:
package main

import "github.com/pingcap/failpoint"

func main() {
    failpoint.Inject("testPanic", func() {
        panic("failpoint triggerd")
    })
}
  1. 使用failpoint-ctl enable转换你的代码

  2. 使用go build构建

  3. 使用GO_FAILPOINTS环境变量启用故障点

GO_FAILPOINTS="main/testPanic=return(true)" ./your-program

注意:GO_FAILPOINTS不适用于InjectCall类型的标记。

  1. 如果你使用go run运行测试,不要忘记在命令中添加生成的binding__failpoint_binding__.go,例如:
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go

完整示例Demo

package main

import (
    "fmt"
    "github.com/pingcap/failpoint"
)

func main() {
    // 注入一个简单的故障点
    failpoint.Inject("simple-failpoint", func(val failpoint.Value) {
        fmt.Println("故障点触发,值:", val)
    })

    // 注入一个会panic的故障点
    failpoint.Inject("panic-failpoint", func() {
        panic("模拟panic故障")
    })

    // 注入带上下文的故障点
    ctx := context.Background()
    failpoint.InjectContext(ctx, "context-failpoint", func(val failpoint.Value) {
        fmt.Println("带上下文的故障点,值:", val)
    })

    fmt.Println("程序正常执行")
}

故障点激活方式

可以通过环境变量激活故障点:

# 激活简单故障点并返回值100
GO_FAILPOINTS="main/simple-failpoint=return(100)" ./your-program

# 激活panic故障点
GO_FAILPOINTS="main/panic-failpoint=panic" ./your-program

# 激活带延迟的故障点
GO_FAILPOINTS="main/context-failpoint=sleep(1000)" ./your-program

设计原则

  1. 在有效的Golang代码中定义故障点,而不是注释或其他方式
  2. 故障点没有额外开销
    • 不会影响常规逻辑
    • 不会导致常规代码性能下降
    • 故障点代码不会出现在最终二进制文件中
  3. 故障点例程可写/可读,并且应该由编译器检查
  4. 由故障点定义生成的代码易于阅读
  5. 保持与注入代码相同的行号(便于调试)
  6. 支持带有context.Context的并行测试

关键概念

故障点(Failpoint): 故障点是只有当相应故障点激活时才会执行的代码片段。如果执行了failpoint.Disable("failpoint-name-for-demo"),闭包将永远不会被执行。

var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
    fmt.Println("unit-test", val, outerVar)
})

标记函数(Marker functions):

  • 它是一个空函数
  • 用于提示重写器用相等语句进行重写
  • 用于接收一些参数作为重写规则
  • 它将在编译时内联,不会向二进制文件发出任何内容(零成本)
  • 通过捕获可以在闭包中访问外部范围内的变量,并且转换后的代码仍然合法,因为所有捕获变量都位于IF语句的外部范围内

支持的故障点环境变量: 故障点可以通过导出的环境变量启用,模式如下:

[<percent>%][<count>*]<type>[(args...)][-><more terms>]

支持的参数类型:

  • off: 不采取任何措施(不触发故障点代码)
  • return: 用指定参数触发故障点
  • sleep: 睡眠指定的毫秒数
  • panic: 引发panic
  • break: 执行gdb并进入调试器
  • print: 打印注入变量的故障点路径
  • pause: 暂停直到故障点被禁用

更多关于golang实现代码故障注入与测试的failpoint插件使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现代码故障注入与测试的failpoint插件使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 故障注入与测试:failpoint 插件使用指南

什么是故障注入

故障注入(Fault Injection)是一种在系统中故意引入错误以测试其容错能力的测试技术。在Golang中,我们可以使用failpoint插件来实现这一功能。

failpoint 简介

failpoint是PingCAP开源的一个Golang故障注入工具,它允许你在代码中注入各种故障场景,如panic、延迟、错误返回等,而无需修改生产代码。

安装 failpoint

go get github.com/pingcap/failpoint

基本使用示例

1. 标记故障注入点

package main

import (
	"fmt"
	"time"

	"github.com/pingcap/failpoint"
)

func processRequest() error {
	// 标记一个故障注入点
	failpoint.Inject("process-failure", func() error {
		return fmt.Errorf("injected failure")
	})

	// 正常业务逻辑
	fmt.Println("Processing request...")
	time.Sleep(100 * time.Millisecond)
	return nil
}

2. 启用故障注入

func main() {
	// 启用故障注入
	failpoint.Enable("process-failure", "return('injected error')")
	
	err := processRequest()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	}
	
	// 禁用故障注入
	failpoint.Disable("process-failure")
}

常用故障注入类型

1. 返回错误

failpoint.Inject("db-error", func() error {
	return fmt.Errorf("database connection failed")
})

2. 注入延迟

failpoint.Inject("slow-query", func() error {
	time.Sleep(2 * time.Second)
	return nil
})

3. 随机故障

failpoint.Inject("random-failure", func() error {
	if rand.Intn(100) < 30 { // 30%概率失败
		return fmt.Errorf("random failure")
	}
	return nil
})

4. 条件触发

failpoint.Inject("conditional-failure", func(val int) error {
	if val > 100 {
		return fmt.Errorf("value too large")
	}
	return nil
})

高级用法

1. 链式调用

failpoint.Inject("chain-failure", func() error {
	return failpoint.Inject("nested-failure", func() error {
		return fmt.Errorf("nested error")
	})
})

2. 概率控制

failpoint.Enable("probabilistic-failure", "30%return('failed')")

3. 动态修改

// 运行时动态修改故障行为
failpoint.Modify("dynamic-failure", "50%return('partial failure')|50%return('complete failure')")

测试中的使用

func TestProcessRequest_Failure(t *testing.T) {
	// 设置故障注入
	failpoint.Enable("process-failure", "return('injected error')")
	defer failpoint.Disable("process-failure")

	err := processRequest()
	if err == nil {
		t.Error("Expected error but got nil")
	}
	
	if err.Error() != "injected error" {
		t.Errorf("Expected 'injected error' but got %v", err)
	}
}

最佳实践

  1. 明确命名:给故障注入点起有意义的名称,如"db-connection-failure"
  2. 文档记录:记录代码中的所有故障注入点
  3. 测试覆盖:为每个故障场景编写测试用例
  4. 生产禁用:确保生产环境中所有故障注入都被禁用
  5. 渐进式:从简单故障开始,逐步增加复杂度

注意事项

  • 故障注入只应在测试环境中使用
  • 注入点不应影响代码的主要逻辑路径
  • 避免在性能关键路径上使用故障注入
  • 确保测试后清理所有注入点

通过合理使用failpoint,你可以大大提高代码的健壮性和容错能力测试覆盖率。

回到顶部