原力注入

图解 TCP 收发包需要注意的内核参数

前期回顾

在《图解 TCP 连接生命周期》一文中,我们回顾了 TCP 建立连接的三次握手过程和结束连接的四次挥手过程,同时介绍了在这个过程中需要注意的一些内核参数,主要有:
今天我们站在收发包视角来看一下,为了避免丢包,需要注意的内核参数吧。

背景

TCP 收包和发包的过程是网络应用中容易出现问题的地方。 收包 是指数据到达网卡并被应用程序开始处理的过程; 发包 则是应用程序调用发包函数到数据包从网卡发出的过程。常见的问题包括:
  • 网卡中断过多,占用大量 CPU,影响业务性能;
  • 应用程序调用 write() 或 send() 发送数据包却未成功;
  • 数据包已被网卡接收但应用程序未收到;
  • 调整缓冲区大小无效;
  • 内核缓冲区可能已满导致丢包,如何观察?
要解决这些问题,我们需要理解 TCP 收发包过程中的关键因素,以及如何配置参数使之与业务场景匹配。下面我们来分析 TCP 数据包的发送和接收过程。

TCP 数据包的发送过程

摘自: https://time.geekbang.org/column/article/285816
在应用程序调用 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 数据包的接收过程

摘自: https://time.geekbang.org/column/article/285816 数据包到达网卡后会触发中断(IRQ)通知 CPU 读取数据包。高性能网络场景中数据包量大,频繁中断影响性能, 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 中存在的字节数。

总结

为了更加系统学习,笔者推荐大家可以学习一下极客时间的《 Linux 内核技术实战课 》: