转载自:

http://www.cnhalo.net/2016/08/13/linux-tcp-nagle-cork/

http://abcdxyzk.github.io/blog/2018/07/08/kernel-nodelay_cork/

糊涂窗口综合症(Silly Windw Syndrome)

  • 发送方: 应用程序产生数据的速度很慢
    发送1字节需要40B(TCP头和IP头), 发送大量的小包会造成网络拥塞,发送窗口抖动,网络利用率低等特性。
    当年OTT(over the top)类应用(如微信), 由于3G/4G没有大规模普及,因为常用的心跳机制,通常发送小的心跳包,造成了信令风暴,影响了运营商网络的稳定。
    解决: nagle和cork算法,尝试延迟发送,积累成大包后再发送。当然交互类应用需要实时性,不能推迟发送。

  • 接收方: 应用程序消耗数据的速度很慢
    接收窗口满了,发送rwnd=0, 再消耗一字节,rwnd=1,消耗并发送反复的情况。 发送方nagle因为推迟发送,可能忽略这部分通告
    解决:

    • clark方法:只要数据到达就发送ACK,但在缓存中有足够大的空间放入最大长度的报文之前,都宣布rwnd=0
    • 推迟确认:优点:减少ACK数量。缺点:可能导致重传

Nagle和Cork

  • Nagle算法的目的:避免发送大量的小包,网络上每次只能一个小包存在,在小包被确认之前,只能积累发送大包,如果包长度达到MSS,则允许发送;如果该包含有FIN,则允许发送;但发生了超时(一般为200ms),则立即发送, 启动TCP_NODELAY,就意味着禁用了Nagle算法
  • Cork算法的目的: CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。 cork是完全避免小包的发送,只发送MSS大小的包及不得不发的小包

setsockopt

TCP_CORK的开关,只会影响TCP_NAGLE_CORK选项,当nagle测试关闭(通过TCP_NODELAY设置了TCP_NAGLE_OFF)的情况下,才会设置TCP_NAGLE_PUSH
而TCP_NODELAY则通过设置TCP_NAGLE_OFF来开关nagle。
TCP_NAGLE_PUSH是个一次性的选项值,每次创建新的skb并放入发送队列的时候,TCP_NAGLE_PUSH都会被清除(skb_entail函数)

#define TCP_NAGLE_OFF        1    /* Nagle's algo is disabled */
#define TCP_NAGLE_CORK 2 /* Socket is corked */
#define TCP_NAGLE_PUSH 4 /* Cork is overridden for already queued data */
case TCP_CORK:
/* When set indicates to always queue non-full frames.
* Later the user clears this option and we transmit
* any pending partial frames in the queue. This is
* meant to be used alongside sendfile() to get properly
* filled frames when the user (for example) must write
* out headers with a write() call first and then use
* sendfile to send out the data parts.
*
* TCP_CORK can be set together with TCP_NODELAY and it is
* stronger than TCP_NODELAY.
*/
if (val) {
tp->nonagle |= TCP_NAGLE_CORK;
} else {
tp->nonagle &= ~TCP_NAGLE_CORK;
if (tp->nonagle&TCP_NAGLE_OFF)
tp->nonagle |= TCP_NAGLE_PUSH;
tcp_push_pending_frames(sk);
}
break;
case TCP_NODELAY:
if (val) {
/* TCP_NODELAY is weaker than TCP_CORK, so that
* this option on corked socket is remembered, but
* it is not activated until cork is cleared.
*
* However, when TCP_NODELAY is set we make
* an explicit push, which overrides even TCP_CORK
* for currently queued segments.
*/
tp->nonagle |= TCP_NAGLE_OFF|TCP_NAGLE_PUSH;
tcp_push_pending_frames(sk);
} else {
tp->nonagle &= ~TCP_NAGLE_OFF;
}

数据发送

