爱心技术专栏专题

Apache性能提示

摘录:linux 来源:linux 加入时间:2007年04月12日
摘要:
Apache性能提示
Apache 性能提示

Dean


简介
Apache是把正确性放在首位、把速度放在其次的通用Web服务器。即使这样,它的性能十分令人满意。许多站点只有不到10M的出口带宽。Apache能够在这些站点的低端Pentium服务器上全速工作。实际上,拥有更多带宽的站点出于一些原因(比如大量的CGI和数据库事务处理)需要用一台以上的机器满足带宽需求。这些原因导…

Apache性能提示

站点:爱心种子小博士 关键字:Apache性能提示

Apache 性能提示

Dean


简介
Apache是把正确性放在首位、把速度放在其次的通用Web服务器。即使这样,它的性能十分令人满意。许多站点只有不到10M的出口带宽。Apache能够在这些站点的低端Pentium服务器上全速工作。实际上,拥有更多带宽的站点出于一些原因(比如大量的CGI和数据库事务处理)需要用一台以上的机器满足带宽需求。这些原因导致了以往的Apache开发工作集中在正确性和可配置性。

不幸的是许多人过于重视某些指标,并把它们的原始数据当作评价Web服务器优劣的标准。被普遍接受标准的是原始最低性能(bare minimum performance),而在这以外的其他速度指标只适用于很小部分的市场需求。但为了避免Apache在一些市场中受到排挤,我们在Apache1.3上尽了相当的努力,将它与高端服务器的差距减至最小。

另有一些人只是想试试这些东东能运行得多快。这些人竭力把Apache最后一滴性能挤出来,他们也想看看究竟是什么影响了Apache的性能。这篇文章的其余部分就是针对他们而撰的。

请注意本文适用于Unix上的Apache1.3,部分内容适用于NT平台。目前的Apache尚未在NT上进行优化。事实上,不同的编程模型使它在NT上的性能表现相当不好。(即POSIX模型。NT借助POSIX子系统模拟这种编程标准,因此效率很低。Apache2.0抛弃了POSIX直接与操作系统打交道,性能将有所飞跃——译者注)

关于硬件平台和操作系统

最直接影响Web服务器性能的硬件要数RAM。一台Web服务器从不应该访问内存交换区。交换增加了每次请求的延时,用户将因此认为不够快。他们会点击[停止]并重新装载网页,这将进一步增加服务器的负担。您能够也有必要调节MaxClients,使您的服务器不会衍生太多的子进程而导致交换。

除此之外的事情就没那么关键了。拥有快速的CPU、快速的网卡和硬盘都可以让您的服务器足够快。其实这足够快个词是需要凭经验去体会的。

操作系统的选用也是值得斟酌的大问题。普遍的准则是:及时得到操作系统提供商的最新TCP/IP补丁。迅速涌现的HTTP服务打破了截止到1994年乃至95年的Unix内核中设定的许多假设情况。理想的选择包括目前的FreeBSD和Linux。

关于运行时设置(Run-Time Configuration)

HostnameLookups
1.3版以前的Apache中,HostnameLookups的缺省值是On,这将导致每次请求时服务器都要进行NDS查询,从而增加了延迟。Apache1.3将此缺省值设为Off。在1.3及以后的版本中,如果您使用了任何allow from domain或deny from domain命令,所付出的代价将是两次DNS查询带来的延时(在一次逆向查询后跟着一次正向查询,以保证前者得到的结果是真实的)。因此为了得到最理想的性能应避免使用HostnameLookups(使用IP地址而非域名也是个好主意)。

限制命令的使用范围是可行的,比如使用类似的容器。这种情况下,DNS查询只发生在符合条件的请求中。下面的例子使查询只发生在.html和.cgi文件的请求中:

HostnameLookups off

HostnameLookups on

关闭了DNS查询后,如果在您的CGI程序中需要DNS名称的话,可以考虑在那些程序中调用gethostbyname。

FollowSymLinks 和 SymLinksIfOwnerMatch
在任何情况下,只要您没有指定FollowSymLinks的选项(即Options FollowSymLinks),或者指定了SymLinksIfOwnerMatch选项,Apache将不得不调用额外的系统函数来检查符号链接。每次针对文件名的请求都将触发一次检查。比如您指定了:

