Apache 性能调优

2021-10-13 18:21 更新

Apache 2.x是一个通用的Web服务器,旨在提供灵活性,可移植性和性能之间的平衡。虽然它没有专门设计用于设置基准记录,但Apache 2.x在许多实际情况下都具有高性能。

与Apache 1.3相比,版本2.x包含许多额外的优化,以提高吞吐量和可伸缩性。默认情况下,大多数这些改进都已启用。但是,存在可能显着影响性能的编译时和运行时配置选择。本文档介绍了服务器管理员可以配置的选项,以调整Apache 2.x安装的性能。其中一些配置选项使httpd能够更好地利用硬件和操作系统的功能,而其他配置选项则允许管理员交换功能以提高速度。

硬件和操作系统问题

影响Web服务器性能的最大硬件问题是服务器的内存(RAM)。网络服务器永远不应该交换,因为交换会增加每个请求的延迟超出用户认为“足够快”的点。这会导致用户点击停止并重新加载,从而进一步增加负载。您可以而且应该控制MaxRequestWorkers设置,以便您的服务器不会产生太多的子节点以便它开始交换。执行此操作的过程很简单:通过顶级工具查看流程列表,确定平均Apache流程的大小,并将其划分为总可用内存,为其他流程留出一些空间。

除此之外,其余的是平凡的:获得足够快的CPU,足够快的网卡和足够快的磁盘,其中“足够快”是需要通过实验确定的东西。

操作系统的选择主要取决于管理员的问题。但是一些经证明通用的指南是:

  • 运行选择的操作系统的最新稳定版本和修补程序级别。近年来,许多OS供应商已经为其TCP堆栈和线程库引入了显着的性能改进。
  • 如果操作系统支持sendfile(2)系统调用,请确保安装启用它所需的版本和/或修补程序。(例如,使用Linux,这意味着使用Linux 2.4或更高版本。对于Solaris 8的早期版本,您、可能需要应用补丁。)在可用的系统上,sendfile使Apache 2能够以更低的速度更快地提供静态内容 CPU利用率。

运行时配置问题

HostnameLookups和其他DNS注意事项

在Apache 1.3之前,HostnameLookups默认为On。这会增加每个请求的延迟,因为它需要在请求完成之前完成DNS查找。在Apache 1.3中,此设置默认为关闭。如果您需要将日志文件中的地址解析为主机名,请使用Apache附带的logresolve程序,或者可用的众多日志报告程序包之一。

建议您在生产Web服务器计算机以外的某台计算机上对日志文件进行此类后处理,以使此活动不会对服务器性能产生负面影响。

如果使用域名允许或域名指令拒绝(即使用主机名或域名,而不是IP地址),那么您将需要付出两次DNS查询(反向,然后进行正向查找以确保反过来没有被欺骗)。因此,为了获得最佳性能,请在使用这些指令时使用IP地址而不是名称(如果可能)。

请注意,可以对指令进行范围限定,例如在<Location "/server-status">部分中。在这种情况下,DNS查找仅在符合条件的请求上执行。这是一个禁用除.html和.cgi文件之外的查找的示例:

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

但即使如此,如果只需要在一些CGI中使用DNS名称,可以考虑在需要它的特定CGI中进行gethostbyname调用。

FollowSymLinks和SymLinksIfOwnerMatch无论您的URL空间中没有Options FollowSymLinks,或者都有选项SymLinksIfOwnerMatch,Apache都需要发出额外的系统调用来检查符号链接。(每个文件名组件一次额外调用。)例如,如果配置有:

DocumentRoot "/www/htdocs"
<Directory "/">
  Options SymLinksIfOwnerMatch
</Directory>
Shell

并且对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>
Shell

这至少避免了对DocumentRoot路径的额外检查。请注意,如果文档根目录之外有任何Alias或RewriteRule路径,则需要添加类似的部分。为了获得最高性能,并且没有符号链接保护,请在任何地方设置FollowSymLinks,并且永远不要设置SymLinksIfOwnerMatch。