tcp_sendmsg在这里我们忽略很多细节,只需要知道根据GSO的大小来copy到skb中,按照合适的时机push各个skb, copy所有数据后(或者内存不足),则调用tcp_push执行发送

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
//size_goal表示GSO支持的大小,为mss_now的整数倍,不支持GSO时则相等
mss_now = tcp_send_mss(sk, &size_goal, flags);
// 把msg的用户态数据,按照GSO支持的最大大小,尽量copy到一个skb中
//skb_entail(sk,skb)到发送队列
//还有数据没copy,但是当前skb已经满了,可以发送了
if (forced_push(tp)) { //超过最大窗口的一半没有设置push了
tcp_mark_push(tp, skb); //设置push标记,更新pushed_seq
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); //调用tcp_write_xmit马上发送
} else if (skb == tcp_send_head(sk)) //第一个包,直接发送
tcp_push_one(sk, mss_now);
else{
//说明发送队列前面还有skb等待发送,且距离之前push的包还不是非常久, 则只是继续放到队列中,继续开始创建下一个skb copy
continue
}
out:
//最后的包调用tcp_push发送
tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
...
}
static void skb_entail(struct sock *sk, struct sk_buff *skb)
{
...
tcp_add_write_queue_tail(sk, skb); if (tp->nonagle & TCP_NAGLE_PUSH)
tp->nonagle &= ~TCP_NAGLE_PUSH; //创建新的skb放入发送队列,立刻清楚push选项
}
static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
if (!tcp_send_head(sk))
return;
skb = tcp_write_queue_tail(sk);
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
//利用tsq机制延后发送
/* avoid atomic op if TSQ_THROTTLED bit is already set */
if (!test_bit(TSQ_THROTTLED, &tp->tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &tp->tsq_flags);
}
/* It is possible TX completion already happened
* before we set TSQ_THROTTLED.
*/
if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE) //应用程序标记了很快有新的数据到来,则标记cork,不发送小包
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle); //最终调用tcp_write_xmit
}

tcp_should_autocork

net.ipv4.tcp_autocorking = 1 默认开启
当tcp_autocorking开启后,如果当前skb还没有达到GSO最大值,并且前面还有数据等待发送,也就是不急着发,
返回true后, 利用tsq机制,在网卡发送完成一个包并释放该skb的时候,设置tasklet,在下一个softirq中再次尝试发送

/* If a not yet filled skb is pushed, do not send it if
* we have data packets in Qdisc or NIC queues :
* Because TX completion will happen shortly, it gives a chance
* to coalesce future sendmsg() payload into this skb, without
* need for a timer, and with no latency trade off.
* As packets containing data payload have a bigger truesize
* than pure acks (dataless) packets, the last checks prevent
* autocorking if we only have an ACK in Qdisc/NIC queues,
* or if TX completion was delayed after we processed ACK packet.
*/
static bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb,
int size_goal)
{
return skb->len < size_goal && //不到最大GSO size
sysctl_tcp_autocorking && //默认开启
skb != tcp_write_queue_head(sk) && //发送队列前面还有其他skb
atomic_read(&sk->sk_wmem_alloc) > skb->truesize; //qdisc中有数据, 说明网卡发送后完成中断释放内存,会很快有新的数据到来
}

tcp_write_xmit

tcp_push/tcp_push_one/__tcp_push_pending_frames最终都调用tcp_write_xmit()
执行到tcp_write_xmit说明已经尽最大可能在当前send()系统调用中作GSO,
在tcp_write_xmit()中,则使用nagle来判断是否要等待下一个应用程序传递更多的数据再发送
如果决定发送则调用tcp_transmit_skb()执行最终的发送

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{ max_segs = tcp_tso_segs(sk, mss_now); //当前tso支持的最大segs数量
while ((skb = tcp_send_head(sk))) { //遍历发送队列
tso_segs = tcp_init_tso_segs(skb, mss_now); //skb->len/mss,重新设置tcp_gso_segs,因为在tcp_sendmsg中被清零了
...
if (tso_segs == 1) {//tso_segs=1表示无需tso分段
/* 根据nagle算法,计算是否需要推迟发送数据 */
if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
(tcp_skb_is_last(sk, skb) ?
nonagle : TCP_NAGLE_PUSH)))) //last skb就直接发送
break; //推迟发送
} else { //tso分段
if (!push_one && //不只一个skb
tcp_tso_should_defer(sk, skb, &is_cwnd_limited, //如果发送窗口剩余不多,并且预计下一个ack将很快到来(意味着可用窗口会增加),则推迟发送
max_segs))
break; //可以推迟
}
//不用推迟发送,马上发送
limit = mss_now;
...
if (tcp_small_queue_check(sk, skb, 0)) //tsq检查,qdisc是否达到限制
break;
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) //发送,如果包被qdisc丢了,则退出循环,不继续发送了
break;
tcp_event_new_data_sent(sk, skb);//更新sk_send_head和packets_out
/* 更新struct tcp_sock中的snd_sml字段。记录非全尺寸发送的最后一个字节序号,主要用来做nagle测试
*/
tcp_minshall_update(tp, mss_now, skb);
sent_pkts += tcp_skb_pcount(skb);
if (push_one) //只发一个skb的则退出循环
break;
}
...
//没有数据包inflight,并且有数据等待发送,则准备尝试0窗口探测
return !tp->packets_out && tcp_send_head(sk);
}

