未加星标

彻底实现Linux TCP的Pacing发送逻辑-普通timer版

字体大小 | |
[系统(linux) 所属分类 系统(linux) | 发布者 店小二05 | 时间 2017 | 作者 红领巾 ] 0人收藏点击收藏
彻底实现linux TCP的Pacing发送逻辑-普通timer版

8小时前来源:CSDN博客

版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创!

又到了周末,过年前的倒数第2个周末,工作和生活上的压力早已卸载,自己也就有必要写点自己觉得感兴趣或者不公道的事情了。即便如此,白天我并不自由,不管是工作日还是周末,我必须在公司或者家里做一些例行的事情,白天无论如何我要去公司上班,不去的话要请假,下班我就要回家,要想彻夜不归,基本没的请假,就这样我一般把路上和晚上的时间当成一种享受,我比较喜欢住在离公司很远的地方,这样我在路上就可以有更多的时间研究古代罗马历史,至于晚上,我是那种随便睡两个小时就好的人。
如果要我说怎么给TCP做优化,我的答案非同寻常,也许有点哗众取宠,曰,降速!作恶的,必被剪除。
Linux的协议栈并不是仅仅指的是TCP,这一点必须要强调!UDP,IP,Netfilter,Bridge,Vlan等等,这些要比TCP重要得多,也好玩的多!说TCP是傻逼协议我不敢,但是我觉得我有资格对它的诸多实现评说一二。
UDP,IP,Netfilter,Bridge,Vlan这些要比TCP重要得多,至少加在一起可以比TCP重要,但是程序员们太待见TCP了,恨不得跪添。视TCP为网络的全部,其实根本就不懂网络,嘴里念念有词,其实也就知道个词。我对这种现状非常气愤,但也只能气愤。
TCP毁了整个网络世界的和谐!
至少迄今为止大多数的TCP实现,根本就不是按照TCP协议最初的规范实现的。你无法重现《TCP/IP详解(卷一)》第20章“TCP的成块数据流”20.7节中的TCP自时钟场景(中文版P218-219),至少是很难重现。大多数人总觉得Linux的实现就是标准的实现,因为他们只见过Linux,TCP也并不例外,所以Linux TCP就是标准。然而这错了!
简单举例,Linux连最基本的双向NAT都没有实现(我自己实现了一个,本寄希望于nftables,然而等了好久一场空!),它在路由和Neighbour以及Bridge实现中也有不如人意之处,你难道能指望Linux的TCP很完美吗?事实上,只是因为用的人多了,认同感就足了,然后就成了标准了吧。外面的世界大得很呐!
Linux TCP实现的最大问题在于TCP数据发送的突发性,如果你计算出来的拥塞窗口是N字节,那么系统会尽可能一次性最多发送N-inflight字节大小的数据,在ACK到达不规律的情况下,这可能是一场悲剧。Linux实现的TCP太依赖ACK时钟了,无奈这个ACK时钟频率可能会变化,这种变化反过来会对数据的发送进行整形!我的意思是说,ACK到达发送端的时候,一般都是被整过形的,一个ACK会触发多个(可能是大量的)数据段的发送,这会影响负反馈的过程。
网络上真实的情况是什么样子的?真实的样子是数据的pacing传输!我们以千兆铜线以太网为例,网卡的极限是一秒传输1000Mbit的数据,一个数据包的大小以1000字节算也就是8000bit,那么网卡在一秒钟内可以传输大约1000000000/8000即125000个数据包,熟悉铜线传输的一般都知道,数据是载波传输的,中间要有几个bit的隔离脉冲,所以说无论怎样,数据都不是背靠背发送的。
仍以上述以太网为例,我们可以算出发送一个数据包所需要的主机延迟,即1/125000秒,几个微秒,这已经非常接近主机的C/Java函数延迟了,问题似乎并不严重。然而现实中,我们的带宽一般都是20-100Mbit/s,此时平均发送一个数据包的时延要几十微秒接近一个毫秒,然而事实上,主机依然是按照主机延迟发送数据的,即几个微秒就发送一个数据包,pacing的任务从网卡(此时可能你依然在使用1000Mbit网卡)转嫁到了更远的交换机/路由器或者其它设备。根源在于主机眼里只有主机,没有BDP!主机意识不到网络延迟和节点处理延迟,主机一厢情愿地以为数据包按照主机延迟到达目的地!
所以,实现TCP的pacing发送势在必行。
其实,社区也早就意识到了这个问题,因此在TCP层根据cwnd和rtt计算了一个pacing rate,其实就是根据BDP的公式计算而得的:
static void tcp_update_pacing_rate(struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); u64 rate; /* set sk_pacing_rate to 200 % of current rate (mss * cwnd / srtt) */ rate = (u64)tp->mss_cache * ((USEC_PER_SEC / 100) << 3); /* current rate is (cwnd * mss) / srtt * In Slow Start [1], set sk_pacing_rate to 200 % the current rate. * In Congestion Avoidance phase, set it to 120 % the current rate. * * [1] : Normal Slow Start condition is (tp->snd_cwnd < tp->snd_ssthresh) * If snd_cwnd >= (tp->snd_ssthresh / 2), we are approaching * end of slow start and should slow down. */ if (tp->snd_cwnd < tp->snd_ssthresh / 2) rate *= sysctl_tcp_pacing_ss_ratio; else rate *= sysctl_tcp_pacing_ca_ratio; rate *= max(tp->snd_cwnd, tp->packets_out); if (likely(tp->srtt_us)) do_div(rate, tp->srtt_us); /* ACCESS_ONCE is needed because sch_fq fetches sk_pacing_rate * without any lock. We want to make sure compiler wont store * intermediate values in this location. */ ACCESS_ONCE(sk->sk_pacing_rate) = min_t(u64, rate, sk->sk_max_pacing_rate); }
然而这个pacing并无卵用。我们看下这个函数的注释:
/* Set the sk_pacing_rate to allow proper sizing of TSO packets.
* Note: TCP stack does not yet implement pacing.
* FQ packet scheduler can be used to implement cheap but effective
* TCP pacing, to smooth the burst on large writes when packets
* in flight is significantly lower than cwnd (or rwin)
*/
这个注释显然并不需要负任何责任。
现在开始本文。
首先,我必须要说的有三点:
1.TCP发送数据完全是突发形式发送,根本就无所谓pacing,这是典型的中国人风格;
2.后来有人在Qdisc层面实现了一个FQ,辅助TCP来完成pacing,但有点词不达意;
3.即便如此,底层Qdisc实现的FQ就是个垃圾!
至于为什么垃圾,为什么不尽人意,尽多的细节,请参见《不同位置的tcptrace分析以及FQ如何减少TCP无效重传》以及《在Wireshark的tcptrace图中看清TCP拥塞控制算法的细节(CUBIC/BBR算法为例)》一文的最后。
这里,我要给出一个“划时代”的思路,有点简单,有点毫无技术含量,但足以抛砖引玉。
在2.6.18版本内核运行的年代,有人提出了一个patch,该patch的LWN文章为《TCP Pacing》,这篇文章作于2006年,那时我还没有毕业但已经参加了工作,我是弃学去工作的,那时我好像已经已经接触到了iptables,但还不懂TCP,所以也就无缘这篇文章。
我是在很晚的时候才开始深入挖掘TCP细节的,因为我并不看好它,我甚至深深地抵触它,直到现在我依然在深深抵触它!XXX!然而,我觉得要想骂谁,首先要了解谁,我觉得我稍微有这个资格。
我一直都想把《TCP Pacing》移植到现在的内核中(几个月前我将它移植到2.6.32内核了),而这是一件非常简单的事情,我的理由只有下面一个:

