[轉載]楊建:網站加速--動態應用篇 (上)

--提高性能的同時爲你節約10倍以上成本
From: http://blog.sina.com.cn/iyangjian

一, 引子
二,整體結構圖
三,系統結構綜述
四,環境配置以及底層基礎類庫
五, Memcache & Mysql 經常使用場景案例
六,更多待續 ......
-----------------------------------------------------------------------------------------

一, 引子

張三丰當初傳授傳授張無忌太極劍法的時候,剛傳授完,就問是否已經忘記。直到張無忌說招式已經忘光了,纔算學會。當時很不理解,如今終於明白了,招隨心出,不該受到固定招式的限制。總有人問我什麼樣的架構是否是就好,或者某個頗有名的網站用什麼架構,咱們就要用嗎?甚至以爲直接拿個別人的配置文件或參數,性能就會日新月異。那麼請問你知作別人當時爲何那麼作嗎?作架構亦如學太極劍法,也請你學習別人的架構的時候,理解了就忘記掉,由於任何架構自己都有環境侷限性,而後用心思考,分析,真正理解你所處的環境,從實際需求出發,你才能作出最適合你應用的架構。

不少人對寫server很感興趣,甚至一開始就直接考慮從底層協議進行優化,我說遠還沒到那個程度,請找出你真正的瓶頸。優化必定是從宏觀到微觀的,有時候一個系統結構,業務邏輯,或策略的優化,帶來的效果甚至更爲可觀。對於天天請求處理量小於100億的系統,我都不建議本身去寫server。若是說寫高性能server的技能是我手中的那把太極劍的話,那麼發揮出太極劍法的威力並不依賴於那把劍,而是取決於對心法的理解。那把劍既然能夠是專用server,固然也能夠是開源的那幾款經典server,取決於你對設計容量,以及硬件成本,維護成本,時間等多方面的預期。架構自己就是一個取捨的過程。

如今有這樣的一個項目,就拿我上一篇文章裏提到的自選股來舉例吧,用戶能夠在各個市場裏建立本身的多個投資組合,而後在組合裏定製本身關注的股票。描述一下環境以及需求。
環境:註冊用戶小几千萬,同時在線預計峯值不到10萬。
需求:
1,3G,IM,Mail,Web 任何一個地方以及不一樣的主流瀏覽器更新數據,其餘地方當即可見。
2,北京,天津,上海,深圳,任何一個IDC數據更新,其餘三個IDC當即可見。
3,各IDC附近用戶,獲取股票列表的平均響應速度要控制在20ms之內。
4,IDC間專線中斷服務不能受影響。
5,IDC內部的相同功能的服務器,容許宕掉一半,服務徹底不受影響。
6,IDC容許宕掉一到兩個,受災IDC 95%的用戶服務影響不超過5分鐘。
7,另外,須要開發人員可以在這個系統上快速開發,要具有易用性,良好擴展性以及移植性。

基於以上的需求構建的實現,動態應用篇側重點主要是如何快速響應,同步更新,如何容災,消除安全隱患,讓系統更穩定,如何容易遷移和擴展應用,如何讓程序員容易使用這個平臺。這個系統性能不是主要關注的問題(10萬同時在線,確實比較微量,況且僅有的很少的壓力,也經過後面介紹的各類緩存機制轉移的差很少了),架構上採用當前主流經典架構(Nginx+PHP Fast-CGI+APC+Mysql+Memcache+LVS+Linux),各層次之間低偶合,每一層都具有單獨優化的空間, 隨着用戶量的增長,我再逐步推出動態應用處理併發和壓力方面的文章。


二,整體結構圖

[轉載]楊建:網站加速--動態應用篇 <wbr>(上)


(實際結構中去掉了中繼slave,改爲全部slave直連master,這樣結構更簡單,易於管理和故障恢復。)

三,系統結構綜述


此係統主要分爲幾個低偶合的層次組裝而成,具有多IDC分佈的特性。從底往上依次爲DB層,MC池子層,Nginx+PHP Fast-CGI層,LVS層,而後經過DNS接入用戶。每一層都具有良好的擴展性以及災備能力。php


