Web系統搭建____單機到分佈式集羣

當一個Web系統從日訪問量10萬逐步增加到1000萬,甚至超過1億的過程當中,Web系統承受的壓力會愈來愈大,在這個過程當中,咱們會遇到不少的問題。爲了解決這些性能壓力帶來問題,咱們須要在Web系統架構層面搭建多個層次的緩存機制。在不一樣的壓力階段,咱們會遇到不一樣的問題,經過搭建不一樣的服務和架構來解決。html

Web負載均衡 

Web負載均衡(Load Balancing),簡單地說就是給咱們的服務器集羣分配「工做任務」,而採用恰當的分配方式,對於保護處於後端的Web服務器來講,很是重要。前端

 

負載均衡的策略有不少,先從簡單的講起吧redis

1. HTTP重定向sql

當用戶發來請求的時候,Web服務器經過修改HTTP響應頭中的Location標記來返回一個新的url,而後瀏覽器再繼續請求這個新url,實際上就是頁面重定向。經過重定向,來達到「負載均衡」的目標。例如,咱們在下載PHP源碼包的時候,點擊下載連接時,爲了解決不一樣國家和地域下載速度的問題,它會返回一個離咱們近的下載地址。重定向的HTTP返回碼是302,數據庫

 

這個重定向很是容易實現,而且能夠自定義各類策略。可是,它在大規模訪問量下,性能不佳。並且,給用戶的體驗也很差,實際請求發生重定向,增長了網絡延時。後端

2. 反向代理負載均衡瀏覽器

反向代理服務的核心工做主要是轉發HTTP請求,扮演了瀏覽器端和後臺Web服務器中轉的角色。由於它工做在HTTP層(應用層),也就是網絡七層結構中的第七層,所以也被稱爲「七層負載均衡」。能夠作反向代理的軟件不少,比較常見的一種是Nginx。緩存

 

Nginx是一種很是靈活的反向代理軟件,能夠自由定製化轉發策略,分配服務器流量的權重等。反向代理中,常見的一個問題,就是Web服務器存儲的session數據,由於通常負載均衡的策略都是隨機分配請求的。同一個登陸用戶的請求,沒法保證必定分配到相同的Web機器上,會致使沒法找到session的問題。服務器

解決方案主要有兩種:swoole

 

  1. 配置反向代理的轉發規則,讓同一個用戶的請求必定落到同一臺機器上(經過分析cookie),複雜的轉發規則將會消耗更多的CPU,也增長了代理服務器的負擔。
  2. 將session這類的信息,專門用某個獨立服務來存儲,例如redis/memchache,這個方案是比較推薦的。

 

反向代理服務,也是能夠開啓緩存的,若是開啓了,會增長反向代理的負擔,須要謹慎使用。這種負載均衡策略實現和部署很是簡單,並且性能表現也比較好。可是,它有「單點故障」的問題,若是掛了,會帶來不少的麻煩。並且,到了後期Web服務器繼續增長,它自己可能成爲系統的瓶頸。

3. IP負載均衡

IP負載均衡服務是工做在網絡層(修改IP)和傳輸層(修改端口,第四層),比起工做在應用層(第七層)性能要高出很是多。原理是,他是對IP層的數據包的IP地址和端口信息進行修改,達到負載均衡的目的。這種方式,也被稱爲「四層負載均衡」。常見的負載均衡方式,是LVS(Linux Virtual Server,Linux虛擬服務),經過IPVS(IP Virtual Server,IP虛擬服務)來實現。

 

在負載均衡服務器收到客戶端的IP包的時候,會修改IP包的目標IP地址或端口,而後原封不動地投遞到內部網絡中,數據包會流入到實際Web服務器。實際服務器處理完成後,又會將數據包投遞迴給負載均衡服務器,它再修改目標IP地址爲用戶IP地址,最終回到客戶端。 

 

上述的方式叫LVS-NAT,除此以外,還有LVS-RD(直接路由),LVS-TUN(IP隧道),三者之間都屬於LVS的方式,可是有必定的區別,篇幅問題,不贅敘。

IP負載均衡的性能要高出Nginx的反向代理不少,它只處理到傳輸層爲止的數據包,並不作進一步的組包,而後直接轉發給實際服務器。不過,它的配置和搭建比較複雜。

4. DNS負載均衡

DNS(Domain Name System)負責域名解析的服務,域名url其實是服務器的別名,實際映射是一個IP地址,解析過程,就是DNS完成域名到IP的映射。而一個域名是能夠配置成對應多個IP的。所以,DNS也就能夠做爲負載均衡服務。

 

