[轉]php加速 PHP APC 淺析

PHP APC提供兩種緩存功能,即緩存Opcode(目標文件),我們稱之爲apc_compiler_cache。同時它還提供一些接口用於PHP開發人員將用戶數據駐留在內存中,我們稱之爲apc_user_cache。我們這裏主要控討php-apc的配置。
安裝PHP APC

作爲測試環境,我們這裏使用的是CentOS5.3(2.6.18-128.el5PAE) + Apache2.0(prefork) + php5.2。我們可以去pecl apc下載APC-3.0.19.tgz

# tar -xzvf APC-3.0.19.tgz
#cd APC-3.0.19
# /usr/bin/phpize
# ./configure --enable-apc --enable-mmap --enable-apc-spinlocks --disable-apc-pthreadmutex
#make
#make install


注意:我們這裏支持mmap,同時採用spinlocks自旋鎖。Spinlocks是Facebook推薦使用,同時也是APC開發者推薦使用的鎖機制。
PHP APC 配置參數

如果你使用的系統環境跟我的測試環境是一樣的話,可以在/etc/php.d目錄下創建文件apc.ini,並且相關配置寫入/etc/php.d/apc.ini文件。這裏,我們挑了一些常用到的配置,並進行探討。把相關的配置放在一起解釋。

apc.enabled=1
apc.enabled默認值是1,你可設成0禁用APC。如果你設置爲0的時候,同樣把extension=apc.so也註釋掉(這樣可以節約內存資源)。一旦啓用了APC功能,則會緩存Opcodes到共享內存。

apc.shm_segments = 1
apc.shm_size = 30
APC既然把數據緩存在內存裏面,我們就有必要對它進行內存資源限定。通過這二個配置可以限定APC可以使用的內存空間大小。 apc.shm_segments指定了使用共享內存塊數,而apc.shm_size則指定了一塊共享內存空間大小,單位是M。所以,允許APC使用的內存大小應該是 apc.shm_segments * apc.shm_size = 30M。你可以調整一塊共享內存的大小空間。當然,一塊共享內存最大值是受操作系統限制的,即不能超過/proc/sys/kernel/shmmax大小。否則APC創建共享內存的時候,會失敗。在apc.shm_size達到了上限的時候,你可以通過設置apc.shm_segments來允許APC 使用更多的內存空間。我們推薦,如果調用APC使用內存空間的話,先考濾apc.shm_size,後考濾apc.shm_segments。具體數值,可以根據apc.php監控情況進行規劃與調整。值得注意的是,每一次調整需要重啓httpd守護進程,這樣可以重新加載apc.so模塊。跟隨着 httpd守護進程啓動,apc.so模塊就會加載。apc.so加載初始化的時候,通過mmap請求分配內存指定大小的內存,即 apc.shm_size * apc.shm_segments。而且,這裏使用的是匿名內存映射方式,通過映射一個特殊設備/dev/zero,提供一個“大型”的,填滿了零的內存供APC管理。
爲了驗證以上陳述,我們註釋掉apc.ini配置,並且寫了以下php腳本觀察apc.so模塊初始化的分配的內存空間。


<?php
//@file: apc_load.php
if (!extension_loaded('apc')) {
dl('apc.so'); #加載apc.so模塊
echo posix_getpid(); #//輸出當前進程的pid,我這裏這裏輸出的是14735
ob_flush();
flush();
sleep(3600); #讓進程進入休眠狀態.這樣,我們可以觀察內存分配情況
}
?>



