在Celery任务中集成Nmap端口扫描导致FastAPI+SQLAlchemy+PostgreSQL应用性能骤降的原因分析
我太懂你这种糟心的处境了——明明Celery的初衷就是把重任务丢去后台异步跑,结果一加Nmap端口扫描,整个FastAPI都慢得离谱,连完全无关的请求都要等10-20秒。咱们一步步拆解可能的核心问题:
一、系统资源被Nmap抢占(最可能的元凶)
Nmap扫描(尤其是带-sV版本探测或-A全面扫描的模式)是CPU、网络双密集型任务:
- 它会发送大量网络包,占用绝大部分带宽;
- 版本探测需要深度分析目标返回的数据包,对CPU的负载极高。
如果你的FastAPI Uvicorn进程、Celery Worker、甚至PostgreSQL数据库都部署在同一台机器/容器上,Nmap一旦跑起来,会把系统的CPU、网络资源榨干,导致FastAPI的进程没有足够资源处理用户请求,自然响应变慢。
结合你的代码来看,scan_ip函数默认用的是--top-ports 100 -sV --version-light -T4参数,-T4属于激进扫描模式,对资源的消耗本来就不小,要是同时跑多个这样的任务,系统直接就扛不住了。
二、数据库连接池耗尽
你的Celery任务里的数据库Session管理存在潜在问题,可能导致连接池被占满,FastAPI主线程拿不到数据库连接:
- 你在任务中先执行了
db.close(),然后又调用CelerySessionLocal()重新创建Session——如果这个操作频繁执行,可能会导致连接泄漏(比如异常情况下Session没被正确释放); - 假设你的Celery Worker并发数设置得过高(比如和CPU核心数一致),每个Worker进程都在扫描任务中占用一个数据库连接,而SQLAlchemy的连接池默认大小(
pool_size)通常是5,一旦并发任务数超过连接池容量,后续的数据库请求(包括FastAPI的请求)就会进入等待队列,直到有连接释放,这直接表现为API响应变慢。
看你的代码,任务中反复打开、关闭Session的操作很容易出问题,比如:
db.close() # ... 中间执行Nmap扫描(耗时很久) db = CelerySessionLocal() # 重新创建Session
这段代码中,Nmap扫描的几十秒时间里,虽然Session被关闭了,但如果连接池的配置是recycle时间过长,或者连接没有被正确归还给池,就会导致连接资源浪费。
三、Celery Worker并发设置不合理
Celery默认的Worker并发数是和机器的CPU核心数一致的,但Nmap是重负载任务,不能按常规的Web任务来设置并发:
- 如果你开了4个Worker并发,同时跑4个Nmap扫描任务,系统资源会直接被打满,所有服务(包括FastAPI、PostgreSQL)都会受影响;
- 每个Nmap扫描任务会阻塞整个Worker进程(因为python-nmap是同步阻塞的库),Worker进程在扫描完成前无法处理其他任务,进一步加剧资源拥堵。
对应的解决建议
1. 先隔离资源(立竿见影)
把Celery Worker和FastAPI、PostgreSQL部署在不同的机器/容器上,彻底隔离资源。如果暂时做不到,至少给Celery Worker分配单独的CPU/内存配额(比如用Docker的--cpus、--memory参数限制)。
2. 调整Celery Worker并发数
修改Celery启动命令,限制并发数为1或2(根据机器资源调整),避免同时跑多个Nmap任务:
celery -A your_app worker --concurrency=2 --loglevel=info
3. 修复数据库Session管理
在Celery任务中用上下文管理器来管理SQLAlchemy Session,确保连接被正确释放,避免泄漏:
@celery.task(name="tasks.scan_entity") def scan_entity(task_id: str): # 用with语句自动管理Session的创建和关闭 with CelerySessionLocal() as db: try: task_uuid = uuid.UUID(task_id) except Exception: return {"error": "Invalid task id"} task = db.query(models.IpDomainScanTask).filter(models.IpDomainScanTask.id == task_id).first() if not task: return {"error": "Task not found"} update_progress(db, task, "Starting port scan") # 直接在同一个Session里执行后续操作,不需要关闭再重建 os_records = [] port_records = [] for idx, ip in enumerate(['1.1.1.1'], start=1): # 你的IP列表逻辑 scan_results = scan_ip(ip) # ... 处理扫描结果并添加到记录列表 db.add_all(os_records) db.add_all(port_records) update_progress(db, task, "Scan completed successfully", models.TaskStatus.completed) db.commit() return {"task_id": str(task.id), "status": "completed"} # with块结束后,Session自动关闭,连接归还给池
4. 优化Nmap扫描参数
尽量用更轻量的扫描模式,减少资源消耗:
- 比如把
-T4降到-T3(平衡速度和资源); - 减少
--top-ports的数量,比如从100降到50; - 非必要的话关闭
-sV版本探测,改用仅端口状态扫描(-sS)。
5. 调整SQLAlchemy连接池配置
在创建数据库引擎时,增大连接池的pool_size和max_overflow,确保能同时支撑FastAPI和Celery的请求:
engine = create_engine( DATABASE_URL, pool_size=10, max_overflow=20, pool_recycle=300, # 定期回收闲置连接,避免泄漏 pool_pre_ping=True # 检测无效连接,自动重新连接 )
内容来源于stack exchange

