图解 TCP 收发包需要注意的内核参数
前期回顾
在《图解 TCP 连接生命周期》一文中,我们回顾了 TCP 建立连接的三次握手过程和结束连接的四次挥手过程,同时介绍了在这个过程中需要注意的一些内核参数,主要有:背景
TCP 收包和发包的过程是网络应用中容易出现问题的地方。 收包 是指数据到达网卡并被应用程序开始处理的过程; 发包 则是应用程序调用发包函数到数据包从网卡发出的过程。常见的问题包括:- 网卡中断过多,占用大量 CPU,影响业务性能;
-
应用程序调用
write()或send()发送数据包却未成功; - 数据包已被网卡接收但应用程序未收到;
- 调整缓冲区大小无效;
- 内核缓冲区可能已满导致丢包,如何观察?
TCP 数据包的发送过程
在应用程序调用
write(2)
或
send(2)
系列系统调用开始发包时,数据包会从用户缓冲区复制到 TCP 发送缓冲区(TCP Send Buffer)。缓冲区大小默认由
net.ipv4.tcp_wmem
控制:
sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 87380 16777216
• min: 最小缓冲区大小
• default: 初始缓冲区大小
• max: 最大缓冲区大小
缓冲区大小在 min 和 max 间动态调 整,初始大小为
default
。
tcp_wmem
中的
max
不能超过
net.core.wmem_max
的值:
sysctl net.core.wmem_max
net.core.wmem_max = 16777216
固定缓冲区设置
有时应用程序会明确指定发送数据大小,可通过
setsockopt(2)
的
SO_SNDBUF
设置固定的缓冲区大小,此时
tcp_wmem
失效,缓冲区大小不再动态调整。
SO_SNDBUF
的值也不能超过
net.core.wmem_max
。
TCP 连接总内存限制
系统中可能有大量 TCP 连接,总内存使用受
net.ipv4.tcp_mem
控制:
sysctl net.ipv4.tcp_mem
net.ipv4.tcp_mem = 94500000 915000000 927000000
• min: 最小值
• pressure: 内存压力阈值
• max: 最大内存
达到 max 限制会影响数据发送。若 tcp_mem 限制触发,可以使用以下命令观察:
# 检查系统是否支持 tracing,如果不支持需要先开启,这里不展开说明
ls /sys/kernel/debug/tracing
# 启用 sock_exceed_buf_limit 事件追踪
echo 1 > /sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
# 监控事件日志
cat /sys/kernel/debug/tracing/trace_pipe
# 例如
tcp_sendmsg: pid=3215 comm=nginx send buf exceed limit: sk=000000007b3ed4f0, pid=12345
# 分析结果并调整参数,例如
sysctl -w net.ipv4.tcp_wmem="4096 87380 16777216"
sysctl -w net.core.wmem_max=16777216
# 继续观察,确保没有想过事件
# 停止事件追踪
echo 0 > /sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
# 清理事件
echo > /sys/kernel/debug/tracing/trace
IP 层配置项
在 IP 层,
net.ipv4.ip_local_port_range
控制本地端口范围。若默认范围过小,可能导致连接创建失败:
sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 1024 65535
qdisc
是 Linux 内核的流控实现,
txqueuelen
是
qdisc
队列长度。队列太小可能导致丢包,可使用以下命令检查:
ip -s -s link ls dev eno1
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 3c:ec:ef:4e:15:06 brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped overrun mcast
515151229849 1290891028 0 0 0 3029910
RX errors: length crc frame fifo missed
0 0 0 0 0
TX: bytes packets errors dropped carrier collsns
397751986726 1242924130 0 0 0 0
TX errors: aborted fifo window heartbeat transns
0 0 0 0 4
若
dropped
项不为 0,可能需增大
txqueuelen
:
ifconfig eth0 txqueuelen 2000
默认的
qdisc
为
pfifo_fast
,通常无需调整;若使用
TCP BBR
拥塞控制,需将其设置为
fq
:
sysctl net.core.default_qdisc=fq
TCP 数据包的接收过程
NAPI
机制通过轮询(poll)批量处理数据包。轮询数量受
net.core.netdev_budget
控制:
sysctl net.core.netdev_budget
net.core.netdev_budget = 300
可根据吞吐量需求适当调大此值,例如增至 600。调大此值会增加 CPU 轮询时间,可能增加其他任务的调度延迟。
TCP 接收缓冲区
TCP 接收缓冲区大小由
net.ipv4.tcp_rmem
控制:
sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096 87380 16777216
• min: 最小缓冲区大小
• default: 初始缓冲区大小
• max: 最大缓冲区大小
默认情况下,接收缓冲区大小动态调节。
tcp_moderate_rcvbuf
控制动态调节开关,通常保持打开(值为 1):
sysctl net.ipv4.tcp_moderate_rcvbuf
net.ipv4.tcp_moderate_rcvbuf = 1
应用程序也可通过
SO_RCVBUF
设置固定的接收缓冲区大小。类似发送缓冲区,
SO_RCVBUF
的值不能超过
net.core.rmem_max
:
sysctl net.core.rmem_max
net.core.rmem_max = 16777216
注意:只有在
tcp_moderate_rcvbuf
为 1,并且应用程序没有通过
SO_RCVBUF
来配置缓冲区大小的情况下,TCP 接收缓冲区才会动态调节。
BPF 工具观测
《BPF 性能之巅》 一书第四章提到的
sormem
工具,用于跟踪套接字接收队列的大小,以直方图的形式显示与可调限制相比,接收队列的满载程度。如果接收队列超出限制,数据包将被丢弃,从而导致性能问题。
/bpf-perf-tools-book-master/originals/Ch10_Networking# ./sormem.btAttaching 4 probes...Tracing socket receive buffer size. Hit Ctrl-C to end.^C@rmem_alloc:[0] 2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[1] 0 | |[2, 4) 0 | |[4, 8) 0 | |[8, 16) 0 | |[16, 32) 0 | |[32, 64) 0 | |[64, 128) 0 | |[128, 256) 0 | |[256, 512) 0 | |[512, 1K) 0 | |[1K, 2K) 0 | |[2K, 4K) 2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|@rmem_limit:[128K, 256K) 4 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
@rmem_alloc
显示为接收缓冲区分配了多少内存。
@rmem_limit
是接收缓冲区的限制大小,使用
net.ipv4.tcp_rmem
进行调整。
关于 bpftrace 的使用可以参考:
《BPF 性能之巅》读书笔记 - bpftrace 入门
。书籍可以通过如下方式购买:
传统工具 - ss 观测
命令行执行:结果解析:ss -tiepm4 | headFailed to open cgroup2 by IDState Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessESTAB 0 0 192.168.103.35:48682 192.168.103.36:12005 users:(("mysqld",pid=1068110,fd=182)) ino:2779900121 sk:548dc90 cgroup:/machine.slice/docker.service <->skmem:(r0,rb6291456,t0,tb3497472,f4096,w0,o0,bl0,d78) ts sack cubic wscale:7,7 rto:220 rtt:18.805/20.763 ato:48 mss:4040 pmtu:4092 rcvmss:140 advmss:4040 cwnd:10 ssthresh:367 bytes_sent:21203293400 bytes_retrans:17832 bytes_acked:21203275569 bytes_received:1347875756 segs_out:21903559 segs_in:20225116 data_segs_out:21277948 data_segs_in:9598986 send 17.2Mbps lastsnd:188 lastrcv:188 lastack:148 pacing_rate 34.4Mbps delivery_rate 2.69Gbps delivered:21277949 app_limited busy:86620844ms rwnd_limited:4ms(0.0%) retrans:0/53 dsack_dups:53 rcv_rtt:1 rcv_space:282380 rcv_ssthresh:1841728 minrtt:0.016
| 字段 | 值 | 详细解释 |
| State | ESTAB | 连接状态,表示 TCP 连接已建立 ("ESTABLISHED"),数据可以在本地和远程主机之间正常传输。 |
| Recv-Q | 0 |
接收队列的大小,单位为字节。值为
0
表示没有数据在接收队列中等待处理。
|
| Send-Q | 0 |
发送队列的大小,单位为字节。值为
0
表示没有数据在发送队列中等待传输。
|
| Local Address:Port | 192.168.103.35:48682 |
本地主机的 IP 地址和端口,此处为
192.168.103.35
,端口号为
48682
。
|
| Peer Address:Port | 192.168.103.36:12005 |
远程主机的 IP 地址和端口,此处为
192.168.103.36
,端口号为
12005
。
|
| Process | mysqld (pid=1068110, fd=182) |
该连接的关联进程信息:
-
mysqld
表示 MySQL 进程
-
pid=1068110
为进程 ID
-
fd=182
为文件描述符。
|
| ino | 2779900121 | 连接套接字的 inode 号,用于唯一标识该连接。 |
| sk | 548dc90 | 套接字的内核引用标识符,表示该连接在内核中的引用地址。 |
| cgroup | /machine.slice/docker.service |
进程所属的控制组 (cgroup),用于资源限制和监控。此处属于 Docker 管理的
machine.slice/docker.service
。
|
| skmem | r0, rb6291456, t0, tb3497472, f4096, w0, o0, bl0, d78 |
套接字内存配置:
-
rb6291456
:接收缓存大小为 6291456 字节
-
tb3497472
:发送缓存大小为 3497472 字节
-
f4096
:用于文件描述符的缓存大小
-
d78
:已延迟的数据量。
|
| ts | enabled | 时间戳是否启用,用于计算往返时间 (RTT) 及其他连接特性。 |
| sack | enabled | 是否启用选择性确认 (SACK),帮助在数据丢包时提高传输效率。 |
| cong | cubic |
拥塞控制算法,使用
cubic
提供更高的吞吐量和稳定性,适用于高速网络。
|
| wscale | 7,7 | 窗口缩放因子,分别为接收端和发送端的窗口缩放值,用于更大流量传输。 |
| rto | 220 |
重传超时 (RTO),单位毫秒。此值表示如果未收到确认,将在
220
毫秒后重传数据包。
|
| rtt | 18.805/20.763 | 当前的往返时间 (RTT) 和波动范围 (RTTvar),分别为 18.805 毫秒和 20.763 毫秒。 |
| ato | 48 | 自动超时时间 (ATO),即自动发送数据包的等待时间,单位为毫秒。 |
| mss | 4040 | 最大分段大小 (MSS),定义单个 TCP 数据包的最大数据部分大小,单位为字节。 |
| pmtu | 4092 | 路径最大传输单元 (PMTU),路径上允许的最大数据包大小,为 4092 字节。 |
| rcvmss | 140 | 实际接收的 MSS,为 140 字节。 |
| advmss | 4040 | 建议的 MSS 值,通常为 MTU 减去 TCP/IP 头的大小。 |
| cwnd | 10 |
拥塞窗口大小 (cwnd),决定在接收确认前允许传输的数据包数量。当前值为
10
。
|
| ssthresh | 367 | 慢启动阈值 (ssthresh),在达到此值之前网络处于慢启动阶段。 |
| bytes_sent | 21203293400 | 已发送的字节数总量。 |
| bytes_retrans | 17832 | 重传的字节数总量,表示由于数据丢失而重发的数据量。 |
| bytes_acked | 21203275569 | 接收到的确认字节数总量。 |
| bytes_received | 1347875756 | 已接收的字节数总量。 |
| segs_out | 21903559 | 已发送的 TCP 段数量总量。 |
| segs_in | 20225116 | 已接收的 TCP 段数量总量。 |
| data_segs_out | 21277948 | 传输的数据段数量,不包含控制段。 |
| data_segs_in | 9598986 | 接收的数据段数量。 |
| send | 17.2Mbps |
当前的发送速率,为
17.2
Mbps。
|
| lastsnd | 188 | 上次发送数据的时间,单位为毫秒。 |
| lastrcv | 188 | 上次接收数据的时间,单位为毫秒。 |
| lastack | 148 | 上次收到 ACK 的时间,单位为毫秒。 |
| pacing_rate | 34.4Mbps | 传输节奏速率,决定数据包发送速率,以防止网络拥塞。 |
| delivery_rate | 2.69Gbps | 实际的数据交付速率。 |
| delivered | 21277949 | 成功传输的数据段数量。 |
| app_limited | true |
传输速率是否受应用层限制。此处为
true
,即受限于应用层速率。
|
| busy | 86620844ms | 套接字忙碌时间,表示连接活跃的时长。 |
| rwnd_limited | 4ms (0.0%) | 因接收窗口限制的时长和百分比。 |
| retrans | 0/53 | 重传次数及重复确认数 (DSACK),此处表示无重传,但收到 53 个重复确认。 |
| dsack_dups | 53 | 重复选择性确认 (DSACK) 的数量,表示对重复包的确认,表明可能存在数据包重复。 |
| rcv_rtt | 1 | 接收端的 RTT,单位为毫秒。 |
| rcv_space | 282380 | 接收缓冲区的剩余空间,单位为字节。 |
| rcv_ssthresh | 1841728 | 接收端的慢启动阈值。 |
| minrtt | 0.016 | 最小往返时间 RTT,反映网络的最佳延迟情况,单位为毫秒。 |
-
•
LISTEN
状态:
Recv-Q表示当前listen backlog队列( 全连接队列 :等待用户调用 accept() 获取的、已完成 3 次握手的 socket 连接队列)中的连接数量,而Send-Q表示了listen socket最大能容纳的backlog,即min(backlog, somaxconn)值。 -
•
非 LISTEN
状态:
Recv-Q表示了receive queue中存在的字节数;Send-Q表示send queue中存在的字节数。
总结