DocumentRoot /www/htdocs

Options SymLinksIfOwnerMatch

当一个指向URI /index.html的请求到来时,Apache将对/www,/www/htdocs和/www/htdocs/index.html分别调用lstat(2)。不仅如此,lstat的结果是从不被缓存的,因此每次请求都要重新这样的检查。如果您的确需要安全的符号链接的话,可以试着这样做:

DocumentRoot /www/htdocs

Options FollowSymLinks


Options -FollowSymLinks +SymLinksIfOwnerMatch

这至少避免了对DocumentRoot目录本身的检查。请注意,如果在RocumentRoot之外有Alias或者RewriteRule涉及的目录,您需要为这些目录增加类似的选项。为了在无符号链接检查的情况下得到最佳性能,请在所有地方设置FollowSymLinks,并去掉所有的SymLinksIfOwnerMatch。

AllowOverride
在任何情况下,只要您允许覆盖(通常是.htaccess文件),Apache将试图为每次针对文件名称的请求打开.htaccess文件。比如:

DocumentRoot /www/htdocs

AllowOverride all

当指向URI /index.html的请求到来时,Apache将试图打开/.htaccess、/www/.htaccess和/www/htdocs/.htaccess。这个问题可以用类似解决FollowSymLinks的方法解决。为了得到最佳性能,在所有地方使用AllowOverride None。

内容协商
如果您对每处细微的性能调节都很在意,在可能的情况下避免内容协商(content-negotiation)。实际应用中,协商的益处超过了给性能带来的损失。您可以在一种情况下提速服务器:避免使用这样的通配符:

DirectoryIndex index
请列出所有可能的情况:

DirectoryIndex index.cgi index.pl index.shtml index.html
并把最常用的选择放在前面。

 

进程的建立
对于1.3版以前的Apache,MinSpareServers、MaxSpareServers、和StartServers这三个参数对性能测试的结果有巨大影响。Apache启动后需要一个爬升期使其子进程数与服务器的负载相平衡。刚刚启动的Apache生成StartServers个子进程。而后将每隔一秒生成一个新的子进程,最终达到MinSpareServers的要求。所以如果服务器用StartServers等于5的默认值启动后被100个客户并发访问,Apache将用后续的95秒种生成足够的子进程以平衡负载。由于现实中的服务器不经常启动,这种技术在实际应用中工作得很好。但在评测软件中的表现就不那么出色了,因为这些软件可能顶多运行10分钟。

一秒一个的规则防止服务器在生成子进程时过于忙碌。如果它忙于繁殖进程,请求将被搁置。但这个规则对直观性能的影响太大了,它必须有所改观。在Apache 1.3中,一秒一个的规则被废弃了。它首先衍生一个子进程,等一秒,衍生两个,等一秒,再衍生两个,直到一秒衍生32个子进程。随后它将保持这个速度直到满足MinSpareServers的要求。

这看起来足够好了。几乎不用在MinSpareServers、MaxSpareServers或StartServers上费工夫了。当每秒钟衍生的进程数超过4时,ErrorLog中会增加一条相应的记录。如果您看到了很多这样的提示,请调整这些参数。mod_status的输出会给您一些提示。

于进程相关的问题是由MaxRequestsPerChild导致的进程终止。MaxRequestsPerChild缺省地设置为0,意味每个子进程处理的请求数不受限制。如果当前的设置值非常小,您可能希望大幅度提升这个值。为了防止内存泄露,在SunOS或者低版本的Solaris上,应把此值设为10000左右。

如果使用了持续连接(keep-alives),子进程将繁忙等待(busy waiting)已打开连接的后续请求而不能做其他的事。缺省的15秒种试图使影响将至最底。您需要在网络带宽和服务器资源之间作出权衡。任何情况下,不应设置持续连接时间超过60秒。否则大部分好处将变成损失。

关于编译时设置

mod_status 和 ExtendedStatus On
如果在编译Apache时您包含了mod_status并且将ExtendedStatus设置为On,Apache将为每个请求进行两次gettimeofday(2)系统调用(或者针对不同的系统调用times(2))及(在1.3以前的版本)许多次time(2)。这些都是为了在报告中含有时间戳。为了得到最佳性能,请将ExtendedStatus设为Off(这是缺省的设置)。