這種負載均衡策略,配置簡單,性能極佳。可是,不能自由定義規則,並且,變動被映射的IP或者機器故障時很麻煩,還存在DNS生效延遲的問題。 

5. DNS/GSLB負載均衡

咱們經常使用的CDN(Content Delivery Network,內容分發網絡)實現方式,其實就是在同一個域名映射爲多IP的基礎上更進一步,經過GSLB(Global Server Load Balance,全局負載均衡)按照指定規則映射域名的IP。通常狀況下都是按照地理位置,將離用戶近的IP返回給用戶,減小網絡傳輸中的路由節點之間的跳躍消耗。 

 

圖中的「向上尋找」,實際過程是LDNS(Local DNS)先向根域名服務(Root Name Server)獲取到頂級根的Name Server(例如.com的),而後獲得指定域名的受權DNS,而後再得到實際服務器IP。

 

CDN在Web系統中,通常狀況下是用來解決大小較大的靜態資源(html/Js/Css/圖片等)的加載問題,讓這些比較依賴網絡下載的內容,儘量離用戶更近,提高用戶體驗。

例如,我訪問了一張imgcache.gtimg.cn上的圖片(騰訊的自建CDN,不使用qq.com域名的緣由是防止http請求的時候,帶上了多餘的cookie信息),我得到的IP是183.60.217.90。 

 

這種方式,和前面的DNS負載均衡同樣,不只性能極佳,並且支持配置多種策略。可是,搭建和維護成本很是高。互聯網一線公司,會自建CDN服務,中小型公司通常使用第三方提供的CDN。

Web系統的緩存機制的創建和優化

剛剛咱們講完了Web系統的外部網絡環境,如今咱們開始關注咱們Web系統自身的性能問題。咱們的Web站點隨着訪問量的上升,會遇到不少的挑戰,解決這些問題不只僅是擴容機器這麼簡單,創建和使用合適的緩存機制纔是根本。

最開始,咱們的Web系統架構多是這樣的,每一個環節,均可能只有1臺機器。 

 

咱們從最根本的數據存儲開始看哈。

1、 MySQL數據庫內部緩存使用

MySQL的緩存機制,就從先從MySQL內部開始,下面的內容將以最多見的InnoDB存儲引擎爲主。

1. 創建恰當的索引

最簡單的是創建索引,索引在表數據比較大的時候,起到快速檢索數據的做用,可是成本也是有的。首先,佔用了必定的磁盤空間,其中組合索引最突出,使用須要謹慎,它產生的索引甚至會比源數據更大。其次,創建索引以後的數據insert/update/delete等操做,由於須要更新原來的索引,耗時會增長。固然,實際上咱們的系統從整體來講,是以select查詢操做居多,所以,索引的使用仍然對系統性能有大幅提高的做用。

2. 數據庫鏈接線程池緩存

若是,每個數據庫操做請求都須要建立和銷燬鏈接的話,對數據庫來講,無疑也是一種巨大的開銷。爲了減小這類型的開銷,能夠在MySQL中配置thread_cache_size來表示保留多少線程用於複用。線程不夠的時候,再建立,空閒過多的時候,則銷燬。 

 

其實,還有更爲激進一點的作法,使用pconnect(數據庫長鏈接),線程一旦建立在很長時間內都保持着。可是,在訪問量比較大,機器比較多的狀況下,這種用法極可能會致使「數據庫鏈接數耗盡」,由於創建鏈接並不回收,最終達到數據庫的max_connections(最大鏈接數)。所以,長鏈接的用法一般須要在CGI和MySQL之間實現一個「鏈接池」服務,控制CGI機器「盲目」建立鏈接數。 

 

創建數據庫鏈接池服務,有不少實現的方式,PHP的話,我推薦使用swoole(PHP的一個網絡通信拓展)來實現。

3. Innodb緩存設置(innodb_buffer_pool_size)

innodb_buffer_pool_size這是個用來保存索引和數據的內存緩存區,若是機器是MySQL獨佔的機器,通常推薦爲機器物理內存的80%。在取表數據的場景中,它能夠減小磁盤IO。通常來講,這個值設置越大,cache命中率會越高。

4. 分庫/分表/分區。

