上文:關(guān)于php的共享內(nèi)存的使用和研究之由起
下文:
關(guān)于php的共享內(nèi)存的使用和研究之深入剖析swoole table
上文中提到了針對(duì)php的共享內(nèi)存方案的嘗試烂叔,最終發(fā)現(xiàn)它并不適用于我的場景筒狠,如果想要兼容多進(jìn)程或多線程并發(fā)讀寫的情況下可靠哨免,一定要有適當(dāng)?shù)臋C(jī)制來保證資源的唯一性磨总。
加鎖肯定是想到的第一選擇井联,對(duì)于每次共享內(nèi)存的時(shí)候瓮栗,先獲取一個(gè)鎖,只有獲取成功了之后才允許進(jìn)行讀和寫胀屿,這應(yīng)該是最簡單的方案,但是同步鎖對(duì)性能的損耗也是比較大的包雀。APC的user data cache的存儲(chǔ)機(jī)制對(duì)數(shù)據(jù)要求嚴(yán)格正確宿崭,鎖比較多,它的效率與本地的memcache相當(dāng)才写。既然這樣葡兑,不如把眼光投向業(yè)界,看看大牛們使用什么樣的方案來解決這個(gè)問題赞草。
YAC
原文鏈接:http://www.laruence.com/2013/03/18/2846.html
laurance為了解決如下的兩個(gè)問題讹堤,設(shè)計(jì)出了這個(gè)cache:
- 想讓PHP進(jìn)程之間共享一些簡單的數(shù)據(jù)
- 希望非常高效的緩存一些頁面
同時(shí)也是基于如下的經(jīng)驗(yàn)假設(shè):
- 對(duì)于一個(gè)應(yīng)用來說, 同名的Cache鍵, 對(duì)應(yīng)的Value, 大小幾乎相當(dāng).
- 不同的鍵名的個(gè)數(shù)是有限的.
- Cache的讀的次數(shù), 遠(yuǎn)遠(yuǎn)大于寫的次數(shù).
- Cache不是數(shù)據(jù)庫, 即使Cache失效也不會(huì)帶來致命錯(cuò)誤.
實(shí)現(xiàn)這個(gè)cache,關(guān)鍵是無鎖化的設(shè)計(jì). 根據(jù)laurance的說法房资,他解決讀鎖的方式是通過不加鎖的讀蜕劝,然后CRC校驗(yàn)。
看了一下他代碼的實(shí)現(xiàn)轰异,是對(duì)key中存儲(chǔ)的固定size的值進(jìn)行了CRC的計(jì)算岖沛,然后把key中附帶存儲(chǔ)的crc信息和內(nèi)容計(jì)算出來的crc信息進(jìn)行校驗(yàn)。如果校驗(yàn)成功了搭独,那么認(rèn)為查詢成功婴削,如果校驗(yàn)失敗了,那么認(rèn)為查詢失敗牙肝。其實(shí)本質(zhì)上這是一種使用CPU來換鎖的方式唉俗,大部分的服務(wù)器是多核的,一旦加鎖配椭,對(duì)CPU是很大的浪費(fèi)虫溜。下面這張圖形象的表明了這一點(diǎn):
這個(gè)是個(gè)不錯(cuò)的trick,能夠解決的問題就是在多個(gè)進(jìn)程頻繁的寫入的時(shí)候股缸,可能導(dǎo)致的讀出錯(cuò)不會(huì)帶來錯(cuò)誤的結(jié)果衡楞,因?yàn)橐坏ヽrc校驗(yàn)不通過,那么讀出來的結(jié)果就是失效了敦姻。這顯然比上一篇文章中的共享內(nèi)存的讀的方式要高明一些瘾境。不過根據(jù)我的觀察,使用共享內(nèi)存的方式镰惦,由于一直是向后不停的寫入迷守,出現(xiàn)被覆蓋的概率幾乎沒有,而laurance這里之所以要校驗(yàn)旺入,則是因?yàn)樗麜?huì)進(jìn)行內(nèi)存的回收和循環(huán)寫入兑凿,這點(diǎn)在下文中會(huì)繼續(xù)說明凯力。
現(xiàn)在重點(diǎn)說說這個(gè)YAC的寫入的問題,首先啟動(dòng)的時(shí)候key空間大小確定礼华,可以通過配置來調(diào)整分配給存儲(chǔ)key的大小沮协,從而擴(kuò)展key的個(gè)數(shù)。4M基本上相當(dāng)于32768個(gè)Cache值卓嫂。首先第一個(gè)很重要的點(diǎn)就是如何設(shè)計(jì)哈希方法來避免寫入沖突,這里他使用的是雙散列法的
MurmurHash.
對(duì)于小于4M的內(nèi)存塊的操作由于key不同聘殖,根據(jù)哈希出來的起始位置也不同晨雳。不同key之間沖突的概率,等同于哈希算法沖突的概率奸腺,這個(gè)還是比較低的餐禁。對(duì)于大的內(nèi)存塊,這里使用了segment->pos指針來控制內(nèi)存的分塊突照。和共享內(nèi)存擴(kuò)展的實(shí)現(xiàn)方式還是比較類似了帮非,反正就是一個(gè)pos指針,找得到就update讹蘑,找不到就向后寫末盔。
那么如果發(fā)生沖突呢,laurance給出了一個(gè)例子:
比如A進(jìn)程申請了40字節(jié), B進(jìn)程申請了60字節(jié), 但是Pos只增加了60字節(jié). 這個(gè)時(shí)候有如下幾種情況:
- A寫完了數(shù)據(jù), 返回成功, 但是B進(jìn)程又寫完了數(shù)據(jù)返回成功, 最終B進(jìn)程的Cache種上了, 而A進(jìn)程的被踢出了.
- B進(jìn)程寫完了數(shù)據(jù), 返回成功, A進(jìn)程又寫完了數(shù)據(jù)返回成功, 最終A進(jìn)程的Cache種上了, B進(jìn)程的被踢出.
- A進(jìn)程寫一半, B進(jìn)程寫一半, 然后A進(jìn)程又寫一半, B進(jìn)程又寫一半, 都返回成功, 但最終, 緩存都失效.
可見, 最嚴(yán)重的錯(cuò)誤, 就是A和B的緩存都失效, 但是Yac不會(huì)把錯(cuò)誤數(shù)據(jù)返回給用戶, 當(dāng)下一次來查詢Cache的時(shí)候, 因?yàn)榇嬖赾rc校驗(yàn), 所以都miss.
看到這兒終于明白了座慰,并沒有解決多進(jìn)程寫的問題陨舱,多進(jìn)程的寫還是可能會(huì)有沖突,不僅僅是單key的沖突版仔,不同key之間也可能會(huì)有沖突游盲。但是沖突了不怕,會(huì)通過校驗(yàn)的方式確保client端能夠判斷出來自己沖突了蛮粮,這點(diǎn)對(duì)應(yīng)用程序確實(shí)非常重要益缎,因?yàn)檫@不是cache error,而僅僅是cache miss而已然想,這兩種情況的嚴(yán)重程度和處理機(jī)制的確完全不同莺奔。
另外還有一個(gè)亮點(diǎn)是內(nèi)存的循環(huán)分配,如果一個(gè)內(nèi)存塊用完了又沾,那么可以重置pos弊仪,從而從頭開始分配,有了這種機(jī)制杖刷,即使并發(fā)寫導(dǎo)致pos一直后移励饵,也不會(huì)出現(xiàn)內(nèi)存耗盡的情況了,這確實(shí)是個(gè)不錯(cuò)的特性滑燃。
總結(jié)來看役听,yac兩個(gè)不錯(cuò)的特性:
- 讀的CRC校驗(yàn),保證最嚴(yán)重是cache miss
- 寫的pos重置,保證內(nèi)存不被寫滿
但是針對(duì)我的使用場景典予,多并發(fā)情況下的同key的多寫多讀甜滨,它并沒有很好的解決這個(gè)問題,而是比較適用于低頻的用戶數(shù)據(jù)的緩存瘤袖,比如登陸用戶的頭像衣摩、昵稱這類信息。拉取頻次不高捂敌,miss了也能向后端請求艾扮。所以怎么辦呢,只能繼續(xù)進(jìn)行求索占婉。
ps:laurance在文章開頭群嘲了一下APC的性能泡嘴,相當(dāng)于本地的memcache,結(jié)果文末貼出的性能對(duì)比逆济,yac完全比不上apc酌予。。有點(diǎn)莫名
Swoole table
接下來說說這兩年在社區(qū)里面比較火奖慌,最近剛剛發(fā)布了內(nèi)置協(xié)程2.0版本的swoole. github地址:https://github.com/swoole/swoole-src
swoole table是swoole中的一個(gè)基于共享內(nèi)存和鎖實(shí)現(xiàn)的超高性能的并發(fā)數(shù)據(jù)結(jié)構(gòu)抛虫,用來解決多進(jìn)程、多線程數(shù)據(jù)共享和同步加鎖的問題简僧。這不就是我們苦苦尋覓的解決方案么莱褒?
先來看一下swoole table的常見的使用方式,首先它支持三種基本的類型:
-
swoole_table::TYPE_INT
整形字段 -
swoole_table::TYPE_FLOAT
浮點(diǎn)字段 -
swoole_table::TYPE_STRING
字符串字段
如果想要在各個(gè)進(jìn)程之間共享高性能的本地?cái)?shù)據(jù)涎劈,那么使用的范例如下:
// 新建swoole table广凸,并且指定類型
$table = new swoole_table(1024);
$table->column('id', swoole_table::TYPE_INT, 4); //1,2,4,8
$table->column('name', swoole_table::TYPE_STRING, 64);
$table->column('num', swoole_table::TYPE_FLOAT);
$table->create();
// 新建swoole的server
$serv = new swoole_server('127.0.0.1', 9501);
//將table保存在serv對(duì)象上
$serv->table = $table;
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
// 使用swoole table存儲(chǔ)全局的數(shù)據(jù)
$ret = $serv->table->set($key, array('from_id' => $data, 'fd' => $fd, 'data' => $data));
});
// 這里需要注意的就是一定要在server啟動(dòng)之前創(chuàng)建swoole table,從而保證它能夠被全局共享
$serv->start();
如果只是你自己的一個(gè)進(jìn)程在不同的請求之間共享高性能的本地?cái)?shù)據(jù)蛛枚,那么使用的范例如下:
class LocalSwooleTable {
private static $_swooleTable;// 靜態(tài)變量谅海,單個(gè)進(jìn)程內(nèi)共享
const SWOOLE_TABLE_SET_FAILED = -1001;
const SWOOLE_TABLE_GET_FAILED = -1002;
// swoole table初始化
private function __construct() {
//預(yù)估數(shù)據(jù)量 100個(gè)服務(wù),每個(gè)長度30 需要3000個(gè)字節(jié),這里申請64k
self::$_swooleTable = new \swoole_table(65536);
self::$_swooleTable->column('ip',\swoole_table::TYPE_STRING, 64);
self::$_swooleTable->column('port',\swoole_table::TYPE_INT, 4);
self::$_swooleTable->column('timestamp',\swoole_table::TYPE_INT, 4);
self::$_swooleTable->column('bTcp',\swoole_table::TYPE_INT, 4);
self::$_swooleTable->create();
}
// 獲取單個(gè)swoole的實(shí)例
public static function getInstance() {
if(self::$_swooleTable) {
return self::$_swooleTable;
}
else {
new LocalSwooleTable();
return self::$_swooleTable;
}
}
}
// 獲取唯一的實(shí)例
$swooleTableIns = LocalSwooleTable::getInstance();
$key = "sample";
$routeInfo['timestamp'] = time();
$routeInfo['ip'] = '10.25.22.33';
$routeInfo['port'] = 1000;
$routeInfo['bTcp'] = 1;
// 設(shè)置swoole table中的內(nèi)容
$flag = $swooleTableIns->set($key,$routeInfo);
// 獲取swoole table中的內(nèi)容
$routeInfo = $swooleTableIns->get($key);
本來,第一種方式應(yīng)該是我們最好的選擇蹦浦,但是因?yàn)槲覀兪褂昧薚SF框架(或者任何不是自己從頭裸寫swoole的框架)扭吁,都不會(huì)把創(chuàng)建server這一步暴露到業(yè)務(wù)代碼中,這就給我們使用全局的swoole的table帶來了很大的難度盲镶。換句話說侥袜,知道好用,但是就是業(yè)務(wù)用起來非常的不方便溉贿,不具備業(yè)務(wù)擴(kuò)展性枫吧。
所以無奈之下,我們還是選取了第二種方案宇色,從性能上面來講的話九杂,確實(shí)是有提升的颁湖,不好的地方就是存儲(chǔ)資源浪費(fèi)了一些,每個(gè)進(jìn)程都用了專屬自己的swoole table例隆,這當(dāng)然是無奈之舉甥捺。還是希望能夠之后通過一些改造,把全局的swoole table這種能力能夠開放出來镀层。
基本上訪問一次是0.03ms镰禾,這個(gè)性能還是比較突出的。