AllowOverride无论您在URL空间中允许覆盖(通常是.htaccess文件),Apache都会尝试为每个文件名组件打开.htaccess。例如,

DocumentRoot "/www/htdocs"
<Directory "/">
  AllowOverride all
</Directory>
Shell

并且请求URI /index.html。然后Apache将尝试打开/.htaccess,/www/.htaccess和/www/htdocs/.htaccess。解决方案类似于之前的Options FollowSymLinks案例。为获得最高性能,请在文件系统中的所有位置使用AllowOverride None。

协商

尽可能避免内容协商。在实践中,协商的好处超过了性能带来的好处。有一种情况可以加快服务器的速度。使用如下的通配符并不是一个好的方法:

DirectoryIndex index
Shell

应该使用完整的选项列表:

DirectoryIndex index.cgi index.pl index.shtml index.html
Shell

内存映射在Apache 2.x需要查看正在传递的文件的内容的情况下 - 例如,在执行服务器端包含处理时 - 如果操作系统支持某种形式的mmap,它通常会对文件进行内存映射(2)。

在某些平台上,此内存映射可提高性能。但是,有些情况下内存映射会损害httpd的性能甚至稳定性:

  • 在某些操作系统上,当CPU数量增加时,mmap不会像read(2)那样扩展。例如,在多处理器Solaris服务器上,当禁用mmap时,Apache 2.x有时会更快地提供服务器解析的文件。
  • 如果内存映射位于NFS挂载的文件系统上的文件,并且另一个NFS客户端计算机上的进程删除或截断该文件,则下次尝试访问映射文件内容时,进程可能会收到总线错误。

对于适用这些因素之一的安装,应使用EnableMMAP off禁用已传递文件的内存映射。(注意:可以在每个目录的基础上覆盖此指令。)

Sendfile在Apache 2.x可以忽略要传递的文件内容的情况下 - 例如,在提供静态文件内容时 - 如果操作系统支持sendfile(2)操作,它通常会对文件使用内核sendfile支持。

在大多数平台上,使用sendfile通过消除单独的读取和发送机制来提高性能。但是,有些情况下使用sendfile会损害httpd的稳定性:

某些平台可能已经破坏了构建系统未检测到的sendfile支持,特别是如果二进制文件是在另一个盒子上构建并移动到这样一台具有损坏的sendfile支持的机器上的话。

使用NFS挂载的文件系统,内核可能无法通过其自己的缓存可靠地提供网络文件。

进程创建在Apache 1.3之前,MinSpareServers,MaxSpareServers和StartServers设置都对基准测试结果产生了极大的影响。特别是,Apache需要一个“加速”期,以便达到足以服务于所应用的负载的多个子项。初始产生StartServers子项后,每秒只会创建一个子项来满足MinSpareServers设置。因此,一个服务器被100个并发客户端访问,使用默认的StartServers为5将需要95秒的时间来产生足够的子进程来处理负载。这在实际服务器上的实际工作正常,因为它们不会经常重启。但它的基准测试确实很差,可能只运行十分钟。

实施每秒一次的规则是为了避免在新子项启动的情况下淹没机器。如果机器忙于产生子项,则无法提供服务请求。但它对Apache的感知性能产生了如此巨大的影响,必须予以取代。从Apache 1.3开始,代码将放宽每秒一次的规则。它将产生一个,等待一秒,然后产生两个,等待一秒,然后产生四个,它将以指数方式继续,直到它每秒产生32个子项。只要满足MinSpareServers设置,它就会停止。

这似乎足够响应,几乎没有必要扭转MinSpareServers,MaxSpareServers和StartServers旋钮。当每秒生成4个以上的子节点时,将向ErrorLog发送一条消息。

