前言: 我們的項(xiàng)目要頻繁讀寫遍歷緩存,起初用的Redis,CPU占用20%左右,顯然不理想。
當(dāng)接觸到php自帶的apcu后,簡直就是神一樣,單機(jī)效率超Redis幾十甚至上百倍,
利用workerman的text協(xié)議來搭配apcu 也能秒Redis 2倍以上,
關(guān)鍵是...關(guān)鍵是...關(guān)鍵是CPU消耗不足redis十分之一,
上測試截圖:
//走tcp
如果只作為單純的進(jìn)程間共享變量函數(shù)以及類,可說是非常完美,高Redis百倍性能。
如果作為kv緩存的話有一些特殊情況,大致根據(jù)自己項(xiàng)目情況來選擇,
第一種:對(duì)單key的過期時(shí)間有要求的,在遍歷全部key會(huì)包含已過期的key且無法分辨過期和未過期。
第二種:對(duì)遍歷全部key只要有效key的話,單key無法自動(dòng)過期。
對(duì)于key未設(shè)置過期的不影響,這些是運(yùn)行在cli下才有的問題,fpm下不存在能完美Redis特性并秒殺百倍性能。
apcu實(shí)際上已經(jīng)邊緣化了,只能單進(jìn)程,整體的設(shè)計(jì)更適合fpm,在cli下面有挺多問題的,包括過期時(shí)間等,甚至不如shmop,早期shmop是為cli下設(shè)計(jì)的;
不考慮分布式、持久化容錯(cuò)的話,用shmop會(huì)更好,memcache也可以,性能都比redis更好;畢竟webman/workerman都常駐內(nèi)存了,連接可以持久化。
早期實(shí)現(xiàn)過shmop/apcu + 邊車定時(shí)器進(jìn)程進(jìn)行過期時(shí)間處理,實(shí)話實(shí)說,那個(gè)性能沒法看,尤其是堆積數(shù)據(jù)過大的時(shí)候,時(shí)間復(fù)雜度畢竟是O(n),而且單進(jìn)程的定時(shí)器在堆積數(shù)據(jù)過大的情況下也存在延時(shí)。
當(dāng)然,這樣玩一玩,了解實(shí)踐一些服務(wù)層的知識(shí)肯定是好處,有益的
而且其實(shí)這里面我們還得出一個(gè)實(shí)踐,用持久化連接的SQLite3的memory也非??欤途彺媸穷愃频?,用file也可以達(dá)到redis的持久化效果,而且還帶事務(wù),當(dāng)然肯定和純內(nèi)存是沒辦法比的;我們后續(xù)的很多小組件都是通過RPC + SQLite3進(jìn)行的數(shù)據(jù)存儲(chǔ)和緩存處理,相當(dāng)穩(wěn)定。
cli下和fpm使用一致,并沒有過期時(shí)間的問題,pecl.php.net下載的存在時(shí)間過期問題,從倉庫拉源碼編譯是正常的,當(dāng)然我們修改了源碼增加了一個(gè)時(shí)間字段來遍歷時(shí)更好的過濾失效key,我們已應(yīng)用在生產(chǎn)環(huán)境。不考慮分布式以及持久化,只用簡單的kv緩存的話,apcu就是神一樣的存在。
在單機(jī)下,apcu可以完美替代workerman提供的globalData 實(shí)現(xiàn)多進(jìn)程變量共享,且是globalData的幾十倍效率。
不考慮持久化和分布式的話,其實(shí)常駐內(nèi)存框架可以直接用php的靜態(tài)變量,其中還可以使用Spl的一些數(shù)據(jù)結(jié)構(gòu),比如堆、優(yōu)先隊(duì)列,在處理包含資源類型的緩存的時(shí)候比apcu更有競爭力,而且性能更好,你們可以在8.0及以上的php版本測試一下,效果是比apcu更好的
下圖是我們生產(chǎn)環(huán)境中的各種方案,起初我們把后端用go+redis和go+自寫cache,并發(fā)上來后都不如fpm+apcu,后來又使用了workerman+redis,起初也一樣,無法完美使用apcu,導(dǎo)致Redis有些扛不住,又切換到了fpm+apcu,然后我們前端APP要websocket,沒辦法又用回了workerman+redis,不死心去研究了下apcu源碼,最后就是workerman+apcu,終于比fpm+apcu強(qiáng)了。
Redis 為 127.0.0.1 本機(jī),該服務(wù)端的Redis還存在IO瓶頸嗎? 手動(dòng)狗頭
是這個(gè)源碼地址嗎?有誰教教我怎么編譯。https://github.com/krakjoe/apcu
拉取到服務(wù)器,cd到源碼目錄,依次執(zhí)行以下幾個(gè)步驟
/server/php/bin/phpize (你的phpize路徑)
./configure --with-php-config=/server/php/bin/php-config (你的php-config路徑)
make && make install
最后在php.ini里添加apcu的配置
[apcu]
apc.enabled=1
apc.shm_segments=1
apc.shm_size=512M
apc.entries_hint=0
apc.ttl=0
apc.gc_ttl=5
apc.mmap_file_mask=
apc.slam_defense=0
apc.enable_cli=1
apc.use_request_time=0
apc.serializer="php"
apc.coredump_unmap=0
apc.preload_path=
;
;
extension=apcu.so
workerman本身已經(jīng)常駐內(nèi)存了,一步到位,直接使用全局變量豈不是更快!
程序啟動(dòng)時(shí)初始化全局變量:$a=[];
寫入/修改:
$a["key"]="value";
$a["key2"]="value2";...
讀?。?br />
$b=$a["key"];...
刪除:
unset($a["key"]);
apcu本身也是單進(jìn)程單線程設(shè)計(jì)的,多個(gè)主進(jìn)程間或者多個(gè)子進(jìn)程間apcu緩存不支持共享,每個(gè)進(jìn)程都有獨(dú)立的apcu緩存
如果僅提供單進(jìn)程單線程服務(wù),高并發(fā)且頻繁讀寫增刪緩存,例如實(shí)時(shí)游戲等追求極致性能的實(shí)例,不用想,使用全局變量肯定比其他外部擴(kuò)展快
例如:
<?php
//error_reporting(0);
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Timer;
require_once __DIR__ . '/vendor/autoload.php';
$worker = new Worker("websocket://0.0.0.0:10001"); // 創(chuàng)建websocket
$worker->name = "桀桀桀桀";
$worker->count = 1; // 啟動(dòng)1個(gè)進(jìn)程 如果開啟多個(gè)進(jìn)程,則每個(gè)進(jìn)程都會(huì)產(chǎn)生獨(dú)立的緩存,業(yè)務(wù)中如果提供向指定用戶推送消息且開啟多進(jìn)程,同進(jìn)程推送成功否則失敗,進(jìn)程間緩存不共享(在A魚塘釣不了B魚塘的羅飛魚)
//$worker啟動(dòng)時(shí)
$worker->onWorkerStart = function () {
//初始化MySQL redis等
global $test; //桀桀桀桀桀 全局變量
$test=[]; //初始化全局變量
//定時(shí)任務(wù) 每隔1800秒同步一次
$time_interval = 1800;
Timer::add($time_interval, function () {
global $test;
//各種邏輯 然后操作$test
}
}
//用戶握手連接時(shí)初始化用戶數(shù)據(jù)
$worker->onConnect = function (TcpConnection $connection) {
$connection->onWebSocketConnect = function (TcpConnection $connection, $request) {
global $test;
//從連接參數(shù)中獲取用戶數(shù)據(jù) 各種邏輯 然后操作$test
};
};
//收發(fā)數(shù)據(jù)
$worker->onMessage = function (TcpConnection $connection, $data) {
global $test;
//業(yè)務(wù)邏輯 操作$test
}
// 用戶連接斷開時(shí)
$worker->onClose = function (TcpConnection $connection) {
global $test;
//各種邏輯 然后操作$test
}
Worker::runAll();
?>
看業(yè)務(wù)情況只需修改下cli的php.ini腳本內(nèi)存限制,如:memory_limit=256M
最近在研究這個(gè),http://www.wtbis.cn/plugin/133 ,可以看看,應(yīng)該能用得上
很多時(shí)候上層需要一個(gè)原子性操作來保證業(yè)務(wù)原子性,apcu底層提供了鎖來保證apcu自己的函數(shù)具備原子性,但是上層的封裝需要多次調(diào)用一個(gè)或N個(gè)apcu函數(shù),需要保證上層業(yè)務(wù)的原子性,所以需要用到apcu提供的原子性鎖,有些地方需要阻塞等待式調(diào)用,所以需要自行在此基礎(chǔ)上實(shí)現(xiàn)搶占式鎖來保證原子性和業(yè)務(wù)完整性
多進(jìn)程下,AB兩個(gè)進(jìn)程在每一個(gè)apcu的函數(shù)上肯定是原子性的,但是有些時(shí)候A上面有多個(gè)apcu操作,B也有多個(gè)apcu函數(shù)操作,相互之間需要各自的原子性,如果不加鎖,這些apcu函數(shù)會(huì)在apcu實(shí)際執(zhí)行中穿插執(zhí)行,并不能保證原子性,這個(gè)是測試后得到的結(jié)果,畢竟apcu只保證自己函數(shù)的原子性
apc在寫入的時(shí)候,內(nèi)部總是以 鎖->寫->釋放的過程,不管你的有多少個(gè)進(jìn)程同時(shí)操作,這不會(huì)變,也可以理解為阻塞寫入,在業(yè)務(wù)代碼上再套一層鎖,真有必要嗎? 麻煩分享下測試過程,這對(duì)我們很重要,目前沒遇到過并發(fā)安全性問題,我們項(xiàng)目深度依賴apcu 希望能漲些知識(shí) ??
你沒懂我的意思
假設(shè)兩個(gè)進(jìn)程同時(shí)執(zhí)行以下操作:
{
1.apuc_get('a',一些數(shù)據(jù));
其他的一些業(yè)務(wù)結(jié)果a
2.apuc_store('a', a);
另一些業(yè)務(wù)
}
業(yè)務(wù)是一次原子性操作,業(yè)務(wù)邏輯是依賴apcu的數(shù)據(jù)
在apuc層面每一次調(diào)用都是原子性帶鎖的沒錯(cuò),會(huì)互斥沒錯(cuò),但是兩個(gè)進(jìn)程同時(shí)進(jìn)行的時(shí)候,1/2的數(shù)據(jù)可能被另一個(gè)進(jìn)程的操作污染,因?yàn)闃I(yè)務(wù)不具備原子性,那么我在上層增加atomic原子性操作,依賴自己實(shí)現(xiàn)的鎖就可以避免這個(gè)問題,但代價(jià)就是當(dāng)多個(gè)相同的進(jìn)程同時(shí)執(zhí)行對(duì)相同數(shù)據(jù)進(jìn)行操作的時(shí)候,會(huì)互斥并排隊(duì);原理和redis的nx 或者 xx是類似的
由于apcu是非常高效的微妙級(jí)別的操作,通常來說上下文執(zhí)行的過程是比apcu處理會(huì)慢的,所以很多時(shí)候感受不到這個(gè)問題,當(dāng)并發(fā)量達(dá)到一定程度,或者其中業(yè)務(wù)存在一定執(zhí)行時(shí)間的時(shí)候,這樣的問題就會(huì)暴露出來;這樣的問題就跟使用數(shù)據(jù)庫對(duì)數(shù)據(jù)進(jìn)行處理的時(shí)候不加事務(wù)一樣,原則上是為了保證整個(gè)業(yè)務(wù)的完整性和原子性
具體場景就是當(dāng)apcu儲(chǔ)存了一個(gè)map,我需要對(duì)map里面的一個(gè)鍵值進(jìn)行自增,那么我需要先讀取出來,比如a=>1,當(dāng)我利用apcu讀取的時(shí)候可能是1,在我自增寫入的時(shí)候我期望是2,但在這時(shí)候其他親緣進(jìn)程可能已經(jīng)先于我操作了,就在我讀取并自增的間隙內(nèi),已經(jīng)自增到了2,那么最后我的結(jié)果還是2,不能保證是3;因?yàn)閍pcu的每次函數(shù)調(diào)用是一個(gè)原子性操作,但是很多業(yè)務(wù)是需要多次apcu的函數(shù)調(diào)用來完成一次原子性操作的
a進(jìn)程map自增,b進(jìn)程map也自增,存在ab進(jìn)程都讀取的是1,ab進(jìn)程最后的結(jié)果都是2,而不是一個(gè)是2一個(gè)是3
原來存入的map,這下就明白了,我們用不到這種場景,我們都是直接存單key,比如5萬臺(tái)車需要實(shí)時(shí)上傳位置,每臺(tái)車一個(gè)key,這種場景下用map效率比單key低。
還有這個(gè)插件支持redis的nx和xx這兩個(gè)場景,都需要鎖+搶占式循環(huán)來進(jìn)行實(shí)現(xiàn),其他的業(yè)務(wù)實(shí)際上沒有用到鎖;包括后續(xù)的業(yè)務(wù)需要對(duì)mmap進(jìn)行支持,我也在想辦法如何對(duì)內(nèi)存的變動(dòng)進(jìn)行有效的監(jiān)聽
老哥說的很詳細(xì),給我就解惑了。我現(xiàn)在有的項(xiàng)目是接收大量的http報(bào)文請(qǐng)求,然后分析以后存儲(chǔ)到當(dāng)前進(jìn)程的內(nèi)存變量二維數(shù)組中,通過條數(shù)積累到一定數(shù)量后統(tǒng)一上報(bào)給后端某個(gè)服務(wù)中。但是過程中出現(xiàn)了一些問題,比如由于要保證接收http報(bào)文的性能,所以開了多個(gè)進(jìn)程,而進(jìn)程隔離導(dǎo)致無法判斷兩個(gè)進(jìn)程中的兩份報(bào)文是否有重復(fù)的。所以打算優(yōu)化這個(gè)操作,使用apcu共享內(nèi)存,但是新的問題又出現(xiàn)了,每個(gè)進(jìn)程中都要判斷當(dāng)前報(bào)文的條數(shù)是否符合上報(bào)的要求,然后符合要求的進(jìn)行上報(bào),上報(bào)完成后清空當(dāng)前key,重復(fù)下一次報(bào)文積累,這樣一定會(huì)出現(xiàn)并發(fā)的問題,比如上報(bào)了兩次,或者上報(bào)的條數(shù)超過了等等并發(fā)問題。 我現(xiàn)在使用上述包試試,使用對(duì)上報(bào)、apcu的查詢、存儲(chǔ)進(jìn)行加鎖試試。
@xiaopi 不要存數(shù)組,存單key,給個(gè)識(shí)別的前綴標(biāo)識(shí),單開一個(gè)進(jìn)程定時(shí)去遍歷key進(jìn)行上報(bào)即可,存數(shù)組這種形式明顯不合理且性能低。
@army 感謝提醒,不過我這個(gè)項(xiàng)目對(duì)上報(bào)的時(shí)效性要求比較高,原先的邏輯是存在靜態(tài)數(shù)組中,然后如果數(shù)組長度達(dá)到100或者10s中后,自動(dòng)上報(bào),所以是放在Timer定時(shí)器中的。改成apcu全局緩存后,請(qǐng)教一下老哥,如果單進(jìn)程上報(bào)會(huì)不會(huì)效率比較低啊,如果多進(jìn)程還是要加鎖的吧? 還有一個(gè)是存單key的話,不太好獲取上報(bào)的條數(shù)吧? 用的是這個(gè)擴(kuò)展 http://www.wtbis.cn/plugin/133
不過里面提供了search方法,還不知道效率如何
\Workbunny\WebmanSharedCache\Cache::Search('/^abc.+$/', function ($key, $value) use (&$result) {
$result[$key] = $value;
}, 50);
@xiaopi 我認(rèn)為跟我的場景差不多,我說下我的場景是怎么實(shí)現(xiàn)的,應(yīng)該對(duì)你有幫助。
我這有5萬臺(tái)車實(shí)時(shí)上報(bào)位置,我將每臺(tái)車的位置信息用apcu去緩存,set key為:“AAA:LINE:DRIVER:車牌號(hào):經(jīng)緯度“,也就是不斷的向緩存中去set,單開了一個(gè)進(jìn)程專門跑timer定時(shí)器,每2秒遍歷一次apcu的key,組合成inert語句入庫,整體下來5萬臺(tái)車的實(shí)時(shí)位置只需要每2秒執(zhí)行一次sql,效率高得離譜,存單key不用考慮搶占鎖什么的問題,希望能給到你思路。
@army 感謝,單進(jìn)程遍歷5萬個(gè)key,這個(gè)過程是阻塞的吧,包括插入數(shù)據(jù)庫的IO都是阻塞的,那么2秒鐘之內(nèi)可以完成這些操作嗎,會(huì)不會(huì)導(dǎo)致Timer不準(zhǔn)了啊,實(shí)際上不是間隔2s了。 還有單開進(jìn)程指的是在worker進(jìn)程中指定某個(gè)進(jìn)程專門用來上報(bào)的,還是process中自定義的task進(jìn)程??? 如果自定義的task進(jìn)程可以訪問到worker進(jìn)程set的全局緩存嗎,我記得有親緣性的進(jìn)程才可以訪問吧,我沒測試過
@xiaopi 是阻塞的,所以要單開進(jìn)程去跑,但是遍歷+入庫幾百毫秒就完成了。單開進(jìn)程是指 new 一個(gè)或多個(gè)Worker專門用來跑task
真的假的,比redis都高?