OpenVPN优化之-巨型帧

近几日忙过了头,一直纠结于OpenVPN的性能问题,这实在是个老问题了,几年来一直都是修修补补,直到多线程多处理的实现,解决了server模式服务端的吞吐量问题,使得多个CPU核心可以得到充分的利用。但是对于客户端的优化,一直都没有很好的解决方案。
       也许,粗犷的作风实在是非常适合服务端优化,而客户端优化需要的却是对细致入微的细节之关注。而我,实则一个得其意而忘其形之人,实则不太适合做细活儿,然而我却曾经用废旧的牛仔裤缝制过一款时尚手袋,表里不是那么如一。对于OpenVPN客户端优化这件事,我遇到了“巨型帧”这个术语。

       效果还不错,测试的结果还比较满意,还是老样子,记录一些想法却不记录技术细节,这是为了让自己或者别人日后看到这篇文章后,知道有这么个事却又不能直接拿来就用,这有什么好处呢?这会让自己好好地再次理一遍思路而不是拿来主义的复制命令或者代码。自己写的代码或者命令,一周后,可能就和自己没关系了,半年后,自己也不懂了...但是想法是永恒的,我依然记得小学四年级的时候,我写的一篇关于巴士底狱的过于早熟的短文...

       最近不喝酒了,因为时间不等人,喝酒之后就会早早睡去,什么也干不了了,晚上夜深人静的时候,看看历史书,写写博客比喝酒好。

以太网的一点历史包袱

以太网自打出生之日,一直保持着向下的兼容性,兼容性这个计算机时代可谓最重要的术语在以太网可谓表现得淋漓尽致,完全可以和IA32以及Win32 API相媲美,满足了投资者的心理的同时,方便了消费者,然而对于技术本身,保持兼容性却如临大敌。
       10M以太网时代,对于帧长是有规定的,最大的帧长和最小的帧长都是可以根据线路的物理特性计算出来的,最大的帧长的计算结果是1500Byte。于是,这个1500就成了一个以太网MTU的默认设置,在10M年代,这个长度绝对不短了,实际上它是可以发送的最大长度,超过这个长度的上层数据都要在IP层被分片,然后在接收端重组分片,而这个过程无疑需要更多的处理器时间,消耗资源。为了避免IP分片,传输介质的MTU必须被IP上层的协议所感知,对于TCP而言,这就是MSS协商,细节我就不讲了。MSS协商之后,可以保证TCP每次发送的数据的长度均小于介质的MTU,对于UDP而言,需要应用层自己管理数据报的长度,或者通过MTU发现机制来动态调整。不管怎么说,IP分片能避免则避免。
       在下一节我会描述MTU确定的细节,现在我们知道它在兼容的意义上是1500,事实上,在10M年代,这个1500是电气特性决定的,但是在10G年代,以太网经过了天翻地覆的变化,1500早就不再是电气特性无法跨越的坎了,因此1500剩下的只是一个软件含义,你完全可以设置它为15000,但是即使如此,只要数据经由的路径上有一段路经的MTU是1500,那么就会发生IP分片。这就是历史包袱,可以发送更长的数据,但是却无法发送,因为发送了便可能产生IP分片,而IP分片的处理和重组可能会抵消掉大型数据帧带来的空间节省和时间节省。

分组交换的两个极端

为了避免IP分片,网卡总是被期待发送最小的数据,但是为了数据包处理效率的最大化,网卡总是被期待发送最大的数据,这就是一个矛盾,需要代偿计算权衡。我们知道,分组交换网的每一个分组均要携带元数据,由于协议栈是分层的,对于每一个分组,都要封装多个不同层的协议头。
       分组交换网,它是除了电路交换外的另一种传输形式,但是你可以换一种理解方式而调和二者,如果你把一个整个的数据流封装到一个分组,那就是电路交换,该数据流只有一个分组,一个分组只能有一条路径,这条路经在分组传输前就是客观存在的;如果你把一个整个的数据流拆分成多个分组,那就是分组交换,对于每一个分组,它们经过网络的路经可能是不同的,而在一个时间段内,一段路经上可能会经过不同数据流的分组,统计复用即体现于此。
       如果分组过小,虽然可以避免IP分片,但是同一个数据流直到处理完毕需要的数据分组会过多,因此会有更多的数据空间消耗在元数据上,同时更多的处理器时间消耗在根据协议元数据来进行的路由与交换上,如果分组过大,虽然会有更少的空间和时间消耗在协议元数据上,但是会有IP分片重组的开销,注意,在分组交换网上,IP分片是数据分组超过一定长度后必须的,这是由线路物理上的电气特性决定的,如果非要传输超长的数据分组,将会出现电气层面的故障,比如脉冲畸形等。因此能发送的避免全程IP分片的最大数据长度便是全程每一段路经MTU的最小值。

合时宜的巨型帧