多socket中的accept 串行化
这部分文章将讨论Unix socket API不利的一方面。假设您的服务器用多个Listen命令侦听多个端口或者多个IP地址。Apache使用select(2)检测每个socket连接(connection)是否就绪。select(2)示意有零个或至少一个连接等待某个socket。Apache含有多个子进程,所有空闲的子进程同时侦听新的连接。原始的实现如下所示(这个例子不是真正的代码,它出于教学目的被简化了)

for (;;) {
for (;;) {
fd_set accept_fds;

FD_ZERO (&accept;_fds);
for (i = first_socket; i <= last_socket; ++i) {
FD_SET (i, &accept;_fds);
}
rc = select (last_socket+1, &accept;_fds, NULL, NULL, NULL);
if (rc < 1) continue;
new_connection = -1;
for (i = first_socket; i <= last_socket; ++i) {
if (FD_ISSET (i, &accept;_fds)) {
new_connection = accept (i, NULL, NULL);
if (new_connection != -1) break;
}
}
if (new_connection != -1) break;
}
process the new_connection;
}
但这种实现会引起严重的饥饿问题。由于多个子进程同时执行这个循环,它们将在select中阻塞。当任何socket上出现一个请求时,所有被阻塞的进程将复苏,并从select返回(苏醒进程的数量取决于操作系统和时间)。它们将继续执行并试图接受这个连接,但只有一个进程会成功(假设目前仍只有一个连接),其余进程将阻塞在accept中。这将把所有失败的进程锁定,使它们只为一个socket上的请求服务。它们会一直被阻塞,直到在那个socket上出现足够的请求把它们唤醒。这一饥饿问题首先在PR#467被提出。至少有两种解决它的方法。

一种方案是使用非阻塞的socket。这种情况下,accept不会阻塞子进程,它们将会立即返回。但这种方案会造成CPU时间的浪费。假设有十个在select中的空闲进程,而后到来了一个连接请求。九个进程将苏醒、试图接受连接、失败,并返回select,这些进程实际什么都没做。而且如果在这期间,其他socket上出现请求,没有哪个进程会为它服务。总而言之,这种方案不是十分有效,除非您拥有和空闲子进程数目相当的CPU——恐怕不切实际。

另一种方案被Apache采纳。这种方案串行化(serialize)对内层循环的调用。代码如下所示(改进的部分被加粗显示):

for (;;) {
accept_mutex_on ();
for (;;) {
fd_set accept_fds;

FD_ZERO (&accept;_fds);
for (i = first_socket; i <= last_socket; ++i) {
FD_SET (i, &accept;_fds);
}
rc = select (last_socket+1, &accept;_fds, NULL, NULL, NULL);
if (rc < 1) continue;
new_connection = -1;
for (i = first_socket; i <= last_socket; ++i) {
if (FD_ISSET (i, &accept;_fds)) {
new_connection = accept (i, NULL, NULL);
if (new_connection != -1) break;
}
}
if (new_connection != -1) break;
}
accept_mutex_off ();
process the new_connection;
}
 

accept_mutex_on和accept_mutex_off 两个函数实现了互斥量(mutual exclusion semaphore),在任意时刻只能有一个子进程拥有互斥量。多种方法可以实现互斥量。在src/conf.h(1.3版之前)或src/include/ap_config.h(1.3版及以后)可以作出以下选择。一些系统不提供任何互斥方法。在这些系统上使用多个Listen命令是不保险的。