DNS:html

從大的結構上來講,此係統分佈在4個主要IDC,網通電信各2 機器數量按照網通:電信 12的比例配置,同一運營商下的兩IDC機器數量等同。這樣在宕掉一個IDC的狀況下,能夠經過切換DNS,臨時訪問相近的IDC,達到IDC間災備。mysql


LVS:nginx

每一個IDC的接入層經過LVS做四層負載均衡。IDC內部任何接入機器宕掉,能夠經過failover機制在一分鐘內自動摘除。程序員

 

Nginx+PHP Fast-CGI:算法

經過fpm管理PHP Fast-CGI進程,Nginxunix域協議與fpm通訊。轉發 *.php的請求以及響應。使用APC做爲OP代碼加速器,加速php響應。PHP直接與MC池子或Mysql層進行交互。sql

 

MC池子層:shell

每一個IDC內部的多個MC機是做爲一個總體來管理的,也就是說,假如你有100個數據,4MC機,那麼每一個MC上只會存25個數據,而不會每一個都存100個。好處是,得到了至關於之前4倍的內存池, 增大了緩存命中率。更額外的好處是,減小了,本IDC內部各MC間數據同步的時間開銷,使得本IDC任何一服務器更新的數據,同IDC內其餘服務器當即可見。也使得IDC之間MC的同步更爲節約,每一個IDC一個池子,每一個池子只須要寫一份數據。編程

 

MC池子增減機器的問題:若是是傳統的hash算法根據key值,將數據平均的hash4臺機器上,那麼按照新的hash規則,增長一臺機器後,將會有近80%的緩存失效,形成大量數據遷移。因此咱們改用Consistent Hashing,首先求出memcached服務器(節點)的哈希值,並將其配置到0232次方的圓上。而後用一樣的方法求出存儲數據的鍵的哈希值,並映射到圓上。而後從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。若是超過232次方仍然找不到服務器,就會保存到第一臺memcached服務器上。它能最大限度地抑制鍵的從新分佈。並且,有的實現還採用了虛擬節點的思想,使得分佈更加均勻。由服務器臺數(m)和增長的服務器臺數(n)計算增長服務器後的命中率計算公式以下:(1 - n/(n+m) ) * 100  ,按這個計算,增長一臺機器後還將獲得80%的命中率。更較幸運的是這個算法已經被php所支持,能夠經過php.ini設置瀏覽器

 

宕機重啓後初始化的問題:若是初始化的數據只從一臺DB上得到,那麼在高峯期間必將壓垮DB,因此,咱們須要將這個壓力,分散到本IDC的多臺DB上。細節會在基礎類庫中進行講述。

 

異地MC池子的數據同步:已經封裝在基礎類庫中,更新MC時指定特定的參數,就會在其餘異地MC池子上也進行更新。因此咱們若是在北京添了一個數據,那麼在深圳也會馬上看到。


DB層:

主從結構集羣,主庫可切換,從庫互爲備份。從DB宕掉一臺,能夠自動跳過,不影響服務,目前經過基礎類庫實現,之後會採用內部DNS全部從庫直接和主庫相連,結構簡單,方便管理。不過寫的數據量相對比較小的,並且咱們容許DB延遲(由MC來保證明時性,或者異地原本就能夠接受短暫的延遲),這裏將不是問題。

 

 

MCDB寫隊列:

在專線中斷時不影響寫操做(讀都是本地的,固然也不受影響),並且在更新數據時可以快速返回(由於不須要遠程通訊)。

