ApacheApache效能調優


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之前,MinSpareServersMaxSpareServersStartServers設定都對基準測試結果產生了極大的影響。特別是,Apache需要一個「加速」期,以便達到足以服務於所應用的負載的多個子項。初始產生StartServers子項後,每秒只會建立一個子項來滿足MinSpareServers設定。因此,一個伺服器被100個並行用戶端存取,使用預設的StartServers為5將需要95秒的時間來產生足夠的子進程來處理負載。這在實際伺服器上的實際工作正常,因為它們不會經常重新啟動。但它的基準測試確實很差,可能只執行十分鐘。

實施每秒一次的規則是為了避免在新子項啟動的情況下淹沒機器。如果機器忙於產生子項,則無法提供服務請求。但它對Apache的感知效能產生了如此巨大的影響,必須予以取代。從Apache 1.3開始,程式碼將放寬每秒一次的規則。它將產生一個,等待一秒,然後產生兩個,等待一秒,然後產生四個,它將以指數方式繼續,直到它每秒產生32個子項。只要滿足MinSpareServers設定,它就會停止。

這似乎足夠響應,幾乎沒有必要扭轉MinSpareServersMaxSpareServersStartServers旋鈕。當每秒生成4個以上的子節點時,將向ErrorLog傳送一條訊息。

與進程建立相關的是由MaxConnectionsPerChild設定引起的進程死亡。預設情況下,它的值是0,這意味著每個孩子處理的連線數沒有限制。如果您的組態當前設定為某個非常低的數位,例如30,您可能希望顯著提高它。如果執行的是SunOS或舊版本的Solaris,請將此限制為10000左右,因為太高可能導致記憶體洩漏。

編譯時組態問題

選擇MPM
Apache 2.x支援可插入的並行模型,稱為多處理模組(MPM)。構建Apache時,必須選擇要使用的MPM。某些平台有特定於平台的MPM:mpm_netwarempmt_os2mpm_winnt。對於一般的Unix型別系統,有幾個MPM可供選擇。MPM的選擇會影響httpd的速度和可延伸性:

  • worker MPM使用多個子進程,每個進程有多個執行緒。每個執行緒一次處理一個連線。對於高流量伺服器,worker通常是一個不錯的選擇,因為它比prefork MPM具有更小的記憶體占用。
  • 事件MPM像Worker MPM一樣具有執行緒,但旨在允許通過將一些處理工作傳遞給支援執行緒來同時提供更多請求,從而釋放主執行緒以處理新請求。
  • prefork MPM使用多個子進程,每個進程只有一個執行緒。每個進程一次處理一個連線。在許多系統上,prefork的速度與worker相當,但它使用更多的記憶體。在某些情況下,Prefork的無線設計優於worker:它可以與非執行緒安全的第三方模組一起使用,並且在具有較差執行緒偵錯支援的平台上更容易偵錯。

模組

由於記憶體使用是效能中非常重要的考慮因素,因此您應該嘗試消除實際上未使用的模組。如果您已將模組構建為DSO,則消除模組只需註釋掉該模組的相關LoadModule指令即可。這使您可以嘗試刪除模組,並檢視您的網站是否仍然在沒有這些模組的情況下執行。

另一方面,如果您將模組靜態連結到Apache二進位制檔案中,則需要重新編譯Apache以刪除不需要的模組。

當然,這裡出現的一個相關問題是,您列出需要哪些模組,哪些模組不需要。當然,這裡的答案因網站而異。但是,您可以獲得的最小模組列表往往包括mod_mimemod_dirmod_log_configmod_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。

其他系統可能具有不同的系統呼叫跟蹤實用程式,例如stracektracepar。它們都產生類似的輸出。

在此跟蹤中,用戶端已從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_poolapr_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 FollowSymLinksAllowOverride 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()可以(並且通常在高負載條件下)與工作執行緒處理剛剛接受的連線並行發生。