#strace -p `cat /var/run/httpd.pid`
open("/var/www/html/apc_load.php", O_RDONLY) = 13
...
mmap2(NULL, 31457280, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0xb5ce7000
...
nanosleep({3600, 0},

紅色部分,我們可以看出。通過mmap系統內核調用分配了30M(31457280/1024/1024)內存空間。 PROT_READ|PROT_WRITE表示該內存空間可供讀取與寫入。MAP_SHARED表示該內存空間與其它進程是共享的,即其它進程也可以進行讀取與寫入,我們可以通過apc.php進行管理該塊內存空間亦是受益於此設定。MAP_ANONYMOUS則表示匿名映射。其中fd=-1表示忽略,因爲這裏映射的特殊設備/dev/zero。最後的0表示無偏移量。我們還可以通過進程映像文件查看該塊內存的具體情況
#cat /proc/14735/smaps

b5ce7000-b7ae7000 rw-s 00000000 00:08 633695 /dev/zero (deleted)
Size: 30720 kB
Rss: 44 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 44 kB

可以很容易地發現起始地址0xb5ce7000與上面mmap系統內核調用返回的地址一樣。該塊內存是可讀寫rw,並與其它進程共享s。而/dev /zero則是映射文件,該文件節點是633695。其中,size表示進程可以使用的內存空間,而rss則表示實際分配的內存空間,且由 Private_Dirty可以看出,實際分配的44kb內存是由當前進程自己分配的。

apc.num_files_hint = 1000
apc.user_entries_hint = 4096
這二配置指定apc可以有多少個緩存條目。apc.num_files_hint說明你估計可能會有多少個文件相應的opcodes需要被緩成,即大約可以有多少個apc_compiler_cache條目。另外apc.user_entries_hint則說明你估計可能會有多少個 apc_userdata_cache條目需要被緩存。如果項目中不使用apc_store()緩存用戶數據的話,該值可以設定得更小。也就是說 apc.num_files_hint與apc.user_entries_hint之和決定了APC允許最大緩存對象條目的數量。準確地設置這二個值可以得到最佳查詢性能。當然,如果你不清楚要進行多少緩存(緩存對象實例)的情況下,你可以不必修改這二項配置。
其中apc.user_entries_hint要根據項目實際開發使用了apc_store()條目估計其值大小。相較而言,apc.num_files_hint可以通過find命令,更容易地估計其大小。比如我們的web根目是/var/vhosts,則使用下面的 find命令可以大致地統計當前apc.num_files_hint數目.
#find /var/vhosts \( -name “*.php” -or -name “*.inc” \) -type f -print |wc -l
1442

apc.stat = 1
apc.stat_ctime = 0
這二個參數,只跟apc_compiler_cache緩存相關,並不影響apc_user_cache。我們前面提到過 apc_complier_cache,它緩存的對象是php源文件一一對應的opcodes(目標文件)。PHP源文件存放在磁盤設備上,與之相對應的 Opcodes目標文件位置內存空間(共享內存),那麼當php源文件被修改以後,怎麼通知更新內存空間的opcodes呢?每次接收到請求後,APC都會去檢查打開的php源文件的最後修改時間,如果文件的最後修改時間與相應的內存空間緩存對象記錄的最後修改時間不一致的話,APC則會認爲存放在內存空間的Opcode目標文件(緩存對象)已經過期了,acp會將緩存對象清除並且保存新解析得到的Opcode。我們關心的是,即便沒有更新任何php源文件,每次接受到http請求後,APC都會請求系統內核調用stat()來獲取php源文件最後修改時。我們可以通過將apc.stat設置爲0,要求 APC不去檢查Opcodes相對應的php源文件是否更新了。這樣可以獲得最佳的性能,我們也推薦這麼做。不過,這樣做有一點不好的就是,一旦有PHP 源文件更新了之後,需要重啓httpd守護進程或者調用apc_cache_clear()函數清空APC緩存來保證php源文件與緩存在內存空間的 Opcodes相一致。

<?php
define('ROOTP', dirname(__FILE__) . '/');
include(ROOTP . 'i1.php');
require(ROOTP . 'i2.php');
include_once(ROOTP . 'i3.php');
require_once(ROOTP . 'i4.php');
require(ROOTP . 'i5.php');
include(ROOTP . 'i6.php');
?>


# strace -e trace=file -p `cat /var/run/httpd.pid`
getcwd("/var/www/html", 4096) = 14
stat64("/var/www/html/i1.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
stat64("/var/www/html/i2.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 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/i3.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
open("/var/www/html/i3.php", O_RDONLY) = 12
stat64("/var/www/html/i3.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 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/i4.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
open("/var/www/html/i4.php", O_RDONLY) = 12
stat64("/var/www/html/i4.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
stat64("/var/www/html/i5.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
stat64("/var/www/html/i6.php", {st_mode=S_IFREG|0644, st_size=39, ...}) = 0
chdir("/tmp")
= 0

# strace -e trace=file -p `cat /var/run/httpd.pid`
getcwd("/var/www/html", 4096) = 14
open("/var/www/html/i3.php", O_RDONLY) = 12
open("/var/www/html/i4.php", O_RDONLY) = 12
chdir("/tmp") = 0

對比可見,當apc.stat=0時,省了很多系統內核調用,我們沒有看到系統內核調用stat64了。其中,i3.php和i4.php分別是 php的include_once和require_once函數調用,它要交給fstat()系統內核調用來檢查文件是否打開過。單從性能角度出發的話,require比require_once性能更佳。

設置apc.stat_ctime的意義並是很大。如果apc.stat_ctime值爲1時,僅當php源文件的創建時間(ctime)大於 php源文件的最後修改時間(mtime)時,緩存對象的mtime時間會被php源文件的ctime所代替,否則緩存對象的mtime依然記錄爲php 源文件的mtime。這樣做是防止通過cvs, svn或者rsync等工具刷新php源文件的mtime,這樣會導致APC通過比對php源文件的創建時間ctime來決定緩存對象有沒有過期。我們推薦該保持默認值,即apc.stat_ctime = 0

apc.ttl=0
apc.user_ttl=0
緩存對象的生命週期。其中ttl表示Time To Live,意味着指定時間後緩存對象會被清除。其中0表示永不過期。我們前面提過,APC能緩存的條目是受限定的,如果你把ttl設置永不過期的話,當緩存條目已滿或者緩存空間不夠,之後的緩存都將失敗。
其中apc.ttl作用於apc_compiler_cache。當apc.ttl大於0時,每次請求都會對比這次的請求時間與上一次請求時間之差是不是大於apc.ttl,如果大於apc.ttl,則會被認緩存條目過期了,會被清理。
比較有意思的是apc.user_ttl,它主要作用於apc_user_cache緩存。我們知道,這種類型的緩存是通過 apc_store($key, $var, $ttl = 0)創建的緩存對象。函數apc_store()中指定的$ttl與php.ini中設定的apc.user_ttl有什麼異同,是我們比較關心的。因爲它們同樣作用於apc_userdata_cache緩存。經過分析,我們知道:判斷apc_user_cache緩存過期的依據是,當 apc.user_ttl大於0,且這次http請求時間與上一次http請求時間之差大於apc.user_ttl,則認爲相應的緩存條目已過期;或者,user.data.ttl(php函數apc_store()中指定的$ttl)大於0,且這次http請求時間與緩存對象創建時間ctime之差大於user.data.ttl,則同樣認爲緩存條目已過期,會被清除。
我們推薦,如果你的項目較爲穩定,並且apc.stat設置爲0。同時apc.shm_size、apc.num_files_hint設置合理的話,apc.ttl建議設置爲0。即apc_compiler_cache永不回收,直到重啓httpd守護進程或者調用函數 apc_cache_clear()清緩存。至於apc.user_ttl,建議設置爲0,由開發人員調用apc_store()函數的時候,設置$ttl來指定該緩存對象的生命週期。

apc.slam_defense=0
apc.write_lock=1
apc.file_update_protection=2
之所以把這三個配置放在一起解釋,是因爲他們的意義很相近。其中apc.file_update_protection最好理解,它的單位是時間單位秒。如果當前http請求時間與php源文件最好修改時間mtime之差小於apc.file_update_protection時間,APC則不會緩存該 php源文件與之對應的Opcodes,直到接下來的某次訪問,並且訪問時間與php源文件的最後修改時間大於 apc.file_update_protection時間,相之相應的Opcodes纔會被緩存到共享內存空間。這樣做的好處是,不容易被用戶訪問到你正在修改的源文件。我們推薦在開發環境,該值可以設置得更大一點,但在運營環境,我們推薦保留默認值即可。
當你的網站併發量很大的時候,可能出現由http守護進程fork的多個子進程同時緩存同一份Opcodes的情況。通過 apc.slam_defense則可以減少這種事情的發生機率。比如,apc.slam_defense值設置爲60的時候,當遇到未緩存的 Opcodes,每100次有60次是不緩存的。對於併發量不大的網站,我們推薦該值設定爲0,對於併發量高的網站我們可以根據統計適當地調整該值。而 apc.write_lock是一個布爾值,當該值設置爲1的時候,當多個進程同時緩存同一份Opcodes時,僅當最先那個進程緩存有效,其它的無效。通過apc.write_lock設置,有效地避免了緩存寫競爭的出現。

apc.max_file_size=1M
apc.filters = NULL
apc.cache_by_default=1
這三個配置放在一起,是因爲他們都用於限制緩存。其中apc.max_file_size表示如果php源文件超過了1M,則與之對應的opcodes不被緩存。而apc.filters指定一個文件過濾列表,以逗號(,)隔開。當apc.cache_by_default等於1時,與 apc.filters列表中指定的文件名相匹配的文件不會被緩存。相反,apc.cache_by_default等於0時,僅緩存與 acp.filters列表中指定的文件相匹配的文件。
總結

1,使用Spinlocks鎖機制,能夠達到最佳性能。
2,APC提供了apc.php,用於監控與管理APC緩存。不要忘記修改管理員名和密碼
3,APC默認通過mmap匿名映射創建共享內存,緩存對象都存放在這塊”大型”的內存空間。由APC自行管理該共享內存
4,我們需要通過統計調整apc.shm_size、apc.num_files_hints、apc.user_entries_hint的值。直到最佳
5,好吧,我承認apc.stat = 0 可以獲得更佳的性能。要我做什麼都可以接受.
6,PHP預定義常量,可以使用apc_define_constants()函數。不過據APC開發者介紹說pecl hidef性能更佳,拋異define吧,它是低效的。
7,函數apc_store(),對於系統設置等PHP變量,生命週期是整個應用(從httpd守護進程直到httpd守護進程關閉),使用APC比Memcached會更好。必竟不要經過網絡傳輸協議tcp。
8,APC不適於通過函數apc_store()緩存頻繁變更的用戶數據,會出現一些奇異現象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章