架構設計--互聯網架構演化

對於一個大型網站,主要有以下幾個特徵:

  1. 支撐海量數據
  2. 非常高的訪問量

我們常見的大型網站,如百度、淘寶、京東等,都是一個分佈式系統。這麼複雜的系統也不是一天建成的,每個系統都經歷了漫長的演變過程。

架構演變

在大型網站中,其最核心的功能就是 計算存儲。因此係統演變過程也主要圍繞這兩點進行。

1 單機系統

在網站剛剛起步時,數據量、訪問量都非常小,通常情況下,只需一臺應用服務器就可以了。

1.1 單機部署方案

起步時,我們把所有資源全部打包到部署文件中(如 XXX.war),其中包括

  1. class 文件、依賴 jar等;
  2. js、css、圖片等靜態資源;
  3. 對於用戶上傳文件的場景,直接在服務器上新建一個目錄,將上傳的文件放置在目錄即可。

然後,將打好的發佈包放到 Web 容器中,比如 Tomcat,最後啓動容器,讓其直接對外提供服務。

單機部署架構

該部署策略有以下幾個特徵:

  1. 用戶通過瀏覽器直接與 Java 應用程序進行交互(通常是 Tomcat);
  2. Java 應用程序通過 JDBC 與本機的數據庫進行交互(如 MySQL);
  3. 如果存在文件讀寫的需求,Java 應用程序通過文件接口直接對文件進行操作。

這時,有人會問,Java 應用程序直接對外,會不會存在一些安全或性能方面的問題呢?

是的,Tomcat 這種 Web 容器對鏈接的保持能力比較弱,當存在大量鏈接時,性能下降很快。同時,Tomcat 並不擅長靜態資源的處理,對此,我們可以引入 Nginx,以緩解 Tomcat 的壓力。

1.2 單機部署方案進階

我們在單機部署基礎上,添加 Nginx,也就有了進階方案。

單機部署方案進階

該方案存在以下特徵:

  1. 用戶不在直接與 Java 應用程序進行交互,而是與 Nginx 進行交互;
  2. Tomcat 掛在 Nginx 後,對動態請求進行處理;
  3. 對於靜態資源的訪問,通過 Nginx 直接訪問文件系統;
  4. 當有文件寫需求時,通過 Java 應用程序直接寫入磁盤。

此時,架構顯得清晰很多,但我們發現一個問題,就是系統對靜態資源和動態資源的處理是完全不同的。

對於靜態資源的處理,相對簡單,只是簡單的文件讀寫。而,動態請求(也就是我們的業務承載者)會隨着業務的發展越來越複雜。

2 動靜分離部署方案

由於 靜態請求動態請求 採用不同的處理策略,我們可以將其進行分離。

動靜分離

該部署方案存在以下特性:

  1. 通過不同的域名對 動態請求靜態請求 進行分離;
  2. 新增 靜態資源服務器,專門處理靜態請求,並在服務器上部署 Java 應用程序,處理文件寫需求;Nginx 只負責文件的讀操作;
  3. 動態請求 進行獨立部署,應用程序將文件的寫請求轉發到靜態服務器進行處理;

靜態資源服務器功能單一,部署繁瑣,有沒有一種更好的策略呢?

答案就是雲服務,比如阿里雲的 OSS 提供靜態資源存儲服務。CDN 提供訪問加速服務,兩者結合使用,就得到了一個海量容量並且性能超強的靜態資源服務器(集羣)。
結合 OSS 和 CDN,靜態請求不會成爲系統的瓶頸,因此,接下來只對動態請求進行討論。

隨着系統訪問量的增加,動態請求出現了明顯的瓶頸。

3 應用集羣化部署

由於所有的動態請求全部由一臺應用服務器進行處理,當訪問量上升時,這臺服務就成了系統的瓶頸。此時,我們需要將系統中的多個組件部署到不同的服務器上。

應用集羣化部署

