[ 永遠的UNIX::UNIX技術資料的寶庫 ]   GB | BIG5

首頁 > 應用技術 > Apache > 正文
Apache性能提示
本文出自:網易 作者: (2001-10-16 15:00:00)
Apache是把正確性放在首位、把速度放在其次的通用Web服務器。即使這樣,它的性能十分令人滿意。
許多站點只有不到10M的出口帶寬。Apache能夠在這些站點的低端Pentium服務器上全速工作。實際上,
擁有更多帶寬的站點出一些原因(比如大量的CGI和數據庫事務處理)需要用一台以上的機器滿足帶
寬需求。這些原因導致了以往的Apache開發工作集中在正確性和可配置性。  

不幸的是許多人過重視某些指標,並把它們的原始數據當作評價Web服務器優劣的標準。被普遍接受
標準的是“原始最低性能(bare minimum performance)”,而在這以外的其他速度指標只適用很
小部分的市場需求。但為了避免Apache在一些市場中受到排擠,我們在Apache1.3上盡了相當的努力,
將它與高端服務器的差距減至最小。  

另有一些人只是想試試這些東東能運行得多快。這些人竭力把Apache最一滴性能擠出來,他們也想看
看究竟是什影響了Apache的性能。這篇文章的其余部分就是針對他們而撰的。  

請注意本文適用Unix上的Apache1.3,部分內容適用NT平台。目前的Apache尚未在NT上進行優化。
事實上,不同的編程模型使它在NT上的性能表現相當不好。(即POSIX模型。NT借助POSIX子系統模擬
這種編程標準,因此效率很低。Apache2.0拋棄了POSIX直接與操作系統打交道,
性能將有所飛躍譯者注)  

關硬件平台和操作系統  
最直接影響Web服務器性能的硬件要數RAM。一台Web服務器從不應該訪問內存交換區。交換增加了每次
請求的延時,用戶將因此認為“不夠快”。他們會點擊[停止]並重新裝載網頁,這將進一步增加服務器
的負擔。您能夠也有必要調節MaxClients,使您的服務器不會衍生太多的子進程而導致交換。  

除此之外的事情就沒那關鍵了。擁有快速的CPU、快速的網卡和硬盤都可以讓您的服務器“足夠快”。
其實這足夠快個詞是需要憑經驗去體會的。  

操作系統的選用也是值得斟酌的大問題。普遍的準則是:及時得到操作系統提供商的最新TCP/IP補丁。
迅速湧現的HTTP服務打破了截止到1994年乃至95年的Unix內核中設定的許多假設情況。理想的選擇包括
目前的FreeBSD和Linux。  

關運行時設置(Run-Time Configuration)  
HostnameLookups  
1.3版以前的Apache中,HostnameLookups的缺省值是On,這將導致每次請求時服務器都要進行NDS查詢,
從而增加了延遲。Apache1.3將此缺省值設為Off。在1.3及以的版本中,如果您使用了任何
allow from domain或deny from domain命令,所付出的代價將是兩次DNS查詢帶來的延時(在一次逆向
查詢跟著一次正向查詢,以保証前者得到的結果是真實的)。因此為了得到最理想的性能應避免使用
HostnameLookups(使用IP地址而非域名也是個好主意)。  

限制命令的使用范圍是可行的,比如使用類似<Location /server-status>的容器。這種情況下,DNS查
詢只發生在符合條件的請求中。下面的例子使查詢只發生在.html和.cgi文件的請求中:  

HostnameLookups off  
<Files ~ ".(html|cgi)$>  
HostnameLookups on  
</Files>  
關閉了DNS查詢,如果在您的CGI程序中需要DNS名稱的話,可以考慮在那些程序中調用gethostbyname。  

FollowSymLinks 和 SymLinksIfOwnerMatch  
在任何情況下,只要您沒有指定FollowSymLinks的選項(即Options FollowSymLinks),或者指定了
SymLinksIfOwnerMatch選項,Apache將不得不調用額外的系統函數來檢查符號鏈接。每次針對文件名
的請求都將觸發一次檢查。比如您指定了:  

DocumentRoot /www/htdocs  
<Directory />  
Options SymLinksIfOwnerMatch  
</Directory>  
當一個指向URI /index.html的請求到來時,Apache將對/www,/www/htdocs和/www/htdocs/index.html
分別調用lstat(2)。不僅如此,lstat的結果是從不被緩存的,因此每次請求都要重新這樣的檢查。如果
您的確需要安全的符號鏈接的話,可以試著這樣做:  

