
本文详解在 celery + django + selenium 架构中防止和清除僵尸进程(zombie processes)的完整方案,涵盖软硬超时控制、selenium 资源安全释放、子进程主动回收及 docker 层面的稳定性加固。
本文详解在 celery + django + selenium 架构中防止和清除僵尸进程(zombie processes)的完整方案,涵盖软硬超时控制、selenium 资源安全释放、子进程主动回收及 docker 层面的稳定性加固。
在基于 Celery 执行 Selenium 自动化任务的生产环境中,频繁出现 <defunct></defunct> 状态的僵尸进程(如 [Utility Process] <defunct></defunct>),本质是子进程退出后其父进程(Celery Worker)未及时调用 wait() 回收导致的资源泄漏。单纯依赖 driver.quit() 或 __del__ 析构函数无法保证 100% 可靠——尤其当异常中断、信号强制终止或 Python 解释器异常退出时,stop() 方法可能根本未被执行。因此,需构建多层防御机制:从任务粒度控制、资源显式清理,到进程级兜底回收,再到容器运行时约束。
✅ 一、强制任务超时:软限 + 硬限双保险
CELERY_TASK_TIME_LIMIT(硬限制)仅会在超时时向工作进程发送 SIGKILL,直接杀死进程,不给任何清理机会。必须配合 soft_time_limit(软限制),它会抛出可捕获的 SoftTimeLimitExceeded 异常,为资源释放争取关键窗口:
from celery import shared_task
from celery.exceptions import SoftTimeLimitExceeded
import logging
import traceback
logger = logging.getLogger(__name__)
@shared_task(soft_time_limit=600, time_limit=650) # 10分钟软限,10分50秒硬限
def check_urls(parsing_result_ids):
parser = None
try:
logger.info("Start check_urls")
parser = Parser()
parsing_results = ParsingResult.objects.filter(
pk__in=parsing_result_ids
).exclude(status__in=["DONE", "FAILED"])
parser.check_parsing_result(parsing_results)
except SoftTimeLimitExceeded:
logger.warning("Task hit soft time limit — initiating graceful cleanup")
# 此处可追加额外清理逻辑(如中断网络请求)
except Exception as e:
logger.error(f"Task failed: {e}", exc_info=True)
finally:
if parser is not None:
parser.stop() # 确保 stop() 总被调用
⚠️ 注意:
soft_time_limit必须在@shared_task装饰器中声明,全局设置(如CELERY_TASK_SOFT_TIME_LIMIT)在较新版本中可能失效;同时避免在finally中依赖locals()判断变量存在——改用明确的parser = None初始化 +if parser is not None检查更健壮。
✅ 二、Selenium 资源清理:从驱动到底层进程
Parser.stop() 需承担“最后防线”职责,不仅要关闭 WebDriver 和 Xvfb,还需主动扫描并终结所有关联子进程:
import psutil
import os
class Parser:
def __init__(self):
self.driver = None
self.display = None
self._process_pids = set()
if not USE_GUI:
self.display = Display(visible=0, size=(800, 600))
self.display.start()
# 记录 display 启动的 PID(Xvfb 进程)
self._process_pids.add(self.display.pid)
self.driver = get_firefox_driver(proxy_data=proxy_data)
# 记录浏览器主进程 PID(Firefox)
if hasattr(self.driver, 'service') and hasattr(self.driver.service, 'process'):
if self.driver.service.process:
self._process_pids.add(self.driver.service.process.pid)
def stop(self):
# 1. 标准关闭流程
if self.driver:
try:
self.driver.quit()
logger.info("Selenium driver quit successfully")
except Exception as e:
logger.error(f"Failed to quit driver: {e}")
if self.display:
try:
self.display.stop()
logger.info("Xvfb display stopped")
except Exception as e:
logger.error(f"Failed to stop display: {e}")
# 2. 主动清理残留进程(关键!)
self._cleanup_child_processes()
def _cleanup_child_processes(self):
"""强制终止所有已知关联进程及其子树"""
for pid in list(self._process_pids):
try:
proc = psutil.Process(pid)
# 终止进程树(包括所有子进程)
proc.terminate()
proc.wait(timeout=5)
except psutil.NoSuchProcess:
pass # 进程已退出
except psutil.TimeoutExpired:
try:
proc.kill() # 强制杀死
logger.warning(f"Force-killed orphaned process {pid}")
except psutil.NoSuchProcess:
pass
except Exception as e:
logger.error(f"Error cleaning up PID {pid}: {e}")
# 3. 扫描并清理疑似 Selenium 相关残留(兜底)
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
name = proc.info['name'].lower()
cmdline = ' '.join(proc.info.get('cmdline', [])).lower()
if 'firefox' in name or 'geckodriver' in name or 'xvfb' in name \
or ('selenium' in cmdline or 'webdriver' in cmdline):
proc.terminate()
proc.wait(timeout=3)
logger.info(f"Cleaned orphaned Selenium-related process: {proc.info['pid']}")
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
? 提示:
psutil是核心依赖,务必在requirements.txt中声明psutil>=5.9.0,并在 Docker 容器内安装(apt-get install -y python3-psutil或通过 pip 安装)。
✅ 三、Celery Worker 层加固:重启 + 限流 + 监控
单靠任务内清理不足以根治问题,需在 Worker 运行时层面持续施压:
-
启用
max-tasks-per-child:强制 Worker 进程处理 N 个任务后自动重启,彻底释放内存与句柄:celery -A core_app worker --concurrency=15 --max-tasks-per-child=100
(推荐值:50–200,视任务内存增长情况调整)
-
Docker 内存与重启策略:
celery: build: ./project command: celery -A core_app worker --loglevel=info --concurrency=15 --max-tasks-per-child=100 mem_limit: 2g # 防止 OOM 导致僵死 mem_reservation: 1g restart: unless-stopped # 关键:启用 init 进程管理僵尸(PID 1) init: true
-
补充监控脚本(可选):在容器内定期检查并清理僵尸:
# /health/zombie-killer.sh #!/bin/bash while true; do ZOMBIES=$(ps aux | grep 'Z' | grep -v grep | wc -l) if [ "$ZOMBIES" -gt 5 ]; then echo "$(date): Found $ZOMBIES zombie processes, cleaning..." >> /var/log/celery-zombies.log # 向所有 zombie 的父进程发送 SIGCHLD(触发 wait) for p in $(ps -eo pid,stat | awk '$2 ~ /Z/ {print $1}'); do kill -s SIGCHLD $(ps -o ppid= -q $p 2>/dev/null | xargs) done fi sleep 60 done并在
docker-compose.yml中以 sidecar 方式运行。
✅ 总结:四步闭环治理法
| 层级 | 措施 | 目标 |
|---|---|---|
| 任务层 |
soft_time_limit + finally 清理 |
确保异常/超时下仍能释放资源 |
| 资源层 |
psutil 主动终结进程树 |
消灭 driver/Xvfb 残留 |
| Worker 层 |
--max-tasks-per-child + --max-memory-per-child
|
定期重置进程状态 |
| 容器层 |
init: true + mem_limit + 自动重启 |
防止系统级僵死,兜底保障 |
最终效果:僵尸进程数趋近于 0,Celery Worker 稳定运行数日无需手动重启,Selenium 任务资源开销可控且可预测。记住——在自动化测试场景中,“优雅退出”不是可选项,而是必须由代码强制保障的契约。










