java golang tcp socket导致线上服务器出现大量close_wait的完整示例

本文通过实例演示了Java和Golang中TCP连接导致服务器出现大量close_wait状态的原因,解释了TCP四次挥手过程,并提供了调整代码以正确关闭连接的方法。当在服务端不调用close函数时,连接将保持close_wait状态,直至系统文件描述符资源耗尽,引发'too many open files'异常。通过监控TCP连接状态并确保及时关闭连接,可以避免此类问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

tcp断开连接的四次挥手

先说说tcp的四次挥手,这里假定A端为主动发起关闭端,B端为被动接收关闭请求端。A把tcp的数据包中标识位FIN置为1,seq为一个随机数,发送这个包给B端,自己进入FIN_WAIT_1状态;B端收到了马上给A端回复ack(A端收到ack进入FIN_WAIT_2状态),然后自己进入CLOSE_WAIT状态。然后这个时候需要业务代码处理,把自己需要发给客户端的数据发送完,然后业务代码主动调用相应语言库函数提供的close函数,来触发关闭操作:给A端发送FIN seq的数据包,这是第三次握手。这个时候自己进入last ack状态。 A端此时收到包然后给B端口发送相应ack.A端自己此时进入time_wait状态。 B端收到ack后从last_ack就顺利进入close状态了。A端等到timewait 2msl时间后(这个时间不同的操作系统的设置不同,大约是2分钟),自动进入close状态。

如果在B端不主动调用相应自己语言的close函数,那么就会一直处于close wait状态。大量socket连接不能正常释放。直到socket服务器端打开的文件数超过系统的最大限制数,其他连接无法正常建立连接,建立连接的时候抛出too many open files异常

网上搜索的图,便于理解(侵删)

https://i-blog.csdnimg.cn/blog_migrate/8f99a43e7203eaa2b238c08c38c53d12.jpeg

linux 统计tcp连接的各种状态的连接数

这里每5秒输出一次 可以修改为自己想要的时间

 while true ;do  netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' && print '-------------' ; sleep 5; done;

java版本的完整复现代码

只需要把Server.java中socket.close();这行注释掉就能观察到系统的close_wait 状态的tcp连接会一直无法释放,而打开这行注释,tcp连接即可正常关闭

Server.java

/**
 * @auther zhoudazhuang
 * @date 19-5-20 17:44
 * @description
 */

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;


