目录

Temporal vs K8s Controller:声明式与编排式,两种控制面范式的深度对比

K8s Controller 和 Temporal 都是"控制面"技术,但代表了两种截然不同的设计哲学。本文深度对比二者的核心差异,并探讨在 AI Infra 平台中如何结合运用。

一、两种控制面范式

1.1 设计哲学对比

维度K8s ControllerTemporal
核心理念声明式 (Declarative)编排式 (Imperative)
关注点期望状态 (Desired State)执行流程 (Workflow)
触发机制Level-Triggered (水平触发)Event Sourcing (事件溯源)
表达方式“我想要 3 个 Pod”“先做 A,再做 B,失败则做 C”

1.2 水平触发 vs 边缘触发

K8s Controller 的水平触发 (Level-Triggered)

1
2
3
4
5
期望状态: replicas=3
当前状态: pods=2
  → Reconcile → 创建 1 个 Pod
  → 再次检查 → pods=3 
  → 无需操作

不管触发 Reconcile 的原因是什么(Pod 被删、新建 Deployment、健康检查失败),Controller 只关心"当前状态 vs 期望状态"的差距

Temporal 的事件溯源

1
2
3
4
5
6
7
Event 1: WorkflowStarted
Event 2: ActivityScheduled(扣款)
Event 3: ActivityCompleted(成功)
Event 4: ActivityScheduled(扣库存)
[崩溃恢复]
→ 重放 Event 1-4
→ 从 Event 4 继续执行

Temporal 需要完整的历史轨迹才能恢复状态。

1.3 适用场景分析

场景更适合的方案原因
管理 Pod/GPU 资源K8s Operator资源是"要维持的状态"
训练任务流程Temporal是"有序的步骤"
故障自愈K8s Controller周期性 Reconcile 自动修复
复杂分支逻辑Temporal代码表达更直观
无限期运行K8s Controller无状态、低开销
有明确结束Temporal天然支持完成/失败/取消

二、代码对比

2.1 K8s Controller 示例

管理一个 AI 训练任务的 Operator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// reconciler.go
func (r *TrainingJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 获取 CR 资源
    var job trainingv1.TrainingJob
    if err := r.Get(ctx, req.NamespacedName, &job); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }
    
    // 获取当前状态
    var pods corev1.PodList
    r.List(ctx, &pods, client.MatchingLabels{"job": job.Name})
    
    // 期望状态 vs 当前状态
    desiredReplicas := job.Spec.Workers
    currentReplicas := len(pods.Items)
    
    if currentReplicas < desiredReplicas {
        // 创建 Pod
        for i := currentReplicas; i < desiredReplicas; i++ {
            r.Create(ctx, r.buildWorkerPod(&job, i))
        }
    } else if currentReplicas > desiredReplicas {
        // 删除多余 Pod
        for i := desiredReplicas; i < currentReplicas; i++ {
            r.Delete(ctx, &pods.Items[i])
        }
    }
    
    // 更新状态
    job.Status.ActiveWorkers = desiredReplicas
    r.Status().Update(ctx, &job)
    
    // 周期性重新检查
    return ctrl.Result{RequeueAfter: time.Minute}, nil
}

特点

  • 每次 Reconcile 都从"当前状态"开始
  • 不关心"如何到达这里",只关心"现在和期望差多少"
  • 适合持续运行的资源管理

2.2 Temporal Workflow 示例

同样的 AI 训练任务用 Temporal 实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// workflow.go
func TrainingWorkflow(ctx workflow.Context, job TrainingJob) error {
    // Step 1: 准备数据
    var dataPath string
    err := workflow.ExecuteActivity(ctx, PrepareData, job.DataSource).Get(ctx, &dataPath)
    if err != nil {
        return fmt.Errorf("数据准备失败: %w", err)
    }
    
    // Step 2: 启动训练(可能运行数小时)
    var modelPath string
    err = workflow.ExecuteActivity(ctx, StartTraining, TrainingParams{
        DataPath: dataPath,
        Epochs:   job.Epochs,
        Workers:  job.Workers,
    }).Get(ctx, &modelPath)
    if err != nil {
        // 清理中间数据
        workflow.ExecuteActivity(ctx, CleanupData, dataPath)
        return fmt.Errorf("训练失败: %w", err)
    }
    
    // Step 3: 模型评估
    var metrics Metrics
    err = workflow.ExecuteActivity(ctx, EvaluateModel, modelPath).Get(ctx, &metrics)
    if err != nil {
        return fmt.Errorf("评估失败: %w", err)
    }
    
    // Step 4: 条件分支 - 精度达标才部署
    if metrics.Accuracy >= job.MinAccuracy {
        workflow.ExecuteActivity(ctx, DeployModel, modelPath)
    } else {
        // 通知人工审核
        workflow.ExecuteActivity(ctx, NotifyHumanReview, job.Owner, metrics)
    }
    
    return nil
}