USE_FLOCK_SERIALIZED_ACCEPT
此方法用flock(2)系统调用对一个锁文件加锁。(此文件在LockFile命令中指定)
USE_FCNTL_SERIALIZED_ACCEPT
此方法用flock(2)系统调用对一个锁文件加锁。(此文件在LockFile命令中指定)
USE_SYSVSEM_SERIALIZED_ACCEPT
(1.3版及以后)此方法借助SysV的信号量(semaphores)实现互斥。但不巧的是SysV信号量有一些负面作用。一是Apache可能在清除信号量之前非正常终止;二是在使用信号量API时需要考虑到任何与服务器UID相同的CGI程序可以进行拒绝服务攻击(就是说所有的CGI程序都可以这样做,除非使用suexec或cgiwrapper之类的方法)。所以,这种方法并不被IRIX之外的系统广泛采纳(由于大多数IRIX系统上,使用前两种方法的代价太大)。
USE_USLOCK_SERIALIZED_ACCEPT
(1.3版及以后)此方法仅在IRIX上可用。它调用usconfig(2)创建互斥量。虽然这种方法避免了对SysV信号量的种种争议,但它不是IRIX的缺省方案。这是由于在单处理器的IRIX系统 (5.3或6.2)上,uslock代码比SysV信号量慢两个数量级;但在多处理器的IRIX中前者比后者快一个数量级。这无非使问题复杂化了。所以在多处理器IRIX系统上,您需要用如下的附加参数编译Apache:
在EXTRA_CFLAGS中添加-DUSE_USLOCK_SERIALIZED_ACCEPT
USE_PTHREAD_SERIALIZED_ACCEPT
(1.3版及以后)此方法实现了POSIX标准互斥量。它理应可以工作在任何实现了全部POSIX线程规范的系统上,但事实是只有在Solaris 2.5或以上的系统及特定的配置中才能工作。如果您尝试这种方法的话,需要小心服务器挂起或者没有响应。服务器在只输出静态网页的情况下运行得很好。
如果您的系统上有其他串行化的方法,为它书写代码(并把补丁寄给Apache)是值得的。

有一个考虑到但从未实现的方案是对循环部分地串行化——即允许一定数目的进程进入循环。在同一时刻可运行若干进程的多处理器系统上,这个主意是满不错的。而且前面提到的方案并没有充分利用带宽。可由于高度并行化的服务器实在少见,这个方案的优先级比较低。

为了得到最佳性能,不用多侦听命令是最理想的。请继续往下看。

单socket中的accept串行化
以上言及的方案对多socket服务器是相当不错的,但只有一个socket的情况又如何呢?理论上,由于在连接请求到来之前所有子进程将阻塞在accept中,单个socket不会产生上述种种问题。但实际上,上述非阻塞解决方案所带来的回旋(spinning)问题在这里只不过被掩盖起来了。在绝大多数TCP协议栈的实现中,一个接请求到来时内核将唤醒所有阻塞在accept中的进程。它们之一将得到此请求并返回用户空间,其余的进程将返回内核重新休眠。这将带来与多socket非阻塞解决方案相同的资源浪费。

由于这点原因,我们发现如果为socket串行化,许多系统表现得更友好——即使是一个socket的情况。这是单个socket串行化作为绝大多数情况的缺省配置的原因。在Linux上不甚精确的(Linux 2.0.30 / 双Pentium Pro 166 w / 128Mb内存)实验表明,对每次请求而言,串行化的单个socket仅比没有串行化的socket损失不到3%的性能。但未串行化的socket显示出每次连接请求100毫秒的延时。这也可能仅仅由于过长的通讯距离造成的。如果您不想串行化单个socket,可以定义宏SINGLE_LISTEN_UNSERIALIZED_ACCEPT。这样,仅有一个socket的服务器将不会串行化。

延迟关闭(Lingering Close)
就象draft-ietf-http-connection-00.txt第8节讨论的那样,为了使服务器能够可靠地实现HTTP协议,有必要独立地关闭每个方向上的通讯(每个TCP连接有两个方向,每个方向是分别独立的)。这个事实往往被其他服务器所忽视,而Apache 1.2就已经正确地处理了。

当这个特性增加到Apache中时却在许多版本的Unix中引起了问题。这是TCP规范的短见造成的——它没有声明FIN_WAIT_2有超时,但也没有阻止这样的实现。在没有超时的系统中,Apache 1.2将导致许多socket将永远处于FIN_WAIT_2的状态。这可以简单地用打最新TCP/IP补丁的方法避免。然而在提供商从不发行补丁的系统上(也就是SunOS4——虽然得到源代码许可证的人可以自己打补丁),我们决定不直接使用这一特性。

有两种实现这个特性的办法:一是socket的SO_LINGER选项。但似乎是命中注定,在多数TCP/IP协议栈中它从来不能正确地实现。即使是在提供了正确实现的平台(即Linux 2.0.31)上,这种方法也要比第二种方法代价(指CPU时间)高得多。

大多数情况下,Apache在一个叫lingering_close的函数中实现了它(在 http_main.c)。这个函数大致如下所示:

