php windows共享内存,关于php的共享内存的使用和研究之由起
最近遇到一個場景,服務尋址的時候,需要請求遠程的服務,獲取一批可用的ip和端口地址及其權重。根據權重和隨機算法選擇最合適的一個服務地址,進行請求。由于服務地址在短時間之內不會發生變化,因此為了避免無限制的進行尋址的請求,有必要將地址緩存至本地。
對于php而言,說到用戶數據緩存本地,第一反應出來的就是APC。但是APC首先被創建出來是給php做內部緩存的,其次才是提供給用戶態使用的。根據laruence在博客的說法,opcache出現了之后,對zend編譯的opcode做了緩存,實際上解決了apc被創建出來想要解決的問題。因此現在APC已經處于不再更新維護的狀態了。
對于想使用opcache,又要使用用戶態的APC的同學,就需要額外的配置,同時性能上也會比原來的APC要差,差不多相當于本機的memcache。這顯然就無法達到本機內存訪問的效率了,因此需要尋求其他的解決方案。
php的共享內存API
隨后我就想到了使用php的共享內存API,反正只是緩存非常少的路由信息,加在一起不超過1k,盡管是多讀多寫的場景,但是覆蓋了也沒關系,出于這種出發點,我就開始了對php的共享內存API的研究。
php中操作共享內存的方式一共有兩組:
System V IPC
編譯增加 --enable-sysvshm
Shared Memory
--enable-shmop
先來看一個shmop的例子:
// 從系統獲取一個共享內存的id
$key = ftok(__FILE__, 'test');
$size = 1024;
// 打開1024字節的共享內存(如果不存在則申請)
$shm_h = @shmop_open($key, 'c', 0644, $size);
if($shm_h === false) {
echo "shmop open failed";
exit;
}
// 讀取共享內存中的數據
$data = shmop_read($shm_h, 0, $size);
// 對讀取的數據進行反序列化
$data = unserialize($data);
//如果沒有數據則寫入
if(empty($data)) {
echo "there is no data";
$data = "imdonkey";
//所有寫入的數據,都必須提前序列化
$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擴展,必須要注意數據的大小,以及讀寫時候的偏移量。同時,不管你寫入的是什么數據類型,都必須進行序列化和反序列化。
再看一下SysV的例子:
// 從系統獲取一個共享內存的id
$shm_key = ftok(__FILE__, 'test');
// 獲取此共享內存資源的操作句柄
$memsize = 1024;
$shm_h = shm_attach($shm_key, $memsize, 0644);
if($shm_h === false) {
echo "shmop open failed";
exit;
}
// 獲取共享內存中key=222時的內容
$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";
// 如果數據不存在,寫入數據,可以是任意類型,無需初始化
shm_put_var($shm_h, $var_key, $data);
} else {
// 否則,輸出數據,并清理相關內存
echo "find data: $data\n";
shm_remove_var($shm_h, $var_key);
}
// 斷開資源的鏈接
shm_detach($shm_h);
?>
原理上來講并無不同,只是SysV做了更多的封裝,讓你使用起來更加方便一些。不用自己控制偏移量,也不用進行序列化和反序列化。同時對于每個數據,都設置了對應的var_key, 這樣在同一個區域可以保存多個數據,而無需再次申請另一片共享內存。
業務中的使用
在使用兩者的時候,都要注意對數據大小的估算。否則很容易出現共享內存溢出的情況。而我在使用的時候,充分評估了要存儲的數據結構的大小,我需要存儲的內容是:
ip(15個字節以內)+port(8字節以內)+timestamp(15字節以內)+分隔符(3字節)=41字節
假設我調用100個后端服務。那么最高需要存儲的路由信息就是4.1k大小。
出于這種考慮,我申請了1M的內存,覺得應該是夠夠的了。就這么悠哉哉的在線上跑了一個星期左右,有天沒事到線上看了下php的錯誤日志,結果一臉懵逼:
屏幕快照 2016-12-25 下午2.51.26.png
什么情況,調用的后端服務一共才5個,共享內存這么快就寫滿了??經過一個初步的判斷之后,我得出的結論是:sysV的接口能力太差,對于shareKey沒有做去重處理,而是每次都寫入了新的key,這樣就導致了共享內存的寫入指針盡管是相同的shareKey,但是卻不斷的后移,最終導致共享內存被寫爆,而尋址的請求全部都打到了尋址服務,還好它比較健壯,也有短時的緩存,才沒有產生運營事故。
在得出了這么個結論之后,我修改了我的代碼,在每次完成對shareKey內容的獲取之后,增加了一行
shm_remove_var($shareKey)
同時寫了一個腳本,把原有的共享內存id對應的內容清空,經過手工處理十臺機器之后,再全量替換一把代碼,打卡下班,感覺自己棒棒噠。
沒想到,這才是悲劇的開始。就在當周的周六,吃著火鍋,突然就有一臺線上機器罷工了。機器服務狂core不止,打開系統配置的core文件輸出之后,迅速占滿磁盤,無奈之下,先讓運維把機器摘掉,再進一步的分析。其他機器也出現了不同程度的core,線上失敗率直線上升。
屏幕快照 2016-12-25 下午3.08.52.png
再把機器摘下來之后,看了一眼core文件,就發現,哎呀,闖禍了。
屏幕快照 2016-12-25 下午3.18.50.png
趕快恢復到沒有remove的版本,至少還能撐一個星期,不至于程序core掉。
踩坑與解決
接下來開始仔細分析源碼,發現sysV的擴展中,remove_var實現如下:
PHP_FUNCTION(shm_remove_var)
{
zval *shm_id;
long shm_key, shm_varpos;
sysvshm_shm *shm_list_ptr;
// 讀取輸入參數
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在共享內存中是否存在
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;
}
// 如果存在,刪除共享內存
php_remove_shm_data((shm_list_ptr->ptr), shm_varpos);
RETURN_TRUE;
}
咋一看沒啥問題,但是深入看一下php_check_shm_data,發現有問題:
// ptr為整個共享內存區塊的頭指針
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;
}
// 向前進一個內存區塊,由當前區塊的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;
}
這個根本就是線程不安全的版本額,在高并發的場景下,非常有可能出現,對一個shareKey內是否存在數據的錯誤判斷,根據swoole的多進程模型,進程A進行尋址,查看共享內存,發現shareKey對應的區塊無數據,所以他準備進行寫入,同時進程B之前已經檢查了shareKey數據,發現shareKey數據已經過期,執行了remove操作。這時候進程A再想去寫入的時候,就會發生不可避免的segmentation fault。
發現了這個問題之后,反過來去想當時為什么共享內存會被寫滿,也是一樣的問題,都怪php_check_shm_data對key的判斷線程不安全,所以不可避免的,高并發下一直會用重復的key不停的向前寫入。當時申請了 12M的內存, 每秒500請求,swoole開了24個進程,假設碰撞概率是1/(24*500)=1/12000。每次寫入的大小是4k*3(四個服務尋址),程序設計的是五分鐘進行一次put。
那么12M共享內存被寫滿的時間應該是12M/12k/(60min/5min)/24h = 3.6天左右。基本上只能撐個這么久。
所以呢,解決方向有兩個:
實現一個有鎖的共享內存API版本
另辟蹊徑,使用別的本地內存存儲方案
權衡之下,準備采取第二種做法,預知后事如何,且看下回分解~
總結
以上是生活随笔為你收集整理的php windows共享内存,关于php的共享内存的使用和研究之由起的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php数字转中文数字排序,php实现中文
- 下一篇: php defunct,通过swool