Apache 2.0手册中文版翻译项目 [本文译者: kajaa * ]

项目说明 | 项目进度 | 项目讨论区 | Apache手册中文版

 


性能方面的提示 - Apache HTTP服务器
<-
Apache主站 > HTTP服务器 > 文档 > 2.0版本 > Miscellaneous Documentation

性能方面的提示

警告: 该文档没有完全包含在Apache HTTP服务器2.0中已经更新的内容,有些内容可能仍然有效,使用中请注意。

原作:Dean Gaudet

Apache 2.0是一个通用的网站服务器,其设计在灵活性、可移植性和性能中求得平衡。 虽然没有在设计上刻意追求性能指标,但是Apache 2.0仍然适用于许多现实的环境。

相比于Apache 1.3, 2.0版本作了大量的优化来提升处理能力和可伸缩性, 而且大多数的改进在默认状态下就可以生效。 但是,在编译时刻和运行时刻,都有许多可以显著提高性能的选择。 本文阐述在建立Apache 2.0中,服务器管理员可以改善性能的各种方法。 其中,部分配置选择使httpd更好地利用硬件和OS的兼容性,其他则是以功能换取速度。

top

硬件和操作系统

影响网站服务器性能的最大的因素是内存。 一个网站服务器应该从不使用交换机制,因为交换产生的滞后使用户总感觉“不够快”, 所以用户就可能去按“停止”和“刷新”,从而带来更大的负载。 你可以,也应该,控制MaxClients的设置, 以避免服务器产生太多的子进程而发生交换。

其他因素就很普通了,装一个足够快的CPU, 一个足够快的网卡,几个足够快的硬盘, 这里说的“足够快”是指能满足际应用的需求。

操作系统是很值得关注的又一个因素,已经被证实的很有用的经验有:

top

运行时刻的配置

HostnameLookups

在Apache 1.3以前的版本中, HostnameLookups被默认地设置为On. 它会带来延迟,因为对每一个请求都需要作一次DNS查询。 在Apache 1.3中,它被默认地设置为Off. 但是(在1.3及其以后的版本中), 如果使用了任何Allow from domain 或者 Deny from domain指令,则代价是要进行两次DNS查询 (一次正向的,和一次反向的,以确认没有作假). 所以,为了得到最高的性能, 应该避免使用这些指令(不用域名而用IP地址也是可以的).

注意,可以把这些指令包含在<Location /server-status>段中使之局部化。 在这种情况下,只有对这个区域的请求才会发生DNS查询。 下例禁止除了.html.cgi以外的所有DNS查询:

HostnameLookups off
<Files ~ "\.(html|cgi)$">
    HostnameLookups on
</Files>

如果在某些CGI中偶尔需要DNS名称,则可以调用gethostbyname

类似地,如果需要日志文件提供主机名信息以生成分析报告, 则可以使用日志后处理程序logresolve, 以完成DNS搜索,而客户端无须等待。最好是在其他机器上, 而不是在网站服务器上执行后处理和其他日志统计操作,以免影响服务器的性能。

FollowSymLinksSymLinksIfOwnerMatch

如果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中含有DocumentRoot以外的路径,那么就需要增加相应的这样的段。 为了得到最佳性能,并放弃对符号连接的保护,可以在需要的地方都设置FollowSymLinks, 而放弃使用SymLinksIfOwnerMatch.

AllowOverride

如果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, 因为必要的信息都在一个单一的文件中,而无须搜索目录.

内存映射

在Apache 2.0需要搜索被发送文件的内容时,比如处理服务器端包含时, 如果OS支持某种形式的mmap(2),则会对此文件执行内存映射。

在某些平台上,内存映射可以提高性能,但是在某些情况下, 内存映射会降低性能甚至影响到httpd的稳定性:

如果有上述情况发生,则应该使用EnableMMAP off关闭对发送文件的内存映射。 (注意: 此指令可以被针对目录的设置覆盖)

进程的建立

在Apache 1.3以前,MinSpareServers, MaxSpareServersStartServers的设置对性能都有很大的影响。 尤其是,为了应对负载而建立足够的子进程时,Apache需要有一个"渐进(ramp-up)"的过程。 在最初建立StartServers子进程后, 为了满足MinSpareServers设置的需要,每一秒钟只能建立一个子进程。 所以,对一个需要同时处理100个客户端的服务器,如果StartServers使用默认的设置5, 则为了应对负载而建立足够的子进程的时间需要95秒。 在实际应用中,如果不频繁重新启动服务器,这样还可以, 但是如果仅仅为了提供10分钟的服务,这样就很糟糕了。