MySQL數據庫表通常承受數據量在百萬級別,再往上增加,各項性能將會出現大幅度降低,所以,當咱們預見數據量會超過這個量級的時候,建議進行分庫/分表/分區等操做。最好的作法,是服務在搭建之初就設計爲分庫分表的存儲模式,從根本上杜絕中後期的風險。不過,會犧牲一些便利性,例如列表式的查詢,同時,也增長了維護的複雜度。不過,到了數據量千萬級別或者以上的時候,咱們會發現,它們都是值得的。 

2、 MySQL數據庫多臺服務搭建

1臺MySQL機器,其實是高風險的單點,由於若是它掛了,咱們Web服務就不可用了。並且,隨着Web系統訪問量繼續增長,終於有一天,咱們發現1臺MySQL服務器沒法支撐下去,咱們開始須要使用更多的MySQL機器。當引入多臺MySQL機器的時候,不少新的問題又將產生。

1. 創建MySQL主從,從庫做爲備份

這種作法純粹爲了解決「單點故障」的問題,在主庫出故障的時候,切換到從庫。不過,這種作法實際上有點浪費資源,由於從庫實際上被閒着了。

 

2. MySQL讀寫分離,主庫寫,從庫讀。

兩臺數據庫作讀寫分離,主庫負責寫入類的操做,從庫負責讀的操做。而且,若是主庫發生故障,仍然不影響讀的操做,同時也能夠將所有讀寫都臨時切換到從庫中(須要注意流量,可能會由於流量過大,把從庫也拖垮)。 

 

3. 主主互備。

兩臺MySQL之間互爲彼此的從庫,同時又是主庫。這種方案,既作到了訪問量的壓力分流,同時也解決了「單點故障」問題。任何一臺故障,都還有另一套可供使用的服務。 

 

不過,這種方案,只能用在兩臺機器的場景。若是業務拓展仍是很快的話,能夠選擇將業務分離,創建多個主主互備。

3、 MySQL數據庫機器之間的數據同步

每當咱們解決一個問題,新的問題必然誕生在舊的解決方案上。當咱們有多臺MySQL,在業務高峯期,極可能出現兩個庫之間的數據有延遲的場景。而且,網絡和機器負載等,也會影響數據同步的延遲。咱們曾經遇到過,在日訪問量接近1億的特殊場景下,出現,從庫數據須要數日才能同步追上主庫的數據。這種場景下,從庫基本失去效用了。

因而,解決同步問題,就是咱們下一步須要關注的點。

1. MySQL自帶多線程同步

MySQL5.6開始支持主庫和從庫數據同步,走多線程。可是,限制也是比較明顯的,只能以庫爲單位。MySQL數據同步是經過binlog日誌,主庫寫入到binlog日誌的操做,是具備順序的,尤爲當SQL操做中含有對於表結構的修改等操做,對於後續的SQL語句操做是有影響的。所以,從庫同步數據,必須走單進程。

2. 本身實現解析binlog,多線程寫入。

以數據庫的表爲單位,解析binlog多張表同時作數據同步。這樣作的話,的確可以加快數據同步的效率,可是,若是表和表之間存在結構關係或者數據依賴的話,則一樣存在寫入順序的問題。這種方式,可用於一些比較穩定而且相對獨立的數據表。 

 

國內一線互聯網公司,大部分都是經過這種方式,來加快數據同步效率。還有更爲激進的作法,是直接解析binlog,忽略以表爲單位,直接寫入。可是這種作法,實現複雜,使用範圍就更受到限制,只能用於一些場景特殊的數據庫中(沒有表結構變動,表和表之間沒有數據依賴等特殊表)。 

4、 在Web服務器和數據庫之間創建緩存

實際上,解決大訪問量的問題,不能僅僅着眼於數據庫層面。根據「二八定律」,80%的請求只關注在20%的熱點數據上。所以,咱們應該創建Web服務器和數據庫之間的緩存機制。這種機制,能夠用磁盤做爲緩存,也能夠用內存緩存的方式。經過它們,將大部分的熱點數據查詢,阻擋在數據庫以前。 

 

1. 頁面靜態化

用戶訪問網站的某個頁面,頁面上的大部份內容在很長一段時間內,可能都是沒有變化的。例如一篇新聞報道,一旦發佈幾乎是不會修改內容的。這樣的話,經過CGI生成的靜態html頁面緩存到Web服務器的磁盤本地。除了第一次,是經過動態CGI查詢數據庫獲取以外,以後都直接將本地磁盤文件返回給用戶。

 