新部署有以下特徵:

  1. 對 Nginx 進行獨立部署,形成 Web 集羣
  2. 對 Java 應用程序進行獨立部署,形成 應用集羣
  3. 數據庫 進行獨立部署;
  4. Web 集羣應用集羣 間通過 HTTP 協議進行交互;
  5. 應用集羣數據庫 間通過 JDBC 協議進行交互。

應用集羣化,會面臨很多挑戰,主要的焦點是如何有效的分配用戶請求。

3.1 DNS 輪詢

首先要解決的問題便是,用戶如何將請求發送到不同的 Nginx 中,最常見的方式便是 DNS 輪詢。

大多域名註冊商都支持多條 A 記錄的解析,其實這就是 DNS 輪詢,DNS 服務器將解析請求按照 A 記錄的順序,逐一分配到不同的 IP 上,這樣就完成了簡單的負載均衡。

3.2 負載均衡器

這裏的負載均衡器主要指的是 Nginx 的反向代理功能。當用戶請求發送到 Nginx 後,Nginx 需要決定將請求轉發到哪臺應用服務器上。

反向代理(Reverse Proxy)是指以代理服務器來接受 internet 上的連接請求,然後將請求轉發給內部網絡上的服務器,並將從服務器上得到的結果返回給 internet 上請求連接的客戶端,此時代理服務器對外就表現爲一個反向代理服務器。

Nginx 對於後臺服務器配置比較靈活,可以同時配置多臺服務器,並根據負載策略將請求分發給後臺服務器。

3.3 會話問題

在單機時代,我們的請求只會發送到同一臺機器上,不存在會話問題。當將應用集羣部署時,用戶的多次請求會發送到不同的應用服務器上。此時,如何對會話進行同步便是棘手問題。

3.3.1 Session Sticky

這種方案主要由 Nginx 處理,讓同樣 session 請求每次都發送到同一臺服務器進行處理。

Session Sticky

Nginx 會將相同用戶的請求發送到同一臺應用服務器中。

這是最簡單的策略,但存在一定的問題:

  1. Web 服務器重啓 Session 丟失;
  2. 負載均衡需要進行應用層解析(第7層),性能損耗較大;
  3. 負載均衡器變爲一個有狀態的點,不易容災;
3.3.2 Session Replication

會話問題的根源在於 Session 由多個應用維護,我們可以使用某種機制,在多臺 Web 服務間進行 Session 的數據同步。

Session Replication

由 Session 同步器在各個 Java 應用程序間完成 Session 的同步,最終使每個服務器中都存在所有用戶的 Session 數據。

這個方案的問題:

  1. 造成網絡開銷;
  2. 每臺 Web 服務器都保存所有的 Session,內存開銷大;
3.3.3 集中式Session

我們可以將 Session 從 Web 服務中抽取出來,並對其進行集中存儲。

集中式 Session 存儲

將 Session 信息保存到 Session 存儲集羣中,Java 應用程序不在負責 Session 的存儲。

這個方案的問題:

  1. 讀取Session引入了網絡開銷;
  2. 存儲設施問題影響應用;
3.3.4 Cookie Based Session

還可以將 session 數據放在 cookie 中,然後在 Web 服務器上從 cookie 中生成對應的 Session 數據。

Cookie Based Session

將 Session 數據編碼到 Cookie 中,每次 Java 應用程序使用 Session 時,都從 Cookie 中重建 Session。

該方案的問題:

  1. 受到 Cookie 大小的限制;
  2. 存在安全性問題;
  3. 每次都攜帶巨大的 Cookie,帶寬消耗嚴重;
  4. 每次都進行 Session 數據恢復,加大應用服務器的負擔;

隨着系統訪問量的持續增加,面對大量的數據讀取請求,數據庫有些不堪重負。

此時,我們需要對數據庫進行優化。

4 數據庫讀寫分離

通常情況下,數據庫的讀會帥選成爲系統的瓶頸。對此,我們可以使用數據庫主從機制,通過添加多個從庫來減緩讀壓力。

數據庫讀寫分離