"一秒钟一个"的规定目的是为了避免在创建子进程过程中服务器对请求的响应停顿, 但是它对服务器性能的影响太大了,必须予以改变。在Apache 1.3中, 这个"一秒钟一个"的规定变得宽松了,创建一个进程,等待一秒钟,继续创建第二个, 等待一秒钟,继而创建四个,如此按指数级增加创建的进程数,最多达到每秒32个, 直到满足MinSpareServers设置的值为止。

从多数反映看来,似乎没有必要调整MinSpareServers, MaxSpareServersStartServers. 如果每秒钟创建的进程数超过4个, 则会在ErrorLog中产生一条消息, 如果产生大量此消息,则可以考虑修改这些设置。 可以使用mod_status的输出作为参考。

与进程的创建相关的是由MaxRequestsPerChild引发的进程的销毁。 其默认值是0, 意味着每个进程所处理的请求数是不受限制的。 如果此值设置得很小,比如30,则可能需要大幅增加。在SunOS或者Solaris的早期版本上, 其最大值为10000以免内存短缺。

如果keep-alives有效,子进程将保持忙碌状态以等待被打开连接上的新的请求。 为了最小化其负面影响,KeepAliveTimeout的默认值被设置为15秒, 以谋求网络带宽和服务器资源之间的权衡。如果比较空闲,则可以提高此值到60秒左右, 参见 most of the benefits are lost.

top

编译时刻的配置

mod_status 和 ExtendedStatus On

如果Apache在编译时包含了mod_status, 而且在运行时设置了ExtendedStatus On,则, 对每个请求Apache都会调用两次gettimeofday(2) (或者根据操作系统的不同,调用times(2)), 以及(pre-1.3)几个额外的time(2)调用,使状态记录带有时间标志。 为了得到最佳性能,可以设置ExtendedStatus off (这也是默认的).

多socket情况下的串行accept

这里要说的是Unix socket API的一个缺点。假设,网站服务器使用了多个Listen语句以监听多个端口或者多个地址, Apache会使用select(2)以检测每个socket是否就绪。 select(2)会表明一个socket有 0 或者 至少一个 连接正等候处理。 由于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, 随即进入循环并试图accept此连接, 但是只有一个进程可以成功执行(假设还有一个连接就绪), 而其余的则会被阻塞accept。 这样,只有那一个socket可以处理请求,而其他都被锁住了,直到有足够多的请求将它们唤醒。 此“饥饿”问题在PR#467中有专门的讲述。 至少有两种解决方案.

一种方案是使用非阻塞型socket,不阻塞子进程并允许它立即继续执行。 但是,这样会浪费CPU时间。设想一下,select有10个子进程, 当一个请求到达的时候,其中9个也被唤醒,并试图accept此连接, 继而进入select循环,而无所事事,并且其间, 没有一个子进程能够响应出现在其他socket的请求,直到退出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_onaccept_mutex_off 实现了一个互斥的信号灯,在任何时刻只能为一个进程所拥有。实现互斥的方法有多种, 其定义位于src/conf.h(1.3以前的版本) 或src/include/ap_config.h (1.3及更新版本)中。在有些根本没有锁定机制的体系中,使用多个Listen指令就是不安全的。

USE_FLOCK_SERIALIZED_ACCEPT

这种方法调用系统函数flock(2)来锁定一个加锁文件 (其位置取决于LockFile指令).

USE_FCNTL_SERIALIZED_ACCEPT

这种方法调用系统函数fcntl(2)来锁定一个加锁文件 (其位置取决于LockFile指令).

USE_SYSVSEM_SERIALIZED_ACCEPT

(1.3及更新版本)这种方案使用SysV-style的信号灯以实现互斥。 不幸的是,SysV-style的信号灯有一些副作用,其一是, Apache有可能不能在结束以前释放这种信号灯(见ipcs(8) man的说明), 另外,这种信号灯API给与网络服务器有相同uid的CGI提供了拒绝服务攻击的机会 (, 所有CGI, 除非用了类似suexeccgiwrapper的程序)。 有鉴于此,在多数体系中都不用这种方法,除了IRIX (因为前两种方法在IRIX中代价太高)。

USE_USLOCK_SERIALIZED_ACCEPT

(1.3及更新版本)这种方法仅用于IRIX, 以usconfig(2)实现信号灯。 虽然这种方法回避了有关SysV-style信号灯的争议,但它不是对IRIX的默认值。 这是因为在单处理器的IRIX(5.3 or 6.2)中, uslock的代码是由两个比SysV信号灯代码慢得多的命令组成的, 而在多处理器的IRIX中,uslock的代码由一个比SysV信号灯代码快得多的命令组成, 这是一个比较混乱的状态。所以,如果你使用的是多处理器的IRIX,那么应该重新编译网络服务器, 并在EXTRA_CFLAGS中指定-DUSE_USLOCK_SERIALIZED_ACCEPT

USE_PTHREAD_SERIALIZED_ACCEPT

(1.3及更新版本)这种方法使用了POSIX信号灯, 按理应该可以用于所有完整实现了POSIX线程规范的体系中, 但是似乎只能在Solaris (2.5及更新版本)中,甚至只能在某种配置下正常运作。 如果遇到这种情况,则应该提防服务器的挂起和失去响应。 只提供静态内容的服务器可能不受影响。

如果你的系统提供了上述方法以外的串行机制,那就可能需要为之增加代码 (或者提交一个补丁给Apache)。

还有一种曾经考虑过但从未予以实施的方案是使循环部分地串行化, 即,只允许一定数量的进程进入循环。 这种方法仅在多个进程可以同时进行的多处理器的系统中才是有价值的, 而且这样的串行方法并没有占用整个带宽。 它也许是将来研究的一个领域,但是由于高度并行的网络服务器并不符合规范, 所以,其被优先考虑的程度会比较低。

当然了,为了得到最佳性能,就不要使用多个Listen语句。但是上述内容还是可以一读。

单socket情况下的串行accept

上述对多socket的服务器进行了一流的讲述,那么对单socket的服务器又怎样呢? 理论上似乎应该没有什么问题,因为所有进程在连接到来的时候可以由accept(2)阻塞, 而不会产生进程“饥饿”的问题,但是在实际应用中,它掩盖了与上述非阻塞方案几乎相同的问题。 按大多数TCP栈的实现方法,在单个连接到来时, 核心事实上唤醒了所有阻塞在accept的进程, 只有其中一个能得到此连接并返回到用户空间,而其余的由于得不到连接而在核心中处于休眠状态。 这种休眠状态为代码所掩盖,但的确存在, 并产生与多socket中采用非阻塞方案相同的负载尖峰的浪费。

同时,我们发现在许多体系中,即使在单socket的情况下, 实施串行化的效果也不错,因此在几乎所有的情况下,事实上都这么处理了。 在Linux (2.0.30,双Pentium pro 166 w/128Mb RAM)下的测试显示, 对单socket,串行化比之不串行化,每秒钟可以处理的请求少了不到3%, 但是,不串行化对每一个请求多了额外的100ms的延迟, 此延迟可能因远程网的线路所致,也仅发生在LAN中。 如果需要改变对单socket的串行化,可以定义SINGLE_LISTEN_UNSERIALIZED_ACCEPT 使单socket的服务器彻底放弃串行化。

延迟的关闭

正如 draft-ietf-http-connection-00.txt section 8 所述, HTTP服务器为了可靠地实现此协议,需要单独地在每个方向上关闭通讯 (重申一下,一个TCP连接是双向的,其中一个方向对另一个方向是独立的)。在这一点上, 其他服务器经常敷衍了之,但从1.2版本开始被Apache正确实现了。

但是增加了此功能以后,由于一些Unix版本的短见,随之也出现了许多问题。 TCP规范并没有指出FIN_WAIT_2有一个超时,但也没有予以禁止。 在没有超时的系统中,Apache 1.2经常会陷在FIN_WAIT_2状态中。 多数情况下,这个问题可以用供应商提供的TCP/IP补丁予以解决。 而如果供应商不提供补丁(SunOS4 -- 尽管用户们持有允许自己修补代码的许可证), 那么只能关闭此功能。

实现方法有两种,其一时socket选项SO_LINGER,但是似乎命中注定, 大多数TCP/IP栈都从未予以正确实现。即使在正确实现的栈中(Linux 2.0.31), 此方法也被证明其代价比下一种方法高昂(cputime)。

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已经成为绝对必须的。 (而且,管道化连接速度更快, 应该考虑予以支持。)

Scoreboard 文件