现在的内核不再需要你自己去计算pacing rate了,内核直接就支持pacing rate!因此,我不再需要那个令人恶心的tcp_pacing_recalc_delta函数了!一切更加简单了!

我的修改在于:

1.我不在针对每一个数据包的传输实施pacing,我只对正常的数据包实施pacing,这意味着重传数据包和纯ACK不会被pacing逻辑控制;
2.我想用Linux内核的hrtimer来作为pacing定时器,这样粒度更加精细,但是失败了;
3.即使使用hrtimer失败了,我依然可以使用timer_list,不同的是,我在计算时间和速度的时候,我使用u64而是u32,最终我只是将结果转成了u32的jiffies;
4.虽然使用hrtimer失败了(原因在于在hrtimer的function里做的事情太多了,造成了interrupt overflow...),我依然可以学着TSQ的样子把事情办成!
好了,代码看起来有点傻,但还是要展出来备忘,懂的人自然知道有多傻,不懂的人自然会觉得高大上。
1.增加数据结构的字段 include/linux/tcp.h里面的tcp_sock结构体里增加pacing支持:
struct { struct timer_list timer; u8 pacing; u64 next_to_send; } pacing;这里就不解释了。
2.修改tcp_write_xmit我的思路很简单。
所有TCP数据的正常发送,最终都要归结于tcp_write_xmit!当然,你可能认为是tcp_transmit_skb,然而我要求的是“更上层”的传输行为,毕竟,我不希望重传数据进行pacing整形,tcp_transmit_skb太底层了!
对于TCP数据的正常发送(而非重传),有两个地方,第一个是顺着socket的write/send调用下来的tcp_write_xmit,这个路径会调用tcp_push_pending_frames,另一个是收到ACK后,在tcp_ack之后的“顺便发送”,这个路径会调用tcp_data_snd_check(内部还是调用tcp_push_pending_frames)。不管怎样,改tcp_write_xmit函数就对了,怎么改呢?超级简单(我基于4.9内核修改),基本就是FQ的逻辑(我并非说FQ的算法垃圾,而是它放错了地方):
while ((skb = tcp_send_head(sk))) { unsigned int limit; u64 now = ktime_get_ns; // 我采集了u64粒度的时间戳 tso_segs = tcp_init_tso_segs(skb, mss_now); ... if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) break; if (sysctl_tcp_pacing && tp->pacing.pacing == 1) { // 这里的if内是我添加的代码 u32 plen; u64 rate, len; if (now < tp->pacing.next_to_send) { // 如果此时不允许发送,那就预约延后的某个时间发送 tcp_pacing_reset_timer(sk, tp->pacing.next_to_send); break; } // 如果设定了确定的rate,那就按照这个rate发送,这个对固定速率需求的流特别好用。更好的做法是将其安装在socket里而不是一个全局的参数! // 然则如果安装在socket里,就必须在应用层用setsockopt来设置,需要修改应用程序,而如果是全局参数,则对应用完全透明。利弊由谁决定?! rate = sysctl_tcp_rate ? sysctl_tcp_rate:sk->sk_pacing_rate; rate = min(rate, sk->sk_pacing_rate); plen = skb->len + MAX_HEADER; // 倾倒在网络中的不光有TCP数据段,还有协议头!别光想着TCP! // 计算延后时间:时间=数据量/速度 len = (u64)plen * NSEC_PER_SEC; if (rate) do_div(len, rate); // 设置下一次发送的时间为:当前时间+发送当前数据的时间 tp->pacing.next_to_send = now + len; } if (tso_segs == 1) { ...
以上代码非常简单!关于发送数据,我指的是正常的发送数据,就是以下一点:
尽情发送,直到速率配额已经完成,然后预约在今后可以发送的时刻继续发送!
3.在net/ipv4/tcp_timer.c中增加pacing timer的init,reset,handler函数首先看下init过程:
void tcp_init_xmit_timers(struct sock *sk) { inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer); init_timer(&(tcp_sk(sk)->pacing.timer)); tcp_sk(sk)->pacing.timer.function = &tcp_pacing_timer; tcp_sk(sk)->pacing.timer.data = (unsigned long) sk; }
再看reset过程:
void tcp_pacing_reset_timer(struct sock *sk, u64 expires) { struct tcp_sock *tp = tcp_sk(sk); u32 timeout = nsecs_to_jiffies(expires); if(!sysctl_tcp_pacing || !tp->pacing.pacing) return; if (!mod_timer(&tp->pacing.timer, timeout)) sock_hold(sk); }
实质上的过程,即timer的回调函数:
static void tcp_pacing_timer(unsigned long data) { struct sock *sk = (struct sock*)data; struct tcp_sock *tp = tcp_sk(sk); if(!sysctl_tcp_pacing || !tp->pacing.pacing) return; bh_lock_sock(sk); if (sock_owned_by_user(sk)) { // 此时socket被用户态进程占据,比如正在recv,poll等,那么就将handler过程委托给这类用户态进程,好在用户态进程在释放socket时可以有release回调可调用! if (!test_and_set_bit(TCP_PACING_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags)) sock_hold(sk); goto out_unlock; } if (sk->sk_state == TCP_CLOSE) goto out; if(!sk->sk_send_head){ goto out; } tcp_push_pending_frames(sk); out: if (tcp_memory_pressure) sk_mem_reclaim(sk); out_unlock: bh_unlock_sock(sk); sock_put(sk); }
在用户态进程释放socket的时候,调用tcp_release_cb:
if (flags & (1UL << TCP_PACING_TIMER_DEFERRED)) { if(sk->sk_send_head) tcp_push_pending_frames(sk); __sock_put(sk); }
罢了,这就罢了。关于hrtimer实现的版本,效果更好,温州老板为何不先试试然后提供给我一些代码呢?给点提示,那就是不要在hrtimer的function里去做那些什么tcp_write_xmit操作,而只是在里面去schedule一个softirq或者一个tasklet即可,就跟TSQ一样。
看看效果吧。我为了不引起质疑,我隐藏了刻度。实际上我根本就没有时间去解释什么没有意义的质疑:
彻底实现Linux TCP的Pacing发送逻辑-普通timer版

左边是pacing的效果,右边是非pacing的效果,统一使用cubic算法。这个pacing对于额定速率的流媒体传输非常有用!这个版本大致可以实现pacing的效果,然而却不够,实际上的效果好坏取决于计算的精准度。以HZ250为例,每隔4毫秒会有一个时钟中断,如果用普通timer的话,RTT在4毫秒以内的pacing将会工作不正常,所以我采用了NS来计算,最终再还原到jiffies,但这仍然不够(不过对于长肥网络这个版本就够了)。所以我必须实现基于hrtimer的高精度版本。

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

分页:12
转载请注明
本文标题:彻底实现Linux TCP的Pacing发送逻辑-普通timer版
本站链接:http://www.codesec.net/view/524500.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 系统(linux) | 评论(0) | 阅读(20)