DocumentRoot /www/htdocs  
<Directory />  
Options FollowSymLinks  
</Directory>  
<Directory /www/htdocs>  
Options -FollowSymLinks +SymLinksIfOwnerMatch  
</Directory>  
這至少避免了對DocumentRoot目錄本身的檢查。請注意,如果在RocumentRoot之外有Alias或者
RewriteRule涉及的目錄,您需要為這些目錄增加類似的選項。為了在無符號鏈接檢查的情況下得到最
佳性能,請在所有地方設置FollowSymLinks,並去掉所有的SymLinksIfOwnerMatch。  

AllowOverride  
在任何情況下,只要您允許覆蓋(通常是.htaccess文件),Apache將試圖為每次針對文件名稱的請求打
開.htaccess文件。比如:  

DocumentRoot /www/htdocs  
<Directory />  
AllowOverride all  
</Directory>  
當指向URI /index.html的請求到來時,Apache將試圖打開
/.htaccess、/www/.htaccess和/www/htdocs/.htaccess。這個問題可以用類似解決FollowSymLinks的方
法解決。為了得到最佳性能,在所有地方使用AllowOverride None。  

內容協商  
如果您對每處細微的性能調節都很在意,在可能的情況下避免內容協商(content-negotiation)。實際
應用中,協商的益處超過了給性能帶來的損失。您可以在一種情況下提速服務器:避免使用這樣的通配符:  

DirectoryIndex index  
請列出所有可能的情況:  

DirectoryIndex index.cgi index.pl index.shtml index.html  
並把最常用的選擇放在前面。  

   

進程的建立  
對1.3版以前的Apache,MinSpareServers、MaxSpareServers、和StartServers這三個參數對性能測
試的結果有巨大影響。Apache啟動需要一個“爬升”期使其子進程數與服務器的負載相平衡。剛剛
啟動的Apache生成StartServers個子進程。而將每隔一秒生成一個新的子進程,最終達到
MinSpareServers的要求。所以如果服務器用StartServers等5的默認值啟動被100個客戶並發訪問,
Apache將用續的95秒種生成足夠的子進程以平衡負載。由現實中的服務器不經常啟動,這種技術
在實際應用中工作得很好。但在評測軟件中的表現就不那出色了,因為這些軟件可能頂多運行10分鐘。  

一秒一個的規則防止服務器在生成子進程時過忙碌。如果它忙繁殖進程,請求將被擱置。但這個
規則對直觀性能的影響太大了,它必須有所改觀。在Apache 1.3中,一秒一個的規則被廢棄了。它首
先衍生一個子進程,等一秒,衍生兩個,等一秒,再衍生兩個,直到一秒衍生32個子進程。隨它將
保持這個速度直到滿足MinSpareServers的要求。  

這看起來足夠好了。幾乎不用在MinSpareServers、MaxSpareServers或StartServers上費工夫了。當
每秒鐘衍生的進程數超過4時,ErrorLog中會增加一條相應的記錄。如果您看到了很多這樣的提示,請
調整這些參數。mod_status的輸出會給您一些提示。  

進程相關的問題是由MaxRequestsPerChild導致的進程終止。MaxRequestsPerChild缺省地設置為0,
意味每個子進程處理的請求數不受限制。如果當前的設置值非常小,您可能希望大幅度提升這個值。為
了防止內存泄露,在SunOS或者低版本的Solaris上,應把此值設為10000左右。  

如果使用了持續連接(keep-alives),子進程將繁忙等待(busy waiting)已打開連接的續請求而不
能做其他的事。缺省的15秒種試圖使影響將至最底。您需要在網絡帶寬和服務器資源之間作出權衡。任
何情況下,不應設置持續連接時間超過60秒。否則大部分好處將變成損失。  

關編譯時設置  
mod_status 和 ExtendedStatus On  
如果在編譯Apache時您包含了mod_status並且將ExtendedStatus設置為On,Apache將為每個請求進行兩
次gettimeofday(2)系統調用(或者針對不同的系統調用times(2))及(在1.3以前的版本)許多次
time(2)。這些都是為了在報告中含有時間戳。為了得到最佳性能,請將ExtendedStatus設為Off(這是
缺省的設置)。  

多socket中的accept 串行化  
這部分文章將討論Unix socket API不利的一方面。假設您的服務器用多個Listen命令偵聽多個端口或
者多個IP地址。Apache使用select(2)檢測每個socket連接(connection)是否就緒。select(2)示意有
零個或至少一個連接等待某個socket。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中阻塞。
當任何socket上出現一個請求時,所有被阻塞的進程將復蘇,並從select返回(蘇醒進程的數量取
決操作系統和時間)。它們將繼續執行並試圖接受這個連接,但只有一個進程會成功(假設目前
仍只有一個連接),其余進程將阻塞在accept中。這將把所有失敗的進程鎖定,使它們只為一個
socket上的請求服務。它們會一直被阻塞,直到在那個socket上出現足夠的請求把它們喚醒。這一
飢餓問題首先在PR#467被提出。至少有兩種解決它的方法。 