虽然为了兼容,1500依然是诸多以太网卡的默认MTU设置,但是厂商对如今1G/10G等高端网卡以及超五类/六类双绞线以及光纤的高大上特性又不能视而不见,因此保留了对MTU的配置接口,由用户自己来决定自己网卡的MTU值,超过1500Byte的以太帧,为了和原汁原味标准以太网的1500Byte区别,叫做巨型帧,名字挺吓人,实际上也没有巨型到哪去。用户完全可以设置自己的网卡的MTU超过1500,但是能到多少呢?
       标准化问题是又一个棘手的问题,因为在交换以太网环境,线路的特征不再仅仅可以通过网卡和单一标准线缆(比如早期的粗缆,细缆)的电气特性来计算,而和交换机配置,双绞线类别,质量,网卡自协商配置,双工模式等息息相关,所以虽然出现了巨型帧,它还真不好驾驭,如果发送巨型帧,中间MTU偏小的窄路由器会将IP分片不说,可能还会影响对端数据帧的接收,因为连接两台设备的线缆很可能不是一根线缆,即便是一根线缆,两端的网卡特征也不一定相同,这就会导致收发设备对电气特性的解释有所不同,导致数据帧的接收失败。
       不管怎么说,巨型帧是合乎时宜的。如果没有1500作为底线,巨型帧长的标准化进程会快很多。

将OpenVPN当作一种介质

前面说了那么多看似和OpenVPN的优化不相关的东西,其实是很相关的。如果你到现在还不明白OpenVPN的处理流程,请看我之前文章的关于OpenVPN的结构图,在那附图里,我把OpenVPN当成了一种介质。数据从tap网卡发出进入了字符设备,这就进入了这种特殊的介质,等数据加密后发往对端,解密后写入字符设备,然后被对端tap网卡接收,这就走出了这种特殊的介质。
       既然是一种介质,那么肯定有自己的“难以跨越的电气特性”了。这种电气特性告诉人们它是怎么传输数据帧的。对于OpenVPN来讲,它的电气特性就是对数据进行加密,解密。在具体实施上,和在铜线上传输数据帧时的前导脉冲一样,OpenVPN也会在tap网卡出来的数据帧前封装一个所谓的“前导脉冲”,这就是OpenVPN的协议头。我们来看一下这个特殊的OpenVPN介质的传输开销。开销分为三大块,空间开销,时间开销,系统开销。
       在空间上,OpenVPN协议头无疑占据了一定的数据空间,如果tap网卡的MTU是1500,那么加上OpenVPN协议头的长度L,那么OpenVPN数据通道的数据最大长度将会是1500+L,我们知道这个数据作为一个buffer是通过UDP(暂且不谈TCP)套接字传输至物理网卡的,如果物理网卡的MTU也为1500,那么总长度1500+L+UDP/IP协议头的最大长度则肯定超过1500,而这将导致物理网卡上的IP分片,数据到达对端OpenVPN后首先要进行IP分片重组,然后再进入OpenVPN,解封装,解密...如果tap网卡的MTU过小,且远远小于物理网卡的MTU,虽然加上OpenVPN协议头,UDP/IP协议头可能不过超过物理网卡的MTU而不会IP分片,但是其载荷率将会大大降低,因为对于相同长度的数据流片断,会有大量的数据空间浪费在OpenVPN协议头,UDP/IP协议头上,反之,如果tap的MTU过大,载荷率将会大大提高,但是会引发IP分片。在分析完时间开销和系统开销后,我会说一下结论,现代高端网卡的IP分片与重组开销可以忽略。
       在时间上,OpenVPN的加密/解密,HMAC,压缩/解压缩等要消耗大量的CPU时间,经过测量,频繁加密小包的效率将会小于一次性加密大包的效率,延时效率不会有太大变化,而对吞吐率而言,大包加解密效率会明显提高。这个细节虽然不是什么cache在影响,倒是和CPU的流水线处理相关,细节就不多说了,特别是在RISC处理器上,效率更高。
       在系统开销上,由于OpenVPN要经由tap字符设备和tap虚拟网卡交互,通过socket和对端交互,因此系统调用read/write,send/recv将会导致用户态和内核态的切换,而这种切换的数量会随着数据分组的增大而减少。
       现在已经很清晰了,那就是将tap虚拟网卡的MTU设置成超级大!
       那么我们难道不怕OpenVPN和对端通信的socket发送数据过大而导致的IP分片吗?是的,我们宁可让它在本机进行IP分片,也不会通过物理网卡的巨型帧将其发送出去,第一,我不知道它能否被链路对端的设备正确接收,第二,我也不晓得整个路径上将巨型帧携带的数据分组进行IP分片的那台设备的处理效率。但是我对自己的这台设备还是能驾驭得了的,它拥有Intel825XX千兆卡,带有TSO功能,即TCP Segment Offload(类似的UFO,LRO,MS烟囱卸载等等),使用硬件进行IP分片,可以想象,这种卡的处理性能是超级的,和中间的路由设备不同的是,我的设备我了解,对端OpenVPN也拥有类似的网卡硬件配置(是发送巨型帧呢?还是使用硬件的Offload功能就地分片,我想你应该可以权衡所以了吧),因此IP分片的开销可以忽略。

