14.1 太牛了!任务编排和规则引擎竟然还能这样设计?
在分布式任务调度系统中,任务编排和规则引擎是核心组件之一。它们决定了任务如何按照预定的逻辑执行,如何处理任务间的依赖关系,以及如何根据条件做出决策。
今天我们就来深入探讨一下如何设计一个强大的任务编排系统和规则引擎,让你的分布式任务调度系统更加智能和灵活!
任务编排的重要性
任务编排是指将多个独立的任务按照一定的规则和顺序组织起来,形成一个完整的业务流程。在复杂的业务场景中,单一任务往往无法满足业务需求,需要多个任务协同工作才能完成一个完整的业务流程。
常见的任务编排需求包括:
- 前置任务:任务B必须在任务A完成后才能执行
- 后置任务:任务A完成后自动触发任务B
- 并行执行:多个任务可以同时执行以提高效率
- 条件分支:根据任务执行结果或其他条件决定下一步执行哪个任务
- 循环执行:重复执行某些任务直到满足特定条件
任务编排系统设计
让我们先来看看一个基本的任务编排系统应该如何设计:
package orchestrator
import (
"context"
"fmt"
"sync"
"time"
)
// Task 任务接口
type Task interface {
Execute(ctx context.Context) (interface{}, error)
GetID() string
}
// TaskNode 任务节点
type TaskNode struct {
ID string
Task Task
Dependencies []string // 依赖的任务ID
Next []string // 下一步执行的任务ID
Condition func(result interface{}) bool // 执行条件
}
// TaskOrchestrator 任务编排器
type TaskOrchestrator struct {
tasks map[string]*TaskNode
mu sync.RWMutex
}
// NewTaskOrchestrator 创建任务编排器
func NewTaskOrchestrator() *TaskOrchestrator {
return &TaskOrchestrator{
tasks: make(map[string]*TaskNode),
}
}
// AddTask 添加任务
func (o *TaskOrchestrator) AddTask(node *TaskNode) {
o.mu.Lock()
defer o.mu.Unlock()
o.tasks[node.ID] = node
}
// Execute 执行任务编排
func (o *TaskOrchestrator) Execute(ctx context.Context) error {
o.mu.RLock()
defer o.mu.RUnlock()
// 记录已完成的任务
completed := make(map[string]interface{})
completedMu := sync.Mutex{}
// 记录正在执行的任务
executing := make(map[string]bool)
executingMu := sync.Mutex{}
// 使用 WaitGroup 等待所有任务完成
var wg sync.WaitGroup
// 错误收集
var errMsgs []string
errMu := sync.Mutex{}
// 查找没有依赖的任务开始执行
for _, node := range o.tasks {
if len(node.Dependencies) == 0 {
wg.Add(1)
go o.executeTask(ctx, node, completed, &completedMu, executing, &executingMu, &wg, &errMu, &errMsgs)
}
}
wg.Wait()
if len(errMsgs) > 0 {
return fmt.Errorf("task execution failed: %v", errMsgs)
}
return nil
}
// executeTask 执行单个任务
func (o *TaskOrchestrator) executeTask(
ctx context.Context,
node *TaskNode,
completed map[string]interface{},
completedMu *sync.Mutex,
executing map[string]bool,
executingMu *sync.Mutex,
wg *sync.WaitGroup,
errMu *sync.Mutex,
errMsgs *[]string,
) {
defer wg.Done()
// 标记任务正在执行
executingMu.Lock()
executing[node.ID] = true
executingMu.Unlock()
// 执行任务
result, err := node.Task.Execute(ctx)
// 标记任务执行完成
executingMu.Lock()
delete(executing, node.ID)
executingMu.Unlock()
if err != nil {
errMu.Lock()
*errMsgs = append(*errMsgs, fmt.Sprintf("task %s failed: %v", node.ID, err))
errMu.Unlock()
return
}
// 记录任务结果
completedMu.Lock()
completed[node.ID] = result
completedMu.Unlock()
// 检查并触发后续任务
o.triggerNextTasks(ctx, node, result, completed, completedMu, executing, executingMu, wg, errMu, errMsgs)
}
// triggerNextTasks 触发后续任务
func (o *TaskOrchestrator) triggerNextTasks(
ctx context.Context,
node *TaskNode,
result interface{},
completed map[string]interface{},
completedMu *sync.Mutex,
executing map[string]bool,
executingMu *sync.Mutex,
wg *sync.WaitGroup,
errMu *sync.Mutex,
errMsgs *[]string,
) {
for _, nextID := range node.Next {
nextNode, exists := o.tasks[nextID]
if !exists {
errMu.Lock()
*errMsgs = append(*errMsgs, fmt.Sprintf("next task %s not found", nextID))
errMu.Unlock()
continue
}
// 检查条件
if nextNode.Condition != nil && !nextNode.Condition(result) {
continue
}
// 检查依赖是否都已完成
allDepsCompleted := true
completedMu.Lock()
for _, depID := range nextNode.Dependencies {
if _, ok := completed[depID]; !ok {
allDepsCompleted = false
break
}
}
completedMu.Unlock()
if allDepsCompleted {
// 检查是否已经在执行
executingMu.Lock()
if _, ok := executing[nextID]; ok {
executingMu.Unlock()
continue
}
executing[nextID] = true
executingMu.Unlock()
wg.Add(1)
go o.executeTask(ctx, nextNode, completed, completedMu, executing, executingMu, wg, errMu, errMsgs)
}
}
}
以上是一个简单的任务编排系统实现,它支持以下特性:
- 任务依赖管理:每个任务可以声明它所依赖的其他任务
- 条件执行:任务可以根据前一个任务的结果决定是否执行
- 并行执行:没有依赖关系的任务可以并行执行
- 错误处理:任务执行失败会被捕获并报告
规则引擎设计
规则引擎是任务编排系统的大脑,它负责解析和执行各种规则。在复杂的业务场景中,我们需要一个强大而灵活的规则引擎来处理各种业务逻辑。
下面是一个基于 ANTLR 的简单规则引擎实现:
package ruleengine
import (
"context"
"fmt"
"strconv"
"strings"
)
// Fact 事实对象
type Fact map[string]interface{}
// Rule 规则接口
type Rule interface {
Evaluate(fact Fact) (bool, error)
GetID() string
}
// SimpleRule 简单规则实现
type SimpleRule struct {
ID string
Field string
Operator string
Value interface{}
}
// GetID 获取规则ID
func (r *SimpleRule) GetID() string {
return r.ID
}
// Evaluate 评估规则
func (r *SimpleRule) Evaluate(fact Fact) (bool, error) {
fieldValue, exists := fact[r.Field]
if !exists {
return false, fmt.Errorf("field %s not found in fact", r.Field)
}
switch r.Operator {
case "==":
return fieldValue == r.Value, nil
case "!=":
return fieldValue != r.Value, nil
case ">":
return compareNumbers(fieldValue, r.Value, ">")
case "<":
return compareNumbers(fieldValue, r.Value, "<")
case ">=":
return compareNumbers(fieldValue, r.Value, ">=")
case "<=":
return compareNumbers(fieldValue, r.Value, "<=")
case "contains":
return strings.Contains(fmt.Sprintf("%v", fieldValue), fmt.Sprintf("%v", r.Value)), nil
default:
return false, fmt.Errorf("unsupported operator: %s", r.Operator)
}
}
// compareNumbers 比较数字
func compareNumbers(a, b interface{}, op string) (bool, error) {
aFloat, err := toFloat64(a)
if err != nil {
return false, fmt.Errorf("cannot convert %v to number: %v", a, err)
}
bFloat, err := toFloat64(b)
if err != nil {
return false, fmt.Errorf("cannot convert %v to number: %v", b, err)
}
switch op {
case ">":
return aFloat > bFloat, nil
case "<":
return aFloat < bFloat, nil
case ">=":
return aFloat >= bFloat, nil
case "<=":
return aFloat <= bFloat, nil
default:
return false, fmt.Errorf("unsupported comparison operator: %s", op)
}
}
// toFloat64 转换为float64
func toFloat64(v interface{}) (float64, error) {
switch val := v.(type) {
case float64:
return val, nil
case float32:
return float64(val), nil
case int:
return float64(val), nil
case int64:
return float64(val), nil
case string:
return strconv.ParseFloat(val, 64)
default:
return 0, fmt.Errorf("cannot convert %T to float64", v)
}
}
// CompositeRule 组合规则
type CompositeRule struct {
ID string
Rules []Rule
Op string // "and" 或 "or"
}
// GetID 获取规则ID
func (r *CompositeRule) GetID() string {
return r.ID
}
// Evaluate 评估组合规则
func (r *CompositeRule) Evaluate(fact Fact) (bool, error) {
if len(r.Rules) == 0 {
return true, nil
}
if r.Op == "and" {
for _, rule := range r.Rules {
result, err := rule.Evaluate(fact)
if err != nil {
return false, err
}
if !result {
return false, nil
}
}
return true, nil
} else if r.Op == "or" {
for _, rule := range r.Rules {
result, err := rule.Evaluate(fact)
if err != nil {
return false, err
}
if result {
return true, nil
}
}
return false, nil
} else {
return false, fmt.Errorf("unsupported composite operator: %s", r.Op)
}
}
// RuleEngine 规则引擎
type RuleEngine struct {
rules map[string]Rule
}
// NewRuleEngine 创建规则引擎
func NewRuleEngine() *RuleEngine {
return &RuleEngine{
rules: make(map[string]Rule),
}
}
// AddRule 添加规则
func (e *RuleEngine) AddRule(rule Rule) {
e.rules[rule.GetID()] = rule
}
// Evaluate 评估规则
func (e *RuleEngine) Evaluate(ruleID string, fact Fact) (bool, error) {
rule, exists := e.rules[ruleID]
if !exists {
return false, fmt.Errorf("rule %s not found", ruleID)
}
return rule.Evaluate(fact)
}
// 示例使用
func Example() {
// 创建规则引擎
engine := NewRuleEngine()
// 创建简单规则
ageRule := &SimpleRule{
ID: "age_check",
Field: "age",
Operator: ">=",
Value: 18,
}
cityRule := &SimpleRule{
ID: "city_check",
Field: "city",
Operator: "==",
Value: "Beijing",
}
// 创建组合规则
compositeRule := &CompositeRule{
ID: "eligibility_check",
Rules: []Rule{ageRule, cityRule},
Op: "and",
}
// 添加规则到引擎
engine.AddRule(ageRule)
engine.AddRule(cityRule)
engine.AddRule(compositeRule)
// 创建事实对象
fact := Fact{
"age": 25,
"city": "Beijing",
}
// 评估规则
result, err := engine.Evaluate("eligibility_check", fact)
if err != nil {
fmt.Printf("Error evaluating rule: %v\n", err)
return
}
fmt.Printf("Eligibility check result: %v\n", result)
}
这个规则引擎支持以下特性:
- 简单规则:支持常见的比较操作符(==, !=, >, <, >=, <=, contains)
- 组合规则:支持 AND 和 OR 逻辑操作符来组合多个规则
- 灵活的事实对象:可以使用 map 结构表示任意的事实对象
- 易于扩展:可以通过实现 Rule 接口来添加自定义规则类型
自定义 DSL 设计
为了让业务人员也能方便地定义任务编排规则,我们可以设计一种领域特定语言(DSL)。以下是一个简单的 DSL 示例:
# 用户注册流程编排
workflow "user_registration" {
# 发送验证码任务
task "send_verification_code" {
type = "email"
depends_on = []
timeout = "30s"
}
# 验证验证码任务
task "verify_code" {
type = "verification"
depends_on = ["send_verification_code"]
timeout = "5m" # 5分钟内有效
}
# 创建用户账户任务
task "create_user_account" {
type = "database"
depends_on = ["verify_code"]
condition = "result.success == true"
timeout = "10s"
}
# 发送欢迎邮件任务
task "send_welcome_email" {
type = "email"
depends_on = ["create_user_account"]
parallel_with = ["update_user_stats"]
timeout = "30s"
}
# 更新用户统计数据任务
task "update_user_stats" {
type = "analytics"
depends_on = ["create_user_account"]
parallel_with = ["send_welcome_email"]
timeout = "10s"
}
# 注册完成通知任务
task "registration_complete" {
type = "notification"
depends_on = ["send_welcome_email", "update_user_stats"]
timeout = "5s"
}
}
这种 DSL 具有以下优点:
- 可读性强:业务人员也容易理解和编写
- 结构清晰:通过缩进和关键字明确表达任务间的关系
- 参数丰富:支持超时、条件、并行等多种配置选项
DAG 模型与循环依赖检测
在任务编排中,我们通常使用有向无环图(DAG)来表示任务间的依赖关系。DAG 可以帮助我们检测循环依赖,确保任务编排的正确性。
package dag
import (
"fmt"
)
// Graph 图结构
type Graph struct {
vertices map[string]bool // 顶点集合
edges map[string][]string // 边集合,key为起始顶点,value为终止顶点列表
}
// NewGraph 创建图
func NewGraph() *Graph {
return &Graph{
vertices: make(map[string]bool),
edges: make(map[string][]string),
}
}
// AddVertex 添加顶点
func (g *Graph) AddVertex(vertex string) {
g.vertices[vertex] = true
}
// AddEdge 添加边
func (g *Graph) AddEdge(from, to string) error {
if _, exists := g.vertices[from]; !exists {
return fmt.Errorf("vertex %s does not exist", from)
}
if _, exists := g.vertices[to]; !exists {
return fmt.Errorf("vertex %s does not exist", to)
}
g.edges[from] = append(g.edges[from], to)
return nil
}
// HasCycle 检测是否有环
func (g *Graph) HasCycle() bool {
// 0: 未访问, 1: 正在访问, 2: 已访问
visitStatus := make(map[string]int)
for vertex := range g.vertices {
if visitStatus[vertex] == 0 {
if g.hasCycleUtil(vertex, visitStatus) {
return true
}
}
}
return false
}
// hasCycleUtil DFS检测环的辅助函数
func (g *Graph) hasCycleUtil(vertex string, visitStatus map[string]int) bool {
// 标记当前节点为正在访问
visitStatus[vertex] = 1
// 遍历所有邻居节点
for _, neighbor := range g.edges[vertex] {
// 如果邻居节点正在访问,则存在环
if visitStatus[neighbor] == 1 {
return true
}
// 如果邻居节点未访问,递归检查
if visitStatus[neighbor] == 0 && g.hasCycleUtil(neighbor, visitStatus) {
return true
}
}
// 标记当前节点为已访问
visitStatus[vertex] = 2
return false
}
// TopologicalSort 拓扑排序
func (g *Graph) TopologicalSort() ([]string, error) {
if g.HasCycle() {
return nil, fmt.Errorf("graph contains cycle, cannot perform topological sort")
}
// 0: 未访问, 1: 已访问
visited := make(map[string]bool)
var result []string
for vertex := range g.vertices {
if !visited[vertex] {
g.topologicalSortUtil(vertex, visited, &result)
}
}
// 反转结果以获得正确的拓扑顺序
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
// topologicalSortUtil 拓扑排序辅助函数
func (g *Graph) topologicalSortUtil(vertex string, visited map[string]bool, result *[]string) {
// 标记当前节点为已访问
visited[vertex] = true
// 递归访问所有邻居节点
for _, neighbor := range g.edges[vertex] {
if !visited[neighbor] {
g.topologicalSortUtil(neighbor, visited, result)
}
}
// 将当前节点加入结果
*result = append(*result, vertex)
}
实际应用案例
让我们看一个实际应用案例,如何使用上述组件来构建一个完整的任务编排和规则引擎系统:
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
// 模拟任务实现
type SampleTask struct {
ID string
Name string
}
func (t *SampleTask) GetID() string {
return t.ID
}
func (t *SampleTask) Execute(ctx context.Context) (interface{}, error) {
fmt.Printf("Executing task: %s (%s)\n", t.Name, t.ID)
// 模拟任务执行时间
sleepTime := time.Duration(rand.Intn(3)+1) * time.Second
time.Sleep(sleepTime)
// 模拟可能的失败
if rand.Float32() < 0.1 { // 10% 失败概率
return nil, fmt.Errorf("task %s failed randomly", t.ID)
}
result := map[string]interface{}{
"success": true,
"task_id": t.ID,
"duration": sleepTime,
}
fmt.Printf("Task %s completed successfully\n", t.ID)
return result, nil
}
func main() {
// 创建任务编排器
orchestrator := NewTaskOrchestrator()
// 创建任务
taskA := &SampleTask{ID: "task_a", Name: "数据准备"}
taskB := &SampleTask{ID: "task_b", Name: "数据清洗"}
taskC := &SampleTask{ID: "task_c", Name: "数据分析"}
taskD := &SampleTask{ID: "task_d", Name: "生成报告"}
taskE := &SampleTask{ID: "task_e", Name: "发送邮件"}
// 添加任务节点
orchestrator.AddTask(&TaskNode{
ID: "task_a",
Task: taskA,
Dependencies: []string{},
Next: []string{"task_b"},
})
orchestrator.AddTask(&TaskNode{
ID: "task_b",
Task: taskB,
Dependencies: []string{"task_a"},
Next: []string{"task_c"},
})
orchestrator.AddTask(&TaskNode{
ID: "task_c",
Task: taskC,
Dependencies: []string{"task_b"},
Next: []string{"task_d"},
})
orchestrator.AddTask(&TaskNode{
ID: "task_d",
Task: taskD,
Dependencies: []string{"task_c"},
Next: []string{"task_e"},
})
orchestrator.AddTask(&TaskNode{
ID: "task_e",
Task: taskE,
Dependencies: []string{"task_d"},
Next: []string{},
})
// 执行任务编排
ctx := context.Background()
err := orchestrator.Execute(ctx)
if err != nil {
fmt.Printf("Orchestration failed: %v\n", err)
} else {
fmt.Println("All tasks completed successfully!")
}
}
总结
今天我们学习了任务编排和规则引擎的设计与实现:
- 任务编排系统:支持任务依赖管理、条件执行、并行执行和错误处理
- 规则引擎:实现了简单规则和组合规则,支持常见的比较操作符
- 自定义 DSL:设计了一种易于理解和编写的领域特定语言
- DAG 模型:使用有向无环图来表示任务依赖关系,并实现循环依赖检测
- 实际应用案例:展示了如何将这些组件整合在一起构建完整的系统
通过这样的设计,我们的分布式任务调度系统变得更加智能和灵活,能够处理各种复杂的业务场景。在下一节中,我们将进一步探讨如何将这个系统做得更加高可用。