void lingering_close (int s)
{
char junk_buffer[2048];

/* shutdown the sending side */
shutdown (s, 1);

signal (SIGALRM, lingering_death);
alarm (30);

for (;;) {
select (s for reading, 2 second timeout);
if (error) break;
if (s is ready for reading) {
read (s, junk_buffer, sizeof (junk_buffer));
/* just toss away whatever is here */
}
}

close (s);
}
这自然增加了连接结束时的开销,但它是可靠的实现所必需的。随着HTTP/1.1的日益盛行,所有连接都是持久的,这种开销将被众多的连接请求抵消。如果您想冒险禁止这一特性的话,可以定义宏NO_LINGCLOSE,但这显然是不被推荐的。实际上,由于在HTTP/1.0中持久的管道式连接越来越普遍,lingering_close几乎是必须的选择。(管道式连接非常高效,所以您还是希望支持它的吧)

记分板文件
Apache利用一种叫做记分板(scoreboard)的技术在父、子进程间通讯。它的理想实现是在共享内存中。有的操作系统允许我们直接访问共享内存,或者提供它们的确切端口。在这些系统中的典型实现就是共享内存记分板。其他的系统则将磁盘上的文件作为缺省实现。磁盘文件不仅低效而且不稳定(又没有什么优势)。请为您的操作系统仔细阅读src/main/conf.h文件,并在其中寻找USE_MMAP_SCOREBOARD或者USE_SHMGET_SCOREBOARD。定义它们之一(以及相应的HAVE_MMAP和HAVE_SHMGET)将允许Apache使用共享内存。如果您系统的内存共享机制与众不同,请编辑src/main/http_main.c并增加Apache所需的挂钩函数(同时请把补丁寄给我们)

注:直到1.2版,Apache的Linux版才开始使用共享内存。这一疏忽使得以前版本的Apache在Linux上表现得很不理想。

DYNAMIC_MODULE_LIMIT
如果您不打算支持动态加载模块的话(准备榨出最后一滴性能的您可能希望如此),编译服务器时请设定参数-DDYNAMIC_MODULE_LIMIT=0。这将节省出为动态加载模块而分配的内存。

附录:对某次跟踪状况的详细分析
本附录描述了运行在Linux上的Apache 1.3系统调用的跟踪情况。运行时(run-time)配置文件中除了必要的缺省选项外还增加了:


AllowOverride none
Options FollowSymLinks

被请求的文件是一个6K的静态网页,其中不包含特殊内容。对于非静态或者伴随有内容协商的请求,跟踪结果将有明显的不同(一些情况下会十分晦涩)。我们将首先列出完整的跟踪结果,然后逐条进行分析。(它是由strace跟踪程序生成的,其他类似的程序包括truss、ktrace和par)

