eBPF 排查 TCP 传输卡顿与连接清理¶
请求变慢不总是握手慢。连接已经建立后,可以用发送窗口、未发送/未确认/未读队列、ACK 推进和内核 RTT 区分排查方向,再与两端应用证据对齐。
查询入口与实测环境见 网络排障实践总览。TCP 队列字段只有在 queue_values_available=true 且快照足够新时才能解释;样本中的 at_ns 与 snapshot_at_ns 都是纳秒时间,差值用于判断观测当时的快照年龄;不要用查询当天的时间判断历史样本是否有效。关闭终态可能将这些字段置零并标为不可用,不能用终态零值证明没有压力。
场景一:对端窗口为零,发送数据堆积¶
下面是实测日志 message 解析后的关键字段节选。测试对端暂停读取,客户端继续发送:
{
"schema": "ebpf_network_event/v1",
"entity": {
"scope": "connection",
"netns": "4026531840",
"pid": 124,
"process_name": "fixture",
"service": "fixture",
"protocol": "tcp",
"role": "client",
"role_source": "connect",
"remote_ip": "10.23.0.1",
"service_port": 40016
},
"problem_type": "tcp_zero_window_probe",
"occurrences": 4,
"evidence_partial": false,
"samples": [
{
"local": "10.23.0.1:50330",
"remote": "10.23.0.1:40016",
"at_ns": 1791779039232233570,
"snapshot_at_ns": 1791779038712695250,
"stage": "transfer",
"state": "ESTABLISHED",
"srtt_snapshot_us": 5995,
"unacked_bytes": 0,
"unsent_bytes": 259072,
"unread_bytes": 0,
"send_window_bytes": 0,
"queue_values_available": true,
"probe0_calls": 4
}
]
}
| 观测 | 当前值 | 意义 |
|---|---|---|
| 状态与阶段 | ESTABLISHED / transfer | 问题发生在已建立连接的传输阶段 |
unsent_bytes |
259072 B,约 253 KiB | 已进入本地发送队列、尚未发送的数据 |
send_window_bytes |
0 | 快照中的对端通告窗口为零 |
unacked_bytes |
0 | 此次快照没有待确认序列空间 |
probe0_calls |
4 | 观测到零窗口探测调用 |
| 观测时的快照年龄 | 约 0.52 秒 | 队列快照新鲜度可用于这次判断 |
srtt_snapshot_us |
5995 μs,约 6 ms | 最近内核平滑 RTT;不是应用请求耗时 |
组合证据把调查方向缩小到接收端窗口与消费能力,而不是仅凭“发送慢”推断线路丢包。TCP 零窗口探测属于正常流量控制机制,参见 RFC 9293 的窗口与探测规则。持续零窗口才需要结合业务受阻进一步排查;单次零窗口既不证明业务故障,也不证明整条路径健康。
建议在对端相同命名空间查看对应连接:
结合接收队列、应用读取速度、事件循环、线程阻塞、CPU 节流及 GC 时间。如果对端也有采集器,查看它的 tcp_receive_backlog 样本。确认消费慢后修复阻塞或调整并发;业务需要持续高吞吐时,再评估缓冲区和背压设计。修复后观察窗口重新打开、未发送队列下降、业务请求恢复,不能只以探测次数减少作为验收。
场景二:窗口开放,但数据迟迟没有确认¶
以下是教学示例,不是客户事故或本次原生实测。假设同一已建立连接的新鲜快照给出:
| 字段或观测 | 教学值 |
|---|---|
queue_values_available |
true |
| 快照距观测时间 | 2 秒 |
unacked_bytes / unsent_bytes |
65536 / 0 B |
send_window_bytes |
262144 B |
| 距最近一次观测到 ACK 推进 | 12 秒 |
srtt_snapshot_us |
450000 μs,即 450 ms |
| 同连接 RTO 观测增量 | 3 |
这组数据与零窗口不同:发送窗口还有空间,但已有数据长期未确认。采集器在有效且不超过 10 秒的快照中,对已建立或 CLOSE_WAIT、仍有未确认序列空间且至少 10 秒未观测到 ACK 推进的连接记录 tcp_ack_stall。这里“未观测到”是证据范围,未确认序列空间也可能包含控制序号,不能换算成同等业务载荷丢失。
优先比较两端证据:服务端是否接收到了数据、对端是否持续推进接收与 ACK、两端重传/RTO 是否增加、网卡或命名空间丢弃是否与故障同窗。接着检查路径拥塞、访问策略、连接跟踪和中间设备;如果必须判断具体哪段丢包,在少量目标连接上短时抓包并结合路径证据。
SRTT 450 ms 与 RTO 增量支持“TCP 响应异常”的方向,不能据此定位某一交换机,也不能代替接口请求 P99。重传和定时器调用次数都不是线路丢包数。修复验收需要业务延迟恢复,并且 ACK 停滞、RTO 和未确认队列回到原有基线。
场景三:本地应用没有及时读取¶
以下是教学示例:
| 字段或观测 | 教学值 |
|---|---|
| 连接状态 | ESTABLISHED |
queue_values_available |
true |
| 快照年龄 | 2 秒 |
unread_bytes |
131072 B,即 128 KiB |
unsent_bytes / unacked_bytes |
0 / 0 B |
| 问题类型 | tcp_receive_backlog |
本地未读数据达到 64 KiB 时,采集器会保留接收积压线索。它提示先检查本地应用消费,而不是直接要求对端优化发送。一次大响应或应用合法批处理也会产生未读队列;要对齐持续时间、业务延迟和消费速率。
根据异常实体的进程和命名空间用 ss -tinmp 核对连接,关联应用线程、GC、限流和容器 CPU 节流。如果持续消费慢,优化读取和处理路径;缓冲区调整应服务于已确认的吞吐需求。修复后用相同负载确认队列不再持续积压,业务吞吐和延迟改善。
场景四:CLOSE_WAIT 持续较久或连接被 reset¶
netflow.tcp_close_wait 是聚合窗口内进入该状态的观测次数,不是当前存量。要排查清理慢,查看 tcp_close_wait 异常样本的 state/age_ms;采集器以至少 60 秒作为信息级取样阈值。
CLOSE_WAIT 表示收到对端 FIN,等待本地完成关闭;TCP 允许合法的半关闭,参见 RFC 9293 的连接状态与关闭。持续时间是否异常取决于协议和应用。只有业务不需要长时间半关闭、且样本和 ss 持续显示该状态时,才进一步检查遗漏的关闭路径、引用未释放或应用阻塞。避免把正常短连接的状态转换误当作泄漏。
reset 则要同时查看 problem_type、连接结果、stage/state 和 socket/I/O 错误:
| 组合证据 | 优先检查 |
|---|---|
connect_refused 加收到 reset,连接没有成功结果 |
建立阶段拒绝,见 连接失败实践 |
| 有成功建立事实,传输阶段收到 reset,并有 I/O 错误 | 对端进程退出、关闭方式、代理或中间设备连接清理 |
| 主动 reset 调用 | 本地关闭/错误处理路径;不能把调用次数当成业务失败次数 |
样本 evidence_partial=true 或生命周期不完整 |
先核对 采集可信度,保留阶段判断的不确定性 |
这类数据的价值是把“网络卡顿”细化成等待连接、等待发送窗口、等待 ACK、等待应用读取或等待关闭。最终根因仍应通过对应一侧的应用与网络证据验证。