背景
服務(wù)器用了PHP Memcache擴(kuò)展,但上線后頻繁出現(xiàn)set/add失敗草巡。
測試
為了重現(xiàn)故障,寫了一個簡單腳本測試test_mc.php
,如下:
<?php
define('HOST', 'YOUR_HOST');
define('PORT', 'YOUR_PORT');
$mc = new Memcache();
$mc->connect(HOST, PORT);
while(1) {
$key = 'test' . microtime(1);
$value = 'val';
$expire = 100;
$result = $mc->set($key, $value, $expire);
if($result) {
echo date('Y-m-d H:i:s') . "\tset success" . PHP_EOL;
}else {
echo date('Y-m-d H:i:s') . "\tset fail" . PHP_EOL;
}
sleep(1);
}
執(zhí)行/path/to/php test_mc.php
佛舱,結(jié)果如下:
從執(zhí)行可以看出,的確會間歇出現(xiàn)set不成功的情況,而且"set fail"會連續(xù)出現(xiàn)15次请祖。
那么問題就來了:
- 為什么會出現(xiàn)fail订歪?
- 為什么每次都連續(xù)15次fail?
排查
執(zhí)行strace /path/to/php test_mc.php
肆捕,分析系統(tǒng)調(diào)用陌粹,得到結(jié)果如下:
綠色的是執(zhí)行成功的"set success",紅色的是執(zhí)行失敗的"set fail"福压。
從上述信息可以看出兩個問題:
- 第1次的"set fail"是因?yàn)?strong>poll timeout掏秩,超時(shí)時(shí)間是1000ms。網(wǎng)絡(luò)問題荆姆,我們可以根據(jù)實(shí)際情況蒙幻,把超時(shí)時(shí)間再設(shè)置長一點(diǎn)。
- 第2次~第15次就有點(diǎn)奇怪了胆筒,并沒有發(fā)生網(wǎng)絡(luò)請求邮破,就直接失敗了。
針對這兩個問題仆救,繼續(xù)排查抒和。
為什么fail剛好是15次?
查閱Memcache擴(kuò)展的文檔沒有說明彤蔽,那就直接看擴(kuò)展源碼摧莽。Memcache擴(kuò)展版本是2.2.7。
源碼地址:https://pecl.php.net/package/memcache
在源碼里直接搜索"15"顿痪,就可以找到以下的常量定義:
在mmc_open函數(shù)有這樣的用法:
注:
mmc->retry_interval = MMC_DEFAULT_RETRY
那么結(jié)論就很明顯了镊辕,在同一個mc連接,一旦出現(xiàn)了
MMC_STATUS_FAILED
(連接失敗)蚁袭,只能在15s過后再重新連接征懈。期間客戶端的任何調(diào)用都是失敗。所以就出現(xiàn)上面的測試結(jié)果揩悄。MMC_DEFAULT_RETRY
對單例模式和守護(hù)進(jìn)程會比較麻煩卖哎。所以建議一旦發(fā)現(xiàn)連接失敗,就調(diào)用
close()
把連接干掉删性。
如何設(shè)置超時(shí)時(shí)間
先看文檔:http://php.net/manual/zh/memcache.connect.php
文檔比較簡單亏娜,
Memcache::connect()
第3個參數(shù)就是超時(shí)選項(xiàng)。我們把超時(shí)時(shí)間改為3s镇匀,修改如下:
<?php
...
$mc->connect(HOST, PORT, 3);
...
為了驗(yàn)證是否真的設(shè)置成功照藻,執(zhí)行strace /path/to/php test_mc.php
奇怪了,怎么超時(shí)還是1000ms汗侵?設(shè)置3s不生效幸缕?
會不會是個bug群发?于是又翻起了Memecache擴(kuò)展的源代碼。
Memcache::connect()
方法的實(shí)現(xiàn)在php_mmc_connect()
里:這里發(fā)現(xiàn)兩個很有趣的事情:
- 超時(shí)時(shí)間其實(shí)有兩個選項(xiàng):
timeout
(s)和timeoutms
(ms) - 文檔里
Memcache::connect()
的參數(shù)列表只列了3個參數(shù)(host, port, timeout)
发乔,但實(shí)際上是可以接收第4個參數(shù)作為timeoutms
熟妓。
如果timeoutms
沒有傳(小于1),那就取默認(rèn)值:default_timeout_ms
(1000)栏尚。
這段代碼看起來還算比較正常起愈,但答案還是沒出來,timeout
參數(shù)明明是有讀取的译仗,為什么就是設(shè)置不成功抬虽?不急,再看看處理連接的_mmc_open()
函數(shù):
當(dāng)我看到這段代碼的時(shí)候纵菌,倒吸一口涼氣阐污,心情久久不能平復(fù)。
認(rèn)真看下代碼咱圆,紅框里的是說笛辟,如果
timeoutms
大于0,那就用它作為超時(shí)時(shí)間序苏,否則就用timeout
手幢。剛剛我們只傳入
timeout
,那么timeoutms
就會默認(rèn)賦值default_timeout_ms
(默認(rèn)1000大于0)忱详。于是timeout
就沒有用了围来,超時(shí)永遠(yuǎn)都是取timeoutms
的1000ms。也就是說踱阿,正確的設(shè)置超時(shí)時(shí)間應(yīng)該是使用第4個隱藏參數(shù)
timeoutms
(問你服了沒有管钳?):
<?php
...
$mc->connect(HOST, PORT, 3, 3000); //第三個參數(shù)"3"可以隨便設(shè)钦铁,因?yàn)樗疾粫?...
再執(zhí)行strace /path/to/php test_mc.php
驗(yàn)證是否設(shè)置成功:
OK软舌,果然如此,超時(shí)時(shí)間設(shè)置成功牛曹。
當(dāng)然佛点,還有一種簡單粗暴的方法:
<?php
...
ini_set('memcache.default_timeout_ms', 3000);
...
在php.ini修改或者動態(tài)修改都可以。
總結(jié)
不太清楚Memcache擴(kuò)展2.2.7以后的版本有沒有修復(fù)這個bug黎比。但2.2.7以后的都是beta版超营,2013年后就沒更新了。
建議有條件都把Memcache擴(kuò)展換成Memcached阅虫,不過要注意兩者的數(shù)據(jù)兼容情況演闭,因?yàn)閮烧叩男蛄谢绞接胁町惖摹?/p>