1. 为什么需要异步任务编排框架
在数据处理、爬虫开发、API集成等场景中,我们经常遇到这样的困境:任务之间存在复杂的依赖关系,但传统同步执行方式导致资源利用率低下。以电商订单处理为例,支付成功后才能触发库存扣减,而库存更新后又要通知物流系统,这种链式依赖用普通代码会写成"回调地狱"。
我曾接手过一个数据ETL项目,需要从20多个API获取数据,每个API响应时间在200ms到2s不等。最初用同步请求实现,总耗时高达45秒。改用asyncio直接编写后,虽然缩短到8秒,但依赖管理代码变得难以维护——这就是典型的"异步编程陷阱":解决了IO等待问题,却引入了流程控制复杂度。
一个设计良好的Workflow框架应该解决三个核心痛点:
- 依赖可视化:用声明式语法描述任务关系,避免手动管理回调
- 错误隔离:单个任务失败不应导致整个流程崩溃
- 资源控制:限制并发量防止系统过载
2. 核心架构设计
2.1 任务节点抽象
每个可执行单元抽象为TaskNode类,关键属性包括:
python复制class TaskNode:
def __init__(self, task_id: str, coro_func: Coroutine):
self.task_id = task_id # 唯一标识
self.dependencies = set() # 前置任务ID集合
self.status = TaskStatus.PENDING # 状态机
self._coro = coro_func # 协程函数
2.2 有向无环图(DAG)引擎
任务依赖关系用图结构存储,拓扑排序算法确保执行顺序。这里采用邻接表实现:
python复制from collections import defaultdict
class DAG:
def __init__(self):
self._graph = defaultdict(set) # {task_id: {dependent_task_ids}}
self._reverse_graph = defaultdict(set) # 逆邻接表
def add_edge(self, from_task: str, to_task: str):
self._graph[from_task].add(to_task)
self._reverse_graph[to_task].add(from_task)
2.3 执行器设计
核心是asyncio.Semaphore控制并发度,配合事件循环实现高效调度:
python复制class AsyncExecutor:
def __init__(self, max_concurrent: int = 10):
self.semaphore = asyncio.Semaphore(max_concurrent)
self.task_results = {} # 存储各任务输出
async def _run_task(self, task: TaskNode):
async with self.semaphore:
try:
result = await task._coro
task.status = TaskStatus.SUCCESS
self.task_results[task.task_id] = result
except Exception as e:
task.status = TaskStatus.FAILED
raise
3. 关键实现细节
3.1 依赖解析算法
采用Kahn算法进行拓扑排序,时间复杂度O(V+E):
python复制def topological_sort(dag: DAG) -> List[str]:
in_degree = {u: 0 for u in dag._graph}
for u in dag._graph:
for v in dag._graph[u]:
in_degree[v] += 1
queue = deque([u for u in in_degree if in_degree[u] == 0])
sorted_order = []
while queue:
u = queue.popleft()
sorted_order.append(u)
for v in dag._graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
if len(sorted_order) != len(in_degree):
raise ValueError("存在循环依赖")
return sorted_order
3.2 错误处理机制
实现任务重试和断路器模式:
python复制class RetryPolicy:
def __init__(self, max_retries=3, backoff=1.0):
self.max_retries = max_retries
self.backoff = backoff # 指数退避基数
async def execute_with_retry(task: TaskNode, policy: RetryPolicy):
for attempt in range(policy.max_retries + 1):
try:
return await task._coro
except Exception:
if attempt == policy.max_retries:
raise
await asyncio.sleep(policy.backoff ** attempt)
3.3 上下文传递方案
通过ContextVar实现跨任务上下文共享:
python复制from contextvars import ContextVar
workflow_ctx = ContextVar('workflow_ctx', default={})
async def task_wrapper(task: TaskNode):
ctx = workflow_ctx.get()
ctx.update({'current_task': task.task_id})
workflow_ctx.set(ctx)
return await task._coro
4. 实战案例:电商订单流程
4.1 定义工作流
python复制async def payment():
print("处理支付...")
await asyncio.sleep(0.5)
return {"order_id": 123, "amount": 99.9}
async def inventory():
print("扣减库存...")
await asyncio.sleep(0.3)
async def logistics():
print("创建物流单...")
await asyncio.sleep(0.8)
dag = DAG()
dag.add_edge("payment", "inventory")
dag.add_edge("inventory", "logistics")
4.2 执行与监控
python复制async def main():
tasks = {
"payment": TaskNode("payment", payment()),
"inventory": TaskNode("inventory", inventory()),
"logistics": TaskNode("logistics", logistics())
}
executor = AsyncExecutor(max_concurrent=2)
await executor.run(tasks, dag)
4.3 性能优化技巧
- 批量任务处理:对无依赖的同类任务使用
asyncio.gather
python复制async def batch_process(items):
return await asyncio.gather(
*[process_item(item) for item in items]
)
- 超时控制:为每个任务设置合理超时
python复制async def safe_execute(task, timeout=5.0):
try:
return await asyncio.wait_for(task, timeout)
except asyncio.TimeoutError:
task.status = TaskStatus.TIMEOUT
raise
- 资源隔离:按任务类型使用不同Semaphore
python复制io_sem = asyncio.Semaphore(100) # 高IO型任务
cpu_sem = asyncio.Semaphore(4) # 计算密集型任务
5. 生产环境注意事项
5.1 日志追踪方案
为每个工作流实例生成唯一trace_id:
python复制from uuid import uuid4
class Workflow:
def __init__(self):
self.trace_id = f"wf_{uuid4().hex[:8]}"
self.logger = structlog.get_logger()
async def run(self):
self.logger.bind(trace_id=self.trace_id)
await self._execute()
5.2 持久化策略
使用Redis存储任务状态:
python复制async def save_checkpoint(task: TaskNode):
await redis.hset(
f"workflow:{workflow_id}",
task.task_id,
json.dumps({"status": task.status.value})
)
5.3 监控指标暴露
通过Prometheus客户端记录关键指标:
python复制from prometheus_client import Counter
TASK_SUCCESS = Counter(
'workflow_task_success_total',
'成功任务数',
['workflow_type']
)
async def instrumented_task(task):
try:
result = await task._coro
TASK_SUCCESS.labels(task.type).inc()
return result
except:
raise
6. 高级特性扩展
6.1 条件分支支持
python复制class ConditionalTask(TaskNode):
def __init__(self, condition: Callable[[dict], bool]):
self.condition = condition
async def execute_conditional_flow():
ctx = get_context()
if task.condition(ctx):
await task.execute()
6.2 人工干预接口
通过REST API挂起/恢复工作流:
python复制@app.post("/workflow/{wf_id}/pause")
async def pause_workflow(wf_id: str):
workflow = get_workflow(wf_id)
workflow.status = WorkflowStatus.PAUSED
return {"status": "paused"}
@app.post("/workflow/{wf_id}/resume")
async def resume_workflow(wf_id: str):
workflow = get_workflow(wf_id)
await workflow.resume()
return {"status": "running"}
6.3 可视化调试器
生成DAG的Graphviz描述:
python复制def generate_dot(dag: DAG):
lines = ["digraph workflow {"]
for u in dag._graph:
for v in dag._graph[u]:
lines.append(f' "{u}" -> "{v}"')
lines.append("}")
return "\n".join(lines)
提示:在实现超时控制时,建议采用分层超时策略——为整个工作流设置总超时,同时每个任务有独立超时,避免单个任务卡死整个流程。
这个框架在实际项目中经过多次迭代,最初版本只有基础DAG执行功能,后来逐步加入了错误恢复、上下文传递等企业级特性。最深刻的教训是:异步代码的异常堆栈往往难以追踪,务必在任务包装层做好错误捕获和上下文记录。