在Web系統規模比較小的時候,這種作法看似完美。可是,一旦Web系統規模變大,例如當我有100臺的Web服務器的時候。那樣這些磁盤文件,將會有100份,這個是資源浪費,也很差維護。這個時候有人會想,能夠集中一臺服務器存起來,呵呵,不如看看下面一種緩存方式吧,它就是這樣作的。

2. 單臺內存緩存

經過頁面靜態化的例子中,咱們能夠知道將「緩存」搭建在Web機器本機是很差維護的,會帶來更多問題(實際上,經過PHP的apc拓展,可經過Key/value操做Web服務器的本機內存)。所以,咱們選擇搭建的內存緩存服務,也必須是一個獨立的服務。

內存緩存的選擇,主要有redis/memcache。從性能上說,二者差異不大,從功能豐富程度上說,Redis更勝一籌。 

 

3. 內存緩存集羣

當咱們搭建單臺內存緩存完畢,咱們又會面臨單點故障的問題,所以,咱們必須將它變成一個集羣。簡單的作法,是給他增長一個slave做爲備份機器。可是,若是請求量真的不少,咱們發現cache命中率不高,須要更多的機器內存呢?所以,咱們更建議將它配置成一個集羣。例如,相似redis cluster。

Redis cluster集羣內的Redis互爲多組主從,同時每一個節點均可以接受請求,在拓展集羣的時候比較方便。客戶端能夠向任意一個節點發送請求,若是是它的「負責」的內容,則直接返回內容。不然,查找實際負責Redis節點,而後將地址告知客戶端,客戶端從新請求。 

 

對於使用緩存服務的客戶端來講,這一切是透明的。

 

內存緩存服務在切換的時候,是有必定風險的。從A集羣切換到B集羣的過程當中,必須保證B集羣提早作好「預熱」(B集羣的內存中的熱點數據,應該儘可能與A集羣相同,不然,切換的一瞬間大量請求內容,在B集羣的內存緩存中查找不到,流量直接衝擊後端的數據庫服務,極可能致使數據庫宕機)。

4. 減小數據庫「寫」

上面的機制,都實現減小數據庫的「讀」的操做,可是,寫的操做也是一個大的壓力。寫的操做,雖然沒法減小,可是能夠經過合併請求,來起到減輕壓力的效果。這個時候,咱們就須要在內存緩存集羣和數據庫集羣之間,創建一個修改同步機制。

先將修改請求生效在cache中,讓外界查詢顯示正常,而後將這些sql修改放入到一個隊列中存儲起來,隊列滿或者每隔一段時間,合併爲一個請求到數據庫中更新數據庫。 

 

除了上述經過改變系統架構的方式提高寫的性能外,MySQL自己也能夠經過配置參數innodb_flush_log_at_trx_commit來調整寫入磁盤的策略。若是機器成本容許,從硬件層面解決問題,能夠選擇老一點的RAID(Redundant Arrays of independent Disks,磁盤列陣)或者比較新的SSD(Solid State Drives,固態硬盤)。

5. NoSQL存儲

無論數據庫的讀仍是寫,當流量再進一步上漲,終會達到「人力有窮時」的場景。繼續加機器的成本比較高,而且不必定能夠真正解決問題的時候。這個時候,部分核心數據,就能夠考慮使用NoSQL的數據庫。NoSQL存儲,大部分都是採用key-value的方式,這裏比較推薦使用上面介紹過Redis,Redis自己是一個內存cache,同時也能夠當作一個存儲來使用,讓它直接將數據落地到磁盤。

這樣的話,咱們就將數據庫中某些被頻繁讀寫的數據,分離出來,放在咱們新搭建的Redis存儲集羣中,又進一步減輕原來MySQL數據庫的壓力,同時由於Redis自己是個內存級別的Cache,讀寫的性能都會大幅度提高。 

 

國內一線互聯網公司,架構上採用的解決方案不少是相似於上述方案,不過,使用的cache服務卻不必定是Redis,他們會有更豐富的其餘選擇,甚至根據自身業務特色開發出本身的NoSQL服務。

6. 空節點查詢問題

當咱們搭建完前面所說的所有服務,認爲Web系統已經很強的時候。咱們仍是那句話,新的問題仍是會來的。空節點查詢,是指那些數據庫中根本不存在的數據請求。例如,我請求查詢一個不存在人員信息,系統會從各級緩存逐級查找,最後查到到數據庫自己,而後才得出查找不到的結論,返回給前端。由於各級cache對它無效,這個請求是很是消耗系統資源的,而若是大量的空節點查詢,是能夠衝擊到系統服務的。

 