与进程创建相关的是由MaxConnectionsPerChild设置引起的进程死亡。默认情况下,它的值是0,这意味着每个孩子处理的连接数没有限制。如果您的配置当前设置为某个非常低的数字,例如30,您可能希望显着提高它。如果运行的是SunOS或旧版本的Solaris,请将此限制为10000左右,因为太高可能导致内存泄漏。

编译时配置问题

选择MPMApache 2.x支持可插入的并发模型,称为多处理模块(MPM)。构建Apache时,必须选择要使用的MPM。某些平台有特定于平台的MPM:mpm_netware,mpmt_os2和mpm_winnt。对于一般的Unix类型系统,有几个MPM可供选择。MPM的选择会影响httpd的速度和可扩展性:

  • worker MPM使用多个子进程,每个进程有多个线程。每个线程一次处理一个连接。对于高流量服务器,worker通常是一个不错的选择,因为它比prefork MPM具有更小的内存占用。
  • 事件MPM像Worker MPM一样具有线程,但旨在允许通过将一些处理工作传递给支持线程来同时提供更多请求,从而释放主线程以处理新请求。
  • prefork MPM使用多个子进程,每个进程只有一个线程。每个进程一次处理一个连接。在许多系统上,prefork的速度与worker相当,但它使用更多的内存。在某些情况下,Prefork的无线设计优于worker:它可以与非线程安全的第三方模块一起使用,并且在具有较差线程调试支持的平台上更容易调试。

模块

由于内存使用是性能中非常重要的考虑因素,因此您应该尝试消除实际上未使用的模块。如果您已将模块构建为DSO,则消除模块只需注释掉该模块的相关LoadModule指令即可。这使您可以尝试删除模块,并查看您的网站是否仍然在没有这些模块的情况下运行。

另一方面,如果您将模块静态链接到Apache二进制文件中,则需要重新编译Apache以删除不需要的模块。

当然,这里出现的一个相关问题是,您列出需要哪些模块,哪些模块不需要。当然,这里的答案因网站而异。但是,您可以获得的最小模块列表往往包括mod_mime,mod_dir和mod_log_config。mod_log_config当然是可选的,因为可以运行没有日志文件的网站。但是,不建议这样做。

原子操作

一些模块,例如mod_cache和worker MPM的最新开发版本,使用APR的原子API。此API提供可用于轻量级线程同步的原子操作。

默认情况下,APR使用每个目标OS/CPU平台上可用的最有效机制来实现这些操作。例如,许多现代CPU具有在硬件中执行原子比较和交换(CAS)操作的指令。但是,在某些平台上,APR默认使用较慢的基于互斥锁的原子API实现,以确保与缺少此类指令的旧CPU模型兼容。如果要为其中一个平台构建Apache,并且计划仅在较新的CPU上运行,则可以通过使用--enable-nonportable-atomics选项配置Apache来在构建时选择更快的原子实现:

./buildconf
./configure --with-mpm=worker --enable-nonportable-atomics=yes
Shell

mod_status和ExtendedStatus On

如果包含mod_status并且在构建和运行Apache时也设置了ExtendedStatus On,那么在每次请求时,Apache都会执行两次调用gettimeofday(2)(或者根据您的操作系统的时间(2))和(1.3之前)几次额外的调用time(2)。这一切都已完成,以便状态报告包含时间指示。为获得最高性能,请关闭ExtendedStatus(这是默认设置)。

接受序列化 - 单Socket

以上对于多个套接字服务器来说很好,但是单个套接字服务器呢?从理论上讲,他们不应该遇到任何同样的问题,因为所有的子线程都可以阻止accept()直到连接到来,并且不会产生饥饿。在实践中,这隐藏了上面在非阻塞解决方案中讨论的几乎相同的“旋转”行为。大多数TCP堆栈的实现方式,内核实际上唤醒了单个连接到达时阻塞的所有进程。其中一个进程获取连接并返回用户空间。其余的东西在内核中旋转,当他们发现没有连接时再回到睡眠状态。这种旋转对用户土地代码是隐藏的,但它仍然存在。这可能导致相同的负载尖峰浪费行为,多个插座盒的非阻塞解决方案可以。

