4. 自愈动作抽象:定义自愈动作接口、实现基础自愈动作类
自愈动作是 Runbook 自动化单元测试的核心执行单元。在模拟告警触发后,系统必须能够执行一系列标准化的恢复操作。本章将定义自愈动作的抽象接口,并实现三个最常用的基础动作:重启服务、清理缓存、扩容。通过接口与实现分离,确保自愈流程的可扩展性与可测试性。
4.1 自愈动作接口定义
自愈动作接口规定了所有具体动作必须遵守的契约。接口包含三个核心方法:execute(执行动作)、rollback(回滚动作)、getStatus(获取动作状态)。
| 方法签名 | 输入参数 | 返回类型 | 说明 |
|---|---|---|---|
execute(context) |
ActionContext |
ActionResult |
执行自愈动作,返回成功/失败及详细信息 |
rollback(context) |
ActionContext |
ActionResult |
回滚已执行的动作,恢复到执行前状态 |
getStatus() |
无 | ActionStatus |
查询当前动作的执行状态(PENDING/RUNNING/SUCCESS/FAILED) |
设计要点:接口采用泛型上下文
ActionContext,包含目标主机、服务名称、超时时间、重试次数等通用字段。所有具体动作通过上下文获取执行所需参数,避免接口膨胀。
// 自愈动作接口定义(Go 语言示例)
type Action interface {
Execute(ctx ActionContext) ActionResult
Rollback(ctx ActionContext) ActionResult
GetStatus() ActionStatus
}
type ActionContext struct {
TargetHost string
ServiceName string
TimeoutSec int
RetryCount int
ExtraParams map[string]interface{}
}
type ActionResult struct {
Success bool
Message string
DurationMs int64
Detail map[string]interface{}
}
type ActionStatus int
const (
StatusPending ActionStatus = iota
StatusRunning
StatusSuccess
StatusFailed
)
4.2 基础自愈动作类实现
基于上述接口,我们实现三个基础自愈动作类。每个动作类都包含执行逻辑、回滚逻辑和状态管理。
4.2.1 重启服务动作
重启服务是最常见的自愈动作,适用于服务进程挂死、内存泄漏、连接池耗尽等场景。执行流程包括:发送停止信号、等待进程退出、启动服务、健康检查。
type RestartServiceAction struct {
status ActionStatus
}
func (a *RestartServiceAction) Execute(ctx ActionContext) ActionResult {
a.status = StatusRunning
start := time.Now()
// 步骤1:停止服务
stopCmd := fmt.Sprintf("systemctl stop %s", ctx.ServiceName)
if err := exec.Command("ssh", ctx.TargetHost, stopCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "停止服务失败: " + err.Error()}
}
// 步骤2:等待进程完全退出(最多等待30秒)
waitCmd := fmt.Sprintf("while systemctl is-active %s; do sleep 1; done", ctx.ServiceName)
waitCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
exec.CommandContext(waitCtx, "ssh", ctx.TargetHost, waitCmd).Run()
// 步骤3:启动服务
startCmd := fmt.Sprintf("systemctl start %s", ctx.ServiceName)
if err := exec.Command("ssh", ctx.TargetHost, startCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "启动服务失败: " + err.Error()}
}
// 步骤4:健康检查(等待服务端口就绪)
healthCheckCmd := fmt.Sprintf("curl -sf http://%s:8080/health", ctx.TargetHost)
for i := 0; i < 10; i++ {
if err := exec.Command("ssh", ctx.TargetHost, healthCheckCmd).Run(); err == nil {
a.status = StatusSuccess
return ActionResult{
Success: true,
Message: "服务重启成功",
DurationMs: time.Since(start).Milliseconds(),
}
}
time.Sleep(2 * time.Second)
}
a.status = StatusFailed
return ActionResult{Success: false, Message: "健康检查超时"}
}
func (a *RestartServiceAction) Rollback(ctx ActionContext) ActionResult {
// 回滚逻辑:如果重启失败,尝试恢复到之前的状态
// 实际场景中需要记录重启前的进程状态
return ActionResult{Success: true, Message: "回滚完成(无操作)"}
}
func (a *RestartServiceAction) GetStatus() ActionStatus {
return a.status
}
4.2.2 清理缓存动作
缓存清理适用于缓存雪崩、数据不一致、内存占用过高等场景。支持多种缓存类型:Redis 缓存、本地文件缓存、CDN 缓存。
| 缓存类型 | 清理命令 | 回滚方式 | 适用场景 |
|---|---|---|---|
| Redis | FLUSHDB 或 DEL key_pattern |
从备份恢复(需提前开启 AOF/RDB) | 缓存数据不一致、内存暴涨 |
| 本地文件缓存 | rm -rf /cache/* |
从远端存储重新拉取 | 磁盘空间不足、缓存文件损坏 |
| CDN 缓存 | 调用 CDN API 刷新 URL | 重新预热缓存 | 静态资源更新、回源异常 |
type ClearCacheAction struct {
status ActionStatus
cacheType string // "redis", "local", "cdn"
}
func (a *ClearCacheAction) Execute(ctx ActionContext) ActionResult {
a.status = StatusRunning
start := time.Now()
switch a.cacheType {
case "redis":
// 执行 Redis FLUSHDB 命令
redisCmd := fmt.Sprintf("redis-cli -h %s FLUSHDB", ctx.TargetHost)
if err := exec.Command("ssh", ctx.TargetHost, redisCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "Redis 清理失败: " + err.Error()}
}
case "local":
// 清理本地缓存目录
localCmd := fmt.Sprintf("rm -rf %s/cache/*", ctx.ExtraParams["cacheDir"])
if err := exec.Command("ssh", ctx.TargetHost, localCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "本地缓存清理失败: " + err.Error()}
}
case "cdn":
// 调用 CDN 刷新 API
cdnURL := ctx.ExtraParams["cdnURL"].(string)
if err := callCDNRefreshAPI(cdnURL); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "CDN 刷新失败: " + err.Error()}
}
default:
a.status = StatusFailed
return ActionResult{Success: false, Message: "不支持的缓存类型: " + a.cacheType}
}
a.status = StatusSuccess
return ActionResult{
Success: true,
Message: fmt.Sprintf("%s 缓存清理成功", a.cacheType),
DurationMs: time.Since(start).Milliseconds(),
}
}
func (a *ClearCacheAction) Rollback(ctx ActionContext) ActionResult {
// 缓存清理的回滚通常需要从备份恢复或重新生成缓存
// 此处简化处理,仅记录日志
return ActionResult{Success: true, Message: "缓存清理回滚:需手动恢复"}
}
func (a *ClearCacheAction) GetStatus() ActionStatus {
return a.status
}
4.2.3 扩容动作
扩容动作适用于流量突增、资源瓶颈、限流触发等场景。支持水平扩容(增加实例数)和垂直扩容(增加资源配额)。
type ScaleOutAction struct {
status ActionStatus
scaleType string // "horizontal", "vertical"
}
func (a *ScaleOutAction) Execute(ctx ActionContext) ActionResult {
a.status = StatusRunning
start := time.Now()
switch a.scaleType {
case "horizontal":
// 水平扩容:通过容器编排平台增加 Pod 副本数
scaleCmd := fmt.Sprintf("kubectl scale deployment %s --replicas=%d -n %s",
ctx.ServiceName,
ctx.ExtraParams["targetReplicas"],
ctx.ExtraParams["namespace"])
if err := exec.Command("ssh", ctx.TargetHost, scaleCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "水平扩容失败: " + err.Error()}
}
case "vertical":
// 垂直扩容:调整容器资源限制
patchCmd := fmt.Sprintf("kubectl patch deployment %s -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"%s\",\"resources\":{\"limits\":{\"cpu\":\"%s\",\"memory\":\"%s\"}}}]}}}}' -n %s",
ctx.ServiceName,
ctx.ServiceName,
ctx.ExtraParams["cpuLimit"],
ctx.ExtraParams["memoryLimit"],
ctx.ExtraParams["namespace"])
if err := exec.Command("ssh", ctx.TargetHost, patchCmd).Run(); err != nil {
a.status = StatusFailed
return ActionResult{Success: false, Message: "垂直扩容失败: " + err.Error()}
}
default:
a.status = StatusFailed
return ActionResult{Success: false, Message: "不支持的扩容类型: " + a.scaleType}
}
a.status = StatusSuccess
return ActionResult{
Success: true,
Message: fmt.Sprintf("%s 扩容成功", a.scaleType),
DurationMs: time.Since(start).Milliseconds(),
}
}
func (a *ScaleOutAction) Rollback(ctx ActionContext) ActionResult {
// 扩容回滚:恢复到扩容前的副本数或资源配额
// 需要从上下文或外部存储获取原始值
return ActionResult{Success: true, Message: "扩容回滚:恢复到原始配置"}
}
func (a *ScaleOutAction) GetStatus() ActionStatus {
return a.status
}
4.3 自愈动作类层次结构
下图展示了自愈动作的接口与实现类的层次关系,以及每个动作类的核心职责。
4.4 动作注册与工厂模式
为了在 Runbook 中灵活组合自愈动作,我们采用工厂模式管理动作实例。通过动作名称注册对应的创建函数,实现动作的动态加载。
// 动作工厂:根据名称创建对应的自愈动作实例
type ActionFactory struct {
registry map[string]func() Action
}
func NewActionFactory() *ActionFactory {
factory := &ActionFactory{
registry: make(map[string]func() Action),
}
// 注册基础动作
factory.Register("restart_service", func() Action { return &RestartServiceAction{} })
factory.Register("clear_cache", func() Action { return &ClearCacheAction{cacheType: "redis"} })
factory.Register("scale_out", func() Action { return &ScaleOutAction{scaleType: "horizontal"} })
return factory
}
func (f *ActionFactory) Register(name string, creator func() Action) {
f.registry[name] = creator
}
func (f *ActionFactory) Create(name string) (Action, error) {
creator, exists := f.registry[name]
if !exists {
return nil, fmt.Errorf("未知的自愈动作: %s", name)
}
return creator(), nil
}
// 使用示例:在 Runbook 中组合多个动作
func executeSelfHealingPlan(plan []string, ctx ActionContext) {
factory := NewActionFactory()
for _, actionName := range plan {
action, err := factory.Create(actionName)
if err != nil {
log.Printf("创建动作失败: %v", err)
continue
}
result := action.Execute(ctx)
if !result.Success {
log.Printf("动作 %s 执行失败: %s,开始回滚", actionName, result.Message)
action.Rollback(ctx)
break
}
log.Printf("动作 %s 执行成功,耗时 %dms", actionName, result.DurationMs)
}
}
4.5 动作执行流程与状态转换
每个自愈动作在执行过程中经历明确的状态转换。下图展示了从创建到完成(或失败回滚)的完整生命周期。
4.6 单元测试验证
自愈动作的单元测试需要验证三个维度:正常执行路径、异常处理、回滚逻辑。以下是针对 RestartServiceAction 的测试示例。
func TestRestartServiceAction_Execute_Success(t *testing.T) {
// 模拟 SSH 执行器,返回成功
mockExecutor := new(MockSSHExecutor)
mockExecutor.On("Run", "systemctl stop myservice").Return(nil)
mockExecutor.On("Run", mock.Anything).Return(nil)
action := &RestartServiceAction{}
ctx := ActionContext{
TargetHost: "192.168.1.100",
ServiceName: "myservice",
TimeoutSec: 60,
}
result := action.Execute(ctx)
assert.True(t, result.Success)
assert.Equal(t, StatusSuccess, action.GetStatus())
}
func TestRestartServiceAction_Execute_StopFailed(t *testing.T) {
// 模拟停止服务失败
mockExecutor := new(MockSSHExecutor)
mockExecutor.On("Run", "systemctl stop myservice").Return(errors.New("服务不存在"))
action := &RestartServiceAction{}
ctx := ActionContext{
TargetHost: "192.168.1.100",
ServiceName: "myservice",
}
result := action.Execute(ctx)
assert.False(t, result.Success)
assert.Contains(t, result.Message, "停止服务失败")
assert.Equal(t, StatusFailed, action.GetStatus())
}
func TestRestartServiceAction_Rollback(t *testing.T) {
action := &RestartServiceAction{}
ctx := ActionContext{TargetHost: "192.168.1.100", ServiceName: "myservice"}
result := action.Rollback(ctx)
assert.True(t, result.Success)
assert.Equal(t, "回滚完成(无操作)", result.Message)
}
测试最佳实践:使用接口注入的方式隔离外部依赖(如 SSH 执行器、Kubernetes API 客户端),在单元测试中通过 Mock 对象模拟各种场景。每个动作类应至少包含 3 个测试用例:正常执行、执行失败、回滚验证。
4.7 动作扩展指南
当需要添加新的自愈动作时,遵循以下步骤:
- 实现接口:新建结构体,实现
Action接口的三个方法 - 注册工厂:在
ActionFactory的初始化方法中注册新动作 - 编写测试:覆盖正常执行、异常处理、回滚三种场景
- 添加到 Runbook:在 Runbook 配置中使用动作名称引用
注意事项:自愈动作应保持幂等性,多次执行同一动作不应产生副作用。回滚逻辑必须能够处理部分执行的情况,例如在扩容过程中如果只成功创建了部分实例,回滚时应清理已创建的实例。