收包与发包
收包是指数据到达网卡再到被应用程序开始处理的过程。发包则是应用程序调用发包函数到数据包从网卡发出的过程。
发包过程:
TCP层
发包时,应用程序会调用 write(2) 或者 send(2) 系列系统调用开始往外发包时,这些系统调用会把数据包从用户缓冲区拷贝到 TCP 发送缓冲区(TCP Send Buffer),这个 TCP 发送缓冲区的大小是受限制的,这里也是容易引起问题的地方。
1. tcp_wmem 合理增大发送缓冲区大小
tcp_wmem 中这三个数字的含义分别为 min、default、max。TCP 发送缓冲区的大小会在 min 和 max 之间动态调整,初始的大小是 default,这个动态调整的过程是由内核自动来做的,应用程序无法干预。自动调整的目的,是为了在尽可能少的浪费内存的情况下来满足发包的需要。
net.ipv4.tcp_wmem = 8192 65536 16777216
2. wmem_max tcp 发送缓冲区大小的上限
通常我们需要把该参数设置为大于等于tcp_wmem.max
net.core.wmem_max = 16777216
合理配置发送缓冲区大小
对于 TCP 发送缓冲区的大小,我们需要根据服务器的负载能力来灵活调整。通常情况下我们需要调大它们的默认值。
sk_stream_wait_memory事件监控
在生产环境中,默认的缓冲区大小可能会由于默认太小,从而导致业务延迟很大的问题,这类问题可以使用 systemtap 之类的工具在内核里面打点来进行观察(观察 sk_stream_wait_memory 这个事件):
# sndbuf_overflow.stp
# Usage :
# $ stap sndbuf_overflow.stp
probe kernel.function("sk_stream_wait_memory")
{
printf("%d %s TCP send buffer overflow\n",
pid(), execname())
}
Tips: 如果我们可以观察到 sk_stream_wait_memory 这个事件,就意味着 TCP 发送缓冲区太小了,我们需要继续去调大 wmem_max 和 tcp_wmem:max 的值了。
3. SO_SNDBUF 设置发送缓冲区固定大小
如果我们很明确地知道自己发送多大的数据,需要多大的 TCP 发送缓冲区,这个时候就可以通过 setsockopt(2) 里的 SO_SNDBUF 来设置固定的缓冲区大小,该固定值会覆盖tcp_wmem,内核也不会再对缓冲区进行动态调整。
Tips: SO_SNDBUF 设置的最大值不能超过 net.core.wmem_max,如果超过了该值,内核会把它强制设置为 net.core.wmem_max,通常情况下我们不会通过 SO_SNDBUF 来设置 TCP 发送缓冲区的大小而是使用内核设置的 tcp_wmem,因为如果 SO_SNDBUF 设置得太大就会浪费内存,设置得太小又会引起缓冲区不足的问题。
eBPF
linux现在提供了eBPF 来设置 SO_SNDBUF 和 SO_RCVBUF,进而分别设置 TCP 发送缓冲区和 TCP 接收缓冲区的大小。同样地,使用 eBPF 来设置这两个缓冲区时,也不能超过 wmem_max 和 rmem_max。
What is eBPF? An Introduction and Deep Dive into the eBPF Technology
4. tcp_mem 设置TCP连接总内存大小
tcp_wmem 以及 wmem_max 的大小设置都是针对单个 TCP 连接的,这两个值的单位都是 Byte(字节)。系统中可能会存在非常多的 TCP 连接,如果 TCP 连接太多,就可能导致内存耗尽。因此,所有 TCP 连接消耗的总内存也有限制:
net.ipv4.tcp_mem = 8388608 12582912 16777216
我们通常也会把这个配置项给调大。与前两个选项不同的是,该选项中这些值的单位是 Page(页数),也就是 4K。它也有 3 个值:min、pressure、max。当所有 TCP 连接消耗的内存总和达到 max 后,也会因达到限制而无法再往外发包。
sock_exceed_buf_limit
我们可以使用sock_exceed_buf_limit这个静态观察点来观察因 tcp_mem 达到限制而无法发包或者产生抖动的问题。
#观察时我们只需要打开 tracepiont(需要 4.16+ 的内核版本):
$ echo 1 >
/sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
#然后去看是否有该事件发生:
$ cat /sys/kernel/debug/tracing/trace_pipe
如果有日志输出(即发生了该事件),就意味着你需要调大 tcp_mem 了,或者是需要断
开一些 TCP 连接了。
IP层
1. ip_local_port_range 适当扩大IP端口范围
TCP 层处理完数据包后,就继续往下来到了 IP 层。IP 层这里容易触发问题的地方是
net.ipv4.ip_local_port_range 这个配置选项,它是指和其他服务器建立 IP 连接时本地端
口(local port)的范围。
我们在生产环境中可能会遇到过默认的端口范围太小,以致于无法创建新连接的问题。所以通常情况下,我们都会扩大默认的端口范围:
net.ipv4.ip_local_port_range = 1024 65535
2. txqueuelen 适当增长流控队列长度
为了能够对 TCP/IP 数据流进行流控,Linux 内核在 IP 层实现了 qdisc(排队规则)。我们平时用到的 TC 就是基于 qdisc 的流控工具。qdisc 的队列长度是我们用 ifconfig 来看
到的 txqueuelen,我们可能会遇到因为 txqueuelen 太小导致数据包被丢弃的情况,这类问题可以通过下面这个命令来观察:
$ ip -s -s link ls dev eth0
…
TX: bytes packets errors dropped carrier collsns
3263284 25060 0 0 0 0
#如果观察到 dropped 这一项不为 0,那就有可能是 txqueuelen 太小导致的。当遇到这种情况时,你就需要增大该值了,比如增加 eth0 这个网络接口的 txqueuelen:
$ ifconfig eth0 txqueuelen 2000
$ ip link set eth0 txqueuelen 2000
Tips: 在调整了 txqueuelen 的值后,我们需要持续观察是否可以缓解丢包的问题,这也便于我们将它调整到一个合适的值。
3. default_qdisc 改善拥塞控制
Linux 系统默认的 qdisc 为 pfifo_fast(先进先出),通常情况下我们无需调整它。如果你
想使用TCP BBR来改善 TCP 拥塞控制的话,那就需要将它调整为 fq(fair queue, 公平队列):
net.core.default_qdisc = fq
TCP -> IP -> 网卡
经过 IP 层后,数据包再往下就会进入到网卡了,然后通过网卡发送出去。至此,我们需要发送出去的数据就走完了 TCP/IP 协议栈,然后正常地发送给对端了。