4、常见Socket错误码:EADDRINUSE、ECONNREFUSED、ETIMEDOUT、ECONNRESET

Socket 错误码,说白了就是操作系统给应用层递的小纸条。上面写着:「你刚才那个连接请求,我没办成,原因是这个。」

我干了这么多年 SRE,见过太多人一看到这些错误码就慌。其实不用怕。每个错误码背后,都对应着一个非常具体的网络状态。你读懂了它,就知道该往哪个方向查。

这一章,咱们就把生产环境里最要命的四个错误码掰开揉碎。EADDRINUSE、ECONNREFUSED、ETIMEDOUT、ECONNRESET。这四个货,占了线上 Socket 问题的八成以上。

核心原则: 错误码是内核给你的第一手线索。不要只看应用日志里的「Connection refused」就重启,先看 errno 是什么。

4.1 EADDRINUSE —— 端口被占用了

这个错误的意思是:你试图绑定的那个 IP:端口,已经被别的进程占用了。

我遇到过最典型的一次:某 Java 应用重启失败,报 EADDRINUSE。开发说「我明明 kill 了进程啊」。结果一查,lsof -i :8080 显示进程还在,只是变成了僵尸状态,端口没释放。

为什么会发生?

  • 服务崩溃后快速重启:旧进程的 TIME_WAIT 状态还没结束,新进程就想 bind 同一个端口。
  • 同一个端口被两个进程同时 bind:配置错误,或者启动脚本写岔了。
  • SO_REUSEADDR 没设置:对于 TCP,如果不设置这个 socket 选项,TIME_WAIT 期间端口就是不可重用的。

怎么查?

# 查看哪个进程占用了端口
lsof -i :8080

# 或者用 ss 更高效
ss -tlnp | grep 8080

# 查看 TIME_WAIT 状态的连接
ss -tan state time-wait
我的习惯: 生产环境的服务启动脚本里,一定要加上 SO_REUSEADDR。这样即使旧连接还在 TIME_WAIT,新进程也能立刻 bind 上去。别小看这一行配置,它能省掉你很多「重启失败」的麻烦。

避坑指南

  • 别用 kill -9 杀进程,尽量用 SIGTERM 让进程优雅关闭,释放端口。
  • 如果端口被 systemd 或 docker 占用,光看 lsof 可能不够,试试 fuser 8080/tcp。

4.2 ECONNREFUSED —— 对面没人听

这个错误最直白:你发了个 SYN 过去,对面直接回了 RST。意思就是「我没开这个端口,别打了」。

我记得有一次线上告警,说支付服务连不上数据库。我一看错误码,ECONNREFUSED。第一反应不是看应用配置,而是先 telnet 了一下数据库端口。果然,端口不通。再查,数据库实例因为磁盘满被 OOM kill 了。

常见原因

场景 说明
目标端口没开 服务没启动,或者启动后崩溃了
防火墙拦截 iptables / security group 把 SYN 包丢了,但回了 RST
IP 地址不对 你连到了错误的 IP 上,那台机器根本没跑这个服务
连接数满了 某些老系统 backlog 满了会直接回 RST,而不是丢包

排查步骤

  1. 先确认目标进程活着没:curl -v telnet://target:port 或者 nc -zv target port。
  2. 再确认网络通不通:ping 能通不代表端口通,但 ping 不通肯定有问题。
  3. 最后看防火墙:iptables -L -n 检查是否有 DROP 或 REJECT 规则。
注意: ECONNREFUSED 和超时不一样。超时是包丢了没回应,ECONNREFUSED 是对方明确告诉你「我不接」。前者可能是网络问题,后者基本是服务端的问题。

4.3 ETIMEDOUT —— 连接超时了

这个错误最让人头疼。因为原因太多了。

ETIMEDOUT 的意思是:你发了 SYN,对面没回 SYN-ACK,也没回 RST。就这么晾着你,直到内核的 tcp_syn_retries 重试完,然后告诉你「算了,不等了」。

