跳转至

Linux 网络栈基础

学习目标

学完本章后,学习者应该能够:

  1. 理解 socket、监听端口、连接队列和缓冲区。
  2. 解释 TCP 连接在内核中的基本生命周期。
  3. 理解 listen backlog、accept queue、send buffer、receive buffer。
  4. 能初步分析 TIME_WAIT、CLOSE_WAIT、端口耗尽等问题。

socket

socket 是应用程序进行网络通信的接口。Go 的 net.Listenhttp.ListenAndServenet.Dial 底层都离不开 socket。

服务端常见流程:

socket -> bind -> listen -> accept -> read/write -> close

客户端常见流程:

socket -> connect -> read/write -> close

listen backlog 与 accept queue

服务端监听端口后,内核会维护连接队列。简化理解:

  • 半连接队列:三次握手尚未完全完成。
  • accept queue:握手完成,等待应用程序 accept。

如果应用程序 accept 太慢,队列可能积压甚至溢出,客户端可能连接超时或被拒绝。

send buffer 与 receive buffer

TCP socket 有发送缓冲区和接收缓冲区。

  • send buffer:应用写入的数据先进入发送缓冲区,再由内核协议栈发送。
  • receive buffer:网卡收到的数据进入接收缓冲区,应用再读取。

如果应用读得慢,接收缓冲区可能堆积;如果对端或网络慢,发送缓冲区可能堆积。

TCP 状态与常见问题

常见状态:

  • LISTEN:服务端监听中。
  • ESTABLISHED:连接已建立。
  • TIME_WAIT:主动关闭方等待旧包消失。
  • CLOSE_WAIT:对端已关闭,本端应用还没关闭。

TIME_WAIT 多不一定是问题;CLOSE_WAIT 持续增多通常说明应用没有正确关闭连接。

端口耗尽

客户端发起大量短连接时,需要使用本地临时端口。如果短时间内连接过多,临时端口可能耗尽。

常见原因:

  • HTTP Client 没有复用连接。
  • 响应体没有关闭。
  • 大量短连接调用下游服务。
  • TIME_WAIT 积压严重。

Go 后端实际应用例子

例子一:正确关闭 HTTP 响应体

Go HTTP Client 必须关闭响应体,否则连接无法复用,还可能导致连接泄漏。

func Fetch(ctx context.Context, client *http.Client, url string) error {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return err
    }

    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    _, err = io.Copy(io.Discard, resp.Body)
    return err
}

如果不读取或关闭 resp.Body,连接池复用会受到影响。

例子二:配置 HTTP Client 超时和连接池

client := &http.Client{
    Timeout: 3 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 20,
        IdleConnTimeout:     90 * time.Second,
    },
}

不设置超时可能导致请求长时间挂住;不理解连接池可能导致短连接过多或连接不够用。

常见误区

  • 误区一:TIME_WAIT 一定是异常。

TIME_WAIT 是 TCP 正常状态,问题在于数量是否异常、是否导致端口耗尽或资源压力。

  • 误区二:CLOSE_WAIT 可以靠调内核参数解决。

CLOSE_WAIT 通常是应用没有关闭连接,根因在代码或连接生命周期管理。

  • 误区三:HTTP Client 默认配置适合所有场景。

生产服务应根据调用模式配置超时、连接池、空闲连接和重试策略。

线上问题案例

某服务调用第三方 API 时没有关闭 resp.Body。上线后 CLOSE_WAIT 持续增长,最终文件描述符耗尽,新请求开始失败。

修复方式是确保所有成功返回响应的路径都关闭 body,并对外部调用设置超时;同时增加连接状态、文件描述符数量和错误率监控。

实战任务

排查一个“服务连接越来越多”的问题:

  1. 查看进程的 TCP 连接状态。
  2. 区分 TIME_WAITCLOSE_WAIT
  3. 检查代码是否关闭响应体或连接。
  4. 检查 HTTP Client 是否配置超时。
netstat -ano
ss -antp
lsof -i -p <pid>
参考答案

如果 TIME_WAIT 多,先判断是否大量短连接、连接复用不足或主动关闭过多。如果 CLOSE_WAIT 持续增长,重点检查应用是否关闭连接,例如 Go HTTP Client 是否 defer resp.Body.Close()

还要检查外部调用是否设置超时,连接池参数是否合理,错误路径是否遗漏关闭,监控中是否同时出现文件描述符上涨和 too many open files

面试题

1. listen backlog 和 accept queue 有什么作用?

参考答案

服务端监听端口后,内核需要维护连接队列。backlog 相关参数影响已完成或未完成握手的连接排队能力。accept queue 中的连接已经完成握手,等待应用程序调用 accept。

如果应用 accept 太慢或队列太小,连接可能被拒绝或超时。

2. TIME_WAIT 和 CLOSE_WAIT 有什么区别?

参考答案

TIME_WAIT 通常出现在主动关闭连接的一方,用于等待网络中的旧包消失,是 TCP 正常机制。CLOSE_WAIT 表示对端已经关闭连接,但本端应用还没有关闭。

TIME_WAIT 多要结合短连接和端口资源判断;CLOSE_WAIT 持续增长通常是应用连接关闭逻辑有问题。

3. Go HTTP Client 为什么要关闭 resp.Body?

参考答案

resp.Body 关联底层连接。关闭 body 可以释放资源,并让连接有机会被连接池复用。如果不关闭,连接可能泄漏,导致文件描述符上涨、连接池耗尽、CLOSE_WAIT 增多。

实践中,只要 client.Do 成功返回响应,就应该确保 body 被关闭。