tcp_nagle_test

在GSO没有开启,或者在当前send()中的数据不够一个mss的时候,则会调用tcp_nagle_test,来判断是否推迟发送.
以下情况将直接发送

  • 设置了TCP_NAGLE_PUSH。 比如应用程序设置了TCP_NODELAY选项;或是当前包是在发送队列中的最后一个;或者当前SKB达到GSO的最大值了,并超过最大窗口的一半没有设置push了
  • 紧急数据或者fin包
  • 当前包达到了MSS大小
  • 没有设置TCP_NAGLE_CORK,并且上一个发送的小包已经被确认

也就是说对于设置了CORK的小包就不发;或者没设置CORK但是上一个发送的小包还未被确认都延迟发送

/* Return true if the Nagle test allows this packet to be
* sent now.
*/
static inline bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,
unsigned int cur_mss, int nonagle)
{
/* Nagle rule does not apply to frames, which sit in the middle of the
* write_queue (they have no chances to get new data).
*
* This is implemented in the callers, where they modify the 'nonagle'
* argument based upon the location of SKB in the send queue.
*/
if (nonagle & TCP_NAGLE_PUSH)
return true;
/* Don't use the nagle rule for urgent data (or for the final FIN). */
if (tcp_urg_mode(tp) || (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN))
return true;
if (!tcp_nagle_check(skb->len < cur_mss, tp, nonagle))
return true;
//skb->len < cur_mss且设置了TCP_NAGLE_CORK, 或者上一个发送的小包还未被确认, 则推迟发送
return false;
}
static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,
int nonagle)
{
return partial && //skb->len < mss, 也就是说>=mss就直接发送
((nonagle & TCP_NAGLE_CORK) || //设置了cork则使用nagle
(!nonagle && tp->packets_out && tcp_minshall_check(tp))); //有inflight数据且上一个发送的小包还没被确认则进入nagle
}
/* Minshall's variant of the Nagle send check. */
static bool tcp_minshall_check(const struct tcp_sock *tp)
{
return after(tp->snd_sml, tp->snd_una) && //上一个发送的小包还没确认
!after(tp->snd_sml, tp->snd_nxt); //没有回绕
}
static void tcp_minshall_update(struct tcp_sock *tp, unsigned int mss_now,
const struct sk_buff *skb)
{
if (skb->len < tcp_skb_pcount(skb) * mss_now)
tp->snd_sml = TCP_SKB_CB(skb)->end_seq;
}

tcp_tso_should_defer

对于开启了GSO的情况,并且当前skb不只一个分段,则需要tcp_tso_should_defer来判断是否延迟发送
在剩余发送窗口不足且下一个ack可能很快到来的情况下,则推迟发送