accept(15, {sin_family=AF_INET, sin_port=htons(22283), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
flock(18, LOCK_UN) = 0
sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
read(3, "GET /6k HTTP/1.0rnUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL) = 873959960
gettimeofday({873959960, 404935}, NULL) = 0
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
writev(3, [{"HTTP/1.1 200 OKrnDate: Thu, 11"..., 245}, {""..., 6144}], 2) = 6389
close(4) = 0
time(NULL) = 873959960
write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
shutdown(3, 1 /* send */) = 0
oldselect(4, [3], NULL, [3], {2, 0}) = 1 (in [3], left {2, 0})
read(3, "", 2048) = 0
close(3) = 0
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400ee000, 6144) = 0
flock(18, LOCK_EX) = 0
accept串行化:

flock(18, LOCK_UN) = 0
...
flock(18, LOCK_EX) = 0
这两个调用可以被上文提到的宏SINGLE_LISTEN_UNSERIALIZED_ACCEPT去掉。

对信号SIGUSR1的处理:

sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
...
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
...
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
引起它们的原因是温和重启(graceful restart ——用SIGUSR1而不是SIGHUP使Apache重新启动。这种方式为Apache留出了自行处理的余地——译者注)。当父进程收到SIGUSR1后,它将把这一信号传递给所有子进程(同时递增在共享内存中更新计数器的值generation counter)。所有空闲的子进程(即在两次连接之间的子进程)收到信号后将立即终止。所有处于持续连接(keep-alive)但在两次请求之间的子进程也将立即终止。但处于连接中并等待第一次请求的子进程将不会立即终止。

为了说明它的必要性,请考虑一个浏览器对已关闭连接的处理。如果已关闭的连接是持续连接,而且下一个请求不是该连接的第一个请求,浏览器将不动声色地建立另一个连接并重新发出请求。由于服务器任何时候都可能关闭一个持续连接(可能因为超时或者超过了最大请求数目),这样处理是有必要的。但是,如果在回应第一个请求前连接就被关闭,浏览器通常会显示文档中无数据的对话框(或者显示被折断的图片)。这是在假设服务器非正常终止(或者太忙)的情况下作出的反应。因此,Apache力图避免在作出任何回应前就关闭连接。这就是处理SIGUSR1的原因。

尽管在理论上避免那三个调用是可行的,但在粗略的测试中这样的改进是微不足道的。

为了实现虚拟主机,Apache需要用本地socket地址接受连接:

getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
在许多情况下都有可能去掉这个调用(比如没有虚拟主机,或者在Listen命令中的地址不含通配符)。但目前尚未对此进行优化。

Apache关掉了Nagle算法:

setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
考虑到John Heidemann的论文中提及的问题,请关注两个time调用:

time(NULL) = 873959960
...
time(NULL) = 873959960
第一个出现在请求的开始,另一个用于写入日志。前者是正确实现HTTP协议所必须的,后者的出现是因为通用日志格式指定了记录中包含时间戳。自定义日志模块能够去掉这个调用。或者您可以用某些方法把时间移到共享内存中。参见补丁一节。

正如前文描述的那样,ExtendedStatus On将引发两次gettimeofday调用和一次times调用:

gettimeofday({873959960, 404935}, NULL) = 0
...
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
ExtendedStatus Off (即缺省值)将避免这些调用。

这个调用看起来也许有些奇怪:

stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
它用于生成CGI程序所需的PATH_INFO环境变量。事实上,对于指向/cgi-bin/printenv/foobar的某个请求,stat将被调用两次。第一次调用查询/home/dgaudet/ap/apachen/cgi-bin/printenv/foobar,但它并不存在;第二次调用查询/home/dgaudet/ap/apachen/cgi-bin/printenv,它是存在的。无论如何,对于静态请求而言,至少需要一次调用。文件尺寸和修改时间被用来生成HTTP头标(比如Content-Length和Last-Modified)并用来实现协议的特色部分(比如If-Modified-Since)。一个聪明的服务器能够对非静态的请求避免调用stat,但是这样的实现对于模块化的Apache来说太难了。

所有静态文件使用mmap:

mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
...
munmap(0x400ee000, 6144) = 0
在一些系统上mmap小文件的效率不如直接读取该文件。宏MMAP_THRESHOLD用来设置应用mmap时的最小文件尺寸。缺省值是0(但在SunOS4上的缺省值是8129。实验证明这个值在该系统上比较理想)类似lmbench的工具可以帮助您在您的系统上进行优化设置。

您也许乐意在MMAP_SEGMENT_SIZE上做个实验(缺省值32768)。它决定了被mmap的文件将以一次多少个字节写出。Apache只在每次write之间重置客户的超时时间,因此把这个值设得过大容易把带宽较窄的用户拒之门外——除非同时增加Timeout。

您的系统有可能根本不用mmap。如果是这样的话,定义USE_MMAP_FILES和HAVE_MMAP也许会奏效(如果它真的有效请告诉我们)。

Apache尽全力避免在内存中拷贝数据。对任何请求的首次写出都将借助writev合并头标及第一块数据:

writev(3, [{"HTTP/1.1 200 OKrnDate: Thu, 11"..., 245}, {""..., 6144}], 2) = 6389
当进行HTTP/1.1块状编码时,Apache将生成最多为4个元素的writev。它的目标是将字节拷贝至内核,这是典型情况下必须做的事情(为了组装网络数据包)。2.0.31之前的Linux并不进行合并,而是为每个元素生成一个数据包。因此升级系统是一个好主意。定义NO_WRITEV将阻止这种合并,但将使得块状编码的性能很差。

日志文件的写入工作

write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
能够被宏定义BUFFERED_LOGS推迟。这种情况下,在真正写入文件之前,最多PIPE_BUF个字节(POSIX标准定义的常量)的日志信息将被缓存。由于写入条目不是atomic的(就是说来自不同子进程的信息将混合在一起),因此跨越PIPE_BUF边界的条目不会被分割。当子进程终止时,Apache用出色的方式将缓存排空。

延迟关闭引发了四个系统调用:

shutdown(3, 1 /* send */) = 0
oldselect(4, [3], NULL, [3], {2, 0}) = 1 (in [3], left {2, 0})
read(3, "", 2048) = 0
close(3) = 0
这些在前文已经提及。

当我们使用了-DSINGLE_LISTEN_UNSERIALIZED_ACCEPT、-DBUFFERED_LOGS 和 ExtendedStatus Off 的优化参数后,最终得到的跟踪结果如下:

accept(15, {sin_family=AF_INET, sin_port=htons(22286), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
sigaction(SIGUSR1, {SIG_IGN}, {0x8058c98, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
read(3, "GET /6k HTTP/1.0rnUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL) = 873961916
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400e3000
writev(3, [{"HTTP/1.1 200 OKrnDate: Thu, 11"..., 245}, {""..., 6144}], 2) = 6389
close(4) = 0
time(NULL) = 873961916
shutdown(3, 1 /* send */) = 0
oldselect(4, [3], NULL, [3], {2, 0}) = 1 (in [3], left {2, 0})
read(3, "", 2048) = 0
close(3) = 0
sigaction(SIGUSR1, {0x8058c98, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400e3000, 6144) = 0
只剩下19个系统调用了。其中有四个很容易被移走,但没有什么必要了。

附录:可用的补丁
这里有一些1.3版的性能补丁。但随着1.3.0版的发行它们可能有些过时。对掌握一点C语言的知识的人来说,升级它们并不是难事。特别地:

有一个补丁去掉所有time(2)系统调用。
有一个补丁去掉mod_include中的许多系统调用,它们仅出于向后兼容而被少数站点所使用。
有一个补丁结合了上面两个特性,并通过去掉一些特性使服务器加速。
附录:预分支(Pre-Forking)模型
Unix上的Apache是应用了预分支模型的服务器。父进程的责任仅在于繁衍子进程,它从不响应来自socket的任何请求。真正处理连接的是子进程,每个子进程在终止之前会(逐一地)为多个连接服务。父进程根据服务器负载的变化(通过监视记分板,记分板由子进程负责保持同步)生成新的或者杀掉旧的子进程。

这种模型为服务器提供了其他模型所不具备的健壮。父进程的代码非常简单,它有足够的信心保证在不出现错误的情况下持续运行。子进程就很复杂了,而且当您加入了第三方提供的模块后,将冒segmentation fault和其他崩溃的危险。即便这样的事情发生了,也只会影响到一个连接。父进程将继续为请求服务,并迅速替换掉已经死亡的子进程。

预分支在不同的Unix之间有良好的可移植性。Apache向来将它作为重要的目标之一,并且将保持下去。

但预分支模型由于各种各样的性能问题而饱受批判。主要的因素是分支进程带来的负担、上下文切换带来的负担和多个进程为内存带来的负担。另外它不能为请求提供有效的缓存机制(比如mmap文件池)。另有一些模型。JAWS project的论文(http://www.cs.wustl.edu/~jxh/research/research.html)对它们进行了详细的分析。实际上,所有这些模型带来的性能损失在不同操作系统上的差别迥异。

Apache的内核代码已经支持多线程。NT上的Apache 1.3就是多线程的。至少有另外两种实验性的多线程Apache:一个基于1.3内核并运行在DCE上;另一个基于1.0内核,它使用了一套自定义的用户级线程库。它们都不是对公众发行的。有一个已经发行了的Apache实验版本:运行于Netscape运行时可移植(http://www.mozilla.org/docs/refList/refNSPR/)(Portable Run Time)平台上的1.3版,可以在此下载(http://www.arctic.org/~dgaudet/apache/2.0/)(如果您准备使用它的话,欢迎您加入new-httpd邮件列表(http://dev.apache.org/mailing-lists))。被重新设计的Apache2.0将包含抽象化的服务器模型,它使我们可以既支持预分支模型,又支持多种线程模型


转载:转载请保留本信息,本文来自http://www.51dibs.com/lp07/la/a12/l_a_788925b21d31cf0a.html