具體來講就是將MC的異地寫,以及DB的寫操做封裝成直接追加寫本地隊列文件。每一個機器上有個守護進程,每0.1秒檢查一次,有則rename,而後開始將隊列數據逐條往深圳數據中心post,同時每成功一條記錄當前offset,以便意外宕掉後接着上次的後面處理。(優化點的能夠改成批量確認,冒進點的,可使用內存盤優化寫速度)。若是線路不通,或者數據中心寫失敗,post程序將sleep and retry,線路恢復則繼續,不會丟數據。這裏有個細節須要注意,就是往數據中心同步的時候,儘可能發往同一臺機器,也即綁定,以保證序列的順序,這個保證能夠經過對post守護進程的pid取mod來實現,若是綁定的機器出了故障,則應該選定另一臺機器綁定,一旦檢測到之前的機器好了,則應該切回來,以保證應有的負載均衡。另外,最外層的接入端也一樣須要注意這個問題,咱們對lvs作了會話保持,以確保同一用戶在至關長的一段時間內,會訪問到同一臺機器。對於僅僅使用DNS輪詢,又沒有使用長鏈接的服務,若是在短期內作數據更新,確定會出現亂序的問題。除非你各服務器時間都很精準,並且,每一個記錄寫的時候打上精確到ms的時間戳。


而後,深圳數據中心接收到post的數據後,根據消息類型按必定規則的命名分別存放,這樣即便增長一種新的消息,也能夠方便的使用隊列。好比收到MC的消息後,要寫三份,分別發往三個IDC,各自不受影響。DB,能夠根據不一樣的port實例子來建立隊列,省得有一個port宕了,影響其餘。具體實現細節能夠本身調整,一樣得有個程序按期(好比每0.1秒,0.01秒也無所謂,對cpu的消耗很是小)檢查隊列文件存在就rename,而後往master裏寫。


以上則能保證,DB的master宕掉,或者專線的中斷狀況下,用戶服務基本不受影響,只是異地的同步會延遲一些。額外收穫是未來作DB升級調整的時候,能夠用隊列分流,分別作不一樣的處理。

 

祛除安全隱患:
無論你的系統設計的多麼完美,疏忽這一條足以至命。
以上設計看起來彷佛已經沒什麼問題了,各類容災,異常也基本考慮到了,不幸的是,這個平臺並非僅僅給設計者本身使用,若是有個新手使用了 file_get_contents($url);  若是url所依賴的服務器負載太重,那麼整個系統都有被拖垮的危險。動態應用的一個鐵律就是,凡是依賴本系統外部資源的地方必須加超時限制,儘量的減小依賴。file_get_contents的超時不是很精確,推薦使用curl的庫進行封裝,能夠設置connect超時,也能夠設置整個函數的執行時間超時。我通常會設置my_curl();函數的最大默認執行時間爲一秒,由於是從內網拉取數據,一秒還拉不到,確定有問題。另外,別忘記了mysql讀服務,當某一idc的一臺slave鏈接數滿了之後,若是沒設置過mysql.connect_timeout=1;那這個IDC的服務會整個被拖垮,由於默認的是60秒。(mysql的寫由隊列保證,不存在此問題)。

另外,咱們項目還有個特殊性,即,每次拉取用戶股票信息時,須要從後臺專線到深圳進行身份合法性驗證,因爲歷史的緣由,這個驗證系統暫不具有多IDC分別的能力。假設北京的專線斷了,雖然用戶的數據是沒問題,可是驗證卻通不過,會致使獲取數據失敗。一樣這個驗證是要加超時的,另外爲了在專線斷掉的狀況下依然正常提供服務,就須要在得不到驗證的狀況下,要經過代理的手段到鄰近的IDC去驗證。同時,咱們也支持另一種驗證方式,對於已經經過驗證的用戶,專線中斷一小段時間,好比一小時,服務是能夠不受影響。

總之就是儘可能低偶合,少依賴,加超時。另一個維護的安全,則由安全中心把關,我就很少說了。


,環境配置以及底層基礎類庫

 

PHP網絡版環境:IDC之間差別化的Nginx環境變量配置,使得相同的應用程序在不一樣的IDC運行時,使用各自IDC內部的MC以及DB等資源。


 

PHP  Shell版環境:

PHP  Shell是指經過命令行方式執行的php腳本程序。

好比/path/php/bin/php test.php

或在php程序第一行加上  

#!/path/php/bin/php 

而後賦予php腳本可執行權限,使做爲shell程序運行。

因爲 php 做爲 shell 運行時,沒法繼承 nginx 配置的環境變量。因此它必須依賴一個獨立的配置文件。

 


