linux tcp Nagle算法,TCP_NODELAY和TCP_CORK 转载
转载自:
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_CORK
大多数Web Server为了提高性能,在发送数据是并不会直接使用write()
,一个典型的例子就是,Web Server响应客户端请求的时候,它需要先发送HTTP响应header,接着发送网页的内容,而网页的内容存在于磁盘中,为了减少数据的拷贝开销,通常是使用sendfile()
去发送页面内容的,这种情况下,应用程序就不需要在用户态分配内存来存储页面内容了。
const char *filename = "index.html"; fd = open(filename, O_RDONLY) write(http_resp_header); sendfile(sockfd, fd, &off, len);
为了发送HTTP响应,Server调用了一次write()
和一次sendfile()
,在开启TCP_NODELAY
的情况下,这会导致至少两个TCP segment发送出去。但更多时候页面的数据是很少的,在这种情况下,write()
会发送一个segment,sendfile()
也会发送一个segment,那么有没有办法让这两个segment合并在一起再发送出去呢?
为解决这个问题,Linux提供了TCP_CORK
选项,如果在某个TCP socket上开启了这个选项,那就相当于在这个socket的出口堵上了塞子,往这个socket写入的数据都会聚集起来。虽然堵上了塞子,但是segment总得发送,不然数据会塞满整个TCP发送缓冲区的,那么什么时候塞子会打开呢?下面几种情况都会导致这个塞子打开,这样TCP就能继续发送segment出来了。
- 程序取消设置TCP_CORK这个选项。
- socket聚集的数据大于一个MSS的大小。
- 自从堵上塞子写入第一个字节开始,已经经过200ms。
- socket被关闭了。
一旦满足上面的任何一个条件,TCP就会将数据发送出去。对于Server来说,发送HTTP响应既要发送尽量少的segment,同时又要保证低延迟,那么需要在写完数据后显式取消设置TCP_CORK
选项,让数据立即发送出去:
int state = 1; setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &state, sizeof(state)); write(http_resp_header); sendfile(sockfd, fd, &off, len); state = 0; setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &state, sizeof(state));
数据发送
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 一般禁用
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两次;有损于网络,通常不考虑;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!