大型分佈式 Web 系統的架構演進

** 00 前言  **

我們以 Java Web 爲例,來搭建一個簡單的電商系統,看看這個系統可以如何一步步演變。

該系統具備的功能:

** 01 正文  **

階段一、單機構建網站

網站的初期,我們經常會在單機上跑我們所有的程序和軟件。此時我們使用一個容器,如 Tomcat、Jetty、Jboss,然後直接使用 JSP/Servlet 技術,或者使用一些開源的框架如 Maven + Spring + Struts + Hibernate、Maven + Spring + Spring MVC + Mybatis。最後再選擇一個數據庫管理系統來存儲數據,如 MySQL、SqlServer、Oracle,然後通過 JDBC 進行數據庫的連接和操作。

把以上的所有軟件包括數據庫、應用程序都裝載同一臺機器上,應用跑起來了,也算是一個小系統了。此時系統結果如下:

圖片


階段二、應用服務器與數據庫分離

隨着網站的上線,訪問量逐步上升,服務器的負載慢慢提高,在服務器還沒有超載的時候,我們應該就要做好準備,提升網站的負載能力。假如我們代碼層面已難以優化,在不提高單臺機器的性能的情況下,採用增加機器是一個不錯的方式,不僅可以有效地提高系統的負載能力,而且性價比高。

增加的機器用來做什麼呢?此時我們可以把數據庫服務器Web 服務器拆分開來,這樣不僅提高了單臺機器的負載能力,也提高了容災能力。

應用服務器與數據庫分開後的架構如下圖所示:

圖片

階段三、應用服務器集羣

隨着訪問量繼續增加,單臺應用服務器已經無法滿足需求了。在假設數據庫服務器沒有壓力的情況下,我們可以把應用服務器從一臺變成了兩臺甚至多臺,把用戶的請求分散到不同的服務器中,從而提高負載能力。而多臺應用服務器之間沒有直接的交互,他們都是依賴數據庫各自對外提供服務。著名的做故障切換的軟件有 KeepAlived,KeepAlived 是一個類似於 Layer3、4、7 交換機制的軟件,他不是某個具體軟件故障切換的專屬品,而是可以適用於各種軟件的一款產品。KeepAlived 配合上 ipvsadm 又可以做負載均衡,可謂是神器。

我們以增加了一臺應用服務器爲例,增加後的系統結構圖如下:

圖片

系統演變到這裏,將會出現下面四個問題

  1. 用戶的請求由誰來轉發到到具體的應用服務器?

  2. 有那些轉發的算法和策略可以使用?

  3. 應用服務器如何返回用戶的請求?

  4. 用戶如果每次訪問到的服務器不一樣,那麼如何維護 session 的一致性?

針對以上問題,常用的解決方案如下:

1、負載均衡的問題

一般以下有 5 種解決方案:

  1. HTTP 重定向

HTTP 重定向就是應用層的請求轉發。用戶的請求其實已經到了 HTTP 重定向負載均衡服務器,服務器根據算法要求用戶重定向,用戶收到重定向請求後,再次請求真正的集羣

  1. DNS 域名解析負載均衡

DNS 域名解析負載均衡就是在用戶請求 DNS 服務器,獲取域名對應的 IP 地址時,DNS 服務器直接給出負載均衡後的服務器 IP。

  1. 反向代理服務器

在用戶的請求到達反向代理服務器時(已經到達網站機房),由反向代理服務器根據算法轉發到具體的服務器。常用的 Apache,Nginx 都可以充當反向代理服務器。

  1. IP 層負載均衡

在請求到達負載均衡器後,負載均衡器通過修改請求的目的 IP 地址,從而實現請求的轉發,做到負載均衡。

  1. 數據鏈路層負載均衡

在請求到達負載均衡器後,負載均衡器通過修改請求的 MAC 地址,從而做到負載均衡,與 IP 負載均衡不一樣的是,當請求訪問完服務器之後,直接返回客戶。而無需再經過負載均衡器。

2、集羣調度轉發算法

  1. rr 輪詢調度算法

顧名思義,輪詢分發請求。

  1. wrr 加權調度算法

我們給每個服務器設置權值 Weight,負載均衡調度器根據權值調度服務器,服務器被調用的次數跟權值成正比。

  1. sh 原地址散列算法

提取用戶 IP,根據散列函數得出一個 key,再根據靜態映射表,查處對應的 value,即目標服務器 IP。過目標機器超負荷,則返回空。

  1. dh 目標地址散列算法

原理同上,只是現在提取的是目標地址的 IP 來做哈希。

  1. lc 最少連接算法