static bool tcp_tso_should_defer(struct sock *sk, struct sk_buff *skb,
bool *is_cwnd_limited, u32 max_segs)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
u32 age, send_win, cong_win, limit, in_flight;
struct tcp_sock *tp = tcp_sk(sk);
struct skb_mstamp now;
struct sk_buff *head;
int win_divisor;
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
goto send_now;
if (icsk->icsk_ca_state >= TCP_CA_Recovery)
goto send_now;
/* Avoid bursty behavior by allowing defer
* only if the last write was recent.
*/
if ((s32)(tcp_time_stamp - tp->lsndtime) > 0)
goto send_now;
in_flight = tcp_packets_in_flight(tp);
BUG_ON(tcp_skb_pcount(skb) <= 1 || (tp->snd_cwnd <= in_flight));
send_win = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq; //发送窗口
/* From in_flight test above, we know that cwnd > in_flight. */
cong_win = (tp->snd_cwnd - in_flight) * tp->mss_cache; //拥塞窗口
limit = min(send_win, cong_win); //最大发送窗口剩余
/* If a full-sized TSO skb can be sent, do it. */
if (limit >= max_segs * tp->mss_cache) //支持最大尺寸的tso发送
goto send_now;
/* Middle in queue won't get any more data, full sendable already? */
if ((skb != tcp_write_queue_tail(sk)) && (limit >= skb->len)) //不是发送队列的最后一个,且满足发送窗口
goto send_now; //直接发送,不会有数据被添加到这个skb了
win_divisor = ACCESS_ONCE(sysctl_tcp_tso_win_divisor);
if (win_divisor) {
u32 chunk = min(tp->snd_wnd, tp->snd_cwnd * tp->mss_cache);
/* If at least some fraction of a window is available,
* just use it.
*/
chunk /= win_divisor;
if (limit >= chunk) //剩余的窗口大于总窗口的比例, 默认1/3
goto send_now;
} else {
/* Different approach, try not to defer past a single
* ACK. Receiver should ACK every other full sized
* frame, so if we have space for more than 3 frames
* then send now.
*/
if (limit > tcp_max_tso_deferred_mss(tp) * tp->mss_cache)
goto send_now;
}
head = tcp_write_queue_head(sk);
skb_mstamp_get(&now);
age = skb_mstamp_us_delta(&now, &head->skb_mstamp); //最早的未确认包的距离现在的时间
/* If next ACK is likely to come too late (half srtt), do not defer */
if (age < (tp->srtt_us >> 4)) // 也就是说下一个ack的到来很可能大于1/2的srtt,直接发送
goto send_now;
/* Ok, it looks like it is advisable to defer. */
//当前skb的收到cwnd限制
if (cong_win < send_win && cong_win <= skb->len)
*is_cwnd_limited = true;
//可以推迟发送了
return true;
send_now:
return false;
}

应用程序Tips

    • http服务器的response,要发送http头+sendfile()文件,
      可以先设置TCP_CORK, 然后write() http header, 不让header发出去,
      调用sendfile(), 这时候如果没有达到GSO大小,还是不会发出去
      最后设置TCP_NODELAY,这时候设置了TCP_NAGLE_PUSH, 会马上发出去。 如果你只是取消TCP_CORK, 内核还是会继续判断是否需要nagle。

    • send()的flag参数设置为MSG_MORE, 给内核hint,表示马上会有其他数据到来,内核会自动加上CORK标记,你就不需要多调用一次setsockopt系统调用. 但是设置MSG_EOR并不会马上push数据

    • 启动TCP_NODELAY,就意味着禁用了Nagle算法  http server 一般禁用
       
       
https://www.cnblogs.com/wanpengcoder/p/5366156.html

1. Nagle算法:

是为了减少广域网的小分组数目,从而减小网络拥塞的出现;

该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;

该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发哦送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;

2. 延迟ACK:

如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;

延迟ACK好处:

(1) 避免糊涂窗口综合症;

(2) 发送数据的时候将ack捎带发送,不必单独发送ack;

(3) 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;

3. 当Nagle遇上延迟ACK:

试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS,当第一次写数据到达对端后,对端延迟ack,不发送ack,而本端因为要发送的数据长度小于MSS,所以nagle算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack;这样的情况下,需要等待对端超时发送ack,然后本段才能发送第二次写的数据,从而造成延迟;

4. 关闭Nagle算法:

使用TCP套接字选项TCP_NODELAY可以关闭套接字选项;

如下场景考虑关闭Nagle算法:

(1) 对端不向本端发送数据,并且对延时比较敏感的操作;这种操作没法捎带ack;

(2) 如上写-写-读操作;对于此种情况,优先使用其他方式,而不是关闭Nagle算法:

--使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法;

--把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;

--关闭Nagle算法,调用write两次;有损于网络,通常不考虑;

