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>
但即使如此,如果只需要在一些CGI中使用DNS名稱,可以考慮在需要它的特定CGI中進行gethostbyname
調用。
FollowSymLinks和SymLinksIfOwnerMatch
無論您的URL空間中沒有Options FollowSymLinks
,或者都有選項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路徑,則需要添加類似的部分。為了獲得最高性能,並且沒有符號鏈接保護,請在任何地方設置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
記憶體映射
在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
左右,因為太高可能導致記憶體洩漏。
編譯時配置問題
選擇MPM
Apache 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
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.
-l
選項告訴truss記錄調用每個系統調用的LWP(羽量級進程 - Solaris形式的內核級線程)的ID。
其他系統可能具有不同的系統調用跟蹤實用程式,例如strace
,ktrace
或par
。它們都產生類似的輸出。
在此跟蹤中,客戶端已從httpd請求了一個10KB的靜態檔。具有內容協商的非靜態請求或請求的痕跡看起來非常不同。
/67: accept(3, 0x00200BEC, 0x00200C0C, 1) (sleeping...)
/67: accept(3, 0x00200BEC, 0x00200C0C, 1) = 9
在此跟蹤中,偵聽器線程在LWP#67中運行。
/65: lwp_park(0x00000000, 0) = 0
/67: lwp_unpark(65, 1) = 0
在接受連接時,偵聽器線程喚醒工作線程以執行請求處理。在此跟蹤中,處理請求的工作線程將映射到LWP#65。
/65: getsockname(9, 0x00200BA4, 0x00200BC4, 1) = 0
為了實現虛擬主機,Apache需要知道用於接受連接的本地套接字地址。在許多情況下(例如,當沒有虛擬主機,或者使用沒有通配符地址的Listen指令時),可以消除此調用。但是還沒有做出這些優化的努力。
/65: brk(0x002170E8) = 0
/65: brk(0x002190E8) = 0
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
接下來,worker 線程以非阻塞模式將連接放入客戶端(檔描述符9)。setsockopt()
和getsockopt()
調用是Solaris的libc如何在套接字上處理fcntl()
的副作用。
/65: read(9, " G E T / 1 0 k . h t m".., 8000) = 97
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
此httpd已使用Options FollowSymLinks
和AllowOverride None
進行配置。因此,它不需要lstat()
導致所請求檔的路徑中的每個目錄,也不需要檢查.htaccess
檔。它只是調用stat()
來驗證檔:1)是否存在,2)是常規檔,而不是目錄。
/65: sendfilev(0, 9, 0x00200F90, 2, 0xFAF7B53C) = 10269
在此示例中,httpd能夠使用單個sendfilev()
系統調用發送HTTP回應頭和所請求的檔。Sendfile
語義因操作系統而異。在某些其他系統上,必須執行write()
或writev()
調用以在調用sendfile()
之前發送標頭。
/65: write(4, " 1 2 7 . 0 . 0 . 1 - ".., 78) = 78
此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
worker 線程會延遲關閉連接。
/65: close(10) = 0
/65: lwp_park(0x00000000, 0) (sleeping...)
最後,工作線程關閉它剛剛傳遞的檔並阻塞,直到偵聽器為其分配另一個連接。
/67: accept(3, 0x001FEB74, 0x001FEB94, 1) (sleeping...)
同時,監聽器線程一旦將此連接分派給工作線程,就能夠接受另一個連接(受制於工作者MPM中的某些流控制邏輯,如果所有可用工作者都忙,則會限制監聽器)。雖然從這個跟蹤中看不出來,但是下一個accept()
可以(並且通常在高負載條件下)與工作線程處理剛剛接受的連接並行發生。