TCP Window 相关知识点介绍

在高速网络时代,tcp 协议为了适应需求,扩展了 tcp 选项。下面重点介绍其中的两个。更详细的信息可以参考 RFC7323

系统版本为 centos 7.5, 源码来自于内核版本 3.10。

net.ipv4.tcp_window_scaling

看下 tcp header

window size 用于拥塞控制,接受方会把这个 size 通告给发送方,这样发送方就可以一直发送 size 字节而不用等收到接受方的ACK。window size 在 tcp header 中占用 16 字节, 最大为 2^16 = 65536(64KB)。在现代网络下, 64KB 太小了,会严重降低网络的传输速率。

tcp 选项加入了一个扩展因子(scale), 2^scale * window size 就是实际的窗口大小。scale 使用1字节的偏移量表示,比如 scale 值为7, 扩展因子实际值为 2^7 = 128。scale 最大值为 14, 最大的窗口大小为 2^16 * 2^14 = 2^30(1GB) 。用 wireshark 看数据包,

实际用的值是 Calulated window size。 上图的扩展因子值为 128 。

net.ipv4.tcp_timestamps

引入这个选项有两个功能,精确的测试 RTT 以及 PAWS 机制。

RTTM

之前的计算 RTT 机制是有问题的,尤其是在丢包的情况下。 发送者发生了包 A 时间点 为 send_time1, 没有得到 ACK 又重复发送了,时间点为 send_time2, 之后收到了 ACK recv_time。 recv_time – send_time1 还是 recv_time – send_time2 都是不准确的。因为根本分不清楚 recv_time 是响应的最早的包,还是重复的包。

之前的计算方式,只能根据不重传的包进行测量,如果出现大量重传现象,RTT 是不准确的。tcp_timestamps 解决了这个问题,发送者会记录时间点,接受者会把这个时间点在回包时带上,当前的时间点 – 记录的时间点就是RTT。 这里的时间点不是真实的时间戳,而是虚拟时钟,当然时间点间隔是和实际的时间有比例关系的。

tcp_timestamps 实际长度为 10 字节, 但是在包处理时为 12字节。

image-20180920085828012

从抓包上看,tcp header 长度为 32字节,不开启 tcp_timestamps 情况下为 20字节。

从内核代码上看, net/ipv4/tcp_output.c 里有说明。其中, TCPOLEN_TSTAMP_ALIGNED 定义为 12。

在 tcp_timestamps 开启的情况下, header 长度都为 32 字节,会损失一部分带宽。

PAWS

在有丢包的情况下,怎么甄别是非法的包呢?非法的包产生有多种情况

  1. 假如网络包 A ,在发送过程中丢失了, 发送者重传了包 A,记为 A1,接受者收到了包 A1, 但之后又收到原始的包 A
  2. 假设链接断开,重新打开了,可能又会收到打开之前的包

之前是用 sequence number 判断包是否合法,从 tcp header 上看,sequence 为32字节。 但是在高速网络下,32 字节的 sequence 很快就会用完,导致 sequence 轮转,导致无法根据单一的 sequence 判断包的合法性。

上图显示的在不同的网络速度上,sequence 轮转需要的时间。MSL 定义了包的最大生存时间,很多系统设置为 2分钟(120秒)。这样在 FDDI 网络上,轮转时间与 MSL 相差不大了。

引入 tcp_timestamps,可以根据包的时间点来判断包的有效性。接受者会记录最新的接受包的时间点,当接受到旧包时,根据时间点判断,就可以把旧包舍弃。

tcp_timestamps 的时间间隔是根据主机频率来的,间隔一般为 1ms。那么是够用的,轮转需要的时间为 2^31 / (86400 * 1000) = 24.8 day。未来如果主机频率提速的话,轮转时间变短,用 tcp_timestamps也是不可靠的。

TCP Window Size

关注下 7484 行,9028 端口号是接收者,通告了自己的窗口大小为 1152。11414 行,发送者只发送了 1152 字节数据,之后接收者没有了缓存区空间,只有等到上层应用处理后,13902行,接收者通告窗口大小变更,之后发送者可以再次发送数据。

