目前httpd守护进程越来越丰富,而Apache2或许是大家最熟悉,应用范围最广泛的。该篇幅主要探讨一下Apache2与性能相关的配置。我们从简单的配置说起。我们开始吧…
HostnameLookups Off
HostnameLookups设置如果一旦启用,服务器会对客户端的hostname进行nslookup查询。这将延迟对用户的响应。我们截取了一段,开启了HostnameLookups选项的进程调用记录。
open(“/etc/hosts”, O_RDONLY) = 22
fcntl64(22, F_GETFD) = 0
fcntl64(22, F_SETFD, FD_CLOEXEC) = 0
fstat64(22, {st_mode=S_IFREG|0644, st_size=174, …}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ff1000
read(22, “# Do not remove the following li”…, 4096) = 174
read(22, “”, 4096) = 0
close(22) = 0
munmap(0xb7ff1000, 4096) = 0
socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 22
connect(22, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr(“202.96.209.5”)}, 28) = 0
fcntl64(22, F_GETFL) = 0x2 (flags O_RDWR)
fcntl64(22, F_SETFL, O_RDWR|O_NONBLOCK) = 0
gettimeofday({1271519250, 942822}, NULL) = 0
poll([{fd=22, events=POLLOUT}], 1, 0) = 1 ([{fd=22, revents=POLLOUT}])
send(22, “0q1 1 0265 014 03173 02617in-addr”…, 42, MSG_NOSIGNAL) = 42
poll([{fd=22, events=POLLIN}], 1, 5000) = 1 ([{fd=22, revents=POLLIN}])
ioctl(22, FIONREAD, [102]) = 0
recvfrom(22,”0q201200 1 1 0265 014 03173 02617in-addr”…,1024,0,
{sa_family=AF_INET,sin_port=htons(53), sin_addr=inet_addr(“202.96.209.5”)}, [16]) = 102
close(22)
从这份快照可以看出,Apache首先会从/etc/hosts中查找是否有与客户端hostname相同的DNS记录,如果没有找到,则会连接指定的域名服务器进行nslookup操作。
AllowOverride None
一般,将AllowOverride设置为AllowOverride None的性能是最优的。如果该值设置All,目录设置允许被.htaccess文件覆盖。那么Apache则会在文件名的每一个组成部分都尝试打开.htaccess文件。要避免这种情况,可以将AllowOverride设置为None。 下面是一段将AllowOverride all设置的系统调用快照,以此来说明取值all的劣性。
stat64(“/opt/virtaul_hosts/perfgeeks.com/wp-content/themes/perfgeeks/style.css”, {st_mode=S_IFREG|0755, st_size=7929, …}) = 0
open(“/opt/virtaul_hosts/perfgeeks.com/.htaccess”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open(“/opt/virtaul_hosts/perfgeeks.com/wp-content/.htaccess”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open(“/opt/virtaul_hosts/perfgeeks.com/wp-content/themes/.htaccess”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open(“/opt/virtaul_hosts/perfgeeks.com/wp-content/themes/perfgeeks/.htaccess”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open(“/opt/virtaul_hosts/perfgeeks.com/wp-content/themes/perfgeeks/style.css/.htaccess”, O_RDONLY|O_LARGEFILE) = -1 ENOTDIR (Not a directory)
open(“/opt/virtaul_hosts/perfgeeks.com/wp-content/themes/perfgeeks/style.css”, O_RDONLY|O_LARGEFILE) = 22
mmap2(NULL, 7929, PROT_READ, MAP_SHARED, 22, 0) = 0xb7f85000
munmap(0xb7f85000, 7929) = 0
我们可以看到,自stat64()开始,Apache一共进行了5次open()的内核调用,并且都返回-1(表示文件不存在),分别在文件名/wp-content/themes/perfgeeks/style.css每一个组成部分下面试图打开.htaccess文件。必竟这5次open内核函数调用不是很必要。如果有条件的读者可将.htacess中的配置合并到相应的Apache的配置文件中,并且把AllowOverride设置为None,以获得最佳性能。
Options FollowSymLinks
Options的选值很多,除了All之外,我们还比较关心Indexes、FollowSymLinks和SymLinksIfOwnerMatch。其中FollowSymLinks表示允许在此目录使用符号连接。这是一种什么概念呢,比如/va/ftp/data是你存ftp上传数据的地方,而web目录是/var/www/perfgeeks,假定你想通过http://www.perfgeeks.com/ftp访问/var/ftp/data目录的数据,你可以在/var/www/perfgeeks目录下建立一个符号连接ftp指向/var/ftp/data。而FollowSymLinks就是指明这种操作是允许的,指示Apache不必去检查ftp文件是否为链接。另外SymLinksIfOwnerMatch则要求和符号连接与其指向的目录或文件属主是同一人(相同的uid)才允许上述操作,即/var/www/perfgeeks/ftp与/var/ftp/data拥有者的uid要求是一样的,这样Apache就必须通过请求系统内核调用stat()来检查文件名每一个组成部分是否为链接,如果是链接就要去核实是否与链接指向的原文件具有相同的uid。我们推荐设置FollowSymLinks,而不要设置SymLinksIfOwnerMatch,这样可以获取更高的性能。因为,假定没有设置FollowSymLinks或者一旦设置了SymLinksIfOwnerMatch,则会额外地调用系统内核函数lstat()来验证目录是否为符号连接。而且是验证文件的每一个组成部分。下面,我们来看二份快照,分别是设置了FollowSymLinks和SymLinksIfOwnerMatch的strace记录。
#—–Option FollowSymLinks————————
read(11, “GET /test/test.php HTTP/1.1rnHos”…, 8000) = 417
gettimeofday({1271731674, 100730}, NULL) = 0
stat64(“/var/www/html/test/test.php”, {st_mode=S_IFREG|0644, st_size=27, …}) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
#—–Option SymLinksIfOwnerMatch—————
read(11, “GET /test/test.php HTTP/1.1rnHos”…, 8000) = 417
gettimeofday({1271731349, 444196}, NULL) = 0
stat64(“/var/www/html/test/test.php”, {st_mode=S_IFREG|0644, st_size=27, …}) = 0
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/test.php”, {st_mode=S_IFREG|0644, st_size=27, …}) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
对比两份快照,很明显。SymLinksIfOwnerMatch比FollowSymLinks设置额外地调用了5次lstat(),验证是否为符号连接。
DirectoryIndex index.php index.html index.html.var
当用户访问的URL以一个”/”结尾的时候,DirectoryIndex则指明了要寻找的资源列表。也就是说,Apache会依次找/path/index.php, /path/index.html等等。所以,DirectoryIndex指定的资源列表顺序与数量都会影响性能,我们推荐数量不宜太多,把最常用的资源放在列表的最前面。这里,我们分别提供不同顺序,不同长度的三份strace快照:
#–配置 DirectoryIndex index.html index.html.var index.php 且资源列表的所有资源都不存在
read(11, “GET /test/ HTTP/1.1rnHost: 192.1″…, 8000) = 366
gettimeofday({1271745805, 985450}, NULL) = 0
stat64(“/var/www/html/test/”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
stat64(“/var/www/html/test/index.html”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/index.html”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
stat64(“/var/www/html/test/index.html.var”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/index.html.var”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
stat64(“/var/www/html/test/index.php”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/index.php”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
gettimeofday({1271745805, 988039}, NULL) = 0
#–配置:DirectoryIndex index.html index.html.var index.php 其中index.php资源存在
read(11, “GET /test/ HTTP/1.1rnHost: 192.1″…, 8000) = 366
gettimeofday({1271746049, 388916}, NULL) = 0
stat64(“/var/www/html/test/”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
stat64(“/var/www/html/test/index.html”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/index.html”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
stat64(“/var/www/html/test/index.html.var”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/index.html.var”, 0xbfd3691c) = -1 ENOENT (No such file or directory)
stat64(“/var/www/html/test/index.php”, {st_mode=S_IFREG|0644, st_size=27, …}) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
#–配置DirectoryIndex index.php index.html index.html.var 且资源index.php存在
read(11, “GET /test/ HTTP/1.1rnHost: 192.1″…, 8000) = 392
gettimeofday({1271746373, 504237}, NULL) = 0
stat64(“/var/www/html/test/”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
stat64(“/var/www/html/test/index.php”, {st_mode=S_IFREG|0644, st_size=27, …}) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
对比上面三份不同的DirectoryIndex配置与快照,我们可以看到正确位置、合适的资源列表数目给我们省下了不少的系统内核函数调用。当用户访问一个目录的时候,Apache会根据DirectoryIndex指定的资源列表依次查找。所以,第三份是性能最佳的配置。
Options MultiViews
我们应该尽量避免使用Options MultiViews来实现内容协商,它非常低效,因为要搜索整个目录。我们可以通过type-map的方式实现内容协商。把所有要用到的type-map放在一个文件里面,比搜索文件性能会更高。所谓,内容协商,指从几个有效资源中选择一个最匹配用户端请求的资源给用户端,这个过程就叫做内容协商。比如,用户请求了资源/path/test/foo, 而且/path/test/foo文件并不存在。MultiViews的做法就是在整个目录查找foo.*所有文件,并且跟据用户请求的content-type, content-encoding等信息,把一个最接近用户请求的资源作为用户请求的资源,返回给用户。这里给出了一份Options MultiViews方式内容协商的快照,以供参考。
read(11, “GET /test/foo HTTP/1.1rnHost: 19″…, 8000) = 369
gettimeofday({1271751239, 855470}, NULL) = 0
stat64(“/var/www/html/test/foo”, 0xbf93f62c) = -1 ENOENT (No such file or directory)
lstat64(“/var”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test”, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
lstat64(“/var/www/html/test/foo”, 0xbf93f62c) = -1 ENOENT (No such file or directory)
open(“/var/www/html/test/”, O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 12
fstat64(12, {st_mode=S_IFDIR|0755, st_size=4096, …}) = 0
fcntl64(12, F_SETFD, FD_CLOEXEC) = 0
getdents64(12, /* 9 entries */, 4096) = 272
stat64(“/var/www/html/test/foo.gif”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
stat64(“/var/www/html/test/foo.php”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
stat64(“/var/www/html/test/foo.pdf”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
stat64(“/var/www/html/test/foo.jpg”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
stat64(“/var/www/html/test/foo.txt”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
getdents64(12, /* 0 entries */, 4096) = 0
close(12) = 0
…
lstat64(“/var/www/html/test/foo.php”, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
open(“/var/www/html/test/foo.php”, O_RDONLY) = 12
fstat64(12, {st_mode=S_IFREG|0644, st_size=0, …}) = 0
lseek(12, 0, SEEK_CUR) = 0
read(12, “”, 8192) = 0
close(12) = 0
chdir(“/etc/httpd/conf”) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
writev(11, [{“HTTP/1.1 200 OKrnDate: Tue, 20 A”…, 254}], 1) = 254
write(8, “192.168.0.98 – – [20/Apr/2010:16″…, 172) = 172
shutdown(11, 1 /* send */)
我们在/test目录下面分别创建了foo.*共5个文件,当我们请求/test/foo的时候,我们可以从加粗部分,很容易地发现Apache在找不到/test/foo的情况下,它会在/test查看所有foo.*文件状态。我们再从红色部分可以发现Apache经过内容协商之后,最终决定以/test/foo.php作为用户的响应,而并没有返回用户404错误。所以,我们在配置Apache的时候,尤其要避免使用Option MultiViews,因为这种配置效率较低。
EnableMMAP on
尽可能地打开内存映射文件设置。所谓内存映射文件,指的就是Linux内核通过内核调用mmap()将磁盘文件与进程某段内存地址空间(有可能是物理内存,但也有可能是虚拟内存)对映起来,自此Apache就可以通过访问内存直接访问磁盘文件,它不需要使用read()和write()等系统内核调用来访问磁盘了。所以,在大多数情况下,可以通过内存映射文件的机制来提高磁盘I/O性能。Apache2开始支持mmap功能,对于较大的静态文件,Apche是不会使用mmap的,因为需要不小的内存开销。另外,mmap仅对较小的静态文件有效。也就是说,Apache2对于较小的静态文件,才会启用mmap将文件映射到进程内存地空间。
read(11, “GET /test/test.html HTTP/1.1rnHo”…, 8000) = 375
gettimeofday({1271778564, 175940}, NULL) = 0
stat64(“/var/www/html/test/test.html”, {st_mode=S_IFREG|0644, st_size=10, …}) = 0
open(“/var/www/html/test/test.html”, O_RDONLY|O_LARGEFILE) = 12
mmap2(NULL, 10, PROT_READ, MAP_SHARED, 12, 0) = 0xb6107000
writev(11, [{“HTTP/1.1 200 OKrnDate: Tue, 20 A”…, 260}, {“hello wold”, 10}], 2) = 270
munmap(0xb6107000, 10) = 0
write(8, “192.168.0.98 – – [20/Apr/2010:23″…, 179) = 179
在这里,加粗部分我们可以看得出。open()以只读的方式打开请求文件,接下来通过mmap2()将文件映射到进程内存地址空间。我们并没有看到系统内核read()调用,这是因为Apache直接读取了进程内存空间的数据,属于用户态行为。即,一旦映射之后,用户就不用像从前一次一次通过系统内核调用read()去将磁盘数据读入用户态内存空间了。这样省去了与用户空间与内核空间读文件上下切换的开销。
EnableSendfile On
如果你的系统支持Sendfile机制的话,通过EnableSendfile On设置开启该机制。Sendfile机制可以提高Apache性能。所谓Sendfile,就是我们平常所提到过的”零拷贝”。一般,我们对一个磁盘文件写操作,要经过几次拷贝才能够完成。首先用户进程,通过fgets()向请求系统内核调用read,这时候read会将磁盘文件数据拷到系统内核空间的缓冲区,然后系统内核调用返回的数据拷到用户空间的缓冲区。当回写数据的时候,又会通过fwrite()函数请求系统内核调用write,将数据写回磁盘。磁盘设备->内核缓冲(内存)->用户缓冲(内存)来回共4次拷贝。对于一个静态文件请求,我们会发现,首先从磁盘设备将数据拷贝到内核空间缓冲区,又从内核空间缓冲区拷贝到用户空间缓冲区,马上又从用户空间缓冲区拷贝回内核缓冲区,接着从内核缓冲区写入到网络接口设备。我们发现,静态数据从内核空间交给用户空间之后,并没有被程序做任何处理,又原原本本地传回了内核空间,数据就这样白白地兜了一圈。所以,Linux自支持Sendfile机制以后,它可以让数据不用交给用户空间缓冲区,直接在内核空间处理掉。这样节约了内核态的切换和用户态数据的复制开销。Apache对于较大的静态资源请求,会启用Sendfile。下面,我们来对比一下启用与不启用sendfile的快照
open(“/var/www/html/test/20090316.pdf”, O_RDONLY|O_LARGEFILE) = 13
writev(11, [{“HTTP/1.1 200 OKrnDate: Wed, 21 A”…, 260}], 1) = 260
_llseek(13, 0, [0], SEEK_SET) = 0
read(13, “%PDF-1.4n%n3 0 objn<