workerman本身不支持分布式任务调度,必须依赖redis等外部服务实现节点注册、任务分发与故障恢复;其仅可作为执行端,调度逻辑需由独立服务承担。

Workerman 本身不支持分布式任务调度,必须搭配外部协调服务
Workerman 是单机异步 PHP 框架,没有内置节点发现、心跳检测、任务分发或状态同步能力。想实现“全网分布式爬虫”的节点控制与任务指派,Workerman 只能作为执行端(Worker),不能当调度中心。真正的调度逻辑必须由独立服务承担,比如 Redis(用 PUB/SUB 或 LIST 做任务队列)、ZooKeeper(做节点注册与选主)、或轻量级 ETCD。
常见错误是直接在 Workerman 进程里写 file_get_contents 轮询某个 URL 获取任务——这会导致节点间无协调、重复抓取、漏任务、无法扩缩容。
- 所有爬虫节点启动时,需向
Redis的SET结构注册自身 ID 和 IP,设置EXPIRE(如 30 秒),模拟心跳 - 调度服务(可另起一个
Workerman进程,但角色完全不同)监听节点存活、从待抓队列(LIST)取任务、按负载或哈希分发到在线节点的私有队列(如queue:node_abc123) - 每个爬虫节点只消费自己名下的队列,用
BRPOP阻塞等待,避免空轮询
用 Redis LIST + BRPOP 实现低延迟任务指派
相比 HTTP 轮询或长连接推送,Redis 的 BRPOP 是最简单可靠的节点拉取模式,延迟可控(毫秒级),且天然支持多消费者竞争同一队列(适合故障转移)。
示例:节点 node-001 启动后执行:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
while (true) {
// 阻塞最多 5 秒,从自己的队列取任务
$task = $redis->brpop(['queue:node-001'], 5);
if ($task) {
$data = json_decode($task[1], true);
// 执行抓取:$data['url']、$data['headers'] 等
handleCrawl($data);
}
}
注意点:
- 不要用
LPUSH/RPOP配合sleep(1)——会积压延迟,且浪费 CPU - 每个节点必须有唯一、稳定的 ID(建议用配置文件写死或从环境变量读取),不能依赖随机生成,否则任务发错队列无法恢复
-
BRPOP第二个参数超时时间不宜设为 0(永久阻塞),否则进程无法响应信号退出;设为 5–30 秒较安全
节点上下线如何不丢任务?靠“任务重入队”机制
节点宕机时,它正在处理但未确认完成的任务会丢失。解决办法不是让节点上报“完成”,而是默认所有任务都可能失败,由调度服务主动兜底。
具体做法:
- 调度服务维护一个
pending:{task_id}的HASH,记录任务下发时间、目标节点、重试次数 - 每 10 秒扫描一次,对超过 60 秒未被
DEL的pending条目,视为节点失联,把任务重新LPUSH回公共队列或其它活跃节点队列 - 爬虫节点成功抓取后,立刻
DEL pending:{task_id},而不是等入库后再删——失败重试就靠这个 TTL 触发
这个机制比 ACK/NACK 模式更适配爬虫场景:网页超时、解析异常、网络抖动都算失败,无需区分“业务失败”和“系统失败”。
Workerman 进程模型下,别在 onMessage 里做耗时爬取
很多人把 onMessage 当作入口,直接在里面调 file_get_contents 或 cURL,结果整个事件循环被阻塞,后续消息积压,节点假死。
正确做法是把网络 I/O 转移到子进程或协程(PHP 8.1+ 可用 Swoole\Coroutine\Http\Client),但 Workerman 原生不带协程,所以推荐:
- 用
pcntl_fork()派生子进程处理单个任务,父进程保持监听 - 或改用
exec()调外部脚本(如 Python 爬虫),通过proc_open控制超时与资源限制 - 绝对不要在
onWorkerStart里开无限while(true)+sleep,这会让 Workerman 失去进程管理能力
复杂点在于子进程异常退出后,父进程要捕获 SIGCHLD 并 pcntl_waitpid 清理僵尸进程——这点容易被忽略,跑几天后 ps aux | grep php 会看到一堆 defunct。