在我曾經的工做經歷中,曾深受其害。所以,爲了維護Web系統的穩定性,設計適當的空節點過濾機制,很是有必要。

咱們當時採用的方式,就是設計一張簡單的記錄映射表。將存在的記錄存儲起來,放入到一臺內存cache中,這樣的話,若是還有空節點查詢,則在緩存這一層就被阻擋了。 

 

異地部署(地理分佈式)

完成了上述架構建設以後,咱們的系統是否就已經足夠強大了呢?答案固然是否認的哈,優化是無極限的。Web系統雖然表面上看,彷佛比較強大了,可是給予用戶的體驗卻不必定是最好的。由於東北的同窗,訪問深圳的一個網站服務,他仍是會感到一些網絡距離上的慢。這個時候,咱們就須要作異地部署,讓Web系統離用戶更近。

1、 核心集中與節點分散

有玩過大型網遊的同窗都會知道,網遊是有不少個區的,通常都是按照地域來分,例如廣東專區,北京專區。若是一個在廣東的玩家,去北京專區玩,那麼他會感受明顯比在廣東專區卡。實際上,這些大區的名稱就已經說明了,它的服務器所在地,因此,廣東的玩家去鏈接地處北京的服務器,網絡固然會比較慢。

當一個系統和服務足夠大的時候,就必須開始考慮異地部署的問題了。讓你的服務,儘量離用戶更近。咱們前面已經提到了Web的靜態資源,能夠存放在CDN上,而後經過DNS/GSLB的方式,讓靜態資源的分散「全國各地」。可是,CDN只解決的靜態資源的問題,沒有解決後端龐大的系統服務還只集中在某個固定城市的問題。

這個時候,異地部署就開始了。異地部署通常遵循:核心集中,節點分散。

  • 核心集中:實際部署過程當中,總有一部分的數據和服務存在不可部署多套,或者部署多套成本巨大。而對於這些服務和數據,就仍然維持一套,而部署地點選擇一個地域比較中心的地方,經過網絡內部專線來和各個節點通信。
  • 節點分散:將一些服務部署爲多套,分佈在各個城市節點,讓用戶請求儘量選擇近的節點訪問服務。

例如,咱們選擇在上海部署爲核心節點,北京,深圳,武漢,上海爲分散節點(上海本身自己也是一個分散節點)。咱們的服務架構如圖: 

 

須要補充一下的是,上圖中上海節點和核心節點是同處於一個機房的,其餘分散節點各自獨立機房。 
國內有不少大型網遊,都是大體遵循上述架構。它們會把數據量不大的用戶核心帳號等放在覈心節點,而大部分的網遊數據,例如裝備、任務等數據和服務放在地區節點裏。固然,核心節點和地域節點之間,也有緩存機制。 

2、 節點容災和過載保護

節點容災是指,某個節點若是發生故障時,咱們須要創建一個機制去保證服務仍然可用。毫無疑問,這裏比較常見的容災方式,是切換到附近城市節點。假如系統的天津節點發生故障,那麼咱們就將網絡流量切換到附近的北京節點上。考慮到負載均衡,可能須要同時將流量切換到附近的幾個地域節點。另外一方面,核心節點自身也是須要本身作好容災和備份的,核心節點一旦故障,就會影響全國服務。

過載保護,指的是一個節點已經達到最大容量,沒法繼續接接受更多請求了,系統必須有一個保護的機制。一個服務已經滿負載,還繼續接受新的請求,結果極可能就是宕機,影響整個節點的服務,爲了至少保障大部分用戶的正常使用,過載保護是必要的。

解決過載保護,通常2個方向:

  • 拒絕服務,檢測到滿負載以後,就再也不接受新的鏈接請求。例如網遊登入中的排隊。
  • 分流到其餘節點。這種的話,系統實現更爲複雜,又涉及到負載均衡的問題。

小結

Web系統會隨着訪問規模的增加,漸漸地從1臺服務器能夠知足需求,一直成長爲「龐然大物」的大集羣。而這個Web系統變大的過程,實際上就是咱們解決問題的過程。在不一樣的階段,解決不一樣的問題,而新的問題又誕生在舊的解決方案之上。

系統的優化是沒有極限的,軟件和系統架構也一直在快速發展,新的方案解決了老的問題,同時也帶來新的挑戰。

相關文章
相關標籤/搜索