Connection refused间歇性出现的问题定位
出现Connection refused的问题原因一般有三种:
1. 服务器的端口没有打开 这种直接就是一直会Connection refused,不会间歇出现,可以直接排除;
2. 服务器的防火墙没有开白名单 很多跟外部对接的时候,是需要将公司出口ip加到对方防火墙白名单,这种也会直接Connection refused,不会间歇出现,可以直接排除;
3. 服务器上的backlog设置的太小,导致连接队列满了,服务器可能会报Connection refused,或者Connecttion reset by peer,这个看服务器上的连接队列满时的设置;
详细的异常堆栈信息如下:
看报错方法:
是个native方法,毫不意外。因为是跟第三方云服务商对接,只能让他们查服务器配置的backlog大小(最后通过将backlog从50调到了4096),这里回顾一下tcp三次握手的过程。
正常的发起请求的三次握手如下:
第一步:client 发送syn到server发起握手;
第二步: server收到syn后回复syn + ack 给client;
第三步:client收到syn + ack后,回复server一个ack表示收到server的syn + ack;
Tcp连接详细状态如下图:
1. 服务端调用bind() & listen() 函数后,会监听本地某个端口,例如8080;
2. 客户端发SYN,服务端收到,连接状态变为SYN_RCVD,将连接放到半连接队列syns queue中,同时回复syn+ack给client;
3. 客户端收到syn + ack,回复ack,客户端连接状态变为ESTABLISHED,服务器接收到客户端的ack,先看accept queue是否已满,如果没有满,将连接放到全连接队列,如果满了的话,有二种处理方式:
根据服务端tcp_abort_on_overflow的配置决定,如果配置为0,会丢弃客户端的ack, 过段时间重发syn + ack,也就是三次握手的第二步(如果客户端超时时间设置的太短,就容易引发Connection refused),如果配置为1,会直接返回RET,客户端的表示就是Connection reset by peer。
其中半连接队列的大小看: max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)
上面是我机器上半连接的配置,挺大的,26万。
全连接队列的大小: min(backlog, somaxconn), backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数,不同操作系统不一样。
代码涉及到Socket这一层的操作时,需要自己传backlog的大小,否则默认值是50.
1 2 3 | public ServerSocket( int port, int backlog) throws IOException { this (port, backlog, null ); } |
所有上面Connection Refused很容易因为backlog设置的太小而发生,例如,nginx的配置就有backlog, 默认是511,Tomcat 默认是100。
一般来说,如果是公司自己的服务器,可以通过TCP建连接的时候全连接队列(accept队列)满了,通过一些命令可以查询队列情况:
netstat -s 命令
通过netstat -s | egrep "listen" 看队列的溢出统计数据,多执行几次,看全连接队列overflow次数有没有增长:
ss 命令
上面看Send-Q的值就是listen端口上全连接队列的最大值,Recv-Q就是当前全连接队列用了多少。
netstat跟ss命令一样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数。
因此如果出现间歇性Connection Refused,检查是否有设置backlog, backlog设置的是否过小。
压力测试实践:
服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | public class BaseSocketServer { private ServerSocket server; private Socket socket; private int port; private InputStream inputStream; private static final int MAX_BUFFER_SIZE = 1024 ; public int getPort() { return port; } public void setPort( int port) { this .port = port; } public BaseSocketServer( int port) { this .port = port; } public void runServerMulti() throws IOException { //故意设置backlog就只有10 this .server = new ServerSocket( this .port, 10 ); System.out.println( "base socket server started." ); ExecutorService executorService = Executors.newFixedThreadPool( 10 ); while ( true ){ // the code will block here till the request come. this .socket = server.accept(); Runnable run = () ->{ InputStream inputStream = null ; try { inputStream = this .socket.getInputStream(); byte [] readBytes = new byte [ 1024 ]; int msgLen; StringBuilder stringBuilder = new StringBuilder(); while ((msgLen = inputStream.read(readBytes)) != - 1 ) { stringBuilder.append( new String(readBytes, 0 , msgLen, "UTF-8" )); } System.out.println( "get message from client: " + stringBuilder); } catch (Exception ex){ ex.printStackTrace(); } finally { try { Thread.sleep( 10000 ); inputStream.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }; executorService.submit(run); } //server.close(); } public static void main(String[] args) { BaseSocketServer bs = new BaseSocketServer( 9799 ); try { bs.runServerMulti(); } catch (IOException e) { e.printStackTrace(); } } } |
backlog设置的只有10,这样让客户端连接的时候可以快速队列满。
客户端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public class BaseSocketClient { private String serverHost; private int serverPort; private Socket socket; private OutputStream outputStream; public BaseSocketClient(String host, int port) { this .serverHost = host; this .serverPort = port; } public void connetServer() throws IOException { this .socket = new Socket( this .serverHost, this .serverPort); this .outputStream = socket.getOutputStream(); // why the output stream? } public void sendSingle(String message) throws IOException { try { this .outputStream.write(message.getBytes( "UTF-8" )); } catch (UnsupportedEncodingException e) { System.out.println(e.getMessage()); } this .outputStream.close(); this .socket.close(); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool( 100 ); Runnable run = () ->{ BaseSocketClient bc = new BaseSocketClient( "127.0.0.1" , 9799 ); try { bc.connetServer(); bc.sendSingle(String.format( "%s, 你好, 我是吉米" , Thread.currentThread().getName())); } catch (IOException e) { e.printStackTrace(); } }; for ( int i = 0 ; i < 28 ; i++){ executorService.submit(run); try { Thread.sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
然后ss 可以看到全队列overflow次数马上爆了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现