一種方案是使用非阻塞的socket。這種情況下,accept不會阻塞子進程,它們將會立即返回。但這
種方案會造成CPU時間的浪費。假設有十個在select中的空閑進程,而到來了一個連接請求。九個
進程將蘇醒、試圖接受連接、失敗,並返回select,這些進程實際什都沒做。而且如果在這期間,
其他socket上出現請求,沒有哪個進程會為它服務。總而言之,這種方案不是十分有效,除非您擁
有和空閑子進程數目相當的CPU恐怕不切實際。 

另一種方案被Apache採納。這種方案串行化(serialize)對內層循環的調用。代碼如下所示(改進
的部分被加粗顯示): 

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 兩個函數實現了互斥量(mutual exclusion semaphore),
在任意時刻只能有一個子進程擁有互斥量。多種方法可以實現互斥量。在src/conf.h(1.3版之前)
或src/include/ap_config.h(1.3版及以)可以作出以下選擇。一些系統不提供任何互斥方法。
在這些系統上使用多個Listen命令是不保險的。 

USE_FLOCK_SERIALIZED_ACCEPT 
此方法用flock(2)系統調用對一個鎖文件加鎖。(此文件在LockFile命令中指定) 
USE_FCNTL_SERIALIZED_ACCEPT 
此方法用flock(2)系統調用對一個鎖文件加鎖。(此文件在LockFile命令中指定) 
USE_SYSVSEM_SERIALIZED_ACCEPT 
(1.3版及以)此方法借助SysV的信號量(semaphores)實現互斥。但不巧的是SysV信號量有一些
負面作用。一是Apache可能在清除信號量之前非正常終止;二是在使用信號量API時需要考慮到任何
與服務器UID相同的CGI程序可以進行拒絕服務攻擊(就是說所有的CGI程序都可以這樣做,除非使用
suexec或cgiwrapper之類的方法)。所以,這種方法並不被IRIX之外的系統廣泛採納(由大多數
IRIX系統上,使用前兩種方法的代價太大)。 
USE_USLOCK_SERIALIZED_ACCEPT 
(1.3版及以)此方法僅在IRIX上可用。它調用usconfig(2)創建互斥量。雖然這種方法避免了對
SysV信號量的種種爭議,但它不是IRIX的缺省方案。這是由在單處理器的IRIX系統 (5.3或6.2)上,
uslock代碼比SysV信號量慢兩個數量級;但在多處理器的IRIX中前者比者快一個數量級。這無非使
問題復雜化了。所以在多處理器IRIX系統上,您需要用如下的附加參數編譯Apache: 
在EXTRA_CFLAGS中添加-DUSE_USLOCK_SERIALIZED_ACCEPT 
USE_PTHREAD_SERIALIZED_ACCEPT 
(1.3版及以)此方法實現了POSIX標準互斥量。它理應可以工作在任何實現了全部POSIX線程規范的
系統上,但事實是只有在Solaris 2.5或以上的系統及特定的配置中才能工作。如果您嘗試這種方法的
話,需要小心服務器掛起或者沒有響應。服務器在只輸出靜態網頁的情況下運行得很好。 
如果您的系統上有其他串行化的方法,為它書寫代碼(並把補丁寄給Apache)是值得的。 

有一個考慮到但從未實現的方案是對循環部分地串行化即允許一定數目的進程進入循環。在同一時
刻可運行若幹進程的多處理器系統上,這個主意是滿不錯的。而且前面提到的方案並沒有充分利用帶寬。
可由高度並行化的服務器實在少見,這個方案的優先級比較低。 

為了得到最佳性能,不用多偵聽命令是最理想的。請繼續往下看。 

單socket中的accept串行化 
以上言及的方案對多socket服務器是相當不錯的,但只有一個socket的情況又如何呢?理論上,由
在連接請求到來之前所有子進程將阻塞在accept中,單個socket不會產生上述種種問題。但實際上,
上述非阻塞解決方案所帶來的“回旋(spinning)”問題在這裡只不過被掩蓋起來了。在絕大多數TCP
協議棧的實現中,一個接請求到來時內核將喚醒所有阻塞在accept中的進程。它們之一將得到此請求並
返回用戶空間,其余的進程將返回內核重新休眠。這將帶來與多socket非阻塞解決方案相同的資源浪費。 