與之前部署相比,該架構只是爲數據庫增加了若干個從庫:

  1. 對數據庫實施 主從 部署策略;
  2. 對於數據的寫請求,只能在 主庫 上進行;
  3. 對於數據的讀請求,可以在任意的 從庫 上進行;
  4. 主庫與從庫間,通過 數據庫同步策略 進行數據同步。
由於主庫與從庫間的數據同步需要時間,會出現數據不一致的情況,這塊是業務上需要慎重考慮的一點。

隨着業務越來越複雜,對功能和性能的要求也越來越高,最常見的便是數據庫 like 語句性能已經無法滿足需求;對於某些熱點數據的訪問,其性能也下降很快。

此時,我們需要引入其他組件來有針對性的解決問題。

5 引入搜索和緩存

針對數據庫的 like 語句,通常情況下,是通過引入搜索引擎來解決;而熱點數據的訪問加速,是通過引入緩存服務來解決。

引入搜索和緩存

該架構的特徵如下:

  1. 添加 搜索集羣,用以提升數據檢索性能;
  2. 添加 緩存集羣,用以提升熱點數據訪問性能。

在對數據查詢進行優化後,慢慢的系統的寫性能成爲了瓶頸。

此時,需要對數據的寫性能進行擴展。

6 數據庫分庫分表

隨着數據量的增長,寫請求量的增加,數據庫的寫入逐漸成爲了瓶頸。常規的寫性能優化便是對數據庫進行分庫分表。

數據庫分庫分表

6.1 垂直拆分

將不同的業務數據放到不同的數據庫實例中。

6.2 水平切分

把同一個表中的數據拆分到多的數據庫中。

隨着研發團隊的規模越來越多,大家同時在一個項目中進行開發,導致頻繁的衝突和相互影響。

此時,會將整個應用程序根據功能模塊進行拆分,從而形成多個子網站或子頻道。

7 應用垂直拆分

面對一個巨無霸式的應用,就像面對一團毛線團,總有一種無法下手的感覺。對此,可以將其進行拆分,將其拆分爲多個應用,每個應用獨立開發、獨立部署、獨立維護。

應用垂直拆分

該部署方案更加靈活,大大降低維護成本。

  1. 通過不同的域名或 URL 將整個系統分解爲多個子系統;
  2. 用戶通過瀏覽器將各子系統拼接成一個完整的系統;
  3. 各系統間存在少量交互,甚至沒有交互;

問題慢慢展現出來,系統間公共部分沒有統一維護點,同樣的功能、同樣的代碼分佈在各個系統中。

當然,我們可以通過發佈 jar 包的方式,共享功能代碼;但當 jar 升級時,就需要所有的子系統同步升級,運維開銷巨大。此時,我們需要引入服務化架構。

8 服務化架構

我們可以將通用功能封裝成一個服務,獨立開發、獨立部署、獨立維護。

服務化架構

在該方案中,我們將業務邏輯進行了進一步拆分:

  1. 整理各個系統間通用業務功能,將其封裝爲服務,以承載核心業務邏輯,構建成 服務集羣
  2. 原來的子系統或子頻道,變成薄薄的一層,不承載核心業務,只是根據業務流程對業務服務進行編排;
  3. 應用服務與業務服務間通過 HTTP 或 其他協議進行通信,常見的包括 Dubbo、Thrift等。

服務化解決了系統之間的直接調用問題,也就是常說的 RPC,整個系統的協調點全部由應用服務完成。這種架構適用於多種場景,但在一些需要異步處理的極端場景就顯得有心無力了。

此時,我們需要引入消息中間件。

9 引入消息隊列

服務化解決了直接調用問題,對於異步調用,最常見的便是消息中間件。

引入消息隊列

相比之前的架構,變化很小,只是在各個業務服務間添加了另外的一種調用方式。

10 小結

冰凍三尺非一日之寒,一個大型系統的構建也不是一朝一夕的事情。我們需要根據業務情況、數據量情況、請求量情況對系統進行合理規劃。

切記,架構不是越複雜越好,而是“適合自己的便是最好的”。

個人微信

掃一掃,添加我的微信,一起交流共同成長(備註爲技術學習)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章