Apache是把正確性放在首位、把速度放在其次的通用Web服務(wù)器。即使這樣,它的性能十分令人滿(mǎn)意。許多站點(diǎn)只有不到10M的出口帶寬。Apache能夠在這些站點(diǎn)的低端Pentium服務(wù)器上全速工作。實(shí)際上,擁有更多帶寬的站點(diǎn)出于一些原因(比如大量的CGI和數據庫事務(wù)處理)需要用一臺以上的機器滿(mǎn)足帶寬需求。這些原因導致了以往的Apache開(kāi)發(fā)工作集中在正確性和可配置性。
不幸的是許多人過(guò)于重視某些指標,并把它們的原始數據當作評價(jià)Web服務(wù)器優(yōu)劣的標準。被普遍接受標準的是"原始最低性能(bare minimum performance)",而在這以外的其他速度指標只適用于很小部分的市場(chǎng)需求。但為了避免Apache在一些市場(chǎng)中受到排擠,我們在A(yíng)pache1.3上盡了相當的努力,將它與高端服務(wù)器的差距減至最小。
另有一些人只是想試試這些東東能運行得多快。這些人竭力把Apache最后一滴性能擠出來(lái),他們也想看看究竟是什么影響了Apache的性能。這篇文章的其余部分就是針對他們而撰的。
請注意本文適用于Unix上的Apache1.3,部分內容適用于NT平臺。目前的Apache尚未在NT上進(jìn)行優(yōu)化。事實(shí)上,不同的編程模型使它在NT上的性能表現相當不好。(即POSIX模型。NT借助POSIX子系統模擬這種編程標準,因此效率很低。Apache2.0拋棄了POSIX直接與操作系統打交道,性能將有所飛躍--譯者注)
二. 關(guān)于硬件平臺和操作系統
最直接影響Web服務(wù)器性能的硬件要數RAM。一臺Web服務(wù)器從不應該訪(fǎng)問(wèn)內存交換區。交換增加了每次請求的延時(shí),用戶(hù)將因此認為"不夠快"。他們會(huì )點(diǎn)擊[停止]并重新裝載網(wǎng)頁(yè),這將進(jìn)一步增加服務(wù)器的負擔。您能夠也有必要調節M(mǎn)axClients,使您的服務(wù)器不會(huì )衍生太多的子進(jìn)程而導致交換。
除此之外的事情就沒(méi)那么關(guān)鍵了。擁有快速的CPU、快速的網(wǎng)卡和硬盤(pán)都可以讓您的服務(wù)器"足夠快"。其實(shí)這足夠快個(gè)詞是需要憑經(jīng)驗去體會(huì )的。
操作系統的選用也是值得斟酌的大問(wèn)題。普遍的準則是:及時(shí)得到操作系統提供商的最新TCP/IP補丁。迅速涌現的HTTP服務(wù)打破了截止到1994年乃至95年的Unix內核中設定的許多假設情況。理想的選擇包括目前的FreeBSD和Linux。
三. 關(guān)于運行時(shí)設置(Run-Time Configuration)
1) HostnameLookups
1.3版以前的Apache中,HostnameLookups的缺省值是On,這將導致每次請求時(shí)服務(wù)器都要進(jìn)行NDS查詢(xún),從而增加了延遲。Apache1.3將此缺省值設為Off。在1.3及以后的版本中,如果您使用了任何allow from domain或deny from domain命令,所付出的代價(jià)將是兩次DNS查詢(xún)帶來(lái)的延時(shí)(在一次逆向查詢(xún)后跟著(zhù)一次正向查詢(xún),以保證前者得到的結果是真實(shí)的)。因此為了得到最理想的性能應避免使用HostnameLookups(使用IP地址而非域名也是個(gè)好主意)。
限制命令的使用范圍是可行的,比如使用類(lèi)似
HostnameLookups off
這看起來(lái)足夠好了。幾乎不用在MinSpareServers、MaxSpareServers或StartServers上費工夫了。當每秒鐘衍生的進(jìn)程數超過(guò)4時(shí),ErrorLog中會(huì )增加一條相應的記錄。如果您看到了很多這樣的提示,請調整這些參數。mod_status的輸出會(huì )給您一些提示。
于進(jìn)程相關(guān)的問(wèn)題是由MaxRequestsPerChild導致的進(jìn)程終止。MaxRequestsPerChild缺省地設置為0,意味每個(gè)子進(jìn)程處理的請求數不受限制。如果當前的設置值非常小,您可能希望大幅度提升這個(gè)值。為了防止內存泄露,在SunOS或者低版本的Solaris上,應把此值設為10000左右。
如果使用了持續連接(keep-alives),子進(jìn)程將繁忙等待(busy waiting)已打開(kāi)連接的后續請求而不能做其他的事。缺省的15秒種試圖使影響將至最底。您需要在網(wǎng)絡(luò )帶寬和服務(wù)器資源之間作出權衡。任何情況下,不應設置持續連接時(shí)間超過(guò)60秒。否則大部分好處將變成損失。
四. 關(guān)于編譯時(shí)設置
1) mod_status 和 ExtendedStatus On
如果在編譯Apache時(shí)您包含了mod_status并且將ExtendedStatus設置為On,Apache將為每個(gè)請求進(jìn)行兩次gettimeofday(2)系統調用(或者針對不同的系統調用times(2))及(在1.3以前的版本)許多次time(2)。這些都是為了在報告中含有時(shí)間戳。為了得到最佳性能,請將ExtendedStatus設為Off(這是缺省的設置)。
2) 多socket中的accept 串行化
這部分文章將討論Unix socket API不利的一方面。假設您的服務(wù)器用多個(gè)Listen命令偵聽(tīng)多個(gè)端口或者多個(gè)IP地址。Apache使用select(2)檢測每個(gè)socket連接(connection)是否就緒。select(2)示意有零個(gè)或至少一個(gè)連接等待某個(gè)socket。Apache含有多個(gè)子進(jìn)程,所有空閑的子進(jìn)程同時(shí)偵聽(tīng)新的連接。原始的實(shí)現如下所示(這個(gè)例子不是真正的代碼,它出于教學(xué)目的被簡(jiǎn)化了)
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;
}
但這種實(shí)現會(huì )引起嚴重的饑餓問(wèn)題。由于多個(gè)子進(jìn)程同時(shí)執行這個(gè)循環(huán),它們將在select中阻塞。當任何socket上出現一個(gè)請求時(shí),所有被阻塞的進(jìn)程將復蘇,并從select返回(蘇醒進(jìn)程的數量取決于操作系統和時(shí)間)。它們將繼續執行并試圖接受這個(gè)連接,但只有一個(gè)進(jìn)程會(huì )成功(假設目前仍只有一個(gè)連接),其余進(jìn)程將阻塞在accept中。這將把所有失敗的進(jìn)程鎖定,使它們只為一個(gè)socket上的請求服務(wù)。它們會(huì )一直被阻塞,直到在那個(gè)socket上出現足夠的請求把它們喚醒。這一饑餓問(wèn)題首先在PR#467被提出。至少有兩種解決它的方法。
一種方案是使用非阻塞的socket。這種情況下,accept不會(huì )阻塞子進(jìn)程,它們將會(huì )立即返回。但這種方案會(huì )造成CPU時(shí)間的浪費。假設有十個(gè)在select中的空閑進(jìn)程,而后到來(lái)了一個(gè)連接請求。九個(gè)進(jìn)程將蘇醒、試圖接受連接、失敗,并返回select,這些進(jìn)程實(shí)際什么都沒(méi)做。而且如果在這期間,其他socket上出現請求,沒(méi)有哪個(gè)進(jìn)程會(huì )為它服務(wù)??偠灾?,這種方案不是十分有效,除非您擁有和空閑子進(jìn)程數目相當的CPU--恐怕不切實(shí)際。
另一種方案被Apache采納。這種方案串行化(serialize)對內層循環(huán)的調用。代碼如下所示(改進(jìn)的部分被加粗顯示):
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 兩個(gè)函數實(shí)現了互斥量(mutual exclusion semaphore),在任意時(shí)刻只能有一個(gè)子進(jìn)程擁有互斥量。多種方法可以實(shí)現互斥量。在src/conf.h(1.3版之前)或src/include/ap_config.h(1.3版及以后)可以作出以下選擇。一些系統不提供任何互斥方法。在這些系統上使用多個(gè)Listen命令是不保險的。
USE_FLOCK_SERIALIZED_ACCEPT
此方法用flock(2)系統調用對一個(gè)鎖文件加鎖。(此文件在LockFile命令中指定)
USE_FCNTL_SERIALIZED_ACCEPT
此方法用flock(2)系統調用對一個(gè)鎖文件加鎖。(此文件在LockFile命令中指定)
USE_SYSVSEM_SERIALIZED_ACCEPT (1.3版及以后)
此方法借助SysV的信號量(semaphores)實(shí)現互斥。但不巧的是SysV信號量有一些負面作用。一是Apache可能在清除信號量之前非正常終止;二是在使用信號量API時(shí)需要考慮到任何與服務(wù)器UID相同的CGI程序可以進(jìn)行拒絕服務(wù)攻擊(就是說(shuō)所有的CGI程序都可以這樣做,除非使用suexec或cgiwrapper之類(lèi)的方法)。所以,這種方法并不被IRIX之外的系統廣泛采納(由于大多數IRIX系統上,使用前兩種方法的代價(jià)太大)。
USE_USLOCK_SERIALIZED_ACCEPT
?。?.3版及以后)此方法僅在IRIX上可用。它調用usconfig(2)創(chuàng )建互斥量。雖然這種方法避免了對SysV信號量的種種爭議,但它不是IRIX的缺省方案。這是由于在單處理器的IRIX系統 (5.3或6.2)上,uslock代碼比SysV信號量慢兩個(gè)數量級;但在多處理器的IRIX中前者比后者快一個(gè)數量級。這無(wú)非使問(wèn)題復雜化了。所以在多處理器IRIX系統上,您需要用如下的附加參數編譯Apache:
在EXTRA_CFLAGS中添加-DUSE_USLOCK_SERIALIZED_ACCEPT
USE_PTHREAD_SERIALIZED_ACCEPT
(1.3版及以后)此方法實(shí)現了POSIX標準互斥量。它理應可以工作在任何實(shí)現了全部POSIX線(xiàn)程規范的系統上,但事實(shí)是只有在Solaris 2.5或以上的系統及特定的配置中才能工作。如果您嘗試這種方法的話(huà),需要小心服務(wù)器掛起或者沒(méi)有響應。服務(wù)器在只輸出靜態(tài)網(wǎng)頁(yè)的情況下運行得很好。
如果您的系統上有其他串行化的方法,為它書(shū)寫(xiě)代碼(并把補丁寄給Apache)是值得的。
有一個(gè)考慮到但從未實(shí)現的方案是對循環(huán)部分地串行化--即允許一定數目的進(jìn)程進(jìn)入循環(huán)。在同一時(shí)刻可運行若干進(jìn)程的多處理器系統上,這個(gè)主意是滿(mǎn)不錯的。而且前面提到的方案并沒(méi)有充分利用帶寬??捎捎诟叨炔⑿谢姆?wù)器實(shí)在少見(jiàn),這個(gè)方案的優(yōu)先級比較低。
為了得到最佳性能,不用多偵聽(tīng)命令是最理想的。請繼續往下看。
3) 單socket中的accept串行化
以上言及的方案對多socket服務(wù)器是相當不錯的,但只有一個(gè)socket的情況又如何呢?理論上,由于在連接請求到來(lái)之前所有子進(jìn)程將阻塞在accept中,單個(gè)socket不會(huì )產(chǎn)生上述種種問(wèn)題。但實(shí)際上,上述非阻塞解決方案所帶來(lái)的"回旋(spinning)"問(wèn)題在這里只不過(guò)被掩蓋起來(lái)了。在絕大多數TCP協(xié)議棧的實(shí)現中,一個(gè)接請求到來(lái)時(shí)內核將喚醒所有阻塞在accept中的進(jìn)程。它們之一將得到此請求并返回用戶(hù)空間,其余的進(jìn)程將返回內核重新休眠。這將帶來(lái)與多socket非阻塞解決方案相同的資源浪費。
由于這點(diǎn)原因,我們發(fā)現如果為socket串行化,許多系統表現得更"友好"--即使是一個(gè)socket的情況。這是單個(gè)socket串行化作為絕大多數情況的缺省配置的原因。在Linux上不甚精確的(Linux 2.0.30 / 雙Pentium Pro 166 w / 128Mb內存)實(shí)驗表明,對每次請求而言,串行化的單個(gè)socket僅比沒(méi)有串行化的socket損失不到3%的性能。但未串行化的socket顯示出每次連接請求100毫秒的延時(shí)。這也可能僅僅由于過(guò)長(cháng)的通訊距離造成的。如果您不想串行化單個(gè)socket,可以定義宏SINGLE_LISTEN_UNSERIALIZED_ACCEPT。這樣,僅有一個(gè)socket的服務(wù)器將不會(huì )串行化。
4) 延遲關(guān)閉(Lingering Close)
就象draft-ietf-http-connection-00.txt第8節討論的那樣,為了使服務(wù)器能夠可靠地實(shí)現HTTP協(xié)議,有必要獨立地關(guān)閉每個(gè)方向上的通訊(每個(gè)TCP連接有兩個(gè)方向,每個(gè)方向是分別獨立的)。這個(gè)事實(shí)往往被其他服務(wù)器所忽視,而Apache 1.2就已經(jīng)正確地處理了。
當這個(gè)特性增加到Apache中時(shí)卻在許多版本的Unix中引起了問(wèn)題。這是TCP規范的短見(jiàn)造成的--它沒(méi)有聲明FIN_WAIT_2有超時(shí),但也沒(méi)有阻止這樣的實(shí)現。在沒(méi)有超時(shí)的系統中,Apache 1.2將導致許多socket將永遠處于FIN_WAIT_2的狀態(tài)。這可以簡(jiǎn)單地用打最新TCP/IP補丁的方法避免。然而在提供商從不發(fā)行補丁的系統上(也就是SunOS4--雖然得到源代碼許可證的人可以自己打補?。?,我們決定不直接使用這一特性。

