【編者按】在公司的發展中,保證服務器的可擴展性對於擴大企業的市場須要具備重要做用,所以,這對架構師提出了必定的要求。Octivi聯合創始人兼軟件架構師Antoni Orfin將向你介紹一個很是簡單的架構,使用HAProxy、PHP、Redis和MySQL就能支撐每週10億請求。同時,你還能瞭解項目將來的橫向擴展途徑及常見的模式。前端
如下爲譯文:web
在這篇文章中,我將展現一個很是簡單的架構,使用HAProxy、PHP、Redis和MySQL支撐每週10億請求。除此以外,我還將展現項目將來的橫向擴展途徑及常見的模式,下面咱們一塊兒看細節。redis
狀態:數據庫
服務器緩存
3個應用程序節點服務器
2個MySQL+1個備份網絡
2個Redis架構
應用程序負載均衡
應用程序每週處理10億請求框架
峯值700請求每秒的單Symfony2實例(平均工做日約550請求每秒)
平均響應時間30毫秒
Varnish,每秒請求超過1.2萬次(壓力測試過程當中得到)
數據存儲
Redis儲存了1.6億記錄,數據體積大約100GB,同時它是咱們的主要數據存儲
MySQL儲存了3億記錄,數據體積大約300GB,一般狀況下它做爲三級緩存層
平臺:
監視:
Icinga
Collectd
應用程序
HAProxy + Keepalived
Varnish
PHP(PHP-FPM)+ Symfony2 Framework
數據存儲
MySQL(主從配置),使用HAProxy作負載均衡
Redis (主從配置)
大約1年前,一個朋友找到我並提出了一個苛刻的要求:它們是一個飛速發展的電子商務初創公司,而當時已經準備向國際發展。介於那個時候他們仍然是一個創業公司,初始解決方案必須符合所謂的成本效益,所以也就沒法在服務器上投入更多的資金。遺留系統使用了標準的LAMP堆棧,所以他們擁有一個強力的PHP開發團隊。若是必須引入新技術的話,那麼這些技術必須足夠簡單,不會存在太多架構上的複雜性;那麼,他們當下的技術團隊就能夠對應用進行長期的維護。
爲了知足他們擴展到下一個市場的需求,架構師必須使用可擴展理念進行設計。首先,咱們審視了他們的基礎設施:
老系統使用了單模塊化設計思路,底層是一些基於PHP的Web應用程序。這個初創公司有許多所謂的前端網站,它們大多都使用了獨立的數據庫,並共享了一些支撐業務邏輯的通用代碼。絕不客氣的說,長期維護這種應用程序絕對是一個噩夢:由於隨着業務的發展,有些代碼必須被重寫,這樣的話,修改某個網站將不可避免致使業務邏輯上的不一致,這樣一來,他們不得不在全部Web應用程序上作相同的修改。
一般狀況下,這該歸結於項目管理問題,管理員必須對橫跨多個代碼庫的那些代碼負責。基於這個觀點,整改第一步就是提取核心的業務關鍵功能,並將之拆分爲獨立的服務(這也是本文的一個重點部分),也就是所謂的面向服務架構,在整個系統內遵循「separation of concern」原則。每一個服務只負責一個業務邏輯,同時也要明確更高等級的業務功能。舉個形象的例子也就是,這個系統多是個搜索引擎、一個銷售系統等。
前端網站經過REST API與服務交互,響應則基於JSON格式。爲了簡單起見,咱們選擇了SOAP,一個開發者比較無愛的協議,由於誰都不肯意解析一堆的XML。
提取一些不會常常處理的服務,好比身份驗證和會話管理。這是很是必要的一個環節,由於它們的處理等級比較高。前端網站負責這個部分,只有它們能夠識別用戶。這樣一來咱們能夠保持服務的足夠簡單,在處理擴展和代碼相關問題時都具備巨大的優點,可謂各司其職,天衣無縫。
帶來的好處:
獨立子系統(服務)能夠便捷的在不一樣團隊中開發,開發者互不干涉,效率理所固然提高。
身份驗證和會話不會經過它們來管理,所以它們形成的擴展問題不知去向。
業務邏輯被區分,不一樣的前端網站不會再存在功能冗餘。
顯著地提升了服務的可用性。
共生的缺點:
爲系統管理員帶來更大的工做量。鑑於服務都使用了獨立的基礎設施,這將給管理員帶來更多須要關注的地方。
很難保持向後兼容。在一年的維護以後,API方法中發生了數不盡的變化。所以問題發生了,它們必將破壞向後兼容,由於每一個網站的代碼均可能發生變化,還可能存在許多技術人員同時修改一個網站的狀況……然而,一年後,全部方法匹配的仍然是項目開始時創建的文檔。
着眼請求工做流,第一層是應用程序。HAProxy負載均衡器、Varnish和Symfony2應用程序都在這一層。來自前端網站的請求首先會傳遞給HAProxy,隨後負載均衡器將把他分給不一樣的節點。
應用程序節點配置
Xeon E5-1620@3.60GHz,64GB RAM,SATA
Varnish
Apache2
PHP 5.4.X(PHP-FPM),使用APC字節碼緩存
咱們購買了3個這樣的服務器,N+1冗餘配置的active-active模式,備份服務器一樣處理請求。由於性能不是首要因素,咱們爲每一個節點配置獨立的Varnish以下降緩存hit,同時也避免了單點故障(SPOF)。在這個項目中,咱們更重視可用性。由於一個前端網站服務器中使用了Apache 2,咱們保留了這個堆棧。這樣一來,管理員不會困擾於太多新加入的技術。
Symfony2應用程序
應用程序自己基於Symfony2創建,這是一個PHP全堆棧框架,提供了大量加速開發的組件。做爲基於複雜框架的典型REST服務可能受到不少人質疑,這裏爲你細說:
對 PHP/Symfony 開發者友好。客戶端IT團隊由PHP開發者組成,添加新技術將意味必須招聘新的開發者,由於業務系統必須作長時間的維護。
清晰的項目結構。 PHP/Symfony雖然歷來都不是必需品,但倒是許多項目的默認選擇。引入新的開發者將很是方便,由於對他們來講代碼很是友好。
許多現成的組件。遵循DRY思想……沒有人願意花力氣去作重複的工做,咱們也不例外。咱們使用了大量的Symfony2 Console Component,這個框架很是有利於作CLI命令,以及應用程序性能分析(debug工具欄)、記錄器等。
在選用Symfony2以前,咱們作了大量的性能測試以保證應用程序能夠支撐計劃流量。咱們制定了概念驗證,並使用JMeter執行,咱們獲得了讓人滿意的結果——每秒700請求時響應時間能夠控制在50毫秒。這些測試給了咱們足夠的信心,讓咱們堅信,即便Symfony2這樣複雜的框架也能夠獲得理想的性能。
應用程序分析與監控
咱們使用Symfony2工具來監視應用程序,在收集指定方法執行時間上表現的很是不錯,特別是那些與第三方網絡服務交互的操做。這樣一來,咱們能夠發現架構中潛在的弱點,找出應用程序中最耗時的部分。
冗長的日誌一樣是不可缺乏的一部分,咱們使用PHP Monolog庫把這些日誌處理成優雅的log-lines,便於開發者和管理員理解。這裏須要注意的是儘量多地添加細節,越詳細越好,咱們使用了不一樣的日誌等級:
Debug,可能會發生的事情。好比,請求信息在調用前會傳送給一個外部Web服務;事情發生後從API調用響應。
Error,當錯誤發生時請求流並未被終止,好比第三方API的錯誤響應。
Critical,應用程序崩潰的瞬間。
所以,你能夠清晰地瞭解Error和Critical信息。而在開發/測試環境中,Debug信息一樣被記錄。同時,日誌被存儲在不一樣的文件中,也就是Monolog庫下的「channels」。系統中有一個主日誌文件,記錄了全部應用程序級錯誤,以及各個channel的短日誌,從單獨的文件中記錄了來自各個channel的詳細日誌。
擴展性
擴展平臺的應用程序層並不困難,HAProxy性能並不會在短期耗盡,惟一須要考慮的就是如何冗餘以免單點故障。所以,當下須要作的只是添加下一個應用程序節點。
咱們使用Redis和MySQL存儲全部的數據,MySQL更多做爲三級緩存層,而Redis則是系統的主要數據存儲。
在系統設計時,咱們基於如下幾點來選擇知足計劃需求的數據庫:
在存儲大量數據時不會影響性能,大約2.5億記錄
一般狀況下可能是基於特定資源的簡單GET請求,沒有查找及複雜的SELECT操做
在單請求時儘量多的得到資源以下降延時
在通過一些調查後,咱們決定使用Redis
大部分咱們執行的操做都具備 O(1)或O(N)複雜性, N是須要檢索鍵的數量,這意味着keyspace大小並不會影響性能。
一般狀況下會使用MGET命令行同時檢索100個以上的鍵,這樣能夠儘量的避免網絡延時,而不是在循環中作多重GET操做。
咱們當下擁有兩個Redis服務器,使用主從複製模式。這兩個節點的配置相同,都是Xeon E5-2650v2@2.60GHz,128GB,SSD。內存限制被設置爲100GB,一般狀況下使用率都是100%。
在應用程序並無耗盡單個Redis服務器的全部資源時,從節點主要做做備份使用,用以保證高有效性。若是主節點宕機,咱們能夠快速的將應用程序切換到從節點。在維護和服務器遷移時,複製一樣被執行——轉換一個服務器很是簡單。
你可能會猜測當Redis資源被一直耗盡時的情景,全部的鍵都是持久化類型,大約佔90% keyspace,剩餘資源被所有被用於TTL過時緩存。當下,keyspace已經被分爲兩個部分:一個是TTL集(緩存),另外一個則是用於持久化數據。感謝「volatile-lru」最大化內存設置的可行性,最不常用緩存鍵會被移除。如此一來,系統就能夠一直保持單Redis實例同時執行兩個操做——主存儲和通用緩存。
使用這個模式必須一直監視「期滿」鍵的數量:
db.redis1:6379> info keyspace
# Keyspace
db0:keys=16XXXXXXX,expires=11XXXXXX,avg_ttl=0
「期滿」鍵數量越接近0狀況越危險,這個時候管理員就須要考慮適當的分片或者是增長內存。
咱們如何進行監控?這裏使用Icinga check,儀表盤會顯示數字是否會達到臨界點,咱們還使用了Redis來可視化「丟失鍵」的比率。
在一年後,咱們已經愛上了Redis,它從未讓咱們失望,這一年系統從未發生任何宕機狀況。
在Redis以外,咱們還使用了傳統RDBMS——MySQL。可是區別於他人,咱們一般使用它做爲三級緩存層。咱們使用MySQL存儲一些不會常用對象以下降Redis的資源使用率,所以它們被放到了硬盤上。這裏沒有什麼可說道的地方,咱們只是儘量地讓其保持簡單。咱們使用了兩個MySQL服務器,配置是Xeon E5-1620@3.60GHz,64GB RAM,SSD。兩個服務器使用本地、異步的主-主複製。此外,咱們使用一個單獨的從節點做爲備份。
MySQL的高可用性
在應用程序中,數據庫永遠是最難的瓶頸。當前,這裏還不須要考慮橫向擴展操做,咱們可能是縱向擴展Redis和MySQL服務器。當下這個策略還存在必定的發展空間,Redis運行在一個126GB內存的服務器上,擴展到256GB也並不困難。固然,這樣的服務器也存在劣勢,好比快照,又或是是簡單的啓動——Redis服務器啓動須要很長的時間。
在縱向擴展失效後進行的必然是橫向擴展,值得高興的是,項目開始時咱們就爲數據準備了一個易於分片的結構:
在Redis中,咱們爲記錄使用了4個「heavy」類型。基於數據類型,它們能夠分片到4個服務器上。咱們避免使用哈希分片,而是選擇基於記錄類型分片。這種狀況下,咱們仍然能夠運行MGET,它始終在一種類型鍵上執行。
在MySQL上,結構化的表格很是易於向另外一臺服務器上遷移——一樣基於記錄類型(表格)。固然,一旦基於記錄類型的分片再也不奏效,咱們將轉移至哈希。
不要共享你的數據庫。一旦一個前端網站指望切換會話處理到Redis,Redis緩存空間將被耗盡,同時它會拒絕應用程序保存下一個緩存鍵。這樣一來全部的緩存將轉至MySQL服務器,這將致使大量開銷。
日誌越詳細越好。若是log-lines中沒有足夠的信息,快速Debug問題定位將成爲難點。如此一來,你不得不等待一個又一個問題發生,直到找到根結所在。
架構中使用複雜的框架並不意味着低性能。許多人驚訝咱們使用全堆棧框架來支撐如此流量應用程序,其祕訣在於更聰明的使用工具,不然即便是Node.js也可能變得很慢。選擇一個提供良好開發環境的技術,沒有人指望使用一堆不友好的工具,這將下降開發團隊士氣。