因爲圖片裏含有賬戶等敏感信息,就不在此貼了。

底層基礎類庫:

底層基礎類庫,起到粘合劑的做用,將環境配置,服務器資源等所有結合起來,使得這些資源以及配置信息對上層開發人員透明,無須考慮。總的來講有如下一些功能。

 

1,  兩環境融合,完美無缺。 php 網絡環境和shell使用同一基礎類庫,代碼無任何一行差別。使得平時編寫的php網絡程序,以及類庫積累,能夠方便的直接用來作shell編程,進行復用。具體原理是,類庫須要用到配置信息時,先經過if( isset ($_ENV["SERVER_SOFTWARE"]) )變量判斷本身是否網絡環境,若是是就直接使用配置項好比:_SERVER["DB_stock_host"] ,若不是,則先將配置文件數據項,section名和下面的字段相加轉化成 _SERVER["DB_stock_host"] = 「m3306_sz_gtimg_cn」 ; 跟網絡環境一致後,再繼續後面操做。

 

[DB_stock]

host = m3306_sz_gtimg_cn

類庫目前除支持模擬DNS外,還直接兼容真實域名以及IP地址,方便未來進行數據遷移。之因此沒有直接使用 DNS是也有歷史緣由的。

 

2,  DB資源的封裝。

 

在沒有內部DNS的狀況下,將DB讀寫分離,賬戶選擇,鏈接的創建(包括 什麼時候候真正創建鏈接,創建長鏈接,仍是短鏈接,鏈接的綁定,以及生命週期),負載均衡以及failvoer等封裝成對用戶透明的以下簡單用法:

 

//指定以讀(r) (w) 的方式打開一個庫。不指定的狀況下,默認是」r」方式打開。

$db_r =new MYSQL(testdb,r); 

 

<?

require_once (dirname(__FILE__).'/../mysql.php');

 

$db_w =new MYSQL("test","w");

 

$arr = array(

              "id" => "8",

              "name" => "yangjian8"

           );

 

if( $db_w->insert("test",$arr) )

{

       echo "query ok ...<br>";

}

else

{

       echo "query failed ...<br>";

       echo "errno=$db_w->errno<br> errmsg=$db_w->errmsg<br>";

}

 

echo "read ............<br>";

 

$db_r =new MYSQL("test","r");

$sql = "select * from test";

 

if( $result = $db_r->query($sql))

{

       while ($row = mysql_fetch_array($result, MYSQL_BOTH))

       {

              printf ("id: %s name: %s<br>", $row[id], $row[name]);

       }

       $db_r->free();  //free result. if you not free it,it will auto free at the end of the php script.

}

else   //if not success,you can print the error info.

{

       echo "errno=$db_r->errno<br> errmsg=$db_r->errmsg<br>";

}

 

3,  MC池子資源的封裝。

簡化MC池子使用方法,支持異地MC數據同步。

//if rset=1,the mc data will sync to other idcs, default 0.

function set($key, $value, $flag, $expire, $r_set=0)

 

<?php

require_once (dirname(__FILE__).'/../memcache.php');

 

$mc = new MC("test");

for($i=0;$i<2;$i++)

{

       $mc->set("key".$i,"value".$i,0,100);

}

 

for($i=0;$i<2;$i++)

{

       echo $mc->get("key".$i);

       echo "<br>";

}

 

DB以及MC的異地寫已經封裝進去,對上層開發人員來講都是透明的。

 

 

注意:我在引用頭文件時候,都會使用相似require_once (dirname(__FILE__).'/../mysql.php'); 的方法。我暫且管它叫動態絕對路徑。它的好處是,

跟相對路徑比:當一個頭文件被多層引用時,目錄結構又不一致,不會找不到。

也不會去搜索全部可能的目錄,執行多餘的fstat以及open操做。

跟絕對路徑相比:若是我將整個項目,包括頭文件所有平移到別的目錄,不須要挨個修改文件。

固然,你也能夠採用另一個方法,將本項目相關的配置信息絕對路徑放在一個統一的文件裏,而後經過任何一種方法引用那個文件。

相關文章
相關標籤/搜索