可能的原因

  • 网络丢包:中间某个路由器把你的 SYN 包丢了。
  • 防火墙静默丢包:防火墙直接 DROP 了包,不回 RST。
  • 目标服务器负载太高:内核来不及处理 SYN 包,backlog 队列满了,直接丢包。
  • 目标服务器挂了:机器宕机,但交换机还在,所以没有 ICMP 不可达。

怎么区分?

我个人习惯用 tcpdump 抓包看。如果只看到 SYN 发出去,没有 SYN-ACK 回来,那就是中间丢了。如果看到 SYN 发了多次,那就是重试超时。

# 抓包看三次握手
tcpdump -i eth0 host target_ip and port 8080 -nn

# 调整内核超时参数(谨慎!)
# 默认重试 6 次,总超时约 127 秒
sysctl net.ipv4.tcp_syn_retries
# 内网环境可以改小一点
sysctl -w net.ipv4.tcp_syn_retries=3
避坑: 别把应用层的超时设得比内核的 tcp_syn_retries 还短。否则应用层先超时了,内核还在重试,两边状态就乱了。我见过一个案例,应用超时设了 5 秒,内核默认 127 秒,结果连接池里一堆「半连接」。

4.4 ECONNRESET —— 连接被重置了

这个错误最阴险。它通常发生在连接已经建立之后。

ECONNRESET 的意思是:对方突然给你发了个 RST 包,把连接掐断了。你这边 read 或 write 的时候,内核告诉你「兄弟,那边已经翻脸了」。

典型场景

场景 原因
对方进程崩溃 进程挂了,内核自动发 RST 给所有连接
对方主动关闭了连接 但你的应用还在读/写,触发了 RST
TCP keepalive 检测到死连接 对端机器宕机,内核发 RST 清理连接
写到一个已经关闭的连接 你这边 write 了数据,但对方已经 close 了

我踩过的坑

有一次,一个 Nginx 代理频繁报 ECONNRESET。查了半天,发现是后端 Tomcat 的 keepalive_timeout 设得太短。Tomcat 主动关了连接,但 Nginx 不知道,还在复用那个连接发请求。结果就是 RST。

解决方案很简单:把两边的超时时间对齐,或者让 Nginx 用 proxy_http_version 1.1 配合 proxy_set_header Connection ""。

怎么处理?

  • 如果是读的时候报错:说明对端已经关了,你这边直接 close 就行,别重试。
  • 如果是写的时候报错:说明你写了一个已经挂了的连接。检查连接池,把无效连接清理掉。
  • 如果是频繁出现:抓包看 RST 是谁发的。如果是负载均衡发的,检查健康检查配置。如果是后端发的,检查进程稳定性。
重要: ECONNRESET 和 EOF 不一样。EOF 是对方优雅关闭,read 返回 0。ECONNRESET 是暴力关闭,read 直接抛异常。你的代码里一定要区分这两种情况。

知识体系总览

下面这张图,把这四个错误码在整个连接生命周期里的位置画清楚了。你一看就知道,每个错误对应哪个阶段。

Socket 错误码生命周期图 Socket 连接生命周期与常见错误码 1. Bind 阶段 绑定 IP:端口 EADDRINUSE 2. Connect 阶段 三次握手 ECONNREFUSED ETIMEDOUT 3. 已建立连接 读写数据 ECONNRESET 说明: • EADDRINUSE 发生在服务端 Bind 阶段,端口被占用。 • ECONNREFUSED / ETIMEDOUT 发生在客户端 Connect 阶段,服务端没响应或拒绝。 • ECONNRESET 发生在连接已建立后,对端突然断开。 排查建议: 1. 先看错误码,确定是哪个阶段的问题。 2. 用 ss / lsof 确认端口和进程状态。 3. 用 tcpdump 抓包确认网络行为。 4. 检查防火墙、内核参数、应用超时配置。

小结

这四个错误码,你记住了它们的「性格」就好办了:

  • EADDRINUSE:端口冲突,查进程。
  • ECONNREFUSED:端口没开,查服务。
  • ETIMEDOUT:包丢了,查网络。
  • ECONNRESET:连接被掐,查对端。

嗯,下次线上再报这些错,别急着重启。先看一眼 errno,你就知道该往哪个方向走了。