linux tcp Nagle算法,TCP_NODELAY和TCP_CORK 转载的更多相关文章

  1. TCP Nagle算法&&延迟确认机制

    TCP Nagle算法&&延迟确认机制 收藏 秋风醉了 发表于 3年前 阅读 1367 收藏 0 点赞 0 评论 0 [腾讯云]买域名送云解析+SSL证书+建站!>>> ...

  2. TCP/IP之TCP_NODELAY与TCP_CORK

    TCP/IP之Nagle算法与40ms延迟提到了Nagle 算法.这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagl ...

  3. TCP Nagle算法以及延迟确认(即延迟回复ACK)的学习

    TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认.为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据. (一个连TCP接会 ...

  4. TCP系列29—窗口管理&流控—3、Nagle算法

    一.Nagle算法概述 之前我们介绍过,有一些交互式应用会传递大量的小包(称呼为tinygrams),这些小包的负载可能只有几个bytes,但是TCP和IP的基本头就有40bytes,如果大量传递这种 ...

  5. TCP粘包, UDP丢包, nagle算法

    一.TCP粘包 1. 什么时候考虑粘包 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议,UDP不会出 ...

  6. Nagle算法&&延时确认

    数据流分类 成块数据 交互数据   Rlogin需要远程系统(服务器)回显我们(客户)键入的字符 数据字节和数据字节的回显都需要对方确认 rlogin 每次只发送一个字节到服务器,而Telnet 可以 ...

  7. TCP_NODELAY和TCP_CORK nagle算法和cork算法

    TCP_NODELAY 默认情况下,发送数据採用Nagle 算法.这样尽管提高了网络吞吐量,可是实时性却减少了,在一些交互性非常强的应用程序来说是不同意的.使用TCP_NODELAY选项能够禁止Nag ...

  8. TCP之Nagle算法与TCP_NODELAY

    1. Nagle 算法 在一个 Rlogin 连接上客户一般每次发送一个字节到服务器,这就产生了一些 41 字节长的分组:20 字节的 IP 首部.20 字节的 TCP 首部和 1 个字节的数据.在局 ...

  9. TCP确认延时和Nagle算法

    TCP确认延时和Nagle算法 nagle 算法是   发送端 收到前一个报文的确认然后再发送下一个tcp数据.这样可以避免大量的小数据. TCP_NODELAY选项控制. Delay ACK是   ...

随机推荐

  1. rs232转网络

    rs232转网络 rs232转网络ZLAN5103可以实现RS232/485/422和TCP/IP之间进行透明数据转发.方便地使得串口设备连接到以太网和Internet,实现串口设备的网络化升级.支持 ...

  2. spring-boot-route(十七)使用aop记录操作日志

    在上一章内容中--使用logback管理日志,我们详细讲述了如何将日志生成文件进行存储.但是在实际开发中,使用文件存储日志用来快速查询问题并不是最方便的,一个优秀系统除了日志文件还需要将操作日志进行持 ...

  3. C++ 虚函数简介!程序员必学知识,掌握编程从对象开始!

    本文将简单探究一下 c++ 中的虚函数实现机制.主要基于 vs2013 生成的 32 位代码进行研究,相信其它编译器(比如, gcc )的实现大同小异. 先从对象大小开始 假设我们有如下代码,假设 i ...

  4. 【9】进大厂必须掌握的面试题-DevOps面试

    Q1.DevOps和Agile之间的根本区别是什么? 下表中列出了两者之间的差异. 特征 DevOps--开发运维 Agile--敏捷 敏捷 开发和运营中的敏捷性 只有发展才能敏捷 流程/实践 涉及C ...

  5. 快速掌握Java8 Stream函数式编程技巧

    函数式编程优势 "函数第一位",即函数可以出现在任何地方. 可以把函数作为参数传递给另一个函数,还可以将函数作为返回值. 让代码的逻辑更清晰更优雅. 减少了可变量(Immutabl ...

  6. linux(centos8):zabbix配置邮件报警(监控错误日志)(zabbix5.0)

    一,zabbix5.0发邮件报警的准备工作: zabbix5.0在linux平台上的安装:参见这一篇: https://www.cnblogs.com/architectforest/p/129125 ...

  7. nginx优化:worker_processes/worker_connections/worker_rlimit_nofile

    一,优化nginx的worker进程数 1,worker_processes应设置为多少? worker_processes 4; 如何设置这个值: worker_processes默认值是1,一般要 ...

  8. Linux文件系统和管理-2文件操作命令(中)

    创建空文件和刷新时间 touch touch命令可以用来创建空文件或刷新文件的时间 touch 存在的文件刷新时间,不存在的文件创建空文件 生成指定日期的日志文件 [root@C8-1 ~]# dat ...

  9. 如何使用 Azure Active Directory 认证和 Microsoft Graph 构建 Blazor Web 应用

    如何使用 Azure Active Directory 认证和 Microsoft Graph 构建 Blazor Web 应用 英文原文:https://developer.microsoft.co ...

  10. centos下搭建Jenkins持续集成环境

    1.安装JDK yum install -y java 2.安装jenkins 添加Jenkins库到yum库,Jenkins将从这里下载安装. 1 wget -O /etc/yum.repos.d/ ...