isware

[转]使用sendfile()让数据传输得到最优化

作者: ZDNET CHINA 特稿
Monday, July 22 2002 4:12 PM



当今国互联网的飞速发展让人们获益匪浅,同时人们对于互联网的期望值也变得越来越高。这就形成了一个矛盾,虽然互联网的发展已经是相当迅猛的了,但 是人们还是期望从服务器到客户终端的文件传输的速度能够比现在再快一些,这种要求(当然是合理的要求)好像从来也满足不了。在向人们询问“一种什么样的速 度对于数据传输来说才是最理想的”问题时,几乎每一次你都会得到一种不同的答案:有的人认为数据传输的速率越快越好,有的人则认为数据传输的速率只要在能 够容忍的限度之内就可以了,而另一人则认为一种数据传输时不会有数据损失的最快速率才是他们真正需要的,当然,这里还有许多其它的回答,我就不一一赘述 了。

在现实中,对于这个有关数据传输的问题是没有一个统一的答案的。绝大多数的人在评价数据传输的快慢时,还是会使用“每秒钟传输的兆字节数”来作为一 种衡量的标准。但是,数据传输快慢的真正衡量标准却是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协议栈和操作系统的内部工作原理也是必要的。

posted on 2011-07-21 14:59 艾斯维亚 阅读(696) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理