1. 程式人生 > >PHP APC 淺析

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()快取頻繁變更的使用者資料,會出現一些奇異現象。