在分布式系统中,将计算密集型任务(如 AI 图像生成)从 Web 服务中剥离是常见的架构模式。本文将介绍如何使用 Python 的 asyncio 和 aio_periodic 构建一个健壮的 Worker,它能够调用本地 MLX 推理引擎生成图片,同时保持高并发下的稳定性。
1. 架构概述
我们的目标是构建一个 Worker 节点,它负责:
- 监听任务 :从
periodicd任务服务器领取绘图任务。 - 执行推理 :调用适配 Apple Silicon 的
z-image-turbo-mlx脚本。 - 非阻塞交互:在长达数秒的生成过程中,保持与服务器的心跳连接。
- 结果回传:将生成的图片转为 Base64 传回。
2. 核心挑战:同步 vs 异步
在早期实现中,开发者常直接使用 os.system 调用外部命令。这在异步框架(如 asyncio)中是致命的。
- 问题 :
os.system会阻塞整个 Python 进程。在 AI 生成图片的 5-10 秒内,Event Loop 停止运转,Worker 无法发送心跳包(Heartbeat),导致服务器误判 Worker 掉线并重新分配任务,造成"僵尸任务"循环。 - 解法 :使用
asyncio.create_subprocess_exec。它允许我们在等待子进程结束时,让出 CPU 控制权,使 Worker 能继续处理网络 IO。
3. 代码实现亮点
3.1 安全的子进程调用
为了防止 Shell 命令注入攻击(例如用户在 prompt 中输入 ; rm -rf /),我们放弃字符串拼接,改用列表传参:
Python
ini
# 推荐做法:列表传参
cmd = [
sys.executable, 'generate_mlx.py',
'--prompt', user_prompt,
'--output', output_filename
]
# asyncio 负责安全地将参数传给子进程,无需经过 Shell 解释
process = await asyncio.create_subprocess_exec(*cmd)
3.2 保持 Event Loop 活跃
对于文件读取这种 IO 操作,虽然 Python 的 open() 是同步的,但在高并发下依然可能造成微小的卡顿。我们利用 run_in_executor 将其放入线程池:
Python
ini
# 将同步的文件读取扔到线程池,避免阻塞主循环
loop = asyncio.get_running_loop()
b64_str = await loop.run_in_executor(
None,
partial(blocking_image_to_base64, output_filename)
)
3.3 健壮的资源清理
AI 任务常因显存不足或参数错误而失败。使用 try...finally 模式确保即使生成失败,临时文件也能被清理,保持环境整洁。
Python
csharp
try:
await run_generation()
except Exception:
logger.error("Job failed")
finally:
if os.path.exists(temp_file):
os.remove(temp_file) # 无论成功失败,必须清理垃圾
4. 实时日志流
为了方便运维监控,我们配置子进程直接继承父进程的标准输出。这意味着你在运行 Worker 的终端上,可以直接看到底层 generate_mlx.py 打印的进度条和日志,而无需复杂的管道转发。
5. 总结
通过将 aio_periodic 的任务调度能力与 asyncio 的子进程管理结合,我们构建了一个既能利用本地强大算力(MLX),又完全符合分布式系统稳定性要求的 Worker。这种模式同样适用于视频转码、PDF 生成等其他耗时任务。