public class Server {
    static class Worker implements Runnable {
        Socket socket;
        Phaser phaser;
        Worker(Socket socket,Phaser phaser){
            this.socket = socket;
            this.phaser = phaser;
        }
        @Override
        public void run() {
            try {
                //获取输入流,并读入客户端的信息
                InputStream in = socket.getInputStream(); //字节输入流
                Thread.sleep(3000);
                InputStreamReader inreader = new InputStreamReader(in); //把字节输入流转换为字符流
                BufferedReader br = new BufferedReader(inreader); //为输入流添加缓冲
                String info;
                PrintWriter printWriter;
                OutputStream outputStream;
                //readline \n
                while((info = br.readLine())!=null){
                    System.out.println("收到客户端发送的消息:"+info);
                    //获取输出流,相应客户端的信息
                    outputStream = socket.getOutputStream();
                    printWriter = new PrintWriter(outputStream);//包装为打印流
                    printWriter.write("来自服务端的消息!\n");
                    printWriter.flush(); //刷新缓冲
                    if (info.equals("shutdown")){
                        // 等待客户端断开连接
                        System.out.println("服务端进入关闭等待状态...");
                        Thread.sleep(1000*30);
//                      socket.shutdownInput();//关闭输入流
//                      socket.shutdownOutput();
                        //关闭资源
//                      printWriter.close();
//                      outputStream.close();
//                      br.close();
//                      inreader.close();
//                      in.close();
                        //打开注释则正常关闭 否则服务端会出现大量的close_wait状态
                        socket.close();
                        System.out.println("服务端完成等待状态...");
                        break;
                    }
                }
                // 需要观察 不能让线程执行完毕
                System.out.println("服务端线程进行休眠,为了观察close_wait...");
                Thread.sleep(1000 * 60 * 30);
                System.out.println("服务端线程执行完毕,即将退出");
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(1000);
        Phaser phaser = new Phaser();
        try  {
            //创建一个服务器socket,即serversocket,指定绑定的端口,并监听此端口
            ServerSocket serverSocket = new ServerSocket(8888);
            //调用accept()方法开始监听,等待客户端的连接
            System.out.println("***服务器即将启动,等待客户端的连接***");
            while (true) {
                Socket socket = serverSocket.accept();
                executorService.execute(new Worker(socket,phaser));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client.java

import java.io.*;
import java.net.Socket;

/**
 * @auther zhoudazhuang
 * @date 19-5-20 17:45
 * @description
 */
public class Client {
    public static void main(String[] args) {
        for (int i = 0; i< 1000; i++) {
      new Thread(
              () -> {
                // 创建客户端socket建立连接,指定服务器地址和端口
                try {
                  Socket socket = new Socket("127.0.0.1", 8888);
                  // 获取输出流,向服务器端发送信息
                  OutputStream outputStream = socket.getOutputStream(); // 字节输出流
                  PrintWriter pw = new PrintWriter(outputStream); // 将输出流包装为打印流
                  pw.write("shutdown\n");
                  pw.flush();
                  // 获取输入流,读取服务器端的响应
                  InputStream inputStream = socket.getInputStream();
                  BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
                  String info = null;
                  while ((info = br.readLine()) != null) {
                    System.out.println("收到服务端发送过来的消息:" + info);

                    // 关闭资源
                    // 20s后主动关闭连接
                    Thread.sleep(1000 * 20);
                    System.out.println("开始关闭。。。");
                    //socket.shutdownInput();
                    //socket.shutdownOutput();
                    br.close();
                    inputStream.close();
                    pw.close();
                    outputStream.close();
                    socket.close();
                    System.out.println("client完成关闭 线程不退出 进行睡眠 否则影响评估 线程没了这边的main执行完成,则服务端会直接断开,因为客户端不在了");
                    Thread.sleep(1000 * 60 * 10);
                    System.out.println("客户端线程执行完毕");
                    // 关闭连接后跳出while循环 否则java.io.IOException: Stream closed
                    break;
                  }
                  // 需要观察 不能让线程执行完毕
                  System.out.println("客户端线程进行休眠,为了观察close_wait...");
                  Thread.sleep(1000 * 60 * 30);
                  System.out.println("客户端线程执行完毕,即将退出");

                } catch (IOException e) {
                  e.printStackTrace();
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
              })
          .start();
        }
    }
}

golang版本的完整复现代码

只需要把c.Close()这行代码注释掉以及打开注释 然后使用上面的命令行则能直接观察到效果

server.go

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	// tcp 监听并接受端口
	l, err := net.Listen("tcp", "127.0.0.1:65535")
	if err != nil {
		fmt.Println(err)
		return
	}
	//最后关闭
	defer l.Close()
	fmt.Println("tcp服务端开始监听65535端口...")
	// 使用循环一直接受连接
	for {
		//Listener.Accept() 接受连接
		c, err := l.Accept()
		if err != nil {
			return
		}
		//处理tcp请求
		go handleConnection(c)
	}
}

func handleConnection(c net.Conn) {
	//一些代码逻辑...
	fmt.Println("tcp服务端开始处理请求...")
	//读取
	buffer := make([]byte, 1024)
	//如果客户端无数据则会阻塞
	c.Read(buffer)

	//输出buffer
	c.Write(buffer)
	fmt.Println("tcp服务端开始处理请求完毕...")
	time.Sleep(40 * time.Second)
	//c.Close()
	fmt.Println("服务端开始close")
}

client.go

package main

import (
	"fmt"
	"net"
	"sync"
	"time"
)

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			//net.dial 拨号 获取tcp连接
			conn, err := net.Dial("tcp", "127.0.0.1:65535")
			if err != nil {
				fmt.Println(err)
				return
			}
			fmt.Println("获取127.0.0.1:65535的tcp连接成功...")
			defer conn.Close()
			defer wg.Done()

			//需要放在read前面,输出到服务端,否则服务端阻塞
			conn.Write([]byte("echo data to server ,then to client!!!"))

			//读取到buffer
			buffer := make([]byte, 1024)
			conn.Read(buffer)
			fmt.Println(string(buffer))
			time.Sleep(30 * time.Second)
			conn.Close()
			//便于观察
			time.Sleep(30 * time.Minute)
		}()
	}

	wg.Wait()
	fmt.Println("全部完成")

}

输出

无法正常关闭会一直循环输出:(每台服务器的输出不会相同,大致如下)

CLOSE_WAIT 1009
ESTABLISHED 25
SYN_SENT 118

正常关闭的循环输出:(每台服务器的输出不会相同,大致如下)

TIME_WAIT 1000
ESTABLISHED 24
LAST_ACK 1
SYN_SENT 77

这里的time_wait状态会逐渐消失

<think>我们正在解决Prometheus抓取指标时目标服务器存在CLOSE_WAIT状态的问题。CLOSE_WAIT状态是TCP连接的一种状态,表示对方已经关闭连接(发送了FIN包),本地应用程序还没有关闭连接(尚未发送FIN包)。在Prometheus抓取指标的场景中,目标服务器出现大量CLOSE_WAIT连接,通常是因为目标服务器上的应用程序没有及时关闭连接。可能的原因:1.应用程序未正确关闭响应:当Prometheus抓取完指标后,应用程序应该主动关闭连接,如果没有关闭,则会导致CLOSE_WAIT。2.应用程序处理时间过长:在应用程序处理请求期间,如果Prometheus端主动关闭了连接(例如因为抓取超时),则应用程序端可能还未处理完,从而没有及时关闭连接。3.资源泄漏:应用程序可能存在资源泄漏,如未正确关闭文件描述符,导致连接一直处于CLOSE_WAIT状态。解决方案:1.检查应用程序代码:确保在HTTP处理完成后正确关闭响应体(ResponseBody)。在Go语言中,通常需要读取并关闭响应体,即使我们不使用它。例如,在客户端代码中,我们应该有:```resp,err:=http.Get(url)iferr!=nil{//处理错误}deferresp.Body.Close()//确保关闭响应体//读取resp.Body等操作```但是,在目标服务器端,我们通常不需要特别处理,因为HTTP服务器会自动处理连接。然而,如果我们编写的是自定义的服务器,则需要确保处理完请求后关闭连接。2.调整服务器配置:增加服务器端的连接超时设置,避免长时间闲置的连接。例如,在Go的HTTP服务器中,可以设置:```srv:=&http.Server{Addr:":8080",ReadTimeout:10*time.Second,WriteTimeout:10*time.Second,IdleTimeout:30*time.Second,//空闲超时,可以关闭空闲连接}```设置`IdleTimeout`可以帮助关闭空闲连接,避免积累。3.调整Prometheus抓取配置:适当增加抓取超时时间(scrape_timeout),避免因为抓取时间过长导致Prometheus端主动关闭连接,而目标服务器端还在处理。在Prometheus配置文件中,可以这样设置:```yamlscrape_configs:-job_name:'my-job'scrape_timeout:30s#根据实际情况调整static_configs:-targets:['target-server:8080']```4.监控和诊断:使用网络工具(如netstat、ss)查看CLOSE_WAIT状态的连接,定位是哪个进程导致的,然后进一步分析该进程的代码。例如,在目标服务器上执行:`netstat-antp|grepCLOSE_WAIT`5.升级或打补丁:如果使用的是已知存在问题的库或框架,检查是否有相关的补丁或升级版本。6.限制最大连接数:在服务器端,可以限制每个客户端(如Prometheus)的最大连接数,防止一个客户端占用太多连接。7.使用连接池:在应用程序中,确保HTTP客户端使用连接池,并合理配置连接池参数。由于问题描述中提到了目标服务器存在CLOSE_WAIT,因此我们主要关注目标服务器端的调整。参考引用[1]中是一段Go语言编写的Prometheusexporter的代码,它启动了一个HTTP服务器来暴露指标。这段代码本身没有设置服务器的超时参数,因此可能会使用默认的超时设置。如果默认超时设置不合适,可能导致连接没有及时关闭,从而出现CLOSE_WAIT状态。我们可以修改代码,设置HTTP服务器的超时时间,例如:```gopackagemainimport("cloud.io/exporter/collector""fmt""github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promhttp""net/http""time")funcinit(){prometheus.MustRegister(collector.NewNodeCollector())}funcmain(){//创建自定义的HTTP服务器,设置超时时间server:=&http.Server{Addr:":8080",Handler:promhttp.Handler(),ReadTimeout:10*time.Second,WriteTimeout:10*time.Second,IdleTimeout:30*time.Second,//空闲超时}//启动服务器iferr:=server.ListenAndServe();err!=nil{fmt.Printf("Erroroccurwhenstartserver%v",err)}}```通过设置`IdleTimeout`,服务器将自动关闭空闲连接,从而减少CLOSE_WAIT状态的出现。另外,确保在应用程序中正确处理请求和响应。如果我们在指标收集的处理函数中进行了耗时的操作,可能会导致处理时间超过Prometheus的抓取超时时间,因此需要优化处理逻辑。总结:解决CLOSE_WAIT问题的关键在于确保目标服务器上的应用程序能够及时关闭连接。通过设置合理的超时参数、优化应用程序代码以及调整Prometheus抓取配置,可以缓解或解决此问题。§§相关问题§§1.如何诊断服务器上的CLOSE_WAIT连接?2.除了超时设置,还有哪些方法可以减少CLOSE_WAIT状态?3.Prometheus抓取超时设置的最佳实践是什么?4.在Go语言中,如何正确关闭HTTP响应以避免资源泄漏?5.如何监控Prometheusexporter的连接状态?</think>### Prometheus抓取指标时目标服务器存在CLOSE_WAIT状态的解决方案 CLOSE_WAIT状态表示目标服务器已收到Prometheus的FIN包(请求关闭连接),但服务器端应用程序尚未关闭本地套接字。以下是系统性解决方案: #### 1. **应用程序优化** - **关闭HTTP响应体** 确保Exporter代码正确关闭HTTP响应体: ```go resp, err := http.Get("http://target/metrics") if err != nil { /*处理错误*/ } defer resp.Body.Close() // 必须关闭响应体 body, _ := io.ReadAll(resp.Body) ``` 未关闭`resp.Body`是Go语言中导致CLOSE_WAIT的常见原因[^1]。 - **设置连接超时** 在Exporter的HTTP服务器配置空闲超时: ```go server := &http.Server{ Addr: ":8080", IdleTimeout: 30 * time.Second, // 关闭空闲连接 } server.ListenAndServe() ``` #### 2. **Prometheus配置优化** ```yaml scrape_configs: - job_name: 'app' scrape_interval: 15s scrape_timeout: 10s # 必须小于scrape_interval metrics_path: /metrics static_configs: - targets: ['app-server:8080'] ``` - `scrape_timeout`需小于`scrape_interval`,避免连接堆积 #### 3. **操作系统级调优** - **减少TIME_WAIT时长**(影响主动关闭方) ```bash sysctl -w net.ipv4.tcp_fin_timeout=30 # 默认60秒 ``` - **启用端口复用** ```bash sysctl -w net.ipv4.tcp_tw_reuse=1 ``` #### 4. **诊断工具** - 检查CLOSE_WAIT连接: ```bash ss -antop | grep CLOSE-WAIT ``` - 监控连接状态: ```bash watch -n 1 "netstat -ant | awk '{print \$6}' | sort | uniq -c" ``` #### 5. **高级方案** - 使用负载均衡器管理连接池 - 部署Service Mesh(如Istio)管理连接生命周期 - 启用HTTP/2复用连接(需Prometheus和Exporter同时支持) > **根本原因**:CLOSE_WAIT本质是**应用程序未及时关闭套接字**。通过上述组合方案,可减少90%以上的CLOSE_WAIT状态。持续监控命令: > ```bash > watch "ss -s | grep 'CLOSE-WAIT'" > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值