下文:
關于php的共享內(nèi)存的使用和研究之外部存儲
關于php的共享內(nèi)存的使用和研究之深入剖析swoole table
最近遇到一個場景浅侨,服務尋址的時候,需要請求遠程的服務,獲取一批可用的ip和端口地址及其權重寥假。根據(jù)權重和隨機算法選擇最合適的一個服務地址,進行請求霞扬。由于服務地址在短時間之內(nèi)不會發(fā)生變化糕韧,因此為了避免無限制的進行尋址的請求,有必要將地址緩存至本地喻圃。
對于php而言萤彩,說到用戶數(shù)據(jù)緩存本地,第一反應出來的就是APC斧拍。但是APC首先被創(chuàng)建出來是給php做內(nèi)部緩存的雀扶,其次才是提供給用戶態(tài)使用的。根據(jù)laruence在博客的說法肆汹,opcache出現(xiàn)了之后怕吴,對zend編譯的opcode做了緩存,實際上解決了apc被創(chuàng)建出來想要解決的問題县踢。因此現(xiàn)在APC已經(jīng)處于不再更新維護的狀態(tài)了转绷。
對于想使用opcache,又要使用用戶態(tài)的APC的同學硼啤,就需要額外的配置议经,同時性能上也會比原來的APC要差,差不多相當于本機的memcache。這顯然就無法達到本機內(nèi)存訪問的效率了煞肾,因此需要尋求其他的解決方案咧织。
php的共享內(nèi)存API
隨后我就想到了使用php的共享內(nèi)存API,反正只是緩存非常少的路由信息籍救,加在一起不超過1k习绢,盡管是多讀多寫的場景,但是覆蓋了也沒關系蝙昙,出于這種出發(fā)點闪萄,我就開始了對php的共享內(nèi)存API的研究。
php中操作共享內(nèi)存的方式一共有兩組:
- System V IPC
- 編譯增加 --enable-sysvshm
- Shared Memory
- --enable-shmop
先來看一個shmop的例子:
<?php
// 從系統(tǒng)獲取一個共享內(nèi)存的id
$key = ftok(__FILE__, 'test');
$size = 1024;
// 打開1024字節(jié)的共享內(nèi)存(如果不存在則申請)
$shm_h = @shmop_open($key, 'c', 0644, $size);
if($shm_h === false) {
echo "shmop open failed";
exit;
}
// 讀取共享內(nèi)存中的數(shù)據(jù)
$data = shmop_read($shm_h, 0, $size);
// 對讀取的數(shù)據(jù)進行反序列化
$data = unserialize($data);
//如果沒有數(shù)據(jù)則寫入
if(empty($data)) {
echo "there is no data";
$data = "imdonkey";
//所有寫入的數(shù)據(jù)奇颠,都必須提前序列化
$write_size = shmop_write($shm_h, serialize($data), 0);
if($write_size === false) echo "shmop write failed!";
}
//如果有败去,顯示出來,之后刪掉
else {
echo "shared memory data: ";
print_r($data);
shmop_delete($shm_h);
}
shmop_close($shm_h);
?>
使用shmop擴展烈拒,必須要注意數(shù)據(jù)的大小圆裕,以及讀寫時候的偏移量。同時荆几,不管你寫入的是什么數(shù)據(jù)類型吓妆,都必須進行序列化和反序列化。
再看一下SysV的例子:
<?php
// 從系統(tǒng)獲取一個共享內(nèi)存的id
$shm_key = ftok(__FILE__, 'test');
// 獲取此共享內(nèi)存資源的操作句柄
$memsize = 1024;
$shm_h = shm_attach($shm_key, $memsize, 0644);
if($shm_h === false) {
echo "shmop open failed";
exit;
}
// 獲取共享內(nèi)存中key=222時的內(nèi)容
$var_key = 222;
$data = @shm_get_var($shm_h, $var_key);
if(empty($data)) {
$data = ['test'=>'here'];
echo "there is no data, insert $data.\n";
// 如果數(shù)據(jù)不存在吨铸,寫入數(shù)據(jù)耿战,可以是任意類型,無需初始化
shm_put_var($shm_h, $var_key, $data);
} else {
// 否則焊傅,輸出數(shù)據(jù),并清理相關內(nèi)存
echo "find data: $data\n";
shm_remove_var($shm_h, $var_key);
}
// 斷開資源的鏈接
shm_detach($shm_h);
?>
原理上來講并無不同狈涮,只是SysV做了更多的封裝狐胎,讓你使用起來更加方便一些。不用自己控制偏移量歌馍,也不用進行序列化和反序列化握巢。同時對于每個數(shù)據(jù),都設置了對應的var_key
, 這樣在同一個區(qū)域可以保存多個數(shù)據(jù)松却,而無需再次申請另一片共享內(nèi)存暴浦。
業(yè)務中的使用
在使用兩者的時候,都要注意對數(shù)據(jù)大小的估算晓锻。否則很容易出現(xiàn)共享內(nèi)存溢出的情況歌焦。而我在使用的時候,充分評估了要存儲的數(shù)據(jù)結(jié)構(gòu)的大小砚哆,我需要存儲的內(nèi)容是:
ip(15個字節(jié)以內(nèi))+port(8字節(jié)以內(nèi))+timestamp(15字節(jié)以內(nèi))+分隔符(3字節(jié))=41字節(jié)
假設我調(diào)用100個后端服務独撇。那么最高需要存儲的路由信息就是4.1k
大小。
出于這種考慮,我申請了1M的內(nèi)存纷铣,覺得應該是夠夠的了卵史。就這么悠哉哉的在線上跑了一個星期左右,有天沒事到線上看了下php的錯誤日志搜立,結(jié)果一臉懵逼:
什么情況以躯,調(diào)用的后端服務一共才5個,共享內(nèi)存這么快就寫滿了啄踊?忧设?經(jīng)過一個初步的判斷之后,我得出的結(jié)論是:sysV的接口能力太差社痛,對于shareKey沒有做去重處理见转,而是每次都寫入了新的key,這樣就導致了共享內(nèi)存的寫入指針盡管是相同的shareKey蒜哀,但是卻不斷的后移斩箫,最終導致共享內(nèi)存被寫爆,而尋址的請求全部都打到了尋址服務撵儿,還好它比較健壯乘客,也有短時的緩存,才沒有產(chǎn)生運營事故淀歇。
在得出了這么個結(jié)論之后易核,我修改了我的代碼,在每次完成對shareKey內(nèi)容的獲取之后浪默,增加了一行
shm_remove_var($shareKey)
同時寫了一個腳本牡直,把原有的共享內(nèi)存id對應的內(nèi)容清空,經(jīng)過手工處理十臺機器之后纳决,再全量替換一把代碼碰逸,打卡下班,感覺自己棒棒噠阔加。
沒想到饵史,這才是悲劇的開始。就在當周的周六胜榔,吃著火鍋胳喷,突然就有一臺線上機器罷工了。機器服務狂core不止夭织,打開系統(tǒng)配置的core文件輸出之后吭露,迅速占滿磁盤,無奈之下尊惰,先讓運維把機器摘掉奴饮,再進一步的分析纬向。其他機器也出現(xiàn)了不同程度的core,線上失敗率直線上升戴卜。
再把機器摘下來之后逾条,看了一眼core文件,就發(fā)現(xiàn)投剥,哎呀师脂,闖禍了。
趕快恢復到?jīng)]有remove的版本江锨,至少還能撐一個星期吃警,不至于程序core掉。
踩坑與解決
接下來開始仔細分析源碼啄育,發(fā)現(xiàn)sysV的擴展中酌心,remove_var實現(xiàn)如下:
PHP_FUNCTION(shm_remove_var)
{
zval *shm_id;
long shm_key, shm_varpos;
sysvshm_shm *shm_list_ptr;
// 讀取輸入?yún)?shù)
if (SUCCESS != zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl", &shm_id, &shm_key)) {
return;
}
SHM_FETCH_RESOURCE(shm_list_ptr, shm_id);
// 檢查sharekey在共享內(nèi)存中是否存在
shm_varpos = php_check_shm_data((shm_list_ptr->ptr), shm_key);
// 如果不存在,返回錯誤
if (shm_varpos < 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "variable key %ld doesn't exist", shm_key);
RETURN_FALSE;
}
// 如果存在挑豌,刪除共享內(nèi)存
php_remove_shm_data((shm_list_ptr->ptr), shm_varpos);
RETURN_TRUE;
}
咋一看沒啥問題安券,但是深入看一下php_check_shm_data
,發(fā)現(xiàn)有問題:
// ptr為整個共享內(nèi)存區(qū)塊的頭指針
static long php_check_shm_data(sysvshm_chunk_head *ptr, long key)
{
long pos;
sysvshm_chunk *shm_var;
// 從頭開始尋找
pos = ptr->start;
for (;;) {
// 找到最后了返回
if (pos >= ptr->end) {
return -1;
}
// 向前進一個內(nèi)存區(qū)塊氓英,由當前區(qū)塊的next指針決定
shm_var = (sysvshm_chunk*) ((char *) ptr + pos);
if (shm_var->key == key) {
return pos;
}
pos += shm_var->next;
if (shm_var->next <= 0 || pos < ptr->start) {
return -1;
}
}
return -1;
}
這個根本就是線程不安全的版本額侯勉,在高并發(fā)的場景下,非常有可能出現(xiàn)铝阐,對一個shareKey內(nèi)是否存在數(shù)據(jù)的錯誤判斷址貌,根據(jù)swoole的多進程模型,進程A進行尋址徘键,查看共享內(nèi)存练对,發(fā)現(xiàn)shareKey對應的區(qū)塊無數(shù)據(jù),所以他準備進行寫入吹害,同時進程B之前已經(jīng)檢查了shareKey數(shù)據(jù)螟凭,發(fā)現(xiàn)shareKey數(shù)據(jù)已經(jīng)過期,執(zhí)行了remove操作赠制。這時候進程A再想去寫入的時候,就會發(fā)生不可避免的segmentation fault挟憔。
發(fā)現(xiàn)了這個問題之后钟些,反過來去想當時為什么共享內(nèi)存會被寫滿,也是一樣的問題绊谭,都怪php_check_shm_data
對key的判斷線程不安全政恍,所以不可避免的,高并發(fā)下一直會用重復的key不停的向前寫入达传。當時申請了 12M的內(nèi)存, 每秒500請求篙耗,swoole開了24個進程迫筑,假設碰撞概率是1/(24*500)=1/12000
。每次寫入的大小是4k*3(四個服務尋址)宗弯,程序設計的是五分鐘進行一次put脯燃。
那么12M共享內(nèi)存被寫滿的時間應該是12M/12k/(60min/5min)/24h = 3.6天左右∶杀#基本上只能撐個這么久辕棚。
所以呢,解決方向有兩個:
- 實現(xiàn)一個有鎖的共享內(nèi)存API版本
- 另辟蹊徑邓厕,使用別的本地內(nèi)存存儲方案
權衡之下逝嚎,準備采取第二種做法,預知后事如何详恼,且看下回分解~