我們都知道服務(wù)器的負(fù)載能力的重要性,本文從5個可能影響負(fù)載能力的點上進(jìn)行討論。
首先,有必要了解提高服務(wù)端PHP代碼效率所需的關(guān)鍵操作侠讯。
最重要的是對性能數(shù)據(jù)的收集,如果你想對某個地方進(jìn)行優(yōu)化暑刃,那么你需要測量優(yōu)化前后的數(shù)據(jù)以進(jìn)行對比厢漩。一般來說,程序的響應(yīng)時間以及對內(nèi)存的使用是比較重要的岩臣。對于PHP來說溜嗜,大多數(shù)情況下,頁面的加載時間是影響用戶體驗最大的一個環(huán)節(jié)架谎。當(dāng)然炸宵,還有其他的各種問題同樣對性能有很大的影響,如:網(wǎng)絡(luò)延遲谷扣、I/O等土全。
提示: 對于日志輸出,需要極為謹(jǐn)慎会涎,因為日志系統(tǒng)本身就會對性能有所影響裹匙,如果濫用日志,很可能會成為你的系統(tǒng)中的一塊短板末秃。當(dāng)然概页,也不能完全沒有日志,往往日志是你發(fā)現(xiàn)問題最關(guān)鍵的信息蛔溃。至于如果合理的使用日志绰沥,就得根據(jù)你的業(yè)務(wù)場景來定了。
下面是一個簡單的獲取內(nèi)存使用情況的代碼:
$time = microtime(true);
$mem = memory_get_usage();
// 需要測試的代碼
for ($i = 0; $i < 10000000000; $i ++) {
$b = $i + $i;
$c = $b * $i;
for ($k = 0; $k < 999; $k ++) {
$d = $k * $i;
$e = $k * $b * $c;
}
}
print_r([
'momory' => (memory_get_usage() - $mem) / (1024 * 1024)
'seconds' => microtime(true) - $time;
]);
1. 緩存
這個建議可能會出現(xiàn)在所有的性能清單上贺待,這表明了它的重要性徽曲。有很多不錯的工具可以幫你完成緩存的工作,比如:Memcache
或者強(qiáng)大的Varnish
麸塞。從本質(zhì)上來說秃臣,你必須知道你的程序是否真的需要一遍遍的執(zhí)行。如果你的信息是不變的或者不需要實時的變化,使用緩存可以節(jié)省CPU的執(zhí)行周期奥此,提高程序的速度弧哎。
下面是使用Memcache來緩存數(shù)據(jù)的示例:
function showAndHeavyOperation() {
sleep(1);
return date('Y-m-d H:i:s');
}
$item1 = showAndHeavyOperation();
echo $item1;
上面的代碼利用sleep(1)讓程序睡眠1秒鐘來模擬一個慢操作。結(jié)下來就讓我們用緩存來重構(gòu)上面的代碼:
$memcache = new Memcache;
$memcache->connect('localhost', 11211);
function showAndHeavyOperation() {
sleep(1);
return data('Y-m-d H:i:s');
}
$item1 = $memcache->get('item');
if ($item1 === false) {
$item1 = showAndHeavyOperation();
$memcache->set('item', $item1);
}
echo $item1;
現(xiàn)在腳本在第一次的時候稚虎,showAndHeavyOperation
執(zhí)行一次撤嫩。當(dāng)你再次執(zhí)行的時候,就不會再去執(zhí)行showAndHeavyOperation
蠢终,而是從緩存中獲取數(shù)據(jù)序攘。但是,你肯定發(fā)現(xiàn)一個問題寻拂,從Memcache中獲取到的數(shù)據(jù)總是老的數(shù)據(jù)程奠,但是Memcache允許你設(shè)置存儲的數(shù)據(jù)的TTL(存活時間)。有了這個功能祭钉,你可以設(shè)置一個刷新策略來緩存數(shù)據(jù)瞄沙,雖然還是無法做到真正的實時數(shù)據(jù),但是為服務(wù)器節(jié)省了大量的資源慌核,特別是在高負(fù)載和高并發(fā)的業(yè)務(wù)下距境,作用尤其明顯。對于變化少或者實時性要求低的數(shù)據(jù)就可將其放入到緩存中來提高程序效率遂铡。更多關(guān)于Memcache的信息肮疗,請參見這里晶姊。
提示:Memcache中的數(shù)據(jù)不是持久化的扒接,當(dāng)你重啟Memcache后,存儲在Memcache中的數(shù)據(jù)將不再存在们衙。所有你的應(yīng)用程序必須能夠在緩存數(shù)據(jù)為空的時候钾怔,重建緩存。換句話說蒙挑,你的應(yīng)用程序不應(yīng)該依賴于數(shù)據(jù)的存在宗侦,特別是在云環(huán)境中。當(dāng)然你可以使用 Redis 來替代Memcache忆蚀。
Memcache為你提供了一個簡單而強(qiáng)大的機(jī)制來創(chuàng)建緩存矾利。如果你還想想創(chuàng)建更加高級的高速緩存,使網(wǎng)站的不同部分擁有不同的TTL馋袜,例如:你可能希望網(wǎng)頁標(biāo)題緩存兩個小時男旗,側(cè)邊欄緩存十分鐘,這種情況下欣鳖,你可以使用Varnish察皇。
Varnish 是緩存和HTTP反向代理的混合。有些人把它稱為 HTTP 加速器。Varnish 非常的靈活什荣,且具有高可定制性矾缓。目前主流的PHP框架,如:Symfony2稻爬,已經(jīng)集成了Varnish嗜闻。
回顧一下,緩存可以幫助我們解決三個問題:
- 降低對CPU和內(nèi)存的使用
- 提高頁面的加載時間
- 利于搜索引擎優(yōu)化(谷歌Analytics認(rèn)為任何網(wǎng)頁加載時間超過1.5秒都屬于慢網(wǎng)頁桅锄,慢網(wǎng)頁對于SEO有著不少的弊端)泞辐。
2. 請密切關(guān)注循環(huán)
我們總是習(xí)慣性的使用循環(huán),它們是個強(qiáng)大的編程工具竞滓,但是往往循環(huán)會造成性能瓶頸咐吼。執(zhí)行一個慢操作本身就是一個問題,但是如果這個慢操作在循環(huán)中執(zhí)行商佑,就會將問題放大锯茄。那么,循環(huán)到底好不好呢茶没?循環(huán)當(dāng)然是個好東西肌幽。就好比菜刀,用于切菜是個很好的東西抓半,但是用來傷人喂急,就不對了。所以需要將循環(huán)利用好笛求,且需要仔細(xì)評估你的循環(huán)廊移,特別是在嵌套循環(huán)中,防止出現(xiàn)問題探入。
以下面的代碼為例:
// 錯誤使用循環(huán)的例子
function expexiveOperation() {
sleep(1);
return "Hello";
}
for ($i = 0; $i < 100; $i ++) {
$value = expexiveOperation();
echo $value;
}
上面代碼的問題很明顯狡孔,每循環(huán)一次都設(shè)置相同的變量,做了很多的無用功蜂嗽,下面我們優(yōu)化下上面的代碼:
// 正確的使用案例
function expexiveOperation() {
sleep(1);
return "Hello";
}
$value = expexiveOperation();
for ($i = 0; $i < 100; $i ++) {
echo $value;
}
這段代碼輸出的內(nèi)容和上一段代碼完全一致苗膝,但是這里就不需要每次循環(huán)都去調(diào)用慢操作方法,很大程度上的提高了代碼的執(zhí)行效率植旧。
但是辱揭,上面給的案例很簡單,所以你能給很容易的定位到問題的所在病附。在現(xiàn)實的開發(fā)中问窃,往往沒有這么簡單。為了檢測性能問題胖喳,你需要考慮如下幾點:
- 檢測大循環(huán)(for, foreach, ...)
- 它們是否會大量的遍歷數(shù)據(jù)
- 對他們的執(zhí)行速度進(jìn)行測量
- 是否能夠利用緩存
- 如果是的話泡躯,那你還在等啥?
- 如果不能,將它們標(biāo)記為可能存在危險较剃,并專注于它們的檢查咕别。因為它們可能會無限放大你的小問題。
基本上写穴,你必須清楚的知道惰拱,你寫這個循環(huán)是為什么。你很難記住程序的所有代碼啊送,但是你必須意識到偿短,循環(huán)往往需要昂貴的性能。有時候我需要對以前的代碼進(jìn)行重構(gòu)和優(yōu)化馋没,我往往是先用剖析器查找循環(huán)并重構(gòu)可優(yōu)化的昔逗。
我們可以使用性能分析工具幫助我們完成這個工作。Xdebug 和 Zend Debugger 允許我們創(chuàng)建概要分析報告篷朵。如果我們選擇Xdebug勾怒,我們可以使用Webgrind,它可以幫助我們檢查瓶頸声旺。請記住笔链,一個瓶頸是一個問題,而一個瓶頸迭代10000次是將問題放大10000倍腮猖。
3. 使用隊列
我們真的需要執(zhí)行用戶請求中的所有任務(wù)嗎鉴扫?有時候是必要的,但并非總是如此澈缺。想象一下坪创,例如,你需要在用戶提交一個操作時發(fā)送一個電子郵件給用戶谍椅,你可以使用簡單的php腳本發(fā)送此郵件误堡,但這個操作可能需要一秒鐘古话。如果你等到腳本執(zhí)行完最后一句雏吭,你可以確保郵件已經(jīng)發(fā)送成功。但是我們真的有必要等待這一秒鐘呢陪踩?你可以使用一個隊列杖们,將操作放到隊列中,而不需要在此等待一秒肩狂。郵件稍后將被發(fā)送摘完,用戶不需要等到發(fā)送成功。
Gearman是一個框架傻谁,允許你創(chuàng)建隊列和并行處理任務(wù)孝治,你可以閱讀Gearman文檔來獲得更多關(guān)于Gearman的信息。Gearman的主要思想很簡單,你可以定義主角本調(diào)用Worker
谈飒,而不是在主腳本中執(zhí)行操作岂座。
下面是一個Gearman的簡單案例:
$filename = '/path/to/img.jpg';
if (realpath(__FILE__) == realpath($filename)) {
exit();
}
$stringSize = 3;
$footerSize = ($stringSize == 1) ? 12 : 15;
$footer = date('d/m/Y H:i:s');
list($width, $heigth, $image_type) = getimagesize($filename);
$im = imagecreatefromjpeg($filename);
imagefilledrectangle(
$im,
0,
$height,
$width,
$height - $footerSize,
imagecolorallocate($im, 49, 49, 156)
);
imagestring(
$im,
$stringSize,
$width - (imagefontwidth($stringSize) * strlen($footer)) -2,
$height - $footerSize,
$footer,
imagecolorallocate($im, 255, 255, 255);
);
header('Content-Type: image/jpeg');
下面代碼將上面的操作重寫為為Gearman的Worker
$gmw = new GreamanWorker();
$gmw->addServer();
$gmw->addFunction('watermark', function ($job) {
$workload = $job->workload();
$workload_size = $job->workloadSize();
list($filename, $footer) = json_encode($workload);
$footerSize = 15;
list($width, $height, $image_type) = getimagesize($filename);
$im = imagecreateformjpeg($filename);
imagefilledrectangle(
$im,
0,
$height,
$width,
$height - $footerSize,
imagecolorallocate($im, 49, 49, 156)
);
imagestring(
$stringSize,
$width - (imagefontwidth($stringSize) * strlen($footer)) - 2,
$height->$footerSize,
$footer,
imagecolorallocate($im, 255, 255, 255)
);
ob_start();
ob_implicit_flush(0);
imagepng($im);
$image = ob_get_content();
ob_end_clean();
return $img;
});
while(1) {
$gmw->work();
}
在客戶端腳本上調(diào)用:
$filename = '/path/to/img.jpg';
$footer = date('d/m/Y H:i:s');
$gmclient = new GearmanClient();
$gmclient->addServer();
$handle = $gmclient->do('watermark',json_encode([$filename, $footer]));
if ($gmclient->requestOpc() != GEARMAN_SUCCESS){
echo "Ups someting wrong happen";
} else {
headr('Content-Type: image/jpeg');
echo $handle;
}
關(guān)于Gearman最酷的事情,就是可以平行的增加Worker杭措,而不需要對客戶端代碼進(jìn)行修改费什。這樣當(dāng)用戶量上升后,你只需要多布置幾個Gearman節(jié)點就好了手素。很簡單吧
可能使用Gearman的一些場景:
- 海量郵件系統(tǒng)
- 生成PDF
- 圖像處理
- Logs
- ...
Gearman在web應(yīng)用程序中廣泛被使用鸳址,例如Grooveshark和Instagram就使用了Gearman。Instagram大概有200多個使用Python編寫的Worker泉懦。也就是說稿黍,它是語言無關(guān)的。你可以用任何語言來編寫崩哩。
其他隊列系統(tǒng)還有ZeroMQ
闻察、RabbitMQ
等。
4. 謹(jǐn)慎的訪問數(shù)據(jù)庫
一般在海量數(shù)據(jù)的時候琢锋,數(shù)據(jù)庫往往都是一個大的性能問題來源辕漂。數(shù)據(jù)庫的連接是昂貴的操作,特別是對于PHP這種缺少連接池的語言來說吴超。
此外钉嘹,一個簡單的查詢是否使用索引的差異也是令人難以置信的。強(qiáng)烈建議你檢查數(shù)據(jù)庫索引鲸阻,因為使用錯誤的索引的SQL查詢會大幅的降低程序的性能跋涣。
對索引的檢查不能只檢查一次,因為隨著數(shù)據(jù)的增長鸟悴,索引可能會有所改變陈辱。
另外一個建議是,使用預(yù)處理語句细诸,為什么沛贪?讓我們從例子中看看吧:
$dbh = new PDO('pgsql:dbname=pg1;host=localhost', 'user', 'password');
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$field1 = uniqid();
$dbh->beginTransaction();
foreach (range(1, 5000, 1) as $i) {
$stmt = $dbh->prepare("UPDATE test.tbl1 set field1 = '{$field1}' where id = 1");
$field1 = $i;
$stmt->execute();
}
$dbh->commit();
另外一個:
$dbh = new PDO('pgsql:dbname=pg1;host=localhost', 'user', 'password');
$dbh->setAttribute(PDO_ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$field1 = uniqid();
$dbh->beginTransaction();
$stmt = $dbh->prepare('UPDATE test.tbl1 set field1 = :F1 where id = 1');
foreach (range(1, 5000, 1) as $i) {
$field1 = $i;
$stmt->execute(array('F1' => $field1));
}
$dbh->commit();
第一個例子在循環(huán)中,使用了一個新的SQL語句震贵,并執(zhí)行5000次利赋, 數(shù)據(jù)庫需要解析每條SQL并執(zhí)行它。第二個例子中猩系,使用預(yù)處理語句媚送,只是在循環(huán)中,綁定了5000次不同的參數(shù)而已寇甸,而不需要把SQL解析5000次塘偎。而且疗涉,使用預(yù)處理語句,可以有效的防范SQL注入吟秩。
5. 大流量
如果你的應(yīng)用瞬間增加了數(shù)以萬計的并發(fā)甘改,會發(fā)生什么侨颈?你的服務(wù)器能夠處理好這些并發(fā)嗎?這個問題并不容易回答。所以在開發(fā)過程中芽腾,就應(yīng)該模擬大并發(fā)對程序進(jìn)行壓力測試究孕。
類似的測試用具有不少逊抡,我平時用的是Apache AB來對應(yīng)用進(jìn)行性能測試火窒。
Apache AB的使用非常簡單熏矿,我們看看它的基本操作:
ab -n 100 -c http://www.baidu.com/
執(zhí)行上面的命令會直接在終端中打印出結(jié)果,當(dāng)然褪储,你也可以結(jié)果輸出到文件中:
ab -n 100 -c 10 -e test.csv http://www.baidu.com
總結(jié)
如果你想要提高你的WEB性能鲤竹,你需要回答下面這些問題:
- 我的應(yīng)用程序有多少個數(shù)據(jù)庫連接辛藻?
- 每個select語句花費(fèi)多少時間互订?
- 應(yīng)用程序有多少個select語句仰禽?
- 它們是在循環(huán)內(nèi)嗎坟瓢?
- 是否真的需要每次都執(zhí)行它們,是否可以將它們放入緩存中?
- 是否真的有必要在主線程中執(zhí)行用戶的所有請求识颊?
- 可以將它們放入隊列中嗎奕坟?
- 我的服務(wù)器是否支持大負(fù)載和大并發(fā)月杉?
- 每個請求使用多少CPU抠艾?
- 每個請求使用多少內(nèi)存?
正如你所看到的腌歉,有很多你必須回答的問題齐苛,獲取你開始閱讀這篇文章尋找完美的解決方案凹蜂。但是很抱歉玛痊,沒有什么靈丹妙藥。你必須根據(jù)你的情況來回答上面的問題吟吝,并作出相應(yīng)的調(diào)整剑逃。
還有最后一點蛹磺,對前端的優(yōu)化也不可忽視萤捆,畢竟每個請求俗批,不是只有后端消耗了時間。響應(yīng)時間 = 后端 + 前端岁忘。
Collin
http://ghost.icosplay.cc/