如何在aiohttp协程中正确处理服务优雅关闭?
关于该实现方案的评估
你的核心思路方向是对的,但直接按你描述的方式裸写有不少容易踩的坑,补全几个关键处理后才能稳定达到预期效果,具体问题和修正方案如下:
- 不要用模块级全局数组存任务,最好把任务集合绑定到aiohttp的app实例上。全局变量在多实例测试、服务热重载场景下很容易出现任务串扰、内存泄漏问题,在
startup生命周期回调里初始化app['persist_tasks'] = set()来存任务,生命周期和服务实例完全绑定,可控性高很多。 - 直接调用
await asyncio.gather(*global_jobs)有两个致命问题:- 没有处理默认的任务取消逻辑:aiohttp触发shutdown流程时,默认会先取消所有未被显式持有、未做屏蔽的后台任务,等你走到shutdown回调执行gather的时候,很多存库任务可能已经被抛出
CancelledError中断了,根本等不到执行完成。注册任务时要用asyncio.create_task()显式创建Task对象存入集合,必要时给核心存库逻辑包asyncio.shield()避免被提前取消。 - 没有异常和超时兜底:
gather默认只要有一个任务抛出异常就会整体中断,万一某个存库协程因为数据库连接异常卡死、抛错,整个关闭流程要么直接挂死退不出去,要么打断其他正常执行的存库任务。调用gather时必须加return_exceptions=True参数,再外层套asyncio.wait_for设置合理超时(比如10秒),超时就强制结束剩余任务,别阻塞进程退出。
- 没有处理默认的任务取消逻辑:aiohttp触发shutdown流程时,默认会先取消所有未被显式持有、未做屏蔽的后台任务,等你走到shutdown回调执行gather的时候,很多存库任务可能已经被抛出
- 不要把所有后台任务都放进等待列表,只有带状态持久化逻辑的收尾任务需要等待执行完成。那些长期跑循环的消费任务、定时拉取类任务,要在shutdown的最早期主动发取消信号等它们退出,别占着事件循环资源阻塞存库操作。
- 注意生命周期回调的注册顺序:数据库连接池的销毁逻辑不要放在
on_shutdown阶段,要绑定到on_cleanup信号上。aiohttp会先执行完所有on_shutdown回调,再执行on_cleanup回调做资源销毁,保证你在等待存库任务完成的时候,数据库连接是可用的,不会出现连不上库的问题。
最小可用实现参考
from aiohttp import web import asyncio async def on_startup(app: web.Application): # 初始化持久化任务集合 app["persist_tasks"] = set() async def add_persist_task(app: web.Application, coro): # 统一封装任务注册逻辑 task = asyncio.create_task(coro) app["persist_tasks"].add(task) # 任务执行完成后自动从集合移除,避免内存泄漏 task.add_done_callback(app["persist_tasks"].discard) return task async def on_shutdown(app: web.Application): tasks = app["persist_tasks"] if not tasks: return # 最多等待10秒,捕获所有异常不中断关闭流程 await asyncio.wait_for( asyncio.gather(*tasks, return_exceptions=True), timeout=10 ) # 初始化服务时按顺序注册回调 app = web.Application() app.on_startup.append(on_startup) # 注意shutdown回调要放在cleanup回调前注册 app.on_shutdown.append(on_shutdown) # 数据库连接池的初始化、销毁逻辑分别绑定到startup和cleanup信号即可
如果你用gunicorn这类多进程工具部署aiohttp服务,还要注意确认终止信号的处理逻辑,确保worker进程收到SIGTERM时能正常走完shutdown流程,不要被操作系统强杀导致状态丢失。
内容的提问来源于stack exchange,提问作者comonadd
相关产品推荐
相关产品推荐

