You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express.js使用Throng集群时出现EADDRINUSE端口占用错误

解决Express+Throng集群模式下EADDRINUSE端口占用错误

你遇到的Error: bind EADDRINUSE null:3000确实是多进程抢占同一端口导致的——当前代码中app.listen()在throng()调用之外执行,主进程先启动Express绑定端口,随后Throng创建的每个worker进程又会重复执行这段代码,尝试绑定同一个端口,最终引发冲突。

错误信息

node:events:498
      throw er; // Unhandled 'error' event
      ^

Error: bind EADDRINUSE null:3000
    at listenOnPrimaryHandle (node:net:1397:18)
    at shared (node:internal/cluster/child:149:3)
    at Worker.<anonymous> (node:internal/cluster/child:106:7)
    at process.onInternalMessage (node:internal/cluster/utils:49:5)
    at process.emit (node:events:532:35)
    at emit (node:internal/child_process:938:14)
    at processTicksAndRejections (node:internal/process/task_queues:84:21)
Emitted 'error' event on Server instance at:
    at listenOnPrimaryHandle (node:net:1398:21)
    at shared (node:internal/cluster/child:149:3)
    [... lines matching original stack trace ...]
    at processTicksAndRejections (node:internal/process/task_queues:84:21) {
  errno: -4091,
  code: 'EADDRINUSE',
  syscall: 'bind',
  address: null,
  port: 3000
}

修复方案

方案1:让Worker共享HTTP端口

将Express初始化、路由定义、服务启动全部移入Throng的start函数,Node集群模式会自动将端口连接分发给各个Worker,避免重复绑定:

const express = require('express')
const Queue = require('bull')
const throng = require('throng')
const {getScrapedTikTokData} = require('./local')
const {Webhook} = require('discord-webhook-node')
const hook = new Webhook(process.env.WEBHOOK_CHANNEL || '')

const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379'
const workers = process.env.WEB_CONCURRENCY || 1
const maxJobsPerWorker = 50

function start(id) {
  // 每个Worker创建独立的Express实例
  const app = express()
  const workQueue = new Queue('work', REDIS_URL, {
    defaultJobOptions: {
      removeOnComplete: true,
      removeOnFail: true
    }
  })

  // 定义路由
  app.get('/discord', async (req, res) => {
    const url = req.query.url
    console.log(`Worker ${id} 接收请求:${url}`)
    let job = await workQueue.add({url: url})
    res.json({id: job.id, workerId: id})
  })

  // 处理队列任务
  workQueue.process(maxJobsPerWorker, async job => {
    const url = job.data.url
    console.log(`Worker ${id} 处理任务:${url}`)
    await getScrapedTikTokData(url)
    return {value: 'working'}
  })

  // 监听任务完成事件
  workQueue.on('global:completed', async (jobId, result) => {
    console.log(`Worker ${id}:任务 ${jobId} 完成,结果:${result}`)
    try {
      await hook.send(result)
    } catch (e) {
      console.log(e.message)
    }
  })

  // 启动服务
  app.listen(process.env.PORT || 3000, () => {
    console.log(`Worker ${id} 运行在端口 ${process.env.PORT || 3000}`)
  })
}

throng({workers, start})

方案2:主进程处理HTTP,Worker专注队列任务

更高效的方式是让主进程单独启动Express接收请求,Worker进程仅负责处理队列任务,彻底避免端口冲突:

const express = require('express')
const Queue = require('bull')
const throng = require('throng')
const {getScrapedTikTokData} = require('./local')
const {Webhook} = require('discord-webhook-node')
const hook = new Webhook(process.env.WEBHOOK_CHANNEL || '')

const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379'
const workers = process.env.WEB_CONCURRENCY || 1
const maxJobsPerWorker = 50

// 主进程:负责接收HTTP请求、管理队列事件
function startMaster() {
  const app = express()
  const workQueue = new Queue('work', REDIS_URL, {
    defaultJobOptions: {
      removeOnComplete: true,
      removeOnFail: true
    }
  })

  app.get('/discord', async (req, res) => {
    const url = req.query.url
    console.log(`主进程接收请求:${url}`)
    let job = await workQueue.add({url: url})
    res.json({id: job.id})
  })

  workQueue.on('global:completed', async (jobId, result) => {
    console.log(`任务 ${jobId} 完成,结果:${result}`)
    try {
      await hook.send(result)
    } catch (e) {
      console.log(e.message)
    }
  })

  app.listen(process.env.PORT || 3000, () => {
    console.log(`主进程服务运行在端口 ${process.env.PORT || 3000}`)
  })
}

// Worker进程:仅处理队列任务
function startWorker(id) {
  const workQueue = new Queue('work', REDIS_URL)

  workQueue.process(maxJobsPerWorker, async job => {
    const url = job.data.url
    console.log(`Worker ${id} 处理任务:${url}`)
    await getScrapedTikTokData(url)
    return {value: 'working'}
  })

  console.log(`Worker ${id} 启动,等待处理任务`)
}

throng({
  workers,
  master: startMaster,
  start: startWorker
})

修改核心点

  • 移除全局作用域的app和workQueue实例,避免Worker重复初始化资源
  • 将HTTP服务启动逻辑限制在单个进程(主进程)或让Worker通过Node集群机制共享端口
  • 拆分职责,让主进程专注请求分发,Worker专注任务处理,提升资源利用率

内容的提问来源于stack exchange,提问作者Alaa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 21:21:11