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 自愈动作类层次结构

下图展示了自愈动作的接口与实现类的层次关系,以及每个动作类的核心职责。

自愈动作接口与实现类层次结构 自愈动作接口与实现类层次结构 «interface» Action + Execute(ctx ActionContext) ActionResult + Rollback(ctx ActionContext) ActionResult + GetStatus() ActionStatus RestartServiceAction ClearCacheAction ScaleOutAction 核心职责 • 停止/启动系统服务 • 等待进程完全退出 • 健康检查验证 核心职责 • Redis FLUSHDB • 本地缓存目录清理 • CDN API 刷新 核心职责 • Kubernetes 扩缩容 • 资源配额调整 • 实例数管理 回滚策略对比 重启服务:记录重启前进程状态,失败时尝试恢复旧进程 清理缓存:需提前开启 AOF/RDB 备份,回滚时从备份恢复 扩容动作:从上下文获取扩容前副本数/资源配额,执行反向操作

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 动作执行流程与状态转换

每个自愈动作在执行过程中经历明确的状态转换。下图展示了从创建到完成(或失败回滚)的完整生命周期。

自愈动作状态转换图 自愈动作状态转换图 PENDING RUNNING SUCCESS FAILED ROLLBACK Execute() 执行成功 执行失败 Rollback() 回滚完成,重置为 PENDING 状态说明:PENDING 表示动作已创建待执行;RUNNING 表示正在执行中;SUCCESS 表示执行成功;FAILED 表示执行失败;ROLLBACK 表示正在回滚。 回滚完成后,动作状态重置为 PENDING,允许重新尝试执行。

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 动作扩展指南

当需要添加新的自愈动作时,遵循以下步骤:

  1. 实现接口:新建结构体,实现 Action 接口的三个方法
  2. 注册工厂:在 ActionFactory 的初始化方法中注册新动作
  3. 编写测试:覆盖正常执行、异常处理、回滚三种场景
  4. 添加到 Runbook:在 Runbook 配置中使用动作名称引用
注意事项:自愈动作应保持幂等性,多次执行同一动作不应产生副作用。回滚逻辑必须能够处理部分执行的情况,例如在扩容过程中如果只成功创建了部分实例,回滚时应清理已创建的实例。