關于php的共享內(nèi)存的使用和研究之由起

下文:
關于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é)果一臉懵逼:

屏幕快照 2016-12-25 下午2.51.26.png

什么情況以躯,調(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,線上失敗率直線上升戴卜。

屏幕快照 2016-12-25 下午3.08.52.png

再把機器摘下來之后逾条,看了一眼core文件,就發(fā)現(xiàn)投剥,哎呀师脂,闖禍了。

屏幕快照 2016-12-25 下午3.18.50.png

趕快恢復到?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)存存儲方案

權衡之下逝嚎,準備采取第二種做法,預知后事如何详恼,且看下回分解~

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末补君,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昧互,更是在濱河造成了極大的恐慌挽铁,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硅堆,死亡現(xiàn)場離奇詭異屿储,居然都是意外死亡,警方通過查閱死者的電腦和手機渐逃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門够掠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茄菊,你說我怎么就攤上這事疯潭。” “怎么了面殖?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵竖哩,是天一觀的道長。 經(jīng)常有香客問我脊僚,道長相叁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任辽幌,我火速辦了婚禮增淹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘乌企。我一直安慰自己虑润,他們只是感情好,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布加酵。 她就那樣靜靜地躺著拳喻,像睡著了一般哭当。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冗澈,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天钦勘,我揣著相機與錄音,去河邊找鬼渗柿。 笑死个盆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的朵栖。 我是一名探鬼主播颊亮,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陨溅!你這毒婦竟也來了终惑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤门扇,失蹤者是張志新(化名)和其女友劉穎雹有,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臼寄,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡霸奕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吉拳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片质帅。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖留攒,靈堂內(nèi)的尸體忽然破棺而出煤惩,到底是詐尸還是另有隱情,我是刑警寧澤炼邀,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布魄揉,位于F島的核電站,受9級特大地震影響拭宁,放射性物質(zhì)發(fā)生泄漏洛退。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一杰标、第九天 我趴在偏房一處隱蔽的房頂上張望兵怯。 院中可真熱鬧,春花似錦在旱、人聲如沸摇零。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驻仅。三九已至,卻和暖如春登渣,著一層夾襖步出監(jiān)牢的瞬間噪服,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工胜茧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粘优,地道東北人。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓呻顽,卻偏偏與公主長得像雹顺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子廊遍,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容