Apache父进程和子进程通过scoreboard进行通讯。通过共享内存来实现当然是最理想的。 在我们曾经实践过或者提供了完整移植的操作系统中,一般都使用共享内存,其余的则使用磁盘文件。 磁盘文件不仅速度慢,而且不可靠(功能也少)。仔细阅读你的体系所对应的 src/main/conf.h文件,并查找USE_MMAP_SCOREBOARDUSE_SHMGET_SCOREBOARD。定义其中之一(或者类似的HAVE_MMAPHAVE_SHMGET respectively),可以使共享内容的相关代码生效。 如果你的系统提供其他类型的共享内容,则需要修改src/main/http_main.c文件, 并增加必须的钩子程序。(也请发送一个补丁给我们。)

注意: Apache在对Linux的1.2移植版本之前,没有使用内存共享, 此失误使Apache的早期版本在Linux中表现很差。

DYNAMIC_MODULE_LIMIT

如果你不想使用动态加载模块 (或者是因为看见了这段话,或者是为了获得最后一点点性能上的提高), 可以在编译服务器时定义-DDYNAMIC_MODULE_LIMIT=0, 这样可以节省为支持动态加载模块而分配的内存。

top

附录: 踪迹的详细分析

在Solaris 8的MPM中,Apache 2.0.38使用一个系统调用以收集踪迹:

truss -l -p httpd_child_pid.

-l参数使truss记录每个执行系统调用的 LWP (lightweight process--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)串行化支持的匮乏。在这个特殊的平台上, 其对应的MPM在默认情况下使用非串行的accept,除了在监听多个端口的时候。
/65:    lwp_park(0x00000000, 0)                         = 0
/67:    lwp_unpark(65, 1)                               = 0

接受了一个连接后,监听线程唤醒一个工作线程以处理此请求。 下例中,处理请求的那个工作线程是LWP #65.

/65:    getsockname(9, 0x00200BA4, 0x00200BC4, 1)       = 0

为了实现虚拟主机,Apache需要知道接受连接的本地socket地址。 在许多情况下,有可能无须执行此调用的(比如没有虚拟主机,或者Listen指令中没有使用通配地址), 但是目前并没有对此作优化处理。

/65:    brk(0x002170E8)                                 = 0
/65:    brk(0x002190E8)                                 = 0

brk(2)调用是从堆中分配内存的,它在系统调用记录中并不多见, 因为httpd在多数请求处理中使用了自己的内存分配器(apr_poolapr_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

接着,工作线程使客户端连接处于非阻塞模式。setsockopt(2)getsockopt(2)调用是Solaris的libc对socket执行fcntl(2)所必须的。

/65:    read(9, " G E T   / 1 0 k . h t m".., 8000)     = 97

工作线程从客户端读取请求。

/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 FollowSymLinksAllowOverride None。 所以,无须对每个被请求文件路径中的目录执行lstat(2), 也不需要检查.htaccess文件,它简单地调用stat(2)以检查此文件是否存在, 以及是一个普通的文件还是一个目录。

/65:    sendfilev(0, 9, 0x00200F90, 2, 0xFAF7B53C)      = 10269

此例中,httpd可以通过单个系统调用sendfilev(2)发送HTTP响应头和被请求的文件。 Sendfile因操作系统会有所不同,有些系统中,在调用sendfile(2)以前, 需要调用write(2)writev(2)以发送响应头。

/65:    write(4, " 1 2 7 . 0 . 0 . 1   -  ".., 78)      = 78

write(2)调用在访问日志中对请求作了记录。 注意,其中没有对time(2)的调用的记录。与Apache 1.3不同, Apache 2.0使用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

工作线程对连接作延迟的关闭。

/65:    close(10)                                       = 0
/65:    lwp_park(0x00000000, 0)         (sleeping...)

最后,工作线程关闭发送完的文件和块,直到监听进程把它指派给另一个连接。

/67:    accept(3, 0x001FEB74, 0x001FEB94, 1) (sleeping...)

其间,监听进程可以在把一个连接指派给一个工作进程后,立即接受另一个连接 (但是如果所有工作进程都处于忙碌状态,则会受MPM中的一些溢出控制逻辑的制约)。 虽然在此例中并不明显,在工作线程刚接受了一个连接之后, 下一个accept(2)会(而且在高负荷的情况下更会)立即并行产生。

 


项目维护者: kajaa [本文译者: kajaa * ]

项目说明 | 项目进度 | 项目讨论区 | Apache手册中文版