優先把請求轉發給連接數少的服務器。

  1. wlc 加權最少連接算法

在 lc 的基礎上,爲每臺服務器加上權值。算法爲:(活動連接數 * 256 + 非活動連接數) ÷ 權重,計算出來的值小的服務器優先被選擇。

  1. sed 最短期望延遲算法

其實 sed 跟 wlc 類似,區別是不考慮非活動連接數。算法爲:(活動連接數 +1 ) * 256 ÷ 權重,同樣計算出來的值小的服務器優先被選擇。

  1. nq 永不排隊算法

改進的 sed 算法。我們想一下什麼情況下才能 “永不排隊”,那就是服務器的連接數爲 0 的時候,那麼假如有服務器連接數爲 0,均衡器直接把請求轉發給它,無需經過 sed 的計算。

  1. LBLC 基於局部性最少連接算法

負載均衡器根據請求的目的 IP 地址,找出該 IP 地址最近被使用的服務器,把請求轉發之。若該服務器超載,最採用最少連接數算法。

  1. LBLCR 帶複製的基於局部性最少連接算法

負載均衡器根據請求的目的 IP 地址,找出該 IP 地址最近使用的 “服務器組”,注意,並不是具體某個服務器,然後採用最少連接數從該組中挑出具體的某臺服務器出來,把請求轉發之。若該服務器超載,那麼根據最少連接數算法,在集羣的非本服務器組的服務器中,找出一臺服務器出來,加入本服務器組,然後把請求轉發。

3、集羣請求返回模式問題

  1. NAT

負載均衡器接收用戶的請求,轉發給具體服務器,服務器處理完請求返回給均衡器,均衡器再重新返回給用戶。

  1. DR

負載均衡器接收用戶的請求,轉發給具體服務器,服務器出來玩請求後直接返回給用戶。需要系統支持 IP Tunneling 協議,難以跨平臺。

  1. TUN

同上,但無需 IP Tunneling 協議,跨平臺性好,大部分系統都可以支持。

4、集羣 Session 一致性問題

  1. Session Sticky

Session sticky 就是把同一個用戶在某一個會話中的請求,都分配到固定的某一臺服務器中,這樣我們就不需要解決跨服務器的 session 問題了,常見的算法有 ip_hash 算法,即上面提到的兩種散列算法。

  1. Session Replication

Session replication 就是在集羣中複製 session,使得每個服務器都保存有全部用戶的 session 數據。

  1. Session 數據集中存儲

Session 數據集中存儲就是利用數據庫來存儲 session 數據,實現了 session 和應用服務器的解耦。

  1. Cookie Base

Cookie base 就是把 Session 存在 Cookie 中,由瀏覽器來告訴應用服務器我的 session 是什麼,同樣實現了 session 和應用服務器的解耦。

值得一提的是:

解決了以上的問題之後,系統的結構如下:

圖片

階段四、數據庫讀寫分離化

上面我們總是假設數據庫負載正常,但隨着訪問量的的提高,數據庫的負載也在慢慢增大。那麼可能有人馬上就想到跟應用服務器一樣,把數據庫一份爲二再負載均衡即可。

但對於數據庫來說,並沒有那麼簡單。假如我們簡單的把數據庫一分爲二,然後對於數據庫的請求,分別負載到 A 機器和 B 機器,那麼顯而易見會造成兩臺數據庫數據不統一的問題。那麼對於這種情況,我們可以先考慮使用讀寫分離主從複製的方式。

讀寫分離後的系統結構如下:

圖片

這個結構變化後也會帶來兩個問題:

解決方案:


階段五、用搜索引擎緩解讀庫的壓力

數據庫做讀庫的話,常常對模糊查找力不從心,即使做了讀寫分離,這個問題還未能解決。以我們所舉的交易網站爲例,發佈的商品存儲在數據庫中,用戶最常使用的功能就是查找商品,尤其是根據商品的標題來查找對應的商品。對於這種需求,一般我們都是通過 like 功能來實現的,但是這種方式的代價非常大,而且結果非常不準確。此時我們可以使用搜索引擎倒排索引來完成。

搜索引擎具有的優點:它能夠大大提高查詢速度和搜索準確性。

引入搜索引擎的開銷

搜索引擎並不能替代數據庫,它解決了某些場景下的精準、快速、高效的 “讀” 操作,是否引入搜索引擎,需要綜合考慮整個系統的需求。

引入搜索引擎後的系統結構如下:

圖片

階段六、用緩存緩解讀庫的壓力

常用的緩存機制包括頁面級緩存、應用數據緩存和數據庫緩存。

應用層和數據庫層的緩存

