uWSGI 报 bind(): Address already in use 的排查与彻底解决
uWSGI 报 bind(): Address already in use 的排查与彻底解决
部署 Django 时最常见的一类"卡住":改完配置重启 uWSGI,服务起不来,日志里只有一行 Address already in use。很多人条件反射去 kill -9,但下次还会再遇到。这篇把根因分成四类,逐个给判断依据和解法。
报错 / 症状
uWSGI 启动失败时的典型日志(Linux 上的标准输出形态):
bind(): Address already in use [core/socket.c:769]
如果用的是 HTTP 模式,还可能是:
probably another instance of uWSGI is running on the same address (:8000).
bind(): Address already in use [core/socket.c:230]
对应到 Python 层,本质上就是 bind() 系统调用返回了 EADDRINUSE。下面用标准库把它原样复现一遍。
触发环境
- 生产环境:Ubuntu 22.04 + uWSGI 2.0 + Nginx 1.18.0(Django 4.2 应用)
- 本文复现环境:Windows 11 + Python 3.13.14(脚本仅用标准库,Linux 同样可跑,差异见文末说明)
根因分析
Address already in use 只说明"这个地址我 bind 不上",但成因有四类,处理方式完全不同:
① 旧进程没退干净(最常见)
kill 主进程后 worker 变成孤儿进程继续持有端口;或者用了 --daemonize 启动,你以为停了,其实 pid 文件对不上,进程还在。
② 同一份配置被启动了两次
systemd 管着一个,你手动又 uwsgi --ini 起了一个。systemctl status 显示 running,你还在纳闷为什么端口被占。
③ TIME_WAIT 状态残留
连接关闭后 socket 会在 TIME_WAIT 停留(Linux 默认 60 秒)。此时端口并没有真正被进程持有,但重新 bind 会被拒绝。解法是启用地址复用,而不是干等。
④ 端口真被别的服务占了
比如 8000 被某个调试服务、某个容器的端口映射占用。这种情况改端口或停对方,别硬抢。
判断顺序建议:先查是谁在占,再决定是杀进程还是改配置:
# Linux:看端口持有者(推荐 ss,比 netstat 快)
sudo ss -lntp | grep ':8000'
sudo lsof -i :8000
# 若为 TIME_WAIT 残留,ss 里会看到 State 是 TIME-WAIT 且没有进程名
解决方法
步骤一:用最小代码确认报错就是 bind 冲突
先把问题从"uWSGI 玄学"降维成"一次普通的 bind 失败":
# port_conflict.py —— 复现 "Address already in use",仅标准库
import errno
import socket
PORT = 8899
first = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
first.bind(("127.0.0.1", PORT))
first.listen(1)
print(f"[1] 第一个 socket 已占用 127.0.0.1:{PORT}")
second = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
second.bind(("127.0.0.1", PORT))
except OSError as exc:
print(f"[2] 第二次 bind 失败 -> {type(exc).__name__}: {exc}")
print(f" errno = {exc.errno} (EADDRINUSE={errno.EADDRINUSE})")
print(f" winerror= {getattr(exc, 'winerror', None)}")
finally:
second.close()
first.close()
print("[3] 已释放端口")
实跑输出:
[1] 第一个 socket 已占用 127.0.0.1:8899
[2] 第二次 bind 失败 -> OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
errno = 10048 (EADDRINUSE=10048)
winerror= 10048
[3] 已释放端口
可以看到抛出的就是 OSError,errno 为 EADDRINUSE。平台差异提示:上面是 Windows 实测值(EADDRINUSE = 10048);在 Linux 上同样的代码会得到 errno = 98,报错文案为 [Errno 98] Address already in use——这正是 uWSGI 日志里那一行的来源。
步骤二:按根因处理
对应上面四类:
# ① / ② 旧进程或重复启动:优雅停止,别一上来就 kill -9
sudo systemctl stop uwsgi
pkill -f 'uwsgi.*your_app.ini' # 确认残留
sudo ss -lntp | grep ':8000' # 再确认端口已释放
# 用 uWSGI 自己的 pid 文件停更稳妥
uwsgi --stop /run/uwsgi/your_app.pid
; ③ TIME_WAIT 残留:在 uwsgi ini 里开启地址复用与优雅退出
[uwsgi]
socket = 127.0.0.1:8000
; 允许重用处于 TIME_WAIT 的地址,重启不再等 60 秒
reuse-port = true
; 收到 SIGTERM 时优雅关闭 worker,避免留下孤儿进程
die-on-term = true
vacuum = true
pidfile = /run/uwsgi/your_app.pid
vacuum = true 会在退出时自动清理 socket 文件与 pid 文件,是防止"下次起不来"的关键项,很多教程会漏掉。
# ④ 端口被别的服务占:换端口,同步改 Nginx upstream
sudo ss -lntp | grep ':8000' # 先看清是谁
步骤三:改用 Unix socket,从根上少一类问题
uWSGI 和 Nginx 在同一台机器上时,没必要走 TCP 端口:
[uwsgi]
socket = /run/uwsgi/your_app.sock
chmod-socket = 660
vacuum = true
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/your_app.sock;
}
Unix socket 不占端口,也就不会有端口冲突;配合 vacuum = true,退出时 sock 文件自动清理。代价是只能本机通信——跨机部署仍需 TCP。
预防措施
上线前先探一次端口,比起起服务失败再回头翻日志要省事得多:
# port_probe.py —— 上线前预检端口是否可用,仅标准库
import socket
import sys
def is_port_free(host: str, port: int) -> bool:
"""尝试 bind 一次;能 bind 上说明端口空闲。"""
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
probe.bind((host, port))
return True
except OSError:
return False
finally:
probe.close()
if __name__ == "__main__":
host = "127.0.0.1"
holder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
holder.bind((host, 8899))
holder.listen(1)
for port in (8899, 8900):
state = "空闲,可启动" if is_port_free(host, port) else "已被占用,启动会失败"
print(f"{host}:{port} -> {state}")
holder.close()
sys.exit(0)
实跑输出(脚本自己先占住 8899,再分别探测 8899 与 8900):
127.0.0.1:8899 -> 已被占用,启动会失败
127.0.0.1:8900 -> 空闲,可启动
其余几条习惯:
- 统一用 systemd 管理 uWSGI,不要混用手动
--daemonize,从源头消灭"启动了两次"。 - ini 里固定写上
vacuum = true和pidfile,让退出路径自己收拾干净。 - 重启用
systemctl restart而不是kill -9,kill -9不给进程释放资源的机会,最容易留下孤儿 worker。 - 同机多应用规划好端口段(如 8001/8002/8003)并记录在部署文档里,避免互相抢。
小结:这个报错的解法不是"换个端口绕过去",而是先用 ss -lntp 看清占用者,再按四类根因分别处理;配置层面把 vacuum 和优雅退出补上,它基本就不会再出现了。
本文环境:Django 4.2 / Python 3.11 / Ubuntu 22.04
文中 Python 脚本实跑环境:Windows 11 / Python 3.13.14(仅用标准库,Linux 通用;errno 差异已在文中标注)。uWSGI 日志片段为该报错的标准形态,未在本机复现 uWSGI 服务。