出于这个原因,我们发现如果我们甚至序列化单个插槽的情况,许多架构表现得更“漂亮”。所以这实际上是几乎所有情况下的默认值。Linux下的粗略实验(双Pentium pro 166 w / 128Mb RAM上的2.0.30)表明,单插槽的串行化使得非串行化单插槽的每秒请求数减少不到3%。但是,非序列化的单插槽在每个请求上显示额外的100ms延迟。这种延迟可能是长途线路上的冲洗,而且只是局域网上的一个问题。如果要覆盖单个套接字序列化,可以定义SINGLE_LISTEN_UNSERIALIZED_ACCEPT,然后单个套接字服务器根本不会序列化。

附录:跟踪的详细分析

以下是Apache 2.0.38的系统调用跟踪以及Solaris 8上的worker MPM。此跟踪是使用以下方法收集的:

truss -l -p httpd_child_pid.
Shell

-l选项告诉truss记录调用每个系统调用的LWP(轻量级进程 - Solaris形式的内核级线程)的ID。

其他系统可能具有不同的系统调用跟踪实用程序,例如strace,ktrace或par。它们都产生类似的输出。

在此跟踪中,客户端已从httpd请求了一个10KB的静态文件。具有内容协商的非静态请求或请求的痕迹看起来非常不同。

/67:    accept(3, 0x00200BEC, 0x00200C0C, 1) (sleeping...)
/67:    accept(3, 0x00200BEC, 0x00200C0C, 1)            = 9
Shell

在此跟踪中,侦听器线程在LWP#67中运行。

/65:    lwp_park(0x00000000, 0)                         = 0
/67:    lwp_unpark(65, 1)                               = 0
Shell

在接受连接时,侦听器线程唤醒工作线程以执行请求处理。在此跟踪中,处理请求的工作线程将映射到LWP#65。

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

为了实现虚拟主机,Apache需要知道用于接受连接的本地套接字地址。在许多情况下(例如,当没有虚拟主机,或者使用没有通配符地址的Listen指令时),可以消除此调用。但是还没有做出这些优化的努力。

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

brk()调用从堆中分配内存。在系统调用跟踪中很少见到这些,因为httpd使用自定义内存分配器(apr_pool和apr_bucket_alloc)进行大多数请求处理。在此跟踪中,httpd刚刚启动,因此必须调用malloc()来获取用于创建自定义内存分配器的原始内存块。

/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
Shell

接下来,worker 线程以非阻塞模式将连接放入客户端(文件描述符9)。setsockopt()和getsockopt()调用是Solaris的libc如何在套接字上处理fcntl()的副作用。

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

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
Shell

此httpd已使用Options FollowSymLinks和AllowOverride None进行配置。因此,它不需要lstat()导致所请求文件的路径中的每个目录,也不需要检查.htaccess文件。它只是调用stat()来验证文件:1)是否存在,2)是常规文件,而不是目录。

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

在此示例中,httpd能够使用单个sendfilev()系统调用发送HTTP响应头和所请求的文件。Sendfile语义因操作系统而异。在某些其他系统上,必须执行write()或writev()调用以在调用sendfile()之前发送标头。

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

此write()调用在访问日志中记录请求。请注意,此跟踪中缺少的一件事是time()调用。与Apache 1.3不同,Apache 2.x使用gettimeofday()来查找时间。在某些操作系统(如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
Shell

worker 线程会延迟关闭连接。

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

最后,工作线程关闭它刚刚传递的文件并阻塞,直到侦听器为其分配另一个连接。

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

同时,监听器线程一旦将此连接分派给工作线程,就能够接受另一个连接(受制于工作者MPM中的某些流控制逻辑,如果所有可用工作者都忙,则会限制监听器)。虽然从这个跟踪中看不出来,但是下一个accept()可以(并且通常在高负载条件下)与工作线程处理刚刚接受的连接并行发生。




以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号