隨着訪問量的增加,逐漸出現了許多用戶訪問同一部分熱門內容的情況,對於這些比較熱門的內容,沒必要每次都從數據庫讀取。我們可以使用緩存技術,例如可以使用 Google 的開源緩存技術 Guava 或者使用 Memecahed 作爲應用層的緩存,也可以使用 Redis 作爲數據庫層的緩存。

另外,在某些場景下,關係型數據庫並不是很適合,例如我想做一個 “每日輸入密碼錯誤次數限制” 的功能,思路大概是在用戶登錄時,如果登錄錯誤,則記錄下該用戶的 IP 和錯誤次數,那麼這個數據要放在哪裏呢?假如放在內存中,那麼顯然會佔用太大的內容;假如放在關係型數據庫中,那麼既要建立數據庫表,還要簡歷對應的 Java bean,還要寫 SQL 等等。而分析一下我們要存儲的數據,無非就是類似 {ip:errorNumber} 這樣的 key:value 數據。對於這種數據,我們可以用 NOSQL 數據庫來代替傳統的關係型數據庫。

頁面緩存

除了數據緩存,還有頁面緩存。比如使用 HTML5 的 localstroage 或者 Cookie。除了頁面緩存帶來的性能提升外,對於併發訪問且頁面置換頻率小的頁面,應儘量使用頁面靜態化技術。

值得一提的是:

緩存集羣的調度算法不同與上面提到的應用服務器和數據庫。最好採用一致性哈希算,這樣才能提高命中率

加入緩存後的系統結構如下:

圖片

階段七、數據庫水平拆分與垂直拆分

我們的網站演進到現在,交易、商品、用戶的數據都還在同一個數據庫中。儘管採取了增加緩存讀寫分離的方式,但隨着數據庫的壓力繼續增加,數據庫數據量的瓶頸越來越突出,此時,我們可以有數據垂直拆分水平拆分兩種選擇。

數據垂直拆分

垂直拆分的意思是把數據庫中不同的業務數據拆分到不同的數據庫中,結合現在的例子,就是把交易、商品、用戶的數據分開。

優點:

缺點:

問題:

解決問題方案:

數據垂直拆分後的結構如下:

圖片

數據水平拆分

數據水平拆分就是把同一個表中的數據拆分到兩個甚至多個數據庫中。產生數據水平拆分的原因是某個業務的數據量或者更新量到達了單個數據庫的瓶頸,這時就可以把這個表拆分到兩個或更多個數據庫中。

優點:

問題:

解決問題方案:

數據水平拆分後的結構如下:

圖片

階段八、應用的拆分

按微服務拆分應用

隨着業務的發展,業務越來越多,應用越來越大。我們需要考慮如何避免讓應用越來越臃腫。這就需要把應用拆開,從一個應用變爲倆個甚至更多。還是以我們上面的例子,我們可以把用戶、商品、交易拆分開。變成 “用戶、商品” 和“用戶,交易”兩個子系統。  

拆分後的結構:

圖片

問題:

這樣拆分後,可能會有一些相同的代碼,如用戶相關的代碼,商品和交易都需要用戶信息,所以在兩個系統中都保留差不多的操作用戶信息的代碼。如何保證這些代碼可以複用是一個需要解決的問題。

解決問題:

通過走服務化 SOA 的路線來解決頻繁公共的服務。

走 SOA 服務化治理道路

爲了解決上面拆分應用後所出現的問題,我們把公共的服務拆分出來,形成一種服務化的模式,簡稱 SOA。

採用服務化之後的系統結構:

圖片

優點:

問題:

如何進行遠程的服務調用?

解決方法:

可以通過下面的引入消息中間件來解決。


階段九、引入消息中間件

隨着網站的繼續發展,的系統中可能出現不同語言開發的子模塊和部署在不同平臺的子系統。此時我們需要一個平臺來傳遞可靠的,與平臺和語言無關的數據,並且能夠把負載均衡透明化,能在調用過程中收集分析調用數據,推測出網站的訪問增長率等等一系列需求,對於網站應該如何成長做出預測。開源消息中間件有阿里的 Dubbo,可以搭配 Google 開源的分佈式程序協調服務 Zookeeper 實現服務器的註冊發現

引入消息中間件後的結構:

圖片

**02 總結  **

以上的演變過程只是一個例子,並不適合所有的網站,實際中網站演進過程與自身業務和不同遇到的問題有密切的關係,沒有固定的模式。只有認真的分析和不斷地探究,才能發現適合自己網站的架構。

作者:零壹技術棧

來源:http://juejin.im/post/5b4c4e566fb9a04f83464102

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sPUG1UvUwHCQlYVCfvrptg