4、常见Socket错误码:EADDRINUSE、ECONNREFUSED、ETIMEDOUT、ECONNRESET
Socket 错误码,说白了就是操作系统给应用层递的小纸条。上面写着:「你刚才那个连接请求,我没办成,原因是这个。」
我干了这么多年 SRE,见过太多人一看到这些错误码就慌。其实不用怕。每个错误码背后,都对应着一个非常具体的网络状态。你读懂了它,就知道该往哪个方向查。
这一章,咱们就把生产环境里最要命的四个错误码掰开揉碎。EADDRINUSE、ECONNREFUSED、ETIMEDOUT、ECONNRESET。这四个货,占了线上 Socket 问题的八成以上。
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,而不是丢包 |
排查步骤
- 先确认目标进程活着没:
curl -v telnet://target:port或者nc -zv target port。 - 再确认网络通不通:
ping能通不代表端口通,但 ping 不通肯定有问题。 - 最后看防火墙:
iptables -L -n检查是否有 DROP 或 REJECT 规则。
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
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 是谁发的。如果是负载均衡发的,检查健康检查配置。如果是后端发的,检查进程稳定性。
知识体系总览
下面这张图,把这四个错误码在整个连接生命周期里的位置画清楚了。你一看就知道,每个错误对应哪个阶段。
小结
这四个错误码,你记住了它们的「性格」就好办了:
- EADDRINUSE:端口冲突,查进程。
- ECONNREFUSED:端口没开,查服务。
- ETIMEDOUT:包丢了,查网络。
- ECONNRESET:连接被掐,查对端。
嗯,下次线上再报这些错,别急着重启。先看一眼 errno,你就知道该往哪个方向走了。