对于 TCP 的接管缓存以及通告窗口,一般而言懂 TCP 的都能说出个大略,然而波及到细节的话可能了解就不那么深刻了。
问题:
明明在接收端有 8192 字节的接管缓存,为什么收了不到 8000 字节的数据就 ZeroWindow 了呢?
0.network buffer & application buffer
深刻接管缓存管理机制的过程中,你可能会在代码的正文中看到这样的宰割,将接管缓存宰割成了所谓的 network buffer 和 application buffer,具体参见__tcp_grow_window 的正文:
/* 2. Tuning advertised window (window_clamp, rcv_ssthresh)
*
- All tcp_full_space() is split to two parts: “network” buffer, allocated
- forward and advertised in receiver window (tp->rcv_wnd) and
- “application buffer”, required to isolate scheduling/application
- latencies from network.
- window_clamp is maximal advertised window. It can be less than
- tcp_full_space(), in this case tcp_full_space() – window_clamp
- is reserved for “application” buffer. The less window_clamp is
- the smoother our behaviour from viewpoint of network, but the lower
- throughput and the higher sensitivity of the connection to losses. 8)
*
- rcv_ssthresh is more strict window_clamp used at “slow start”
- phase to predict further behaviour of this connection.
- It is used for two goals:
- to enforce header prediction at sender, even when application
- requires some significant “application buffer”. It is check #1.
- to prevent pruning of receive queue because of misprediction
- of receiver window. Check #2.
*
- The scheme does not work when sender sends good segments opening
- window and then starts to feed us spagetti. But it should work
- in common situations. Otherwise, we have to rely on queue collapsing.
*/
而后,简直所有的剖析接管缓存的文章都采纳了这种说法,诚然,说法并不重要,要害是要便于人们去了解。因而我尝试用一种不同的说法去解释它,其实实质上是雷同的,只是更加啰嗦一些。
和我一贯的观点一样,本文不会去大段大段剖析源码,也就是说不会去做给源码加正文的工作,而是心愿能绘制一个对于这个话题的蓝图,就像之前剖析 OpenVPN 以及 Netfilter 的时候那样。
1. 通告窗口与接管缓存
在 TCP 的配置中,有一个接管缓存的概念,另外在 TCP 滑动窗口机制中,还有一个接管窗口的概念,毋庸置疑,接管窗口所应用的内存必须调配自接管缓存,因而二者是容纳的关系。
但这不是重点,重点是:接管窗口无奈齐全占完接管缓存的内存,即接管缓存的内存并不能齐全用于接管窗口!Why?
这是因为接管窗口是 TCP 层的概念,仅仅形容 TCP 载荷,然而这个载荷之所以能够收到,必须应用一个叫做数据包的载体,在 Linux 中就是 skb,另外为了让协定运行,必须为载荷封装 TCP 头,IP 头,以太头 … 等等。
我用下图来解释接管缓存以及其和 TCP 数据包的关系:
【留神,当我说“TCP 数据包”的时候,我的意思是这是一个带有以太头的残缺数据包,当我说“TCP 数据段”的时候,我想表白的则是我并不关系 IP 层及以下的货色。】
图示的最初,我特意标红了一个“竭力要防止”的警示,的确,如果间接把可用的窗口都通告进来了,且发送端并不依照满 MSS 发送的话,是存在溢出危险的,这要怎么解决呢?
须要 C /C++ Linux 服务器架构师学习材料加群 812855908(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)
收费学习地址:c/c++ linux 服务器开发后盾架构师
附:如何确定通告窗口能够应用的接管缓存
在代码中,咱们留神一个函数 tcp_fixup_rcvbuf:
static void tcp_fixup_rcvbuf(struct sock *sk)
{u32 mss = tcp_sk(sk)->advmss;
u32 icwnd = TCP_DEFAULT_INIT_RCVWND;
int rcvmem;
/* Limit to 10 segments if mss <= 1460,
* or 14600/mss segments, with a minimum of two segments.
*/
if (mss > 1460)
icwnd = max_t(u32, (1460 * TCP_DEFAULT_INIT_RCVWND) / mss, 2);
rcvmem = SKB_TRUESIZE(mss + MAX_TCP_HEADER);
// 将 rcvbuf 按比例缩放到其(n-1)/ n 能够齐全包容 TCP 纯载荷的水平,n 由零碎参数 net.ipv4.tcp_adv_win_scale 来确定。while (tcp_win_from_space(rcvmem) < mss)
rcvmem += 128;
rcvmem *= icwnd;
if (sk->sk_rcvbuf < rcvmem)
sk->sk_rcvbuf = min(rcvmem, sysctl_tcp_rmem[2]);
}
以上函数确定了接管缓存,其中有 3 个要点:
1). 初始通告窗口的大小
默认是 10 个 MSS 满 1460 字节的段,这个数值 10 来自 google 的测试,与拥塞窗口的初始值统一,然而因为 MSS 各不同,其会依照 1460/mss 的比例进行缩放来适配经验值 10。
2).TCP 载体开销的最小值 128
开展宏 SKB_TRUESIZE 会发现其最小值就是 128,这对通告窗口慢启动过程定义了一个平安下界,载荷小于 128 字节的 TCP 数据段将不会减少通告的下限大小。
3). 参数 tcp_adv_win_scale 的含意
比照我下面的图示,上述代码的正文,咱们晓得 tcp_adv_win_scale 就是管制“载荷 / 载体”比例的,咱们看一下其 Kernel DOC
tcp_adv_win_scale - INTEGER Count buffering overhead as bytes/2^tcp_adv_win_scale
(if tcp_adv_win_scale > 0) or bytes-bytes/2^(-tcp_adv_win_scale),
if it is <= 0.
Possible values are [-31, 31], inclusive.
Default: 1
这个参数已经的 default 值是 2 而不是 1,这意味着以往 TCP 的载荷占比由 3 / 4 变成了 1 /2,如同是开销更大了,这是为什么呢?以下是该 Change 的 patch 形容:
From: Eric Dumazet <edum…@google.com>
[Upstream commit b49960a05e32121d29316cfdf653894b88ac9190]
tcp_adv_win_scale default value is 2, meaning we expect a good citizen
skb to have skb->len / skb->truesize ratio of 75% (3/4)
In 2.6 kernels we (mis)accounted for typical MSS=1460 frame :
1536 + 64 + 256 = 1856 ‘estimated truesize’, and 1856 * 3/4 = 1392.
So these skbs were considered as not bloated.
With recent truesize fixes, a typical MSS=1460 frame truesize is now the
more precise :
2048 + 256 = 2304. But 2304 * 3/4 = 1728.
So these skb are not good citizen anymore, because 1460 < 1728
(GRO can escape this problem because it build skbs with a too low
truesize.)
This also means tcp advertises a too optimistic window for a given
allocated rcvspace : When receiving frames, sk_rmem_alloc can hit
sk_rcvbuf limit and we call tcp_prune_queue()/tcp_collapse() too often,
especially when application is slow to drain its receive queue or in
case of losses (netperf is fast, scp is slow). This is a major latency
source.
We should adjust the len/truesize ratio to 50% instead of 75%
This patch :
1) changes tcp_adv_win_scale default to 1 instead of 2
2) increase tcp_rmem[2] limit from 4MB to 6MB to take into account
better truesize tracking and to allow autotuning tcp receive window to
reach same value than before. Note that same amount of kernel memory isconsumed compared to 2.6 kernels.
单纯从 TCP 载荷比来讲,开销的减少意味着效率的升高,然而留神到这部分开销的减少并非网络协议头所为,而是 skb_shared_info 构造体被计入开销以及 skb 构造体等零碎载体的收缩所导致:
咱们别离来看一下 2.6.32 和 3.10 两个版本的 sk_buff 的大小,怎么看呢?不要想着写一个模块而后打印 sizeof,间接用 slabtop 去看即可,外面信息很足。
a).2.6.32 版本的 sk_buff 大小
slabtop 的后果是:
skbuff_head_cache 550 615 256 15 1 : tunables 120 60 8 : slabdata 41 41 0
咱们看到其大小是 256 字节。
b).3.10 版本的 sk_buff 大小
slabtop 的后果是:
skbuff_head_cache 3675 3675 320 25 2 : tunables 0 0 0 : slabdata 147 147 0
咱们看到其大小是 320 字节。
差异并不是太大!这不是次要因素,但的确会有所影响。
除了 skb 的收缩之外,零碎中还有别的收缩,比方为了效率的“对齐开销”,但更大的开销减少是 skb_shared_info 构造体的计入 (集体认为以前开销中不计入 skb_shared_info 构造体是谬误的) 等,最终导致新版本 (以 3.10+ 为例) 的内核计算 TRUESIZE 的办法扭转:
packet_size = mss + MAX_TCP_HEADER + SKB_DATA_ALIGN(sizeof(struct sk_buff)) + SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
然而以往的老内核 (以 2.6.32 为例),其开销的计算是十分莽撞的,少了很多货色:
packet_size = mss + MAX_TCP_HEADER + 16 + sizeof(struct sk_buff);
尽管这种开销的收缩在 TCP 层面简直看不到什么收益(反而付出了代价,你不得不配置更大的 rcvbuf…),然而 skb 等并不单单服务于 TCP,这种收缩的收益可能被调度,中断,IP 路由,负载平衡等机制获取了,记住两点即可:首先,Linux 内核各个子系统是一个整体,其次,内存越来越便宜而工夫一去不复返,空间换工夫,划得来!
2. 如何躲避接管缓存溢出的危险
在谈如何躲避溢出危险之前,我必须先说一下这个危险并不是常在的,如果应用程序十分迅速的读取 TCP 数据并开释 skb,那么简直不会有什么危险,问题在于应用程序并不受 TCP 层的管制,所以我说的“溢出危险”指的是一种正当但很极其的状况,那就是应用程序在 TCP 层收满一窗数据前都不会去读取数据,这尽管很极其,然而合乎 TCP 滑动窗口的标准:通告给发送端的窗口示意发送端能够一次性发送这么多的数据,至于应用程序什么时候来读取,滑动窗口机制并不管制。
在说明了危险的起源后,咱们就能够畅谈何以躲避危险了。
咱们晓得,TCP 拥塞管制通过慢启动来躲避突发造成的网络缓存溢出的的危险,事实上拥塞管制也是一种流量管制,作为规范的计划,慢启动简直是躲避溢出的标配计划!这很好了解,慢启动的含意是“疾速地从终点试探到稳态”,并非其字面含意所说的“缓缓地启动”,之所以有“慢”字是因为与进入稳固状态后相比,它的终点是低的。这和开车是一样的情理,静止的汽车从踩下油门开始始终到匀速,是一个疾速减速的过程,达到 100km/ h 的工夫也是一个重要的指标,当然,很多状况下是越小越好!
所以说,通告窗口也是采纳慢启动形式逐渐张开的。
2.0、收到极小载荷的 TCP 数据包时的慢启动
比如说收到了一个只蕴含 1 个字节载荷的数据包时,此时仅仅 skb,协定头等开销就会超过几百字节,通告窗口减少是十分危险的。Linux TCP 实现中,将 128 字节定为上限,但凡收到小于 128 字节载荷的数据包,接管一大窗的数据十分有可能造成缓存溢出,因而不执行慢启动。
2.1、收到满 MSS 的 TCP 数据包时的慢启动
如果能保障发送端始终发送满 MSS 长度的 TCP 数据包,那么接管缓存是不会溢出的,因为整个通告窗口能够应用的内存就是通过这个满 MSS 长度和接管缓存依照比例缩放而生成的,然而谁也不能保障发送端会始终发送满 MSS 长度的 TCP 数据包,所以就不能允许发送端一下子发送所有可用的窗口缓存那么大的数据量,因而慢启动是必须的。
收到满 MSS 长度的数据或者大于 MSS 长度的数据,窗口能够毫无压力地减少 2 个 MSS 大小。
2.2、收到非满 MSS
这里的状况比较复杂了。尽管收到数据长度比 MSS 小的 TCP 数据包有缓存溢出的危险,然而受限于以后的通告窗口下限 (因为慢启动的功绩) 小于整个可用的通告窗口内存,这种状况下即使是发送一整窗的数据,也不会造成整个接管缓存的溢出。这就是说某些时候,当以后的接管窗口下限未达到整个可用的窗口缓存时,长度小于 MSS 的 TCP 数据包的额定高于 (n-1)/n比例的开销能够临时“借用”残余的窗口可用的缓存,只有不会造成溢出,管它是不是借用,都是能够承受的。
如此简单的状况,我画了一个略微简单点的图来展现,以节俭文字篇幅:
看懂了上图之后,我来补充一个动静过程,如果继续收到小包的状况下,会怎么?
如果继续收到小于 MSS 的小包,假如长度都相等,那么从慢启动开始,通告窗口的最大值,即 rcv_ssthresh 将会在每收到一个数据包后从初始值开始依照 2 倍数据段长度的增量持续增长,直到其达到小于所有可用通告窗口内存的某个值进行再增长,增长到该值的地位时,一整窗的数据连同其开销将会齐全占满整个 rcvbuf。
3. 一个差别:通告窗口大小与通告窗口下限
为什么拥塞窗口的慢启动是间接减少的拥塞窗口的值,通告窗口的慢启动并不间接减少通告窗口而是减少的通告窗口的下限呢?
这是因为通告窗口的理论值并非单单由接管缓存溢出检测这么一个因素管制,这个因素事实上反而不是主导因素,主导因素是应用程序是不是即时腾出了接管缓存。咱们从代码中如何确定通告窗口的逻辑中能够看出:
u32 __tcp_select_window(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
/* MSS for the peer's data. Previous verions used mss_clamp
* here. I don't know if the value based on our guesses
* of peer's MSS is better for the performance. It's more correct
* but may be worse for the performance because of rcv_mss
* fluctuations. --SAW 1998/11/1
*/
int mss = icsk->icsk_ack.rcv_mss;
// free_space 就是应用程序和 TCP 层合力确定的通告窗口基准值,它简略来讲就是 (rcvbuf - sk_rmem_alloc) 中的纯数据局部,缩放比例就是本文开始提到的(n-1)/n。int free_space = tcp_space(sk);
int full_space = min_t(int, tp->window_clamp, tcp_full_space(sk));
int window;
if (mss > full_space)
mss = full_space;
// 这里是为了避免接管缓存溢出的最初防线,当 free_space 小于全副 rcvbuf 按纯数据比例缩放后的大小的一半时,就要小心了!if (free_space < (full_space >> 1)) {
icsk->icsk_ack.quick = 0;
if (sk_under_memory_pressure(sk))
tp->rcv_ssthresh = min(tp->rcv_ssthresh,
4U * tp->advmss);
// 咱们要多大程度上信赖 mss,取决于发送端 mss 的稳定状况,如正文中所提到的“It's more correct but may be worse for the performance because of rcv_mss fluctuations.”if (free_space < mss)
return 0;
}
// 这里的外围是,尽管应用程序为 TCP 接管缓存腾出了 free_space 这么大小的空间,然而并不能全副通告给发送端,须要一点点通告并减少通告的大小,这就是慢启动了。// 留神这里,free_space 不能超过 ssthresh,这便是通告窗口下限慢启动的基本了。if (free_space > tp->rcv_ssthresh)
free_space = tp->rcv_ssthresh;
...// 这里的窗口计算具体过程反而不是本文关注的,能够参见其它源码剖析的文章和书籍
...// 最终 free_space 要落实到 window,为了便于了解外围,能够认为 free_space 就是 window。return window;
}
最初,总结一幅图,将下面谈到的所有这些概念与 Linux 内核协定栈 TCP 实现关联起来: