億級 Web 系統搭建:單機到分布式集群


本文內容

  • Web 負載均衡
    • HTTP 重定向
    • 反向代理
    • IP 負載均衡
    • DNS 負載均衡
  • Web 系統緩存機制的建立和優化
    • MySQL 數據庫內部緩存
    • 搭建多台 MySQL 數據庫
    • MySQL 數據庫機器之間的數據同步
    • 在 Web 服務器和數據庫之間建立緩存
  • 異地部署(地理分布式)
    • 核心集中與節點分散
    • 節點容災和過載保護

當一個 Web 系統從日訪問量10萬逐步增長到1000萬,甚至超過1億的過程中,整個 Web 系統(無論是后端還是數據庫端)承受的壓力會越來越大,為了解決這些不同的性能壓力問題,我們需要在 Web 系統的架構層面想辦法。

Web負載均衡


Web 負載均衡(Load Balancing),簡單地說就是采用適當的方式給服務器集群分配“工作”。

2015-10-03_183622

負載均衡的策略有很多。

1,HTTP 重定向

當用戶發來請求的時候,Web 服務器通過修改 HTTP 響應頭中的Location標記返回一個新的 URL,然后瀏覽器再繼續請求這個新的 URL,這就是頁面重定向。通過重定向達到“負載均衡”的目標。

例如,我們在下載 PHP 源碼包的時候,點擊下載鏈接時,為了解決不同國家和地域下載速度的問題,它會返回一個離我們近的下載地址。重定向的HTTP返回碼是302,如下圖:

2015-10-03_183634

如果使用 PHP 來實現這個功能,代碼如下所示:

2015-10-03_183639

重定向很容易實現,而且可以自定義各種策略。但在大規模訪問下,性能不佳,用戶體驗也不好,重定向會增加網絡延時。

2,反向代理

反向代理的主要工作是轉發 HTTP 請求,扮演了瀏覽器和后台 Web 服務器中轉的角色。因為它工作在 HTTP 層(應用層),也就是網絡七層結構中的第七層,因此也被稱為“七層負載均衡”。可以做反向代理的軟件很多,如 Nginx。

2015-10-03_183735

Nginx是一種非常靈活的反向代理服務,可以自由定制化轉發策略,分配服務器流量的權重等。

反向代理中常見的一個問題,就是 Web 服務器存儲的 session 數據,因為一般負載均衡的策略都是隨機分配請求。同一個登錄用戶的請求,無法保證一定分配到相同的 Web 機器上,會導致無法找到session的問題。解決方案主要有兩種:

  1. 配置反向代理的轉發規則,讓同一個用戶的請求一定落到同一台機器上(通過分析 cookie),復雜的轉發規則將會消耗更多的CPU,也增加了代理服務器的負擔。

  2. 將 Session 這類的信息,專門用某個獨立服務來存儲,例如,redis、memchache,這個方案是比較推薦的。

反向代理服務,也是可以開啟緩存,如果開啟了,會增加反向代理的負擔,需要謹慎使用。這種負載均衡策略實現和部署非常簡單,而且性能表現也比較好。但它有“單點故障”的問題,而且,若后期繼續增加 Web 服務器,它本身可能成為系統的瓶頸。

3,IP 負載均衡

IP 負載均衡是工作在網絡層(修改IP)和傳輸層(修改端口,第四層),比起工作在應用層(第七層)性能要高出很多。其原理是,修改 IP 層數據包的 IP 地址和端口信息,達到負載均衡的目的。這種方式,也被稱為“四層負載均衡”。常見的負載均衡方式,是 LVS(Linux Virtual Server,Linux 虛擬服務),通過 IPVS(IP Virtual Server,IP 虛擬服務)。

2015-10-03_183805

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

2015-10-03_183836

上述的方式叫 LVS-NAT,除此之外,還有 LVS-RD(直接路由),LVS-TUN(IP 隧道),三者之間都屬於 LVS 方式,但是有一定的區別,篇幅有限,不再敖述。

IP 負載均衡的性能要高出像 Nginx 這樣反向代理服務很多,它只處理到傳輸層為止的數據包,並不做進一步的組包,然后直接轉發給實際服務器。不過,它的配置和搭建比較復雜。

4,DNS 負載均衡

DNS(Domain Name System)負責域名解析的服務,域名 URL 實際上是服務器的別名,實際映射是一個 IP 地址,解析過程,就是 DNS 完成域名到IP的映射。而一個域名是可以配置成對應多個IP的。因此,DNS 也就可以作為負載均衡服務。

2015-10-03_183900

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

5,DNS/GSLB 負載均衡

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

2015-10-03_183927

上圖的“向上尋找”,實際過程是 LDNS(Local DNS)先向根域名服務(Root Name Server)獲取到頂級根的 Name Server(例如.com的),然后得到指定域名的授權DNS,然后再獲得實際服務器IP。

2015-10-03_183952

CDN 在 Web 系統中,一般情況下是用來解決較大靜態資源(html/Js/Css/圖片等)的加載問題,讓這些資源盡可能離用戶更近,提升用戶體驗。

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

2015-10-03_184024

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

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


上面是如何優化 Web 系統的網絡環境,接下來,我們開始關注 Web 系統自身的性能問題。隨着 Web 站點訪問量的上升,會遇到很多的挑戰,解決這些問題不僅僅是擴容機器這么簡單,建立和使用合適的緩存機制才是根本。

最開始,我們的 Web 系統架構可能是這樣的,每個環節,只有一台機器。

2015-10-03_184100

1,MySQL 數據庫內部緩存

MySQL 緩存機制,就先從 MySQL 內部開始,下面內容以 InnoDB 存儲引擎為例。

1)建立恰當的索引

最簡單的當然是建立索引,雖然成本還是有的。

  • 首先,索引會占用一定的磁盤空間,其中組合索引產生的索引可能比實際數據還大;
  • 其次,DML 操作(insert/update/delete)更新索引時比較耗時,好在我們以 select 操作居多。但索引對系統性能的作用還是相當大的(姑且不算大數據啊,如果數據相當龐大,那再高效的索引也白搭啊)。

2)數據庫連接線程池緩存

如果每一個數據庫請求都要創建和銷毀連接的話,對數據庫來說,無疑也是一種巨大的開銷。為了減少開銷,可以在 MySQL 中配置 thread_cache_size 表示保留多少線程用於復用。線程不夠的時候,再創建,空閑過多的時候,則銷毀。

2015-10-03_184125

還有更激進一點的做法,使用 pconnect(數據庫長連接),線程一旦創建在很長時間內都保持着。但是,在訪問量比較大,機器比較多的情況下,這種用法很可能會導致“數據庫連接數耗盡”,因為建立連接並不回收,最終達到數據庫的 max_connections(最大連接數)。因此,長連接的用法通常需要在 CGI 和 MySQL 之間實現一個“連接池”服務,控制 CGI 機器“盲目”創建連接數。

2015-10-03_184218

建立數據庫連接池服務,有很多實現的方式,PHP 的話,推薦使用 swoole(PHP 一個網絡通訊拓展)來實現。

3)Innodb 緩存設置(innodb_buffer_pool_size)

innodb_buffer_pool_size 用來保存索引和數據的內存緩存區,如果服務器是 MySQL 獨占的,一般推薦為物理內存的 80%。在取表數據的場景中,它可以減少磁盤 IO。一般來說,這個值設置越大,cache 命中率會越高。

4)分庫/分表/分區

MySQL 數據庫表一般承受數據量在百萬級別,再往上增長,各項性能將會出現大幅度下降,因此,當我們預見數據量會超過這個量級的時候,建議進行分庫/分表/分區。

最好的做法,是服務在搭建之初就設計為分庫分表的存儲模式,從根本上杜絕中后期的風險。不過,會犧牲一些便利性,同時,也增加了維護的復雜度。不過,到了數據量千萬級別或者以上的時候,這樣做是值得的。

2,搭建多台 MySQL 數據庫

一台 MySQL 機器,實際上是高風險的單點,因為如果它掛了,我們 Web 服務就不可用了。而且,隨着 Web 系統訪問量持續增加,總有一天,我們發現一台 MySQL 服務器無法支撐下去,我們開始需要使用更多的MySQL機器。當引入多台MySQL機器的時候,很多新的問題又將產生。

1)建立 MySQL 主從,從庫作為備份

這種做法純粹為了解決“單點故障”的問題,在主庫出故障的時候,切換到從庫。不過,這種做法有點浪費資源,因為從庫實際上閑置的。

2015-10-03_184244

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

兩台數據庫,讀寫分離,主庫負責寫,從庫負責讀。如果主庫發生故障,不影響讀,也可以將全部讀寫都切換到從庫(需要注意流量,可能會因為流量過大,把從庫也拖垮)。

2015-10-03_184313

3)主主互備。

兩台 MySQL 之間互為主從。這種方案,既做到了訪問量的壓力分流,同時也解決了“單點故障”問題。任何一台故障,都還有另外一套可供使用的服務。

2015-10-03_184339

不過,這種方案,只能用在兩台機器的場景。如果業務拓展還是很快的話,可以選擇將業務分離,建立多個主主互備。

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

每當我們解決一個問題,新的問題必然誕生。當我們有多台 MySQL,在業務高峰期,很可能出現兩個庫之間的數據有延遲的場景。而且網絡和負載等也會影響數據同步的延遲。我們曾經遇到過,在日訪問量接近1億的場景下,出現,從庫數據需要很多天才能同步追上主庫的數據。這種場景下,從庫基本失去效用了。因此,解決同步問題,是我們下一步關注點。

1)MySQL 自帶多線程同步

MySQL 5.6 開始支持主庫和從庫數據同步,走多線程。但限制比較明顯,只能以庫為單位。MySQL 數據同步是通過 binlog 日志(5.0 前支持文本格式和二進制格式,5.0 后只支持二進制格式,因為二進制日志在性能、信息處理方面更有優勢),主庫寫入到 binlog 日志的操作,是有順序的,尤其當SQL操作中含有對於表結構的修改等操作,對於后續的SQL語句操作是有影響的。因此,從庫同步數據,必須走單進程。

2)自己實現解析 binlog,多線程寫入

以數據庫的表為單位,解析 binlog 多張表同時做數據同步。這樣做的話,的確能夠加快數據同步的效率,但如果表和表之間存在結構關系或者數據依賴的話,則同樣存在寫入順序的問題。這種方式,可用於一些比較穩定並且相對獨立的數據表。國內一線互聯網公司,大部分都是通過這種方式,來加快數據同步效率。

2015-10-03_184407

還有更為激進的做法,是直接解析 binlog,忽略以表為單位,直接寫入。但是這種做法,實現復雜,使用范圍就更受到限制,只能用於一些特殊場景(沒有表結構變更,表和表之間沒有數據依賴等特殊表)。

4,在 Web 服務器和數據庫之間建立緩存

大訪問量不能僅僅着眼於數據庫層面,根據“二八定律”,80% 的請求只關注在 20% 的熱點數據上。因此,我們應該在 Web 服務器和數據庫之間建立的緩存機制。緩存可以用磁盤,也可以用內存。通過它們,將大部分的熱點數據查詢,阻擋在數據庫之前。

2015-10-03_184424

1)頁面靜態化

用戶訪問網站的某個頁面,頁面上的大部分內容在很長一段時間內,可能都是沒有變化的。例如新聞,一旦發布內容幾乎不會被修改。這樣,通過 CGI 生成的靜態 html 頁面緩存到 Web 服務器的本地磁盤(注意是本地磁盤,也就是緩存在 Web 服務器上)。除第一次,是通過動態 CGI 查詢數據庫獲取之外,之后都直接將本地磁盤文件返回給用戶。

2015-10-03_184452

在 Web 系統規模比較小的時候,這種做法還挺完美。可一旦 Web 系統規模變大,是個 Web 集群,例如,當有 100 台的 Web 服務器時,因為是緩存在本地磁盤,所以磁盤上將會有 100 份,浪費資源,維護性差。那用一台單獨的服務器保存靜態頁面,不就得了,事實也的確如此,下面介紹。

緩存既可以用內存,也可以是磁盤,但內存的訪問速度當然比磁盤快很多。

2)單台內存緩存

頁面靜態化,靜態頁面緩存在 Web 服務器本地磁盤或內存(實際上,通過PHP的apc拓展,可通過Key/value操作Web服務器的本機內存),不好維護,會帶來更多問題。因此,利用一台單獨的機器來搭建內存緩存服務。

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

2015-10-03_184514

3)內存緩存集群

單台內存緩存會面臨單點故障的問題。簡單的做法,是建立集群,增加一個 slave 作為備份機器。但是,如果請求量真的很多,cache 的命中率未必會高,因為,salve 不會接受請求,它只是一個備份而已,此時,不是增加給機器增加更多內存,而是需要建立一個集群。例如,redis cluster。

Redis cluster 集群內的 Redis 互為多組主從,同時每個節點都可以接受請求,在拓展集群的時候比較方便。客戶端可以向任意一個節點發送請求,如果是它的“負責”的內容,則直接返回內容。否則,查找實際負責Redis節點,然后將地址告知客戶端,客戶端重新請求。

2015-10-03_184538

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

2015-10-03_184601

內存緩存服務在切換的時候,是有一定風險的。從A集群切換到B集群的過程中,必須保證B集群提前做好“預熱”(B集群的內存中的熱點數據,應該盡量與A集群相同,否則,切換的一瞬間大量請求內容,在B集群的內存緩存中查找不到,流量直接沖擊后端的數據庫服務,很可能導致數據庫宕機)。

4)減少數據庫“寫”

上面的機制,都試圖減少數據庫的“讀”,但寫操作也是一個大的壓力。寫操作,雖然無法減少,但可以通過合並請求來減輕壓力。這個時候,我們就需要在內存緩存集群和數據庫集群之間,建立一個修改同步機制。

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

2015-10-03_184643

除了上述通過改變系統架構的方式提升寫的性能外,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,讀寫的性能都會大幅度提升。

2015-10-03_184708

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

6)空節點查詢問題

當我們搭建完前面所說的全部服務,認為Web系統已經很強的時候。我們還是那句話,新的問題還是會來的。空節點查詢,是指那些數據庫中根本不存在的數據請求。例如,我請求查詢一個不存在人員信息,系統會從各級緩存逐級查找,最后查到到數據庫本身,然后才得出查找不到的結論,返回給前端。因為各級cache對它無效,這個請求是非常消耗系統資源的,而如果大量的空節點查詢,是可以沖擊到系統服務的。

2015-10-03_184735

在我曾經的工作經歷中,曾深受其害。因此,為了維護 Web 系統的穩定性,設計適當的空節點過濾機制,非常有必要。

我們當時采用的方式,就是設計一張簡單的記錄映射表。將存在的記錄存儲起來,放入到一台內存 cache 中,這樣,如果還有空節點查詢,則在緩存這一層就被阻擋了。
2015-10-03_184820

異地部署(地理分布式)


完成了上述架構建設之后,我們的系統是否就已經足夠強大了呢?答案當然是否定,優化是無極限的。Web 系統雖然表面上看,似乎比較強大了,但是給予用戶的體驗卻不一定是最好的。因為東北的同學,訪問深圳的一個網站服務,他還是會感到一些網絡距離上的慢。這個時候,我們就需要做異地部署,讓Web系統離用戶更近。

1,核心集中與節點分散

有玩過大型網游的同學都會知道,網游是有很多個區的,一般都是按照地域來分,例如廣東專區,北京專區。如果一個在廣東的玩家,去北京專區玩,那么他會感覺明顯比在廣東專區卡。實際上,這些大區的名稱就已經說明了,它的服務器所在地,所以,廣東的玩家去連接地處北京的服務器,網絡當然會比較慢。

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

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

  • 核心集中:實際部署過程中,總有一部分的數據和服務存在不可部署多套,或者部署多套成本巨大。而對於這些服務和數據,就仍然維持一套,而部署地點選擇一個地域比較中心的地方,通過網絡內部專線來和各個節點通訊。

  • 節點分散:將一些服務部署為多套,分布在各個城市節點,讓用戶請求盡可能選擇近的節點訪問服務。

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

2015-10-03_184858

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

2,節點容災和過載保護

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

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

解決過載保護,一般2個方向:

  • 拒絕服務,檢測到滿負載之后,就不再接受新的連接請求。例如網游登入中的排隊。

  • 分流到其他節點。這種的話,系統實現更為復雜,又涉及到負載均衡的問題。

小結


Web系統會隨着訪問規模的增長,漸漸地從1台服務器可以滿足需求,一直成長為“龐然大物”的大集群。而這個Web系統變大的過程,實際上就是我們解決問題的過程。在不同的階段,解決不同的問題,而新的問題又誕生在舊的解決方案之上。

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

 

大型網站架構演化

大規模網站架構的緩存機制和幾何分形學


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM