可伸縮的系統(tǒng)架構(gòu)經(jīng)驗分享
最近,閱讀了Will Larson的文章,感覺很有價值。作者分享了他在Yahoo!與Digg收獲的設(shè)計可伸縮系統(tǒng)的架構(gòu)經(jīng)驗。在我過往的架構(gòu)經(jīng)驗中,由于主要參與開發(fā)企業(yè)軟件系統(tǒng),這種面向企業(yè)內(nèi)部的軟件系統(tǒng)通常不會有太大的負載量,太多的并發(fā)量,因而對于系統(tǒng)的可伸縮性考慮較少。大體而言,只要在系統(tǒng)部署上考慮集群以及負載均衡即可。本文給了我很多啟發(fā),現(xiàn)把本文的主要內(nèi)容摘譯出來,并結(jié)合自己對此的理解。
Larson首先認為,一個理想的系統(tǒng),對于容量(Capacity)的增長應該與添加的硬件數(shù)是線性的關(guān)系。換言之,如果系統(tǒng)只有一臺服務器,在增加了另一臺同樣的機器后,容量應該翻倍。以此類推。這種線性的容量伸縮方式,通常被稱之為水平伸縮“Horizontal Scalability”。
在設(shè)計一個健壯的系統(tǒng)時,自然必須首要考慮失敗的情況。Larson認為,一個理想的系統(tǒng)是當失去其中一臺服務器的時候,系統(tǒng)不會崩潰。當然,對應而言,失去一臺服務器也會導致容量的響應線性減少。這種情況通常被稱為冗余“Redundancy”。
負載均衡
無論是水平伸縮還是冗余,都可以通過負載均衡來實現(xiàn)。負載均衡就好似一個協(xié)調(diào)請求的調(diào)停者,它會根據(jù)集群中機器的當前負載,合理的分配發(fā)往Web服務器的請求,以達到有效利用集群中各臺機器資源的目的。顯然,這種均衡器應該介于客戶端與Web服務器之間,如下圖所示:
本文提到了實現(xiàn)負載均衡的幾種方法。其一是Smart Client,即將負載均衡的功能添加到數(shù)據(jù)庫(以及緩存或服務)的客戶端中。這是一種通過軟件來實現(xiàn)負載均衡的方式,它的缺點是方案會比較復雜,不夠健壯,也很難被重用(因為協(xié)調(diào)請求的邏輯會混雜在業(yè)務系統(tǒng)中)。對此,Larson在文章以排比的方式連續(xù)提出問題,以強化自己對此方案的不認可態(tài)度:
Is it attractive because it is the simplest solution? Usually, no. Is it seductive because it is the most robust? Sadly, no. Is it alluring because it’ll be easy to reuse? Tragically, no.
第二種方式是采用硬件負載均衡器,例如Citrix NetScaler。不過,購買硬件的費用不菲,通常是一些大型公司才會考慮此方案。
如果既不愿意承受Smart Client的痛苦,又不希望花費太多費用去購買硬件,那就可以采用一種混合(Hybird)的方式,稱之為軟件負載均衡器(Software Load Balancer)。Larson提到了HAProxy。它會運行在本地,需要負載均衡的服務都會在本地中得到均衡和協(xié)調(diào)。
緩存
為了減輕服務器的負載,還需要引入緩存。文章給出了常見的對緩存的分類,分別包括:預先計算結(jié)果(precalculating result,例如針對相關(guān)邏輯的前一天的訪問量)、預先生成昂貴的索引(pre-generating expensive indexes,例如用戶點擊歷史的推薦)以及在更快的后端存儲頻繁訪問的數(shù)據(jù)的副本(例如Memcached)。
應用緩存
提供緩存的方式可以分為應用緩存和數(shù)據(jù)庫緩存。此二者各擅勝場。應用緩存通常需要將處理緩存的代碼顯式地集成到應用代碼中。這就有點像使用代理模式來為真實對象提供緩存。首先檢查緩存中是否有需要的數(shù)據(jù),如果有,就從緩存直接返回,否則再查詢數(shù)據(jù)庫。至于哪些值需要放到緩存中呢?有諸多算法,例如根據(jù)最近訪問的,或者根據(jù)訪問頻率。使用Memcached的代碼如下所示:
- key = "user.%s" % user_id
- user_blob = memcache.get(key)
- if user_blob is None:
- user = mysql.query("SELECT * FROM users WHERE user_id=\"%s\"", user_id)
- if user:
- memcache.set(key, json.dumps(user))
- return user
- else:
- return json.loads(user_blob)
數(shù)據(jù)庫緩存
數(shù)據(jù)庫緩存對于應用代碼沒有污染,一些天才的DBA甚至可以在不修改任何代碼的情況下,通過數(shù)據(jù)庫調(diào)優(yōu)來改進系統(tǒng)性能。例如通過配置Cassandra行緩存。
內(nèi)存緩存
為了提高性能,緩存通常是存儲在內(nèi)存中。常見的內(nèi)存緩存包括Memcached和Redis。不過采用這種方式仍然需要合理的權(quán)衡。我們不可能一股腦兒的將所有數(shù)據(jù)都存放在內(nèi)存中,雖然這會極大地改善性能,但比較起磁盤存儲而言,RAM的代價更昂貴,同時還會影響系統(tǒng)的健壯性,因為內(nèi)存中的數(shù)據(jù)沒有持久化,容易丟失。正如之前提到的,我們應該將需要的數(shù)據(jù)放入緩存,通常的算法是least recently used,即LRU。
CDN
提高性能,降低Web服務器負載的另一種常見做法是將靜態(tài)媒體放入CDN(Content Distribution Network)中。如下圖所示:
CDN可以有效地分擔Web服務器的壓力,使得應用服務器可以專心致志地處理動態(tài)頁面;同時,CDN還可以通過地理分布來提高響應請求的性能。在設(shè)置了CDN后,當系統(tǒng)接收到請求時,首先會詢問CDN以獲得請求中需要的靜態(tài)媒體(通常會通過HTTP Header來配置CDN能夠緩存的內(nèi)容)。如果請求的內(nèi)容不可用,CDN會查詢服務器以獲得該文件,并在CDN本地進行緩存,最后再提供給請求者。如果當前網(wǎng)站并不大,引入CDN的效果不明顯時,可以考慮暫不使用CDN,在將來可以通過使用一些輕量級的HTTP服務器如Nginx,為靜態(tài)媒體分出專門的子域名如static.domain.com來提供服務。
緩存失效
引入緩存所帶來的問題是如何保證真實數(shù)據(jù)與緩存數(shù)據(jù)之間的一致性。這一問題通常被稱之為緩存失效(Cache Invalidation)。從高屋建瓴的角度來講,解決這一問題的辦法無非即使更新緩存中的數(shù)據(jù)。一種做法是直接將新值寫入緩存中(通常被稱為write-through cache);另一種做法是簡單地刪除緩存中的值,在等到下一次讀緩存值的時候再生成。
整體而言,要避免緩存實效,可以依賴于數(shù)據(jù)庫緩存,或者為緩存數(shù)據(jù)添加有效期,又或者在實現(xiàn)應用程序邏輯時,盡量考慮避免此問題。例如不直接使用DELETE FROM a WHERE…來刪除數(shù)據(jù),而是先查詢符合條件的數(shù)據(jù),再使得緩存中對應的數(shù)據(jù)失效,繼而根據(jù)其主鍵顯式地刪除這些行。
Off-Line處理
這篇文章還提到了Off-Line的處理方式,即通過引入消息隊列的方式來處理請求。事實上,在大多數(shù)企業(yè)軟件系統(tǒng)中,這種方式也是較為常見的做法。在我撰寫的文章《案例分析:基于消息的分布式架構(gòu)》中,較為詳細地介紹了這種架構(gòu)。在引入消息隊列后,Web服務器會充當消息的發(fā)布者,而在消息隊列的另一端可以根據(jù)需要提供消費者Consumer。如下圖所示。對于Off-Line的任務是否執(zhí)行完畢,通??梢酝ㄟ^輪詢或回調(diào)的方式來獲知。
為了更好地提高代碼可讀性,可以在公開的接口定義中明確地標示該任務是On-Line還是Off-Line。
引入Message Queue,可以極大地緩解Web服務器的壓力,因為它可以將耗時較長的任務轉(zhuǎn)到專門的機器上去執(zhí)行。
此外,通過引入定時任務,也可以有效地利用Web服務器的空閑時間來處理后臺任務。例如,通過Spring Batch Job來執(zhí)行每日、每周或者每月的定時任務。如果需要多臺機器去執(zhí)行這些定時任務,可以引入Spring提供的Puppet來管理這些服務器。Puppet提供了可讀性強的聲明性語言來完成對機器的配置。
Map-Reduce
對于大數(shù)據(jù)的處理,自然可以引入Map-Reduce。為整個系統(tǒng)專門引入一個Map-Reduce層來處理數(shù)據(jù)是有必要的。相對于使用SQL數(shù)據(jù)庫作為數(shù)據(jù)中心的方式,Map-Reduce對可伸縮性的支持更好。Map-Reduce可以與任務的定時機制結(jié)合起來。如下圖所示:
平臺層
Larson認為,大多數(shù)系統(tǒng)都是Web應用直接與數(shù)據(jù)庫通信,但如果能加入一個平臺層(Platform Layer),或許會更好。
首先,將平臺與Web應用分離,使得它們可以獨立地進行伸縮。例如需要添加一個新的API,就可以添加新的平臺服務器,而無需增加Web服務器。要知道,在這樣一個獨立的物理分層架構(gòu)中,不同層次對服務器的要求是不一樣的。例如,對于數(shù)據(jù)庫服務器而言,由于需要頻繁地對磁盤進行I/O操作,因此應保證數(shù)據(jù)庫服務器的IO性能,如盡量使用固態(tài)硬盤。而對于Web服務器而言,則對CPU的要求比較高,盡可能采用多核CPU。
其次,增加一個額外的平臺層,可以有效地提高系統(tǒng)的可重用性。例如我們可以將一些與系統(tǒng)共有特性以及橫切關(guān)注點的內(nèi)容(如對緩存的支持,對數(shù)據(jù)庫的訪問等功能)抽取到平臺層中,作為整個系統(tǒng)的基礎(chǔ)設(shè)施(Infrastructure)。尤其對于產(chǎn)品線系統(tǒng)而言,這種架構(gòu)可以更好地為多產(chǎn)品提供服務。
最后,這種架構(gòu)也可能對跨團隊開發(fā)帶來好處。平臺可以抽離出一些與產(chǎn)品無關(guān)的接口,從而隱藏其具體實現(xiàn)的細節(jié)。如果劃分合理,并能設(shè)計出相對穩(wěn)定的接口,就可以使得各個團隊可以并行開發(fā)。例如可以專門成立平臺團隊,致力于對平臺的實現(xiàn)以及優(yōu)化。