由這點原因,我們發現如果為socket串行化,許多系統表現得更“友好”即使是一個socket的情
況。這是單個socket串行化作為絕大多數情況的缺省配置的原因。在Linux上不甚精確的
(Linux 2.0.30 / 雙Pentium Pro 166 w / 128Mb內存)實驗表明,對每次請求而言,串行化的單個
socket僅比沒有串行化的socket損失不到3%的性能。但未串行化的socket顯示出每次連接請求100毫秒
的延時。這也可能僅僅由過長的通訊距離造成的。如果您不想串行化單個socket,可以定義宏
SINGLE_LISTEN_UNSERIALIZED_ACCEPT。這樣,僅有一個socket的服務器將不會串行化。 

延遲關閉(Lingering Close) 
就象draft-ietf-http-connection-00.txt第8節討論的那樣,為了使服務器能夠可靠地實現HTTP協議,
有必要獨立地關閉每個方向上的通訊(每個TCP連接有兩個方向,每個方向是分別獨立的)。這個事實往
往被其他服務器所忽視,而Apache 1.2就已經正確地處理了。 

當這個特性增加到Apache中時卻在許多版本的Unix中引起了問題。這是TCP規范的短見造成的它沒有
聲明FIN_WAIT_2有超時,但也沒有阻止這樣的實現。在沒有超時的系統中,Apache 1.2將導致許多
socket將永遠處FIN_WAIT_2的狀態。這可以簡單地用打最新TCP/IP補丁的方法避免。然而在提供商從
不發行補丁的系統上(也就是SunOS4雖然得到源代碼許可証的人可以自己打補丁),我們決定不直
接使用這一特性。 

有兩種實現這個特性的辦法:一是socket的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) { 
read (s, junk_buffer, sizeof (junk_buffer)); 
/* just toss away whatever is here */ 



close (s); 

這自然增加了連接結束時的開銷,但它是可靠的實現所必需的。隨著HTTP/1.1的日益盛行,所有連
接都是持久的,這種開銷將被眾多的連接請求抵消。如果您想冒險禁止這一特性的話,可以定義宏
NO_LINGCLOSE,但這顯然是不被推薦的。實際上,由在HTTP/1.0中持久的管道式連接越來越普遍,
lingering_close幾乎是必須的選擇。(管道式連接非常高效,所以您還是希望支持它的吧) 

記分板文件 
Apache利用一種叫做記分板(scoreboard)的技術在父、子進程間通訊。它的理想實現是在共享內
存中。有的操作系統允許我們直接訪問共享內存,或者提供它們的確切端口。在這些系統中的典型
實現就是共享內存記分板。其他的系統則將磁盤上的文件作為缺省實現。磁盤文件不僅低效而且不
穩定(又沒有什優勢)。請為您的操作系統仔細閱讀src/main/conf.h文件,並在其中尋找
USE_MMAP_SCOREBOARD或者USE_SHMGET_SCOREBOARD。定義它們之一(以及相應的HAVE_MMAP和HAVE_SHMGET)
將允許Apache使用共享內存。如果您系統的內存共享機制與眾不同,請編輯src/main/http_main.c
並增加Apache所需的掛鉤函數(同時請把補丁寄給我們) 

注:直到1.2版,Apache的Linux版才開始使用共享內存。這一疏忽使得以前版本的Apache在Linux上
表現得很不理想。 

DYNAMIC_MODULE_LIMIT 
如果您不打算支持動態加載模塊的話(準備榨出最一滴性能的您可能希望如此),編譯服務器時
請設定參數-DDYNAMIC_MODULE_LIMIT=0。這將節省出為動態加載模塊而分配的內存。 

附錄:對某次跟蹤狀況的詳細分析 
本附錄描述了運行在Linux上的Apache 1.3系統調用的跟蹤情況。運行時(run-time)配置文件中
除了必要的缺省選項外還增加了: 

<Directory />  
AllowOverride none  
Options FollowSymLinks  
</Directory>  
被請求的文件是一個6K的靜態網頁,其中不包含特殊內容。對非靜態或者伴隨有內容協商的請求,
跟蹤結果將有明顯的不同(一些情況下會十分晦澀)。我們將首先列出完整的跟蹤結果,然逐條
進行分析。(它是由strace跟蹤程序生成的,其他類似的程序包括truss、ktrace和par)  
(http://www.fanqiang.com)
    進入【UNIX論壇

相關文章
 

★  樊強制作 歡迎分享  ★