Apache HTTP Server 版本 2.4
Apache 2.x 是一个通用的 Web 服务器,旨在提供灵活性和可移植性与性能之间的平衡。尽管它并非专门为设置基准记录而设计,但在许多实际情况下,Apache 2.x 能够实现高性能。
与 Apache 1.3 相比,2.x 版本包含许多额外的优化,以提高吞吐量和可扩展性。大多数这些改进默认情况下都已启用。但是,存在编译时和运行时配置选项,这些选项会显着影响性能。本文档描述了服务器管理员可以配置以调整 Apache 2.x 安装性能的选项。其中一些配置选项使 httpd 能够更好地利用硬件和操作系统的功能,而另一些选项则允许管理员以速度换取功能。
影响 Web 服务器性能的最大硬件问题是 RAM。Web 服务器永远不应该进行交换,因为交换会增加每个请求的延迟,超过用户认为“足够快”的程度。这会导致用户点击停止并重新加载,从而进一步增加负载。您可以(并且应该)控制 MaxRequestWorkers
设置,以使您的服务器不会生成太多子进程,从而导致开始交换。执行此操作的过程很简单:通过使用诸如 top
之类的工具查看进程列表,确定平均 Apache 进程的大小,并将此大小除以您的总可用内存,为其他进程留出一些空间。
除此之外,其余的都是普通的:获得足够快的 CPU、足够快的网卡和足够快的磁盘,其中“足够快”是需要通过实验来确定的。
操作系统选择在很大程度上取决于本地问题。但一些已被证明普遍有用的准则是
运行您选择的操作系统的最新稳定版本和补丁级别。近年来,许多操作系统供应商在其 TCP 堆栈和线程库中引入了重大的性能改进。
如果您的操作系统支持 sendfile(2)
系统调用,请确保您安装了启用它的版本和/或补丁。(例如,对于 Linux,这意味着使用 Linux 2.4 或更高版本。对于早期版本的 Solaris 8,您可能需要应用补丁。)在支持它的系统上,sendfile
使 Apache 2 能够更快地提供静态内容,并且 CPU 利用率更低。
相关模块 | 相关指令 |
---|---|
在 Apache 1.3 之前,HostnameLookups
默认设置为 On
。这会为每个请求增加延迟,因为它需要完成 DNS 查找才能完成请求。在 Apache 1.3 中,此设置默认设置为 Off
。如果您需要将日志文件中的地址解析为主机名,请使用随 Apache 附带的 logresolve
程序,或使用众多可用的日志报告包之一。
建议您在生产 Web 服务器机器以外的其他机器上执行此类日志文件的后期处理,以防止此活动对服务器性能产生负面影响。
如果您使用任何 Allow
from domain` 或 Deny
from domain` 指令(即使用主机名或域名,而不是 IP 地址),那么您将为两次 DNS 查找付费(反向查找,然后是正向查找以确保反向查找没有被欺骗)。因此,为了获得最佳性能,如果可能,请在使用这些指令时使用 IP 地址,而不是名称。
请注意,可以对指令进行范围限定,例如在 <Location "/server-status">
部分内。在这种情况下,DNS 查找仅对与条件匹配的请求执行。以下是一个示例,它禁用除 .html
和 .cgi
文件以外的所有查找
HostnameLookups off <Files ~ "\.(html|cgi)$"> HostnameLookups on </Files>
但即使如此,如果您只需要在某些 CGI 中使用 DNS 名称,您也可以考虑在需要它的特定 CGI 中执行 gethostbyname
调用。
在您的 URL 空间中,无论您是否有 Options FollowSymLinks
,或者您是否有 Options SymLinksIfOwnerMatch
,Apache 都需要发出额外的系统调用来检查符号链接。(每个文件名组件一个额外的调用。)例如,如果您有
DocumentRoot "/www/htdocs" <Directory "/"> Options SymLinksIfOwnerMatch </Directory>
并且对 URI /index.html
发出请求,那么 Apache 将对 /www
、/www/htdocs
和 /www/htdocs/index.html
执行 lstat(2)
。这些 lstats
的结果永远不会被缓存,因此它们将在每个请求上都会发生。如果您确实需要符号链接安全检查,您可以执行以下操作
DocumentRoot "/www/htdocs" <Directory "/"> Options FollowSymLinks </Directory> <Directory "/www/htdocs"> Options -FollowSymLinks +SymLinksIfOwnerMatch </Directory>
这至少可以避免对 DocumentRoot
路径进行额外的检查。请注意,如果您在文档根目录之外有任何 Alias
或 RewriteRule
路径,则需要添加类似的部分。为了获得最高性能,并且没有符号链接保护,请在您的整个文件系统中设置 FollowSymLinks
,并且永远不要设置 SymLinksIfOwnerMatch
。
在您的 URL 空间中,无论您在何处允许覆盖(通常是 .htaccess
文件),Apache 都将尝试为每个文件名组件打开 .htaccess
。例如,
DocumentRoot "/www/htdocs" <Directory "/"> AllowOverride all </Directory>
并且对 URI /index.html
发出请求。然后 Apache 将尝试打开 /.htaccess
、/www/.htaccess
和 /www/htdocs/.htaccess
。解决方案与之前 Options FollowSymLinks
的情况类似。为了获得最高性能,请在您的整个文件系统中使用 AllowOverride None
。
如果可能,如果您真的对每一点性能都感兴趣,请避免内容协商。在实践中,协商的好处超过了性能损失。有一种情况可以加快服务器速度。不要使用通配符,例如
DirectoryIndex index
使用完整的选项列表
DirectoryIndex index.cgi index.pl index.shtml index.html
其中您将最常见的选项列在最前面。
另请注意,显式创建 type-map
文件比使用 MultiViews
提供更好的性能,因为可以通过读取此单个文件来确定必要的信息,而不是必须扫描目录以查找文件。
如果您的网站需要内容协商,请考虑使用 type-map
文件,而不是 Options MultiViews
指令来完成协商。有关协商方法的完整讨论以及创建 type-map
文件的说明,请参阅 内容协商 文档。
在 Apache 2.x 需要查看要传递的文件的内容的情况下(例如,在执行服务器端包含处理时),如果操作系统支持某种形式的 mmap(2)
,它通常会将文件内存映射。
在某些平台上,这种内存映射会提高性能。但是,在某些情况下,内存映射会损害 httpd 的性能甚至稳定性
在某些操作系统上,当 CPU 数量增加时,mmap
的扩展性不如 read(2)
。例如,在多处理器 Solaris 服务器上,当禁用 mmap
时,Apache 2.x 有时会更快地传递服务器解析的文件。
如果您内存映射位于 NFS 挂载文件系统上的文件,并且另一个 NFS 客户端机器上的进程删除或截断该文件,那么您的进程在下次尝试访问映射的文件内容时可能会出现总线错误。
对于适用上述任一因素的安装,您应该使用 EnableMMAP off
来禁用传递文件的内存映射。(注意:此指令可以在每个目录的基础上被覆盖。)
在 Apache 2.x 可以忽略要传递的文件的内容的情况下(例如,在提供静态文件内容时),如果操作系统支持 sendfile(2)
操作,它通常会对文件使用内核 sendfile 支持。
在大多数平台上,使用 sendfile 通过消除单独的读取和发送机制来提高性能。但是,在某些情况下,使用 sendfile 会损害 httpd 的稳定性
某些平台可能存在构建系统未检测到的 sendfile 支持错误,尤其是在其他机器上构建二进制文件并将其移动到具有 sendfile 支持错误的机器时。
使用 NFS 挂载文件系统时,内核可能无法通过自己的缓存可靠地提供网络文件。
对于适用上述任一因素的安装,您应该使用 EnableSendfile off
来禁用 sendfile 传递文件内容。(注意:此指令可以在每个目录的基础上被覆盖。)
在 Apache 1.3 之前,MinSpareServers
、MaxSpareServers
和 StartServers
设置对基准测试结果都有重大影响。特别是,Apache 需要一个“启动”阶段才能达到足以处理所施加负载的子进程数量。在最初生成 StartServers
个子进程之后,每秒只生成一个子进程来满足 MinSpareServers
设置。因此,一个被 100 个同时客户端访问的服务器,使用默认的 StartServers
为 5
,将需要大约 95 秒才能生成足够多的子进程来处理负载。这在实践中对现实生活中的服务器来说很好,因为它们不会经常重启。但它在基准测试中表现很差,基准测试可能只运行十分钟。
每秒一个规则是在努力避免用新子进程的启动淹没机器。如果机器忙于生成子进程,它就无法处理请求。但它对 Apache 的感知性能有如此重大的影响,以至于它必须被替换。从 Apache 1.3 开始,代码将放宽每秒一个规则。它将生成一个,等待一秒,然后生成两个,等待一秒,然后生成四个,它将继续呈指数增长,直到它每秒生成 32 个子进程。它将在满足 MinSpareServers
设置时停止。
这似乎足够响应,几乎不需要调整 MinSpareServers
、MaxSpareServers
和 StartServers
旋钮。当每秒产生超过 4 个子进程时,将向 ErrorLog
发出消息。如果您看到很多这样的错误,请考虑调整这些设置。使用 mod_status
输出作为指南。
与进程创建相关的是由 MaxConnectionsPerChild
设置引起的进程死亡。默认情况下,此值为 0
,这意味着每个子进程处理的连接数没有限制。如果您的配置当前将其设置为某个非常小的数字,例如 30
,您可能需要将其大幅提高。如果您运行的是 SunOS 或旧版本的 Solaris,请将其限制为 10000
左右,因为存在内存泄漏。
当使用 keep-alives 时,子进程将保持忙碌状态,无所事事地等待已打开连接上的更多请求。默认的 KeepAliveTimeout
为 5
秒,试图将这种影响降到最低。这里的权衡是在网络带宽和服务器资源之间。在任何情况下,您都不应该将其提高到 60
秒以上,因为 大部分好处都会消失。
Apache 2.x 支持可插拔的并发模型,称为 多处理模块 (MPM)。在构建 Apache 时,您必须选择要使用的 MPM。某些平台有特定于平台的 MPM:mpm_netware
、mpmt_os2
和 mpm_winnt
。对于一般的类 Unix 系统,有几种 MPM 可供选择。MPM 的选择会影响 httpd 的速度和可扩展性。
worker
MPM 使用多个子进程,每个子进程都有多个线程。每个线程一次处理一个连接。Worker 通常是高流量服务器的不错选择,因为它比 prefork MPM 的内存占用更小。event
MPM 与 Worker MPM 一样是多线程的,但旨在通过将一些处理工作传递给支持线程来允许同时服务更多请求,从而释放主线程来处理新的请求。prefork
MPM 使用多个子进程,每个子进程只有一个线程。每个进程一次处理一个连接。在许多系统上,prefork 的速度与 worker 相当,但它使用更多内存。在某些情况下,prefork 的无线程设计比 worker 具有优势:它可以与非线程安全的第三方模块一起使用,并且在线程调试支持较差的平台上更容易调试。有关这些和其他 MPM 的更多信息,请参阅 MPM 文档。
由于内存使用是性能中非常重要的考虑因素,因此您应该尝试消除实际上没有使用的模块。如果您已将模块构建为 DSO,则消除模块很简单,只需注释掉与该模块关联的 LoadModule
指令即可。这使您可以尝试删除模块,并查看您的站点在没有这些模块的情况下是否仍然可以正常运行。
另一方面,如果您将模块静态链接到 Apache 二进制文件中,则需要重新编译 Apache 才能删除不需要的模块。
这里出现的一个相关问题是,当然,您需要哪些模块,哪些模块不需要。这里的答案当然会因网站而异。但是,您可以使用的最小模块列表通常包括 mod_mime
、mod_dir
和 mod_log_config
。mod_log_config
当然可选,因为您可以在没有日志文件的情况下运行网站。但是,不建议这样做。
某些模块,例如 mod_cache
和 worker MPM 的最新开发版本,使用 APR 的原子 API。此 API 提供可用于轻量级线程同步的原子操作。
默认情况下,APR 使用每个目标操作系统/CPU 平台上可用的最有效机制来实现这些操作。例如,许多现代 CPU 都有一个指令,可以在硬件中执行原子比较并交换 (CAS) 操作。但是,在某些平台上,APR 默认使用较慢的基于互斥锁的原子 API 实现,以确保与缺少此类指令的旧 CPU 模型的兼容性。如果您正在为这些平台之一构建 Apache,并且您计划仅在较新的 CPU 上运行,则可以在构建时通过使用 --enable-nonportable-atomics
选项配置 Apache 来选择更快的原子实现。
./buildconf
./configure --with-mpm=worker --enable-nonportable-atomics=yes
--enable-nonportable-atomics
选项与以下平台相关
--enable-nonportable-atomics
进行配置,APR 将生成使用 SPARC v8plus 操作码进行快速硬件比较和交换的代码。如果您使用此选项配置 Apache,原子操作将更加高效(允许更低的 CPU 利用率和更高的并发性),但生成的执行文件将仅在 UltraSPARC 芯片上运行。--enable-nonportable-atomics
进行配置,APR 将生成使用 486 操作码进行快速硬件比较和交换的代码。这将导致更有效的原子操作,但生成的执行文件将仅在 486 及更高版本的芯片上运行(而不是在 386 上运行)。如果您包含 mod_status
并且您还在构建和运行 Apache 时设置了 ExtendedStatus On
,那么在每次请求时,Apache 将执行两次对 gettimeofday(2)
(或 times(2)
,具体取决于您的操作系统)的调用,以及(在 1.3 之前)对 time(2)
的几次额外调用。所有这些都是为了使状态报告包含计时指示。为了获得最高性能,请设置 ExtendedStatus off
(这是默认设置)。
本节尚未完全更新以考虑 Apache HTTP Server 2.x 版本中所做的更改。某些信息可能仍然相关,但请谨慎使用。
这讨论了 Unix 套接字 API 中的一个缺点。假设您的 Web 服务器使用多个 Listen
语句来监听多个端口或多个地址。为了测试每个套接字以查看是否有连接就绪,Apache 使用 select(2)
。select(2)
指示套接字上有零个或至少一个连接正在等待。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
处阻塞。当任何套接字上出现单个请求时,所有这些阻塞的子进程都会唤醒并从 select
返回。(唤醒的子进程数量因操作系统和计时问题而异。)然后,它们都会进入循环并尝试 accept
连接。但是,只有一个会成功(假设仍然只有一个连接就绪)。其余的将在 accept
中阻塞。这实际上将这些子进程锁定到为来自该套接字的请求提供服务,而不是其他套接字,并且它们将一直卡在那里,直到该套接字上出现足够多的新请求来唤醒它们。此饥饿问题首先在 PR#467 中记录。至少有两种解决方案。
一种解决方案是使套接字非阻塞。在这种情况下,accept
不会阻塞子进程,并且它们将被允许立即继续。但这会浪费 CPU 时间。假设您在 select
中有十个空闲子进程,并且有一个连接到达。然后,九个子进程将唤醒,尝试 accept
连接,失败,并循环回到 select
,什么也没做。同时,这些子进程中没有一个在为其他套接字上发生的请求提供服务,直到它们回到 select
。总的来说,这种解决方案似乎不太有效,除非您拥有与空闲子进程一样多的空闲 CPU(在多处理器框中)(这种情况不太可能)。
另一种解决方案(Apache 使用的解决方案)是序列化进入内部循环。循环看起来像这样(突出显示差异)
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
实现了一个互斥信号量。一次只有一个子进程可以拥有互斥锁。有几种选择可以实现这些互斥锁。选择在 src/conf.h
(1.3 之前)或 src/include/ap_config.h
(1.3 或更高版本)中定义。某些体系结构没有做出任何锁定选择,在这些体系结构上,使用多个 Listen
指令是不安全的。
Mutex
指令可用于在运行时更改 mpm-accept
互斥锁的互斥锁实现。该指令中记录了不同互斥锁实现的特殊注意事项。
另一个已经考虑过但从未实现的解决方案是部分序列化循环——也就是说,让一定数量的进程进入。这只有在多处理器框中才有趣,在多处理器框中,多个子进程可以同时运行,并且序列化实际上没有利用全部带宽。这是一个未来调查的可能领域,但优先级仍然很低,因为高度并行的 Web 服务器并不常见。
理想情况下,如果您想要获得最高性能,您应该运行没有多个 Listen
语句的服务器。但请继续阅读。
以上内容对于多个套接字服务器来说很好,但对于单个套接字服务器呢?理论上,它们不应该遇到任何相同的问题,因为所有子进程都可以一直阻塞在accept(2)
中,直到连接到达,并且不会出现饥饿现象。实际上,这隐藏了在非阻塞解决方案中讨论的几乎相同的“自旋”行为。大多数 TCP 堆栈的实现方式是,内核在单个连接到达时实际上会唤醒所有阻塞在accept
中的进程。其中一个进程获取连接并返回到用户空间。其余进程在内核中自旋,并在发现没有连接时返回睡眠状态。这种自旋对用户空间代码是隐藏的,但它确实存在。这会导致与非阻塞解决方案处理多个套接字的情况相同的负载峰值浪费行为。
出于这个原因,我们发现许多架构在序列化单个套接字情况时表现得更“好”。因此,这实际上是几乎所有情况下的默认设置。在 Linux(2.0.30,双奔腾 Pro 166,128MB 内存)上的粗略实验表明,单个套接字情况的序列化导致每秒请求数减少不到 3%,而未序列化的单个套接字在每个请求上显示了额外的 100 毫秒延迟。这种延迟在长途线路中可能无关紧要,只有在局域网中才会出现问题。如果您想覆盖单个套接字序列化,可以定义SINGLE_LISTEN_UNSERIALIZED_ACCEPT
,然后单个套接字服务器将完全不进行序列化。
如draft-ietf-http-connection-00.txt第 8 节所述,为了使 HTTP 服务器可靠地实现协议,它需要独立地关闭通信的每个方向。(回想一下,TCP 连接是双向的。每一半都独立于另一半。)
当此功能添加到 Apache 时,由于短视,它在各种版本的 Unix 上引起了很多问题。TCP 规范没有说明FIN_WAIT_2
状态是否有超时,但它也没有禁止超时。在没有超时的系统上,Apache 1.2 会导致许多套接字永远卡在FIN_WAIT_2
状态。在许多情况下,只需升级到供应商提供的最新 TCP/IP 补丁就可以避免这种情况。在供应商从未发布补丁的情况下(例如,SunOS4 - 尽管拥有源代码许可证的人可以自己打补丁),我们决定禁用此功能。
有两种方法可以实现这一点。一种是套接字选项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) { if (read (s, junk_buffer, sizeof (junk_buffer)) <= 0) { break; } /* just toss away whatever is here */ } } close (s); }
这自然会在连接结束时增加一些开销,但它是可靠实现所必需的。随着 HTTP/1.1 变得越来越普遍,并且所有连接都保持持久性,这种开销将在更多请求中分摊。如果您想玩火并禁用此功能,可以定义NO_LINGCLOSE
,但这根本不建议这样做。特别是,随着 HTTP/1.1 管道式持久连接的使用,lingering_close
是绝对必要的(并且管道式连接更快,因此您需要支持它们)。
Apache 的父进程和子进程通过名为记分板的东西相互通信。理想情况下,这应该在共享内存中实现。对于我们能够访问或已获得详细端口的那些操作系统,它通常使用共享内存实现。其余部分默认使用磁盘上的文件。磁盘上的文件不仅速度慢,而且不可靠(并且功能更少)。仔细查看您的架构的src/main/conf.h
文件,并查找USE_MMAP_SCOREBOARD
或USE_SHMGET_SCOREBOARD
。定义这两个中的一个(以及它们的伴侣HAVE_MMAP
和HAVE_SHMGET
)将启用提供的共享内存代码。如果您的系统具有其他类型的共享内存,请编辑src/main/http_main.c
文件,并添加在 Apache 中使用它的必要钩子。(请将补丁发回给我们。)
如果您没有使用动态加载模块的意图(如果您正在阅读本文并为服务器调整每一盎司性能,您可能不会这样做),那么您应该在构建服务器时添加-DDYNAMIC_MODULE_LIMIT=0
。这将节省仅用于支持动态加载模块的内存。
以下是 Solaris 8 上使用 worker MPM 的 Apache 2.0.38 的系统调用跟踪。此跟踪使用以下方法收集
truss -l -p httpd_child_pid.
-l
选项告诉 truss 记录调用每个系统调用的 LWP(轻量级进程 - Solaris 的内核级线程)的 ID。
其他系统可能具有不同的系统调用跟踪实用程序,例如strace
、ktrace
或par
。它们都产生类似的输出。
在此跟踪中,客户端已从 httpd 请求了一个 10KB 的静态文件。非静态请求或具有内容协商的请求的跟踪看起来大不相同(在某些情况下非常丑陋)。
/67: accept(3, 0x00200BEC, 0x00200C0C, 1) (sleeping...) /67: accept(3, 0x00200BEC, 0x00200C0C, 1) = 9
在此跟踪中,监听器线程在 LWP #67 中运行。
accept(2)
序列化。在这个特定平台上,worker MPM 默认使用未序列化的 accept,除非它在多个端口上监听。/65: lwp_park(0x00000000, 0) = 0 /67: lwp_unpark(65, 1) = 0
接受连接后,监听器线程唤醒一个 worker 线程来处理请求。在此跟踪中,处理请求的 worker 线程映射到 LWP #65。
/65: getsockname(9, 0x00200BA4, 0x00200BC4, 1) = 0
为了实现虚拟主机,Apache 需要知道用于接受连接的本地套接字地址。在许多情况下可以消除此调用(例如,当没有虚拟主机时,或者当使用没有通配符地址的Listen
指令时)。但尚未进行任何努力来进行这些优化。
/65: brk(0x002170E8) = 0 /65: brk(0x002190E8) = 0
brk(2)
调用从堆中分配内存。在系统调用跟踪中很少看到这些,因为 httpd 使用自定义内存分配器(apr_pool
和apr_bucket_alloc
)来处理大多数请求。在此跟踪中,httpd 刚刚启动,因此它必须调用malloc(3)
来获取用于创建自定义内存分配器的原始内存块。
/65: fcntl(9, F_GETFL, 0x00000000) = 2 /65: fstat64(9, 0xFAF7B818) = 0 /65: getsockopt(9, 65535, 8192, 0xFAF7B918, 0xFAF7B910, 2190656) = 0 /65: fstat64(9, 0xFAF7B818) = 0 /65: getsockopt(9, 65535, 8192, 0xFAF7B918, 0xFAF7B914, 2190656) = 0 /65: setsockopt(9, 65535, 8192, 0xFAF7B918, 4, 2190656) = 0 /65: fcntl(9, F_SETFL, 0x00000082) = 0
接下来,worker 线程将与客户端的连接(文件描述符 9)置于非阻塞模式。setsockopt(2)
和getsockopt(2)
调用是 Solaris 的 libc 如何处理套接字上的fcntl(2)
的副作用。
/65: read(9, " G E T / 1 0 k . h t m".., 8000) = 97
worker 线程从客户端读取请求。
/65: stat("/var/httpd/apache/httpd-8999/htdocs/10k.html", 0xFAF7B978) = 0 /65: open("/var/httpd/apache/httpd-8999/htdocs/10k.html", O_RDONLY) = 10
此 httpd 已配置为Options FollowSymLinks
和AllowOverride None
。因此,它不需要lstat(2)
通往请求文件的路径中的每个目录,也不需要检查.htaccess
文件。它只需调用stat(2)
来验证文件:1) 存在,以及 2) 是一个普通文件,而不是一个目录。
/65: sendfilev(0, 9, 0x00200F90, 2, 0xFAF7B53C) = 10269
在此示例中,httpd 能够使用单个sendfilev(2)
系统调用发送 HTTP 响应头和请求的文件。Sendfile 语义在不同的操作系统之间有所不同。在其他一些系统上,需要执行write(2)
或writev(2)
调用来发送标头,然后再调用sendfile(2)
。
/65: write(4, " 1 2 7 . 0 . 0 . 1 - ".., 78) = 78
此write(2)
调用将请求记录在访问日志中。请注意,此跟踪中缺少的一件事是time(2)
调用。与 Apache 1.3 不同,Apache 2.x 使用gettimeofday(3)
来查找时间。在某些操作系统(如 Linux 或 Solaris)上,gettimeofday
具有优化的实现,不需要像典型的系统调用那样多的开销。
/65: shutdown(9, 1, 1) = 0 /65: poll(0xFAF7B980, 1, 2000) = 1 /65: read(9, 0xFAF7BC20, 512) = 0 /65: close(9) = 0
worker 线程对连接进行延迟关闭。
/65: close(10) = 0 /65: lwp_park(0x00000000, 0) (sleeping...)
最后,worker 线程关闭它刚刚传送的文件,并阻塞,直到监听器分配给它另一个连接。
/67: accept(3, 0x001FEB74, 0x001FEB94, 1) (sleeping...)
同时,监听器线程能够在将此连接分派给 worker 线程后立即接受另一个连接(受 worker MPM 中的一些流量控制逻辑的约束,该逻辑在所有可用 worker 都忙时会限制监听器)。虽然从此跟踪中看不出来,但下一个accept(2)
可以(并且通常在高负载条件下会)与 worker 线程处理刚刚接受的连接并行发生。