特点

  • 代码就是流程,顺序执行、分支判断一目了然
  • 步骤之间有依赖关系(dataPath → training → modelPath)
  • 适合有明确开始和结束的任务

三、核心机制对比

3.1 故障恢复

K8s Controller

1
2
3
4
5
Pod 挂了
  → Informer 收到 Delete 事件
  → 触发 Reconcile
  → 发现 current=2, desired=3
  → 创建新 Pod

恢复靠的是周期性对比状态,不需要历史记录。

Temporal

1
2
3
4
5
6
Worker 挂了
  → Workflow 执行被中断
  → 启动新 Worker
  → 从 Event History 重放
  → 代码重新执行到断点
  → 继续向后执行

恢复靠的是完整的事件历史

3.2 复杂分支

K8s Controller 处理复杂分支很痛苦:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 充斥着状态机逻辑
switch job.Status.Phase {
case "Pending":
    if allPodsReady() {
        job.Status.Phase = "Running"
    }
case "Running":
    if trainingComplete() {
        job.Status.Phase = "Evaluating"
    } else if hasFailed() {
        job.Status.Phase = "Failed"
    }
case "Evaluating":
    // 更多分支...
}

Temporal 天然支持:

1
2
3
4
5
6
7
8
// 直接用 if-else,清清楚楚
if metrics.Accuracy >= threshold {
    deployModel()
} else if retryCount < 3 {
    retrainWithMoreData()
} else {
    notifyHuman()
}

3.3 资源占用

场景K8s ControllerTemporal
1000 个任务1 个 Controller Pod1000 个 Workflow 历史
内存占用O(1) - 无状态O(N) - 每个 Workflow 占内存
长期运行低开销历史膨胀风险

Temporal 的解法ContinueAsNew 切分历史

1
2
3
4
// 当历史太长时,"重生"为新 Workflow
if workflow.GetInfo(ctx).GetHistorySize() > 10000 {
    return workflow.NewContinueAsNewError(ctx, TrainingWorkflow, job)
}

四、AI Infra 最佳实践

4.1 职责分离架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
┌─────────────────────────────────────┐
│           Temporal Workflow         │
│  (任务编排: 训练 → 评估 → 部署)        │
└───────────────┬─────────────────────┘
                │ 调用
┌───────────────▼──────────────────────┐
│           K8s Operator               │
│  (资源管理: Pod, GPU, PVC)            │
└───────────────┬──────────────────────┘
                │ 管理
┌───────────────▼────────────────────┐
│           Kubernetes 集群          │
│  (GPU 节点, 存储, 网络)              │
└────────────────────────────────────┘

4.2 Temporal Activity 调用 K8s API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func CreateTrainingPod(ctx context.Context, spec PodSpec) (string, error) {
    clientset := getKubernetesClient()
    
    pod := &corev1.Pod{
        ObjectMeta: metav1.ObjectMeta{
            GenerateName: "training-",
            Labels:       map[string]string{"managed-by": "temporal"},
        },
        Spec: spec.ToK8sPodSpec(),
    }
    
    created, err := clientset.CoreV1().Pods("training").Create(ctx, pod, metav1.CreateOptions{})
    if err != nil {
        return "", err
    }
    
    return created.Name, nil
}

func WaitForPodComplete(ctx context.Context, podName string) error {
    clientset := getKubernetesClient()
    
    watch, _ := clientset.CoreV1().Pods("training").Watch(ctx, metav1.ListOptions{
        FieldSelector: "metadata.name=" + podName,
    })
    
    for event := range watch.ResultChan() {
        pod := event.Object.(*corev1.Pod)
        if pod.Status.Phase == corev1.PodSucceeded {
            return nil
        }
        if pod.Status.Phase == corev1.PodFailed {
            return fmt.Errorf("Pod 失败: %s", pod.Status.Message)
        }
    }
    
    return fmt.Errorf("Watch 意外结束")
}

4.3 何时用哪个?

需求选择
“我需要 3 个 GPU Pod”K8s Operator
“先预处理数据,再训练,失败重试 3 次”Temporal
“某个 Pod 挂了自动拉起”K8s (内置能力)
“训练成功后自动触发评估和部署”Temporal
“管理 GPU 资源池”K8s Operator + Device Plugin
“协调多个任务的执行顺序”Temporal

五、总结

维度K8s Controller 胜出Temporal 胜出
资源管理
无限期运行
故障自愈
复杂流程编排
长事务补偿
代码即流程

核心理解:两者不是替代关系,而是互补关系。在 AI Infra 控制面架构中:

  • K8s Operator 是"地基"——管理底层资源
  • Temporal 是"上层建筑"——编排业务流程

结合使用可以构建完整的控制面架构。