或許是這5件事導(dǎo)致你的web性能低下

我們都知道服務(wù)器的負(fù)載能力的重要性,本文從5個可能影響負(fù)載能力的點上進行討論。

首先,有必要了解提高服務(wù)端PHP代碼效率所需的關(guān)鍵操作。

最重要的是對性能數(shù)據(jù)的收集,如果你想對某個地方進行優(yōu)化,那么你需要測量優(yōu)化前后的數(shù)據(jù)以進行對比。一般來說,程序的響應(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或者強大的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為你提供了一個簡單而強大的機制來創(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),它們是個強大的編程工具,但是往往循環(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í)行速度進行測量
  • 是否能夠利用緩存
    • 如果是的話,那你還在等啥?
    • 如果不能,將它們標(biāo)記為可能存在危險,并專注于它們的檢查。因為它們可能會無限放大你的小問題。

基本上,你必須清楚的知道,你寫這個循環(huán)是為什么。你很難記住程序的所有代碼,但是你必須意識到,循環(huá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,而不需要對客戶端代碼進行修改。這樣當(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這種缺少連接池的語言來說。

此外,一個簡單的查詢是否使用索引的差異也是令人難以置信的。強烈建議你檢查數(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ā)對程序進行壓力測試。

類似的測試用具有不少,我平時用的是Apache AB來對應(yīng)用進行性能測試。

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語句花費多少時間?
  • 應(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、memcache的概念? Memcache是一個高性能的分布式的內(nèi)存對象緩存系統(tǒng),通過在內(nèi)存里維護一個統(tǒng)一的巨...
    桖辶殤閱讀 2,361評論 2 12
  • 一、MemCache簡介 session MemCache是一個自由、源碼開放、高性能、分布式的分布式內(nèi)存對象緩存...
    李偉銘MIng閱讀 4,013評論 2 13
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,233評論 25 708
  • 這里要變動了 違章拆建 地鐵通車 周圍的花花草草 都將被鏟除 幾近失業(yè)的我 站在十字街頭 何去何從 每個路口都有一...
    yhbmoren閱讀 250評論 0 0
  • 誤導(dǎo)孩子一生的20個壞習(xí)慣, 真后悔沒早看到! 榜樣的力量是無窮的,當(dāng)孩子還不理解真正意義上的對和錯的時候,他只能...
    碧海清天閱讀 188評論 0 0

友情鏈接更多精彩內(nèi)容