Linux 网络栈基础¶
学习目标¶
学完本章后,学习者应该能够:
- 理解 socket、监听端口、连接队列和缓冲区。
- 解释 TCP 连接在内核中的基本生命周期。
- 理解 listen backlog、accept queue、send buffer、receive buffer。
- 能初步分析 TIME_WAIT、CLOSE_WAIT、端口耗尽等问题。
socket¶
socket 是应用程序进行网络通信的接口。Go 的 net.Listen、http.ListenAndServe、net.Dial 底层都离不开 socket。
服务端常见流程:
客户端常见流程:
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,并对外部调用设置超时;同时增加连接状态、文件描述符数量和错误率监控。
实战任务¶
排查一个“服务连接越来越多”的问题:
- 查看进程的 TCP 连接状态。
- 区分
TIME_WAIT和CLOSE_WAIT。 - 检查代码是否关闭响应体或连接。
- 检查 HTTP Client 是否配置超时。
参考答案
如果 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 被关闭。