ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Nginx 端口冲突详解:从 Address already in use 报错到彻底解决

Nginx 端口冲突详解:从 Address already in use 报错到彻底解决 1. 读懂这条报错从懵了到哦原来如此第一次见到这条报错的人多半是在刚装完nginx、兴致勃勃敲下启动命令的时候nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)先说结论这不是配置文件写错了也不是nginx装坏了更不需要重装。这句话的意思非常直白——有一个程序比你先占了80端口nginx的socket想绑上去绑不了。1.1 报错发生的时机nginx启动过程中的第二个环节我见过不少同事第一次遇到这个报错时的反应——先检查nginx.conf是不是哪里写错了改来改去发现还是同样的错最后才想到去查端口。其实这条报错给出的信息已经很明确了只是大多数人没耐心拆开看。先回顾一下nginx冷启动时会做什么。它大概经历三个阶段读取并解析配置、根据配置创建socket并尝试绑定端口、然后才fork出worker进程对外服务。bind() to 0.0.0.0:80 failed发生在第二阶段。换句话说你的配置文件语法是没问题的nginx也成功运行到了网络绑定这一步只是这一步没走通。搞清楚这个时机你就不用再去怀疑配置语法可以直奔谁占了端口这个方向。这个细节很重要。因为很多人在网上搜到答案说检查配置文件或者重装nginx照着做纯粹浪费时间。配置文件有问题时nginx一般会在更早的阶段报出unknown directive或者[emerg] invalid parameter这种明确指向语法问题的信息跟bind()半毛钱关系都没有。1.2 拆开看0.0.0.0:80、98、Address already in use到底在说什么先把报错拆成三块。第一块是bind() to 0.0.0.0:80。bind是一个系统调用作用是把一个socket和某个地址端口绑定在一起。这里的0.0.0.0不是某个具体IP而是本机所有IPv4地址的通配写法0.0.0.0:80就是监听所有IPv4网卡上的80端口。之所以显示成0.0.0.0:80是因为你的nginx配置里写的是listen 80;nginx默认会绑定到所有地址。你如果写listen 192.168.1.10:80;报错时这里就会变成具体的IP。第二块是括号里的98。这是Linux的错误码编号对应errno中的EADDRINUSE也就是地址已被使用。每个操作系统对错误码的编号不一定一样Linux上80端口冲突的经典错误码就是98所以很多帖子标题里都带着这个数字。看到98你可以直接确认就是端口占用问题不用再怀疑其他。第三块Address already in use是98的人类可读描述意思直白到不需要解释这个地址端口已经被别的进程占用了。打个比方。你把服务器想象成一家酒店端口就是房间号进程就是客人。一个房间同一时间只能住一位客人这是酒店前台操作系统内核写在规章制度里的。nginx现在拿着80号房间的钥匙去登记入住前台告诉他这间房已经有客人了。于是nginx只好把钥匙退回去整个入住流程就终止了。从这个角度看这个报错其实一点都不神秘它只是内核在严格执行一个端口同一时刻只能被一个进程监听这个基本规则。1.3 一条容易误读的信息为什么提示的是0.0.0.0而不是具体IP有一个细节经常被忽略。如果占用80端口的进程绑定的是0.0.0.0:80那么不管从哪个角度看这台机器上的80端口已经整体被占了你再尝试绑定任何一个具体IP的80端口也一样会失败。反过来如果一个进程只绑定了192.168.1.10:80你倒是可以尝试让nginx监听192.168.1.20:80这种另一个具体IP来绕开冲突——但这种情况在实际服务器上很少见开发环境里偶尔会出现明白原理就好。2. 三分钟锁定占用者ss、netstat、lsof三条命令的完整用法2.1 ss现代Linux发行版的首选一旦确认是端口占用下一步就是找出嫌疑人。现代Linux发行版CentOS 7、Ubuntu 16.04等都自带了ss命令它比老牌的netstat输出更友好速度也更快。ss -tlnp | grep :80我来解释一下参数。-t表示只看TCP连接-l表示只看监听状态的socket-n表示不要解析域名和服务名直接显示数字端口-p表示显示使用这个socket的进程信息。这几个参数组合起来就是看看哪些TCP端口处于监听状态顺便告诉我每个端口是哪个进程在用。输出的典型样子是LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:((nginx,pid12345,fd6))看到最后一段users:((nginx,pid12345,fd6))答案就出来了一个PID为12345的nginx进程正监听在80端口上。如果在你机器上看到的是httpd、apache2、java、node或者docker-proxy那说明80端口被这些服务占了处理方法放在第3章里说。2.2 netstat与lsof老牌工具兜底有些精简环境或者老机器可能没装ss这时候用netstatnetstat -tlnp | grep :80输出格式跟ss大同小异。注意-p参数要显示进程名和PID需要root权限如果你当前用户权限不够这列会显示成-加sudo再看一次就行。lsof是另外一个利器专门用来列出打开的文件。在Linux里socket也是文件的一种所以可以用它按端口反查进程lsof -i:80-i:80的意思是帮我列出所有跟80端口相关的网络连接。输出里能看到COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME这些列COMMAND和PID就是你要找的进程名和进程号。我的建议是三条命令都记住哪个能用用哪个。实际工作中你可能会遇到ss没装、netstat也没装、只有lsof的情况也可能正好反过来。反正最终目的都一样拿到占用80端口的PID和进程名。2.3 看到结果之后怎么判断拿到进程名之后不要急着杀先分清场景。我整理了一个简单的对照表占用进程典型场景处理方向另一个nginx之前手动启动过nginx忘了停或启动脚本重复执行正常停止旧实例再启新的httpd/apache2机器上装了面板类工具或集成环境默认启用了Apache停掉Apache或者让nginx换端口docker-proxyDocker容器映射了宿主机80端口改容器端口映射或先停容器java/node等业务进程业务服务直接监听80和业务方确认挪端口或停服务sshd等意外占用极其少见一般是有进程绑错了地址谨慎核实后再处理关键原则先确认这个进程是不是你预期中的、可以安全停止的服务再动手。杀错进程造成业务中断可比换个端口麻烦多了。3. 分场景处理的完整操作从杀进程到换端口再到Docker排雷3.1 场景A旧nginx进程没退干净这是最常见的一种。比如你之前执行过nginx启动了服务后来又改了配置想重启。如果你用的是nginx -s reload通常不会出问题但如果你在一台机器上装了两套nginx或者systemd拉起的nginx还在运行你又手动执行了一次nginx就会撞车。先用第2章的ss -tlnp | grep :80拿到占用进程的PID。如果是nginx再确认一下是不是你要用的那个ps -ef | grep nginx输出里会有nginx: master process和一堆nginx: worker process。看看master进程的启动路径和命令行参数跟你当前要启动的nginx是否一致。确认无误后优雅停止旧进程nginx -s stop这条命令会让正在运行的nginx以优雅方式退出。如果nginx -s stop提示找不到进程或者你知道PID可以直接用kill加PID让进程收到SIGTERM后自行退出kill 12345这里要特别提一句不要一上来就用pkill -9 nginx。-9是强杀nginx的worker进程正在处理的请求会被直接掐断如果当时有正在写日志或者做代理转发的请求可能留下半截日志或者短暂的服务中断。先用-s stop或者普通kill实在没反应再考虑kill -9。这不是教条是我见过太多因为强杀nginx导致日志文件损坏的案例。停止之后再执行一次ss -tlnp | grep :80确认输出为空80端口腾出来了这时候再启动nginx报错就会消失。3.2 场景B80端口被其他业务占住你又必须用80如果占用80端口的不是nginx而是Apache、Tomcat、Node服务这些你需要根据业务情况做选择。情况一这个服务可以停。那么按它的规范停掉即可比如systemctl stop httpd、systemctl stop tomcat然后再启动nginx。情况二这个服务不能停你还想让nginx监听80。那就只能让nginx换一个端口了。找到nginx配置文件里的server块server { listen 80; server_name example.com; ... }把listen 80;改成listen 8080;或者listen 8443;。改完之后先用nginx -t检查配置语法nginx -t看到nginx: configuration file /etc/nginx/nginx.conf test is successful再重启。改了监听端口之后别忘了防火墙那边要同步放行新端口。CentOS 7用firewall-cmd --add-port8080/tcp --permanent firewall-cmd --reloadUbuntu可以用ufw allow 8080/tcp。这里提醒一句改了端口之后如果你原来有server_name对应的域名解析、反向代理的upstream、或者下游服务调用的是http://域名这种固定地址那下游也得跟着改不然访问就断了。所以换端口看似简单实际牵一发动全身能不动80就尽量不动。3.3 场景CDocker容器或者虚拟机里的服务占了80开发环境里本地虚拟机 多端口nginx 开发环境多站点自定义域名配置这个场景非常典型。很多人用Docker跑了一堆服务容器端口映射到宿主机其中某个容器的映射正好是0.0.0.0:80-80/tcp于是宿主机上的nginx一启动就撞车。这种情况先看容器docker ps找到PORTS列里包含0.0.0.0:80-80/tcp的容器。处理方式有两种。要么把这个容器停掉或者改成其他映射端口再启动要么让nginx避开80端口。比如Docker启动容器时用-p 8080:80把容器的80端口映射到宿主机的8080宿主机上80端口就留给nginx了。虚拟机场景同理。如果你在宿主机上跑了一个VMVM里又装了个nginx或者Apache监听80而且VM用的是桥接网络或者端口转发宿主机上的80端口同样会被占用。排查时ss输出里可能看到进程名是qemu或者其他虚拟化相关进程来源就是虚拟机里的服务。这种场景下我的建议是给整个开发环境做一个端口规划表哪些端口给宿主机nginx哪些端口给容器哪些端口给虚拟机的服务用表格记录下来。开发环境服务一多没有规划表的后果就是今天这个占80、明天那个占8080天天在解决冲突。3.4 Windows环境下的等价排查路径有不少朋友是在Windows上装nginx做本地开发遇到的报错信息几乎一模一样只是错误码可能不是98。Windows上最常见的占用80端口的服务是IIS其次是SQL Server Reporting Services默认占用80偶尔还有老版本的Skype。排查命令换一下netstat -ano | findstr :80注意Windows的netstat不需要加-p进程PID直接在最后一列。拿到PID之后tasklist | findstr PID看到进程名之后用任务管理器结束它或者命令行taskkill /PID PID /FWindows下还有一类特殊情况http.sys内核驱动会占住80端口这个在命令输出里看不到具体的用户态进程名这时可以用netsh http show servicestate查看系统HTTP服务占用情况。不过日常开发环境里遇到最多的还是IIS把IIS停掉基本就能解决。4. 为什么这个坑会反复出现三个容易忽略的复发源4.1 systemd自动拉起和手动启动互相打架明明把占用端口的进程杀了nginx也正常启动了结果下次一重启服务器或者过一会儿又出现同样的报错。这种复活现象十有八九是systemd在搞鬼。如果你用的发行版是用systemd管理服务的而nginx是通过包管理器安装的比如apt安装的nginx自带nginx.service只要这个服务是enabled状态系统启动时就会自动拉起nginx。问题在于很多人在系统启动完成后又手动执行了一次nginx命令。于是systemd拉起的nginx还好好监听在80端口你手动执行的那个nginx当然就绑定失败了。反过来还有一种情况手动启动的nginx正在跑systemd又尝试拉起它管理的nginx两个nginx又撞在一起。排查方法systemctl status nginx看到Active: active (running)说明systemd已经在管它了你就没必要再手动启动。看到Loaded: loaded以及enabled字样说明开机自启是开着的。这种情况下的正解是只用systemctl start nginx、systemctl stop nginx、systemctl restart nginx来管理手动执行nginx只用于临时测试或排查。如果确实不想用systemd先systemctl disable nginx把自启关掉再手动管理。4.2 PID文件残留与多版本nginx并存另一个容易踩的坑是多版本nginx并存。比如系统通过包管理器装了一个nginx你自己又从官网编译安装了一个到/usr/local/nginx两个nginx用的配置路径、PID文件路径都不一样但监听的都是80端口。这种情况下你看到的ss输出里进程名都是nginx很难一眼分辨是哪个版本。我的排查习惯是ps -ef | grep nginx nginx -V /usr/local/nginx/sbin/nginx -V先看进程的启动路径再用不同路径下的nginx -V对比版本。如果发现机器上确实存在两套nginx建议只保留一套。两套并存的后果不仅仅是端口冲突还包括配置混乱、日志分散、排查问题时根本不知道当前流量走的是哪个。还有一种假占用nginx已经退出了但PID文件里还留着旧PID。nginx -s stop就是靠PID文件找进程的如果PID文件里的PID已经被系统回收给了别的进程nginx -s stop会误杀那个新进程。所以当你看到PID文件里的数字和实际进程对不上时先把残留的PID文件删掉再操作。4.3 容易被误解的点TIME_WAIT并不会导致这个报错网上很多文章会把Address already in use和TIME_WAIT状态混为一谈。实际在Linux上nginx默认会设置SO_REUSEADDR这个socket选项所以即使有大量TIME_WAIT状态的连接残留nginx重启时也能正常绑定端口不会报这个错。这个选项的作用就是允许新socket在端口有TIME_WAIT残留时立刻复用。真正该找的原因还是有人在监听。不过有一个变种值得知道如果某个服务自己写了socket绑定逻辑没有设置SO_REUSEADDR那它确实会频繁遇到Address already in use。我见过有些自研程序或第三方组件一重启就报这个错最后查出来就是代码里没设置这个socket选项。如果在nginx领域之外遇到了类似的报错可以考虑往这个方向排查。5. 养成这几个习惯端口冲突从此少一半5.1 启动前检查一把梭我现在每次启动nginx之前都会习惯性地把检查和启动绑在一起做。分享一个小脚本的思路if ss -tln | grep -q :80 ; then echo port 80 is occupied: ss -tlnp | grep :80 exit 1 fi nginx -t nginx先看80端口有没有被监听再测配置语法都通过了才真正启动。这套逻辑看起来简单但真的能省掉很多启动了之后才看到报错的来回折腾。5.2 开发环境端口规划如果你经常在本地或者虚拟机上跑多站点、多服务我的建议是给端口做个约定nginx统一用8080、8081、8082这类高位端口80端口留给真正对外提供服务的正式环境。开发环境的好处是域名可以随便配用/etc/hosts加一行127.0.0.1 dev.example.com然后nginx里配上server_name dev.example.com加listen 8080一样能模拟出正式环境的多站点效果。没必要非在开发环境抢80端口跟自己过不去。5.3 用日志做闭环确认最后说一个我自己的小习惯遇到nginx各种启动问题处理完之后我都会去/var/log/nginx/error.log里看一眼最后几条日志。这个文件会记录nginx每次启动、重载、报错的时间戳和具体错误信息。比如这次端口冲突日志里会有一条对应的bind() to 0.0.0.0:80 failed (98: Address already in use)处理完之后再启动日志里会有一条新的启动成功的记录。看到这个才算真正闭环确认了问题已经解决而不只是好像没报错。踩过这个报错十几次之后我现在的流程很简单先ss -tlnp看占用再根据进程类型决定停掉、换端口还是改容器映射最后用日志确认。不需要背任何复杂的排查步骤只要记住先看清楚再动手这一条端口冲突就真的只是个三分钟的小事了。
返回列表