优化实施中的建议

我将tap网卡的MTU设置成超级大,它得以有用武之地的前提是,你得有那么大的数据报文从tap网卡发出,如果两个OpenVPN端点只是两个网络的边缘节点,那么你不能指望数据的出发地一定发出了一个巨型帧,说不定数据出发的端点的MTU只有1500,那么无论如何数据到达tap网卡的时候,最多只有1500Byte(如果中间经由一个MTU只有500的路由器转发,它将会被分片,每一个片断长度更短),因此建议数据出发的端点均发送巨型帧。
       如果数据端点发出了巨型帧,但是在到达OpenVPN节点的拥有超级大MTU的tap网卡之前被分片了怎么办?好办,在软件上,你可以使用Linux加载一个nf_conntrack_ipv4的模块,因为它依赖defrag模块,所以但凡分片的数据报文都会在forward到tap网卡之前重组。或者说,挖掘一下网卡硬件的自动重组机制(据我所知,好像发往本机的数据在硬件芯片中重组实现更容易些)。
       请永远不要指望将多个短的IP报文合并成一个长的IP报文,特别是对于上层协议是UDP而言的数据报文,因为这会使UDP数据报的边界错乱,进而在应用层发生语义错误,IP本身对于分片和重组的处理是不对称的,它能识别哪些分片属于同一个分组,但是一旦重组就无法按照重组前的原样再次分片,分片的依据只有MTU!正如你能将N堆的豆子搅和在一起,却再也无法将其分离一样。对于TCP而言,虽然这么做没有问题,但是你必须进行复杂的处理,比如维护一个哈希表,然后将属于同一个五元组的IP全报文(非分片报文)组成适当长度的新的IP报文,同时跟踪总长度信息,不能超过tap网卡的MTU。这个算法一定要高效,否则将会抵消组合处理大分组带来的好处。事实证明,不管在内核实现这个算法还是在OpenVPN本身实现这个组合算法,都是困难的。
       对于P2P模式的OpenVPN而言,在tap网卡外准备一个和tap网卡MTU大小一样的箱子是个好主意,事实上就是进行一层新的封装,因为反正我知道不管什么数据报文只要经由tap网卡,都是送往唯一的对端去的,因此也就不需要再区分五元组标识的流了。小IP报文来了就塞进箱子,箱子只要塞满就盖上盖子扔进tap网卡,这个主意不错,但是对于server模式却无能为力,事实上,对于server模式,你要准备N个这样的箱子,N等于OpenVPN客户端的数量,每一个IP报文在进入服务端tap网卡前,第一步就是查找自己将要进入的箱子,不说了,我实现的多队列tun网卡已经实现了这个查找。
       为了取得更高的空间效率,同时也是时间效率,我建议使用tun模式替代tap模式,虽然在网到网部署模式下需要配置复杂的iroute指令(毕竟IPv4没有强制必须使用自动配置,事实上它也没有!DHCP是一个极其不完备的蹩脚协议!)。
       最后谈一下Windows平台的TAP-Win32网卡的MTU修改问题。如果你右键点击TAP网卡的属性,配置其MTU为超过1500的任意值,会得到错误提示,不允许超过1500!但是事实上你确实能够在OpenVPN的配置文件中写上tun-mtu 9014这样的指令,也不会报错,但是在实际运行的时候,它并没有采用9014这个MTU,而毅然(这不是“依然”的别字,而就是毅然!)是1500!那么如何才能配置TAP网卡的MTU为所谓的“巨型帧”的大小呢?答案是使用netsh。
       运行netsh interface ipv4 set subinterfaces "TAP..." mtu=9014 sore=persistent这个命令,你的TAP网卡的MTU将会改变为9014,可以用netsh interface ipv4 show subint查看。但是此时你看一下TAP网卡的属性,依然是1500!不管怎样,它确实能发送长度为9014Byte的巨型帧了。我已经不想再吐槽了!总之,微软的接口就是不好用,并且喜欢将简单问题复杂化。如果你想改变一下TAP的MTU,不但你要修改配置文件,还要使用netsh将其“真正地修改掉”!

代偿

IP分片的开销,协议头的开销,这是矛盾的二者。矛盾的他者却是为了构造tap网卡的巨型帧,先在tap重组,然后加密封装完毕后在物理网卡重新分片。孰重孰轻,自己掂量。
       镁光灯照亮了舞台却驱不走孤独,也许为赋新词强说愁的文艺青年真的需要一点闭上眼睛就是天黑的感觉。
       100年前,你上班要走路一个小时,现在你上班依然需要一个小时,只是在车上,动力的发展总是让你和目标的距离越来越远。
       孰重孰轻,自己掂量,这就是代偿。出来混,早晚要还的...

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。