我们来看下 window size 是如何计算的。首先看下初始化窗口大小。在6630行的回包中,可以看到窗口大小为 13480, MSS 为 1360。先来看下 MSS 是如何计算的。

一般地, MSS = MTU – 20(IP Header) – 20(TCP Header), 很多情况下, MTU 为 1500,但在我们系统中,使用 ifconfig 查看

MTU 为 1400, 那么 MSS 为 1360 是正常的。在 SYN 阶段,双方需要协商 MSS 大小,是为了避免分包重组。

查看net/ipv4/tcp_output.c 中 tcp_select_initial_window ,先来看下 窗口因子是如何计算的。

window_clamp 这里值为最大值,基本取决于这两个 net.core.rmem_max , net.ipv4.tcp_rmem 这两个值,取最大的值然后进行计算,而且因子的最大值为14。

sysctl_tcp_default_init_rwnd 值为 10。初始窗口取决于 rcv_wnd = min(rcv_wnd, init_cwnd * mss),也就是 10 * mss。从包上看,初始窗口大小为 13480。 那么 这里 mss 是 1348。上面我们计算出 mss = 1360,结合 tcp_timestamps 讲解,这里 mss 需要减去 12 字节,也就是 1360 -12 = 1348。

这里我们借助 Systemtap 再分析下。 首先, 列出探测点

我们把输入参数打印出来,并把调用堆栈打印下

这里看到 mss 就是 1348, 最终计算的窗口因子 rcv_wscale 值为 7。直接调用函数为 tcp_make_synack。

再来关注下窗口增长情况。接收者通告了窗口大小,发送者也不会一直发送数据直到用完窗口大小。发送者有慢启动和拥塞避免机制来防止大量的丢包重传。基于这两个机制,发送端实现了自己的流控机制,接收端依靠窗口大小实现了自己的流控。类似与慢启动和拥塞避免,接收窗口大小也不是一上来就设置的最大值。而是一个初始值,然后根据接收的情况,逐步扩大接收窗口,具体大小与系统的内存情况,设置的 buffer 大小,上层应用的处理情况有关。

net.core.rmem_max , net.ipv4.tcp_rmem 这两个值决定了接收缓冲区大小。net.core.rmem_max 适用于所有的 socket 链接,net.ipv4.tcp_rmem 这个只是针对 tcp 协议的。如果 socket 显式的设置了缓冲区大小,那么优先级最高,都会覆盖这两个值。

net.ipv4.tcp_rmem 有三个值,分别是最小,默认,最大的字节数。只有修改默认值才需要重启应用,其余的两个值不需要。net.ipv4.tcp_rmem 最大值可以计算出来。窗口数据大小 = 带宽 * RTT。 假设网络带宽为 1000Mb/s, RTT 为 1ms, 窗口大小 = 1000/8 * (1/1000) = 0.125M, 但这不是最终的大小。这只是存储的数据大小,还要考虑额外的内存开销。net.ipv4.tcp_adv_win_scale 这个值决定了内存开销的占比。如果值为1, 要有一半的大小用于内存开销,如果值为2,要有 1/4 的大小用于内存开销。具体关联函数如下

net.ipv4.tcp_adv_win_scale = 1 的话,rmem 的最大值至少要大于 0.25M, 这是理论值,实际情况要比这要大一些。如果上层应用慢的话,缓冲区的数据会处理的慢,导致缓存区满了,有额外的数据的话,有可能被丢弃。可以通过 netstat -s 查看。

多调用 netstat 几次,如果数值一直增大,要考虑增加 rmem 数值,并且也要关注应用的处理速度。

总结

  1. 介绍了 tcp 的两个扩展选项,都是为了适应高速的网络传输
  2. tcp window size 大小如何设置的,以及系统参数如果影响的
  3. 借助 SystemTap 工具理解代码,可以追踪堆栈,方便的理清代码关系
  4. 借助 wireshark 工具,分析网络包的具体情况
此条目发表在Linux分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

邮箱地址不会被公开。