写爬虫的人都有过这种经历:本地调试的时候跑得顺顺当当,一旦放到服务器上,就变成了“薛定谔的进程”——你不知道它现在是活着还是已经挂了,反正 SSH 一关,第二天再登录,ps aux 里什么都找不到。更头疼的是,服务器偶尔重启一次,所有手工启动的爬虫全部归零,又要一台一台重新拉起来。我最早折腾“爬虫部署”的时候也被这个问题折磨得不轻,后来接触了 Supervisor,才算把这块短板补上。
Supervisor 本质上是一个进程守护和管理工具,它的核心价值用大白话说就是:让你不用再手动去维护那些后台常驻的 Python 爬虫进程。不管是崩溃自动重启、开机自启、日志统一管理,还是同时控制几十个爬虫实例,它都可以比较优雅地搞定。这篇文章我会把 Supervisor 从安装、配置、启动到踩坑排错整个链路讲透,重点贴合爬虫部署场景,适合刚接触服务器运维、打算把自己的爬虫脚本真正“产品化”的开发者参考。
1. 爬虫进程“裸跑”有多疼:为什么需要 Supervisor
1.1 SSH 一关进程就没了,这个问题怎么解的
很多人的第一版爬虫部署流程是这样的:本地写好脚本,丢到服务器上,执行 python spider.py,看到日志开始滚动,觉得大功告成。然后断开 SSH,过一会儿再登进去,发现进程已经没了。原因很简单——直接在终端里运行的进程,会跟随当前终端会话的生命周期,终端关闭时 shell 会向进程组发送挂断信号,进程默认就会退出。
为了解决这个问题,大概率会尝试几种方案。第一种是用 nohup python spider.py &,把进程放到后台并忽略挂断信号。“nohup”这个名字其实就是“no hang up”的缩写,这在早期确实解决了一部分问题,但缺陷也很明显:进程意外崩溃了没人管,它不会自动重新拉起来;服务器重启了,这堆后台进程也不会主动恢复,还是得手动登录操作。第二种是用 screen 或 tmux,开一个守护窗口在里面跑,这样 SSH 断了窗口还在。但严格说这只是“伪后台”,窗口里的进程依然依赖那个会话环境,而且一旦窗口被误删,进程一样逃不掉。
这些方案的共同问题是:它们只是把进程“挂”在了某个地方,并没有对进程做真正的生命周期管理。而真正部署在服务器上长期运行的爬虫,需要的是一套“进程管家”,能盯着进程状态、能自动重启、能统一收集日志——这些恰恰是 Supervisor 的看家本领。
1.2 崩溃重启、开机自启、日志管理:Supervisor 要做的事
具体到爬虫场景,Supervisor 帮我解决的实际问题可以列成这几点:
- 崩溃自动重启:爬虫跑久了难免会遇到网络超时、目标站点返回异常、内存溢出之类的问题,进程一旦崩掉,Supervisor 会在几秒内把它重新拉起来,不需要人工干预。对于需要 7x24 小时不间断采集的任务来说,这是最核心的需求。
- 开机能自启:配置了
autostart=true的进程,会跟随 supervisord 服务一起启动。也就是说,只要服务器重新开机,supervisord 起来了,爬虫也会自动恢复,不用再手动登录服务器执行启动命令。 - 日志统一管理:每个被托管的进程,标准输出和错误输出都可以分别写到指定文件,还能按大小自动切割。排查问题的时候,直接打开统一目录下的日志文件,不用满服务器找 nohup.out。
- 统一操作多进程:用一条命令可以同时启动、停止、重启一组爬虫,不用一个个去
ps找 PID 再kill。
这就是 Supervisor 在爬虫部署里的定位。它不是一个“跑爬虫”的工具,而是一个“管爬虫”的工具。
1.3 和 systemd、screen、nohup 的对比,为什么选 Supervisor
既然提到了进程守护,可能很多人会问:现在 Linux 上不是有 systemd 吗?为什么还要单独用 Supervisor?
systemd 确实可以做进程守护,但它的定位是系统级服务管理,配置起来比较重,而且每次修改配置都要重新加载 service 文件,规则相对繁琐。对一个经常要调整启动参数、会频繁新增爬虫实例的爬虫项目来说,Supervisor 更轻量、更灵活:
- 修改配置后执行
supervisorctl update就能热加载,不需要重启整个服务。 - 新增一个爬虫,只需要在 conf.d 目录里丢一个
.conf文件,再reread和update。 - 支持进程分组管理,对多爬虫项目的操作效率明显更高。
至于 screen / tmux,它们更适合交互式使用,不是为无人值守的长期进程设计的。nohup 则连“崩溃重启”和“开机自启”都做不到。所以从爬虫部署的长远角度来看,Supervisor 是综合成本和效率都比较理想的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Supervisor 的体系结构:两个进程、一份配置和一个命令
2.1 supervisord 与 supervisorctl:服务端和客户端的配合
刚开始用 Supervisor 的时候,我一度搞不清楚 supervisord 和 supervisorctl 的关系。其实它们的关系很像 MySQL 的服务端和客户端:supervisord 是常驻后台的主进程,负责启动、停止、监控所有被托管的子进程;supervisorctl 是你敲命令用的客户端工具,通过它向 supervisord 发指令,比如查询状态、启停进程、查看日志。
我建议把 supervisord 理解成一个“进程托管中心”,你的爬虫进程全都挂在它下面。它启动之后不会退出,一直蹲在后台盯着状态。supervisorctl 则是你用来管理这些进程的“遥控器”。在命令行输入 supervisorctl status 看到的进程列表,就是 supervisord 当前托管的所有程序。
这个架构带来的一个直接好处是:即便某个爬虫进程崩溃了,也只是那一个子进程出问题,supervisord 本身不受影响,它可以按照配置定时重试拉起这个爬虫。这比你自己写一个 shell 脚本去 while 循环检查进程要可靠得多。
2.2 配置文件到底有几个:主配置和 conf.d 目录的加载逻辑
Supervisor 的配置采用“主配置 + 子配置”的结构。主配置文件一般在 /etc/supervisor/supervisord.conf(通过 apt 安装时),里面定义服务端本身的设置,比如 socket 文件位置、日志文件位置、子配置文件的加载目录。最关键的是文件末尾通常有一段:
ini复制[include]
files = /etc/supervisor/conf.d/*.conf
这句的意思是:Supervisor 启动时会自动加载 /etc/supervisor/conf.d/ 目录下所有以 .conf 结尾的文件。这样做的好处是,你不需要把几十个爬虫的配置全部堆在一个大文件里,而是每个爬虫一个独立配置文件,互不干扰,查找、修改、备份都很方便。
在 Ubuntu/Debian 系统上,Supervisor 默认会帮你建好 /etc/supervisor/conf.d/ 目录;在 CentOS 系统上这个目录可能是 /etc/supervisord.d/,但加载逻辑一样,都是通过主配置里的 [include] 段指定。
2.3 一个进程从启动到被托管,内部发生了什么
简单梳理一下进程被托管的流程:supervisord 启动时,会扫描主配置和 include 的子配置,解析出所有 [program:x] 段。对于每个设置了 autostart=true 的程序,supervisord 会以 fork 的方式创建子进程,并把子进程的 PID 记录到自己的管理表里。
之后 supervisord 会持续监控这些子进程的状态。如果子进程退出,supervisord 会通过 waitpid 拿到退出码,然后根据配置决定是否重新拉起。如果进程一直活着,supervisord 也会在内部维护它的状态供 supervisorctl status 查询。
这里有一个容易忽略的细节:supervisord 会把自己设置成这些子进程的“收养人”。即使你手动用 root 在服务器上执行 kill 杀掉一个被托管的爬虫进程,supervisord 也能感知到它的退出,并按配置执行重启逻辑。所以如果你需要临时停掉一个爬虫,正确的姿势不是 kill,而是 supervisorctl stop 进程名,这样才能让 supervisord 知道是“主动停止”,而不是“意外崩溃”。
3. 从零部署:安装、最小配置、把第一个爬虫托管起来
3.1 安装 Supervisor 的几种方式与选型
安装 Supervisor 最省事的做法是使用系统包管理器。以 Ubuntu/Debian 为例:
bash复制apt-get update
apt-get install -y supervisor
CentOS 上则是:
bash复制yum install -y supervisor
系统包管理器安装的版本通常会和当前系统的 Python 环境做了适配,还会自动创建 systemd 服务,你可以直接用 systemctl start supervisor 启动 supervisord。这对爬虫部署来说是最省心的路径。
还有一种通用方式是 pip install supervisor,这在没有 root 权限的机器上比较实用,但你需要自己手动创建配置文件、自己启动 supervisord,相比系统包安装要多折腾几步。我的建议是:除非迫不得已,否则优先用系统包安装,省去一堆手工初始化的步骤。
安装完成后,检查一下版本和服务状态:
bash复制supervisord --version
systemctl status supervisor
如果显示 active,恭喜,服务端已经跑起来了。
3.2 最小可用的 program 配置模板
接下来就是核心环节:写一个托管爬虫的配置文件。在 /etc/supervisor/conf.d/ 下新建一个文件,比如叫 jd_spider.conf,内容如下:
ini复制[program:jd_spider]
command=/home/spider/venv/bin/python /home/spider/jd_spider/main.py
directory=/home/spider/jd_spider
user=spider
autostart=true
autorestart=true
startsecs=5
startretries=3
stderr_logfile=/var/log/supervisor/jd_spider_err.log
stdout_logfile=/var/log/supervisor/jd_spider_out.log
environment=PYTHONUNBUFFERED="1"
先解释一下每个字段的用途:
[program:jd_spider]:方括号里的名字jd_spider就是这个程序的唯一标识,后续supervisorctl start jd_spider都用这个名字。command:启动命令。这里最需要注意的一点是,必须写 Python 解释器的绝对路径。如果你用了虚拟环境,就是venv/bin/python的完整路径,不能写成python /home/spider/xxx.py,因为 supervisord 托管子进程时 PATH 环境变量很干净,不一定会指向你的虚拟环境。directory:设置工作目录。爬虫代码里如果有相对路径的读写操作,比如保存临时文件、加载相对路径的配置文件,这个字段能避免“找不到路径”的诡异问题。user=spider:指定以哪个系统用户来启动这个进程。出于安全考虑,不要一上来就用 root 跑爬虫,单独建一个服务账号更稳妥。autostart:supervisord 启动时是否自动拉起这个程序。autorestart:程序退出后是否自动重启。startsecs:启动后多少秒内没有退出,算启动成功。这个参数对爬虫很重要,后面细说。startretries:启动失败时的重试次数。stdout_logfile/stderr_logfile:标准输出和错误输出分别记录到不同日志文件,方便排查问题。environment:设置环境变量。PYTHONUNBUFFERED="1"强制 Python 不缓冲标准输出,这样日志能立刻刷到文件里,而不是攒一堆才写一次。
3.3 启动服务并验证爬虫被正确托管
写完配置文件,先重新加载配置,让 supervisord 认识新程序:
bash复制supervisorctl reread
supervisorctl update
reread 是让 supervisord 扫描配置文件目录,发现新增或修改的配置;update 才是真正把变更生效。如果你把 autostart=true 加上了,执行完 update 之后进程应该已经被自动拉起。
查看状态:
bash复制supervisorctl status
正常应该看到类似这样的输出:
text复制jd_spider RUNNING pid 23456, uptime 0:00:10
如果显示的是 STARTING 或者 FATAL,说明程序没有正常启动。这时候第一时间去看错误日志:
bash复制tail -f /var/log/supervisor/jd_spider_err.log
大部分启动失败的原因都能在这里直接看到,比如依赖包没装进虚拟环境、路径写错、权限不足等。记住这个排查链路:先 status 看状态,再 tail 看日志,基本能解决八成问题。
4. 爬虫场景的配置细节:让进程不只是一个“能跑”的状态
4.1 虚拟环境、环境变量和工作目录的处理
刚开始用 Supervisor 托管爬虫时,我踩过很多次“本地能跑,托管就跑不起来”的坑,后来总结下来,绝大多数问题出在环境差异上。
本地开发时,终端会激活虚拟环境,PATH 指向当前项目的 venv,系统环境变量也都在。但 supervisord 启动子进程时,继承的是一个精简过的环境变量,很多本地依赖的 PATH、PYTHONPATH 都没有。所以配置里需要注意几个点:
- command 里写死虚拟环境的 python 绝对路径,例如
/home/spider/venv/bin/python,这相当于在 Supervisor 的场景里代替了source activate的动作。 - 如果有数据库连接串、第三方 API 的密钥、代理配置等环境变量,不要依赖服务器的全局环境,直接在
environment字段里列出来:
ini复制environment=DB_HOST="127.0.0.1",DB_PASSWORD="xxxx",SPIDER_PROXY="http://proxy.internal:8080",PYTHONUNBUFFERED="1"
- 爬虫代码里凡是涉及文件路径的,尽量使用基于
directory字段的工作目录,或者干脆在代码里用绝对路径。因为 supervisord 托管的方式不会像你在终端里那样 “cd 到某个目录再执行”,directory 未必和你预期一致。
4.2 startsecs / autorestart / startretries:守护逻辑的参数化
startsecs 这个参数,初看容易忽略,但对爬虫来说含义很深。它的作用是定义“启动成功”的判定窗口。比如 startsecs=5,意思是进程启动后如果持续 5 秒没有退出,supervisord 就认为这次启动是成功的。如果进程在 5 秒内就退出了,supervisord 认为是启动失败,会根据 startretries 重试。
为什么需要这个参数?因为有些爬虫程序启动时会先加载模型、初始化数据库连接、或者做一轮资源检查,这需要几秒时间。如果一看到进程退出就触发重启,很可能会在程序还没有完成初始化、即将正常运行的时候重复重启。设置一个合理的 startsecs,能有效避免这种“启动抖动”。
与它配合的是 autorestart 的取值:
autorestart=unexpected:默认值,只有当程序的退出码不在exitcodes列表中时才自动重启,常用exitcodes=0,表示正常退出不重启,异常退出才重启。autorestart=true:不管什么原因退出都重启。对于需要 7x24 小时采集的爬虫,我通常直接用true,因为就连手动stop之后,如果用update重新加载配置,它也有可能被重新拉起来(这一点后面踩坑部分会细说)。autorestart=false:退出后不自动重启。
还有一个容易被坑的地方是 startretries。如果爬虫因为某个永久性错误(比如代码 bug、数据库中表结构变了)反复启动失败,supervisord 重试到上限之后,进程状态会变成 FATAL。这是一个好设计,因为它避免了服务器上出现“每秒钟拉起一个崩溃进程”的疯狂循环。当看到 FATAL 状态时,不要反复 start,应该先去日志里找到根因。
4.3 停止行为:stopasgroup / killasgroup 与信号处理
爬虫程序和普通的 Web 服务有一点不同:它常常会拉起一堆子进程或子任务。比如基于 Scrapy 的爬虫可能会有多个 worker,比如用 Selenium 的爬虫会启动一个 chromedriver 甚至整个浏览器进程。在停止爬虫的时候,如果只杀主进程,这些子进程会变成孤儿进程,继续留在服务器上跑,下次再启动同一个爬虫时,可能还会因为端口占用、重复数据等问题产生连锁故障。
解决这个问题靠两个配置项:
ini复制stopasgroup=true
killasgroup=true
stopasgroup 表示停止时向整个进程组发送停止信号,killasgroup 表示在必要的时候强杀整个进程组。这两个配置一起用,可以在 supervisorctl stop 时把主进程和它拉起来的子进程一锅端,避免留下后患。
另外还值得关注的是停止信号。stopsignal 默认为 TERM,对于爬虫任务来说,TERM 信号往往意味着立即终止,可能会打断正在进行的请求。如果爬虫代码里实现了优雅关闭的逻辑(比如捕获 SIGINT,保存断点,清理队列),可以把 stopsignal 改成 INT,让进程有机会“体面地退场”。
4.4 日志切分与保留策略
爬虫跑起来之后日志增长很快,尤其是一些喜欢 print 每个商品信息的代码,几天下来日志文件就能到几个 GB。Supervisor 提供了内置的日志大小切分配置:
ini复制stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
stderr_logfile_maxbytes=100MB
stderr_logfile_backups=10
stdout_logfile_maxbytes 表示单个标准输出日志达到 100MB 时开始切割,stdout_logfile_backups 表示保留最近 10 个切割后的备份文件。这样既能保留足够的历史日志用于排查,又不会放任日志无限增长把磁盘塞满。
这里有一个不能忽略的细节:stdout_logfile 和 stderr_logfile 必须指向两个不同的文件。有些初学者会把它们配成同一个路径,Supervisor 启动时会直接报错,不允许这种情况。所以上面模板里我用的是 _out.log 和 _err.log 分开命名。
5. 通过 supervisorctl 和配置文件管理多爬虫
5.1 管理命令速查与日常操作
日常维护爬虫,最常用的就是这几个 supervisorctl 命令:
| 命令 | 作用 |
|---|---|
supervisorctl status |
查看所有托管进程的状态 |
supervisorctl status jd_spider |
查看单个进程状态 |
supervisorctl start jd_spider |
启动某个进程 |
supervisorctl stop jd_spider |
停止某个进程 |
supervisorctl restart jd_spider |
重启某个进程 |
supervisorctl reread |
重新扫描配置文件,发现新增或变更 |
supervisorctl update |
让配置变更生效,并重启受影响的进程 |
supervisorctl tail -f jd_spider |
实时查看进程日志 |
supervisorctl tail -f jd_spider stderr |
实时查看进程错误日志 |
也可以直接进入交互模式,输入 supervisorctl 不带任何子命令,会进入一个类似 psql 的交互环境,直接敲命令就行。
我个人的习惯是,在需要快速重启某个爬虫时,直接 supervisorctl restart 进程名,比 kill 再手动启动要安全得多,因为 restart 会走完整的停止和启动流程,包括信号处理和启动成功判定。
5.2 多个爬虫的分组与统一操作
当爬虫数量多起来之后,单独操作每个进程就有点累了。比如一个电商采集项目,可能有商品爬虫、评论爬虫、价格爬虫、库存爬虫四个程序,发布版本时希望它们作为一个整体统一启停。Supervisor 的 group 功能可以解决这个问题。
在配置文件里定义一个组:
ini复制[group:spiders]
programs=goods_spider,review_spider,price_spider,stock_spider
然后就可以这样操作:
bash复制supervisorctl stop spiders:*
supervisorctl start spiders:*
supervisorctl restart spiders:*
注意组名和进程名之间用冒号连接,spiders:* 表示组内所有进程。这样全量停止、全量启动只需要一条命令,在需要整体更新代码、整体暂停采集的时候非常方便。
5.3 使用 update 和 reread 安全变更配置
很多人在改完 .conf 文件后习惯直接 supervisorctl reload。这个操作确实能让配置生效,但它会把 supervisord 整个服务重启一遍,导致所有托管进程全部重启,哪怕你只改了一个进程的日志路径。在一台服务器上跑着几十个爬虫的场景下,这种“全局重载”的成本有点高。
更稳妥的流程是:
bash复制# 修改了 conf 文件之后
supervisorctl reread
# 查看输出,确认没有语法错误
supervisorctl update
update 会智能对比当前正在运行的进程和磁盘上的配置差异:只有配置发生变化的进程会被重启,新增的进程会被启动,被删除配置的进程会被停止。这样可以把变更的影响范围控制在最小。
6. 部署实战中踩过的坑与排查方法
6.1 配置里的命令找不到或权限不对
我最早用 Supervisor 部署爬虫时遇到的第一类报错,是 FATAL Exited too quickly 或者日志里直接提示找不到模块。排查下来发现,是 command 里写了 python,结果 supervisord 用的是系统默认的 Python 3.8,而我的爬虫依赖装在虚拟环境里。
解决办法就是前面反复强调的:command 里写虚拟环境的解释器绝对路径,例如 /home/spider/venv/bin/python。另外,如果你的爬虫是一个带命令行入口的包,比如 Scrapy,command 可以写成:
ini复制command=/home/spider/venv/bin/scrapy crawl goods -s LOG_LEVEL=INFO
这样不依赖 PATH 环境就能正确找到可执行文件。
6.2 重启不生效,修改了配置却没有反应
这个坑也很经典。改完 .conf 文件,用 supervisorctl restart 进程名,发现进程确实重启了,但新配置似乎没有生效,程序行为还是跟改之前一样。
原因在于 restart 只是重启进程,并不会重新读配置文件。配置文件的新内容需要先 supervisorctl update 才会被加载,update 之后如果检测到配置有变化,会自动重启相关进程。所以我现在的习惯是:改配置后一律走 reread + update,不要动 restart。
6.3 爬虫卡死拖垮 Supervisord 或出现大量子进程
如果你托管一个用了 Selenium 的爬虫,进程 stop 之后用 ps aux | grep chromedriver 一看,还有一堆残留的浏览器驱动进程在跑。这个现象前面已经提到,根因是停止信号只打给了主进程,子进程组没有收到。
解决办法就是上面说的 stopasgroup=true 和 killasgroup=true。这里额外提醒一句:如果已经出现过残留进程,光改配置还不够,需要先手动清理一遍残留进程,再 supervisorctl start,否则可能端口被占用导致新进程起不来。
6.4 日志不写入或没有输出内容
还有一种情况是日志文件被创建了,但里面一直是空的。这时候要区分是“进程没有输出”还是“输出被缓冲了”。Python 的 print 默认是有缓冲的,尤其是在输出重定向到文件的时候,缓冲区可能很久才刷新一次。解决办法就是在 environment 里加上 PYTHONUNBUFFERED="1",强制行缓冲,这样 print 的内容能立即落到日志文件里。
另一个可能是超级用户权限问题。如果你的 user=spider 指定的是一个普通账号,那么日志目录必须允许该账号写入。默认的 /var/log/supervisor/ 目录如果属于 root,普通用户无法创建日志文件,这时候需要调整目录权限,或者把日志路径放到爬虫账号有权限的目录下,比如 /home/spider/logs/。
6.5 排查配置的常用命令与调试流程
如果遇到搞不定的问题,我建议按照下面的顺序排查,不遗漏也不盲目:
supervisorctl status看状态:RUNNING/STARTING/BACKOFF/FATAL分别代表不同阶段的问题。supervisorctl tail -f 进程名 stderr看错误输出,大部分启动失败的直接原因都在这里。- 手动在命令行执行一遍 command 里的命令,加上同样的环境变量,看能不能正常跑起来。如果手动都跑不起来,那就是爬虫本身的问题,别让 Supervisor 背锅。
- 检查配置文件语法:可以用
supervisord -n -c /etc/supervisor/supervisord.conf在前台启动一遍 supervisord,它会对配置做解析,若有语法错误会直接打印出来。注意这只是调试方法,正常情况下不要用-n前台模式运行。
如果配置是从 Windows 机器上传到服务器的,还需要检查文件格式是不是带了 Windows 的 \r 换行符,可以用 dos2unix 转换一下,否则可能出现解析配置时的诡异报错。
最后再分享一个实际操作中的小技巧:不要把所有爬虫都塞进一个 program 配置里,也不要为了省事直接把整个采集流程写成一个巨型脚本。每个爬虫单独一个 .conf 文件,名字和爬虫名一致,再搭配好 group 分组,这样无论是上线新爬虫还是排查老任务,都能做到“指哪打哪”。我现在的服务器上,甚至会把同一个任务的“爬虫程序”和“数据清洗程序”也分开托管,各自独立重启互不影响。Supervisor 的功能其实不少,但对爬虫部署来说,把上面这些核心配置吃透,已经能覆盖绝大多数场景了。
