当今国互联网的飞速发展让人们获益匪浅,同时人们对于互联网
的期望值也变得越来越高。这就形成了一个矛盾,虽然互联网的
发展已经是相当迅猛的了,但是人们还是期望从服务器到客户终
端的文件传输的速度能够比现在再快一些,这种要求(当然是合
理的要求)好像从来也满足不了。在向人们询问“一种什么样的
速度对于数据传输来说才是最理想的”问题时,几乎每一次你都
会得到一种不同的答案:有的人认为数据传输的速率越快越好,
有的人则认为数据传输的速率只要在能够容忍的限度之内就可以
了,而另一人则认为一种数据传输时不会有数据损失的最快速率
才是他们真正需要的,当然,这里还有许多其它的回答,我就不
一一赘述了。
在现实中,对于这个有关数据传输的问题是没有一个统一的答案
的。绝大多数的人在评价数据传输的快慢时,还是会使用“每秒
钟传输的兆字节数”来作为一种衡量的标准。但是,数据传输快
慢的真正衡量标准却是CPU对于每一个传输的兆字节所花费占用
的时间。对于实时应用软件,尤其是那些传输视频或者音频数据
流的软件,最不想出现的一种状况就是所谓的“延迟”。CPU执
行任务的时候如果没有任何的效率,那么,要实现对协议层
(protocol-level)的负载平衡(load balancing)以及将主机
的IP名私人化的支持(一种叫做Virtuozzo的操作系统虚拟化技
术)都是不太可能的。一个加装了Virtuozzo的主机内能够装得
下数以千计的网络站点,所以,尽可能的减少用来处理数据传输
占据的CPU时间是非常重要的。
Sendfile()是一种崭新的操作系统核心,这种新核心能够帮助人
们解决上边所描述的那些问题。而且,这种新内核对于UNIX,
Linux, Solaris 8操作系统来说都是适用的。从技术角度来看,
sendfile()是磁盘和传输控制协议(TCP)之间的一种系统呼叫
,但是sendfile()还能够用来在两个文件夹之间移动数据。在各
种不同的操作系统上实现sendfile()都会有所不同,当然这种不
同只是极为细微的差别。通常来说,我们会假定所使用的操作系
统是Linux核心2.4版本。
系统呼叫的原型有如下几种:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset,
size_t count)
in_fd是一种用来读文件的文件描述符。
out_fd是一种用来写文件的描述符。
Offset是一种指向被输入文件变量位置的指针,sendfile()将会
从它所指向的位置开始数据的读取。
Count表示的是两个文件描述符之间数据拷贝的字节数。
sendfile()的威力在于,它为大家提供了一种访问当前不断膨胀
的Linux网络堆栈的机制。这种机制叫做“零拷贝(zero-copy)”
,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存
储器中传送到网卡的缓存块(network card buffers)中去。为
了更好的理解“零拷贝(zero-copy)”以及sendfile(),让我
们回忆一下以前我们在传送文件时所需要执行的那些步骤。首先
,一块在用户机器存储器内用于数据缓冲的位置先被确定了下来
。然后,我们必须使用read()这条系统呼叫来把数据从文件中拷
贝到前边已经准备好的那个缓冲区中去。(在通常的情况下,这
个操做会把数据从磁盘上拷贝到操作系统的高速缓冲存储器中去
,然后才会把数据从高速缓冲存储器中拷贝至用户空间中去,这
种过程就是所谓的“上下文切换”。)在完成了上述的那些步骤
之后,我们得使用write()系统呼叫来将缓冲区中的内容发送到
网络上去,程序段如下所示:
intout_fd, intin_fd;
char buffer[BUFLEN];
…
/* unsubstantial code skipped for clarity */
…
read(in_fd, buffer, BUFLEN); /* syscall, make context
switch */
write(out_fd, buffer, BUFLEN); /* syscall, make context
switch */
操作系统核心不得不把所有的数据至少都拷贝两次:先是从核心
空间到用户空间的拷贝,然后还得再从用户空间拷贝回核心空间
。每一次操做都需要上下文切换(context-switch)的这个步骤
,其中包含了许多复杂的高度占用CPU的操作。系统自带的工具
vmstat能够用来在绝大多数UNIX以及与其类似的操作系统上显示
当前的“上下文切换(context-switch)”速率。请看叫做“CS
”的那一栏,有相当一部分的上下文切换是发生在取样期间的。
用不同类型的方式进行装载可以让使用者清楚的看到使用这些参
数进行装载时的不同效果。
关于切换过程的一些具体细节
让我们向着有关上下文切换过程的更深层次挖掘,这样做能够让
我们更好的理解有关切换的一些问题。这里有许多种有关从用户
空间呼叫系统的操作的方法。举个例子来说,将虚拟内存页面从
用户空间中切换到核心,然后再切换回去的这种操作是必不可少
的。这种操作过程需要的系统花销是相当大的(尤其是在CPU周
期的占用方面)。这种操做是通过使用叫做全局描述符台面
(Global Descriptor Table)以及局部描述符台面(Local
Descriptor Table)的存储器控制台来实现的。另外的一种结构
被称之为TSS (Task Status Segment任务状态段)的工具也需要
大家给予足够的重视。
此外,还有一些隐藏的非常“昂贵”的操作没有被上下文切换程
序呼叫出来。我们能够通过虚拟内存需要虚拟物理地址翻译操作
的支持才能实现的例子来说明这些隐藏操作的存在。这种翻译所
需要的数据也是存储于存储器中的,所以,CPU每一次对这些数
据存储位置的请求都需要对主存储器进行一次或多次的访问(这
是为了读取翻译台入口),这是除了需要获取的那些数据外还要
进行的一些操作。现在的CPU通常都包含了一个翻译缓存,其缩
写为TLB。TLB是作为页面入口来工作的,其中存储了最近访问过
的对象。(这是对TLB最为简单的解释,其具体的解释应为:OLE
库文件,其中存放了OLE自动化对象的数据类型、模块和接口定
义,自动化服务器通过TLB文件就能了解自动化对象的使用方法
。)TLB高速缓冲存储器拥有巨大的潜在花费,其中包括了几个
存储器的访问操作以及页面错误处理器的实行操作。在拷贝大量
数据的时候,会对TLB高速缓冲存储器产生大量的消耗。在这个
时候,TLB高速缓冲存储器里边会被要拷贝的页面数据占据的容
不下任何别的其它数据。
在有了sendfile()零拷贝(zero-copy)之后,如果可能的话,
通过使用直接存储器访问(Direct Memory Access)的硬件设备
,数据从磁盘读取到操作系统高速缓冲存储器中会变得非常之迅
速。而TLB高速缓冲存储器则被完整无缺的放在那里,没有充斥
任何有关数据传输的文件。应用软件在使用sendfile()
primitive的时候会有很高的性能表现,这是因为系统呼叫没有
直接的指向存储器,因此,就提高了传输数据的性能。通常来说
,要被传输的数据都是从系统缓冲存储器中直接读取的,其间并
没有进行上下文切换的操作,也没有垃圾数据占据高速缓冲存储
器。因此,在服务器应用程序中使用sendfile()能够显著的减少
对CPU的占用。
在我们的这个例子中取代带有mmap() 的read()不会让事情有什
么显著的变化。然而,mmap系统呼叫的请求是从文件中(或者从
其它的一些对象中)生成一些连接信息,这些都是从在虚拟内存
中的文件描述符中指定的。试图从虚拟内存中读取数据会产生一
些磁盘操作。因为系统会把含有映射内容的存储器直接的写入而
不会调用read()以及缓存定位,所以我们能够消除磁盘读取操作
。然而,这种操作会导致TLB高速缓冲存储器的过热,所以CPU在
装载每个字节的传输时的负荷会稍微加大一些。
零拷贝的方法以及应用软件的开发
只要有可能,在任何时候,对性能要求非常苛刻的客户服务器应
用程序的开发都会使用到零拷贝方法(The zero-copy approach
)。设想一下,当我们需要在一个单独的服务器上运行超过一千
个分散的拥有私人IP地址的Apache网络服务器时,就可以使用到
Virtuozzo技术了。为了达到这个目的,我们每一秒钟都不得不
在传输控制协议的层面上处理数以千计的请求来列出客户的请求
并且把主机名页开列出来。如果没有经过最优化的处理和零拷贝
(zero-copy)支持的话,这对于处理器来说将是一项繁重而且
复杂的任务,甚至都超过了网络对它的负荷。零拷贝sendfile()
的实现是基于9K-http-requests-per-second速率的,甚至在速
度相对较慢的Pentium II350 MHz的处理器上也是一样。
然而,零拷贝(zero-copy) sendfile()并不是能解决所有问题
的“万能药”。部分的,减少网络操作的数量,sendfile()系统
呼叫应该和TCP/IP中的TCP_CORK选项在一起共同的使用。在我们
以后的文章中将会讨论应用软件从此选项中获得的好处,以及讨
论其它的一些相关问题。
下面再附上两篇文章供参考。。。
促进高效数据传输的TCP/IP选项
作者: ZDNET CHINA 特稿
2002-07-22 04:11 PM
在前一篇文章里,我们讨论了以下问题:如何采用sendfile()系
统函数降低从磁盘到网络的数据传输负载。接下来我们继续讨论
涉及网络连接控制的另一问题,同时希望通过对这一问题的讨论
能有助于在实际环境下把sendfile()的功能最大化,这就是如何
设置TCP/IP选项来控制套接字的行为。
TCP/IP数据传输
TCP/IP网络的数据传输通常建立在数据块的基础之上。从程序员
的观点来看,发送数据意味着发出(或者提交)一系列“发送数
据块”的请求。在系统级,发送单个数据块可以通过调用系统函
数write() 或者sendfile() 来完成。在网络级可以看到更多的
数据块,通常把它们叫做帧,帧再被包装上一定字节长度的报头
然后通过线路在网络上传输。帧及其报头内部的信息是由若干协
议层定义的,从OSI参考模型的物理层到应用层都可能会牵扯到
。
因为在网络连接中是由程序员来选择最适当的应用协议,所以网
络包的长度和顺序都在程序员的控制之下。同样的,程序员还必
须选择这个协议在软件中得以实现的方式。TCP/IP协议自身已经
有了多种可互操作的实现,所以在双方通信时,每一方都有它自
身的低级行为,这也是程序员所应该知道的情况。
通常情况下,程序员不必关心操作系统和网络协议栈发送和接收
网络数据的方法。系统内置算法定义了低级的数据组织和传输方
式;然而,影响这些算法的行为以及对网络连接施加更大强度控
制能力的方法也是有的。例如,如果某个应用协议使用了超时和
重发机制,程序员就可以采取一定措施设定或者获取超时参数。
他或她还可能需要增加发送和接收缓冲区的大小来保证网络上的
信息流动不会中断。改变TCP/IP协议栈行为的一般的方法是采用
所谓的TCP/IP选项。下面就让我们来看一看你该如何使用这些选
项来优化数据传输。
TCP/IP选项
有好几种选项都能改变TCP/IP协议栈的行为。使用这些选择能对
在同一计算机上运行的其他应用程序产生不利的影响,因此普通
用户通常是不能使用这些选项的(除了root用户以外)。我们在
这里主要讨论能改变单个连接操作(用TCP/IP的术语来说就是套
接字)的选项。
ioctl风格的getsockopt()和setsockopt()系统函数都提供了控
制套接字行为的方式。比方说,为了在Linux上设置TCP_NODELAY
选项,你可以如下编写代码:
intfd, on = 1;
…
/* 此处是创建套接字等操作,出于篇幅的考虑省略*/
…
setsockopt (fd, SOL_TCP, TCP_NODELAY, &on, sizeof (on));
尽管有许多TCP选项可供程序员操作,而我们却最关注如何处置
其中的两个选项,它们是TCP_NODELAY 和 TCP_CORK,这两个选
项都对网络连接的行为具有重要的作用。许多UNIX系统都实现了
TCP_NODELAY选项,但是,TCP_CORK则是Linux系统所独有的而且
相对较新;它首先在内核版本2.4上得以实现。此外,其他UNIX
系统版本也有功能类似的选项,值得注意的是,在某种由BSD派
生的系统上的TCP_NOPUSH选项其实就是TCP_CORK的一部分具体实
现。
TCP_NODELAY和TCP_CORK基本上控制了包的“Nagle化”,Nagle
化在这里的含义是采用Nagle算法把较小的包组装为更大的帧。
John Nagle是Nagle算法的发明人,后者就是用他的名字来命名
的,他在1984年首次用这种方法来尝试解决福特汽车公司的网络
拥塞问题(欲了解详情请参看IETF RFC 896)。他解决的问题就
是所谓的silly window syndrome ,中文称“愚蠢窗口症候群”
,具体含义是,因为普遍终端应用程序每产生一次击键操作就会
发送一个包,而典型情况下一个包会拥有一个字节的数据载荷以
及40个字节长的包头,于是产生4000%的过载,很轻易地就能令
网络发生拥塞,。 Nagle化后来成了一种标准并且立即在因特网
上得以实现。它现在已经成为缺省配置了,但在我们看来,有些
场合下把这一选项关掉也是合乎需要的。
现在让我们假设某个应用程序发出了一个请求,希望发送小块数
据。我们可以选择立即发送数据或者等待产生更多的数据然后再
一次发送两种策略。如果我们马上发送数据,那么交互性的以及
客户/服务器型的应用程序将极大地受益。例如,当我们正在发
送一个较短的请求并且等候较大的响应时,相关过载与传输的数
据总量相比就会比较低,而且,如果请求立即发出那么响应时间
也会快一些。以上操作可以通过设置套接字的TCP_NODELAY选项
来完成,这样就禁用了Nagle算法。
另外一种情况则需要我们等到数据量达到最大时才通过网络一次
发送全部数据,这种数据传输方式有益于大量数据的通信性能,
典型的应用就是文件服务器。应用Nagle算法在这种情况下就会
产生问题。但是,如果你正在发送大量数据,你可以设置
TCP_CORK选项禁用Nagle化,其方式正好同TCP_NODELAY相反
(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。下面就让我们仔
细分析下其工作原理。
假设应用程序使用sendfile()函数来转移大量数据。应用协议通
常要求发送某些信息来预先解释数据,这些信息其实就是报头内
容。典型情况下报头很小,而且套接字上设置了TCP_NODELAY。
有报头的包将被立即传输,在某些情况下(取决于内部的包计数
器),因为这个包成功地被对方收到后需要请求对方确认。这样
,大量数据的传输就会被推迟而且产生了不必要的网络流量交换
。
但是,如果我们在套接字上设置了TCP_CORK(可以比喻为在管道
上插入“塞子”)选项,具有报头的包就会填补大量的数据,所
有的数据都根据大小自动地通过包传输出去。当数据传输完成时
,最好取消TCP_CORK 选项设置给连接“拔去塞子”以便任一部
分的帧都能发送出去。这同“塞住”网络连接同等重要。
总而言之,如果你肯定能一起发送多个数据集合(例如HTTP响应
的头和正文),那么我们建议你设置TCP_CORK选项,这样在这些
数据之间不存在延迟。能极大地有益于WWW、FTP以及文件服务器
的性能,同时也简化了你的工作。示例代码如下:
intfd, on = 1;
…
/* 此处是创建套接字等操作,出于篇幅的考虑省略*/
…
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /*
cork */
write (fd, …);
fprintf (fd, …);
sendfile (fd, …);
write (fd, …);
sendfile (fd, …);
…
on = 0;
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /*
拔去塞子 */
不幸的是,许多常用的程序并没有考虑到以上问题。例如,Eric
Allman编写的sendmail就没有对其套接字设置任何选项。
Apache HTTPD是因特网上最流行的Web服务器,它的所有套接字
就都设置了TCP_NODELAY选项,而且其性能也深受大多数用户的
满意。这是为什么呢?答案就在于实现的差别之上。由BSD衍生
的TCP/IP协议栈(值得注意的是FreeBSD)在这种状况下的操作
就不同。当在TCP_NODELAY 模式下提交大量小数据块传输时,大
量信息将按照一次write()函数调用发送一块数据的方式发送出
去。然而,因为负责请求交付确认的记数器是面向字节而非面向
包(在Linux上)的,所以引入延迟的概率就降低了很多。结果
仅仅和全部数据的大小有关系。而 Linux 在第一包到达之后就
要求确认,FreeBSD则在进行如此操作之前会等待好几百个包。
在Linux系统上,TCP_NODELAY的效果同习惯于BSD TCP/IP协议栈
的开发者所期望的效果有很大不同,而且在Linux上的Apache性
能表现也会更差些。其他在Linux上频繁采用TCP_NODELAY的应用
程序也有同样的问题。
相得益彰
你的数据传输并不需要总是准确地遵守某一选项或者其它选择。
在那种情况下,你可能想要采取更为灵活的措施来控制网络连接
:在发送一系列当作单一消息的数据之前设置TCP_CORK,而且在
发送应立即发出的短消息之前设置TCP_NODELAY。
把零拷贝和sendfile() 系统函数结合起来(前文有述)可以显
著地提升系统整体效率并且降低CPU负载。我们采用这一技术为
Swsoft’s Virtuozzo公司开发了基于名称的主机托管子系统,
实践经验表明,该技术可以在装备350-MHz Pentium II CPU的PC
上实现每秒9000个HTTP请求,这一成绩在以前几乎是不可能实现
的。性能上的提高显而易见。
利用TCP/IP选项优化数据传输(第2部分)
作者: BUILDER.COM
2002-07-22 04:15 PM
上回,我们对TCP_CORK选项如何减少网络传输包的数量做了一番
原理性的解释。减少网络流量当然是非常重要的优化举措之一,
不过这种手段也仅仅是实现高性能网络数据传输领域的一个方面
。其他TCP选项也可能显著提供传输性能同时在某些条件下减少
服务器的响应时间延迟。下面就让我们来了解一些此类选项。
TCP_DEFER_ACCEPT
我们首先考虑的第1个选项是TCP_DEFER_ACCEPT(这是Linux系统
上的叫法,其他一些操作系统上也有同样的选项但使用不同的名
字)。为了理解TCP_DEFER_ACCEPT选项的具体思想,我们有必要
大致阐述一下典型的HTTP客户/服务器交互过程。请回想下TCP是
如何与传输数据的目标建立连接的。在网络上,在分离的单元之
间传输的信息称为IP包(或IP 数据报)。一个包总有一个携带
服务信息的包头,包头用于内部协议的处理,并且它也可以携带
数据负载。服务信息的典型例子就是一套所谓的标志,它把包标
记代表TCP/IP协议栈内的特殊含义,例如收到包的成功确认等等
。通常,在经过“标记”的包里携带负载是完全可能的,但有时
,内部逻辑迫使TCP/IP协议栈发出只有包头的IP包。这些包经常
会引发讨厌的网络延迟而且还增加了系统的负载,结果导致网络
性能在整体上降低。
现在服务器创建了一个套接字同时等待连接。TCP/IP式的连接过
程就是所谓“3次握手”。首先,客户程序发送一个设置SYN标志
而且不带数据负载的TCP包(一个SYN包)。服务器则以发出带
SYN/ACK标志的数据包(一个SYN/ACK包)作为刚才收到包的确认
响应。客户随后发送一个ACK包确认收到了第2个包从而结束连接
过程。在收到客户发来的这个SYN/ACK包之后,服务器会唤醒一
个接收进程等待数据到达。当3次握手完成后,客户程序即开始
把“有用的”的数据发送给服务器。通常,一个HTTP请求的量是
很小的而且完全可以装到一个包里。但是,在以上的情况下,至
少有4个包将用来进行双向传输,这样就增加了可观的延迟时间
。此外,你还得注意到,在“有用的”数据被发送之前,接收方
已经开始在等待信息了。
为了减轻这些问题所带来的影响,Linux(以及其他的一些操作
系统)在其TCP实现中包括了TCP_DEFER_ACCEPT选项。它们设置
在侦听套接字的服务器方,该选项命令内核不等待最后的ACK包
而且在第1个真正有数据的包到达才初始化侦听进程。在发送
SYN/ACK包之后,服务器就会等待客户程序发送含数据的IP包。
现在,只需要在网络上传送3个包了,而且还显著降低了连接建
立的延迟,对HTTP通信而言尤其如此。
这一选项在好些操作系统上都有相应的对等物。例如,在
FreeBSD上,同样的行为可以用以下代码实现:
/* 为明晰起见,此处略去无关代码 */
struct accept_filter_arg af = { "dataready", "" };
setsockopt(s, SOL_SOCKET, SO_ACCEPTFILTER, &af, sizeof
(af));
这个特征在FreeBSD上叫做“接受过滤器”,而且具有多种用法
。不过,在几乎所有的情况下其效果与TCP_DEFER_ACCEPT是一样
的:服务器不等待最后的ACK包而仅仅等待携带数据负载的包。
要了解该选项及其对高性能Web服务器的重要意义的更多信息请
参考Apache文档上的有关内容。
就HTTP客户/服务器交互而言,有可能需要改变客户程序的行为
。客户程序为什么要发送这种“无用的”ACK包呢?这是因为,
TCP协议栈无法知道ACK包的状态。如果采用FTP而非HTTP,那么
客户程序直到接收了FTP服务器提示的数据包之后才发送数据。
在这种情况下,延迟的ACK将导致客户/服务器交互出现延迟。为
了确定ACK是否必要,客户程序必须知道应用程序协议及其当前
状态。这样,修改客户行为就成为必要了。
对Linux客户程序来说,我们还可以采用另一个选项,它也被叫
做TCP_DEFER_ACCEPT。我们知道,套接字分成两种类型,侦听套
接字和连接套接字,所以它们也各自具有相应的TCP选项集合。
因此,经常同时采用的这两类选项却具有同样的名字也是完全可
能的。在连接套接字上设置该选项以后,客户在收到一个
SYN/ACK包之后就不再发送ACK包,而是等待用户程序的下一个发
送数据请求;因此,服务器发送的包也就相应减少了。
TCP_QUICKACK
阻止因发送无用包而引发延迟的另一个方法是使用TCP_QUICKACK
选项。这一选项与 TCP_DEFER_ACCEPT不同,它不但能用作管理
连接建立过程而且在正常数据传输过程期间也可以使用。另外,
它能在客户/服务器连接的任何一方设置。如果知道数据不久即
将发送,那么推迟ACK包的发送就会派上用场,而且最好在那个
携带数据的数据包上设置ACK 标志以便把网络负载减到最小。当
发送方肯定数据将被立即发送(多个包)时,TCP_QUICKACK选项
可以设置为0。对处于“连接”状态下的套接字该选项的缺省值
是1,首次使用以后内核将把该选项立即复位为1(这是个一次性
的选项)。
在某些情形下,发出ACK包则非常有用。ACK包将确认数据块的接
收,而且,当下一块被处理时不至于引入延迟。这种数据传输模
式对交互过程是相当典型的,因为此类情况下用户的输入时刻无
法预测。在Linux系统上这就是缺省的套接字行为。
在上述情况下,客户程序在向服务器发送HTTP请求,而预先就知
道请求包很短所以在连接建立之后就应该立即发送,这可谓HTTP
的典型工作方式。既然没有必要发送一个纯粹的ACK包,所以设
置TCP_QUICKACK为0以提高性能是完全可能的。在服务器方,这
两种选项都只能在侦听套接字上设置一次。所有的套接字,也就
是被接受呼叫间接创建的套接字则会继承原有套接字的所有选项
。
通过TCP_CORK、TCP_DEFER_ACCEPT和TCP_QUICKACK选项的组合,
参与每一HTTP交互的数据包数量将被降低到最小的可接受水平(
根据TCP协议的要求和安全方面的考虑)。结果不仅是获得更快
的数据传输和请求处理速度而且还使客户/服务器双向延迟实现
了最小化。
小结
网络程序的性能优化显然是一项复杂的任务。优化技术包括:尽
可能使用零拷贝、用TCP_CORK及其等价选项组装适当的数据包、
把传输数据包的数量最小化以及延迟优化等。为了提升网络、系
统的性能和可伸缩性,有必要在程序代码中联合一致地采用以上
各种可用方法。当然,清楚了解TCP/IP协议栈和操作系统的内部
工作原理也是必要的。