1. 程式人生 > 實用技巧 >架構筆記二:高效能架構模式

架構筆記二:高效能架構模式

高效能架構模式

一.高效能資料庫叢集

1.1讀寫分離

讀寫分離的基本原理是將資料庫讀寫操作分散到不同的節點上,下面是其基本架構圖。

img

讀寫分離的基本實現是:

  • 資料庫伺服器搭建主從叢集,一主一從、一主多從都可以。
  • 資料庫主機負責讀寫操作,從機只負責讀操作。
  • 資料庫主機通過複製將資料同步到從機,每臺數據庫伺服器都儲存了所有的業務資料。
  • 業務伺服器將寫操作發給資料庫主機,將讀操作發給資料庫從機。

讀寫分離的實現邏輯並不複雜,但有兩個細節點將引入設計複雜度:主從複製延遲和分配機制。

解決主從複製延遲有幾種常見的方法:

  1. 寫操作後的讀操作指定發給資料庫主伺服器

    例如,註冊賬號完成後,登入時讀取賬號的讀操作也發給資料庫主伺服器。這種方式和業務強繫結,對業務的侵入和影響較大,如果哪個新來的程式設計師不知道這樣寫程式碼,就會導致一個 bug。

  2. 讀從機失敗後再讀一次主機

    這就是通常所說的“二次讀取”,二次讀取和業務無繫結,只需要對底層資料庫訪問的 API 進行封裝即可,實現代價較小,不足之處在於如果有很多二次讀取,將大大增加主機的讀操作壓力。例如,黑客暴力破解賬號,會導致大量的二次讀取操作,主機可能頂不住讀操作的壓力從而崩潰。

  3. 關鍵業務讀寫操作全部指向主機,非關鍵業務採用讀寫分離

    例如,對於一個使用者管理系統來說,註冊 + 登入的業務讀寫操作全部訪問主機,使用者的介紹、愛好、等級等業務,可以採用讀寫分離,因為即使使用者改了自己的自我介紹,在查詢時卻看到了自我介紹還是舊的,業務影響與不能登入相比就小很多,還可以忍受。

將讀寫操作區分開來,然後訪問不同的資料庫伺服器,一般有兩種方式:程式程式碼封裝和中介軟體封裝。

程式程式碼封裝指在程式碼中抽象一個數據訪問層(所以有的文章也稱這種方式為“中間層封裝”),實現讀寫操作分離和資料庫伺服器連線的管理。例如,基於 Hibernate 進行簡單封裝,就可以實現讀寫分離,基本架構是:

img

程式程式碼封裝的方式具備幾個特點:

  • 實現簡單,而且可以根據業務做較多定製化的功能。
  • 每個程式語言都需要自己實現一次,無法通用,如果一個業務包含多個程式語言寫的多個子系統,則重複開發的工作量比較大。
  • 故障情況下,如果主從發生切換,則可能需要所有系統都修改配置並重啟。

中介軟體封裝指的是獨立一套系統出來,實現讀寫操作分離和資料庫伺服器連線的管理。中介軟體對業務伺服器提供 SQL 相容的協議,業務伺服器無須自己進行讀寫分離。對於業務伺服器來說,訪問中介軟體和訪問資料庫沒有區別,事實上在業務伺服器看來,中介軟體就是一個數據庫伺服器。其基本架構是:

img

資料庫中介軟體的方式具備的特點是:

  • 能夠支援多種程式語言,因為資料庫中介軟體對業務伺服器提供的是標準 SQL 介面。
  • 資料庫中介軟體要支援完整的 SQL 語法和資料庫伺服器的協議(例如,MySQL 客戶端和伺服器的連線協議),實現比較複雜,細節特別多,很容易出現 bug,需要較長的時間才能穩定。
  • 資料庫中介軟體自己不執行真正的讀寫操作,但所有的資料庫操作請求都要經過中介軟體,中介軟體的效能要求也很高。
  • 資料庫主從切換對業務伺服器無感知,資料庫中介軟體可以探測資料庫伺服器的主從狀態。例如,向某個測試表寫入一條資料,成功的就是主機,失敗的就是從機。

由於資料庫中介軟體的複雜度要比程式程式碼封裝高出一個數量級,一般情況下建議採用程式語言封裝的方式,或者使用成熟的開源資料庫中介軟體。

1.2 分庫分表

讀寫分離分散了資料庫讀寫操作的壓力,但沒有分散儲存壓力,當資料量達到千萬甚至上億條的時候,單臺數據庫伺服器的儲存能力會成為系統的瓶頸,主要體現在這幾個方面:

  • 資料量太大,讀寫的效能會下降,即使有索引,索引也會變得很大,效能同樣會下降。
  • 資料檔案會變得很大,資料庫備份和恢復需要耗費很長時間。
  • 資料檔案越大,極端情況下丟失資料的風險越高(例如,機房火災導致資料庫主備機都發生故障)。

基於上述原因,單個數據庫伺服器儲存的資料量不能太大,需要控制在一定的範圍內。為了滿足業務資料儲存的需求,就需要將儲存分散到多臺資料庫伺服器上。

常見的分散儲存的方法“分庫分表”,其中包括“分庫”和“分表”兩大類。

1.2.1 業務分庫

業務分庫指的是按照業務模組將資料分散到不同的資料庫伺服器。例如,一個簡單的電商網站,包括使用者、商品、訂單三個業務模組,我們可以將使用者資料、商品資料、訂單資料分開放到三臺不同的資料庫伺服器上,而不是將所有資料都放在一臺資料庫伺服器上。

雖然業務分庫能夠分散儲存和訪問壓力,但同時也帶來了新的問題。

  1. join 操作問題

    業務分庫後,原本在同一個資料庫中的表分散到不同資料庫中,導致無法使用 SQL 的 join 查詢。

  2. 事務問題

    原本在同一個資料庫中不同的表可以在同一個事務中修改,業務分庫後,表分散到不同的資料庫中,無法通過事務統一修改。雖然資料庫廠商提供了一些分散式事務的解決方案(例如,MySQL 的 XA),但效能實在太低,與高效能儲存的目標是相違背的。

  3. 成本問題

    業務分庫同時也帶來了成本的代價,本來 1 臺伺服器搞定的事情,現在要 3 臺,如果考慮備份,那就是 2 臺變成了 6 臺。

原本在同一個資料庫中不同的表可以在同一個事務中修改,業務分庫後,表分散到不同的資料庫中,無法通過事務統一修改。雖然資料庫廠商提供了一些分散式事務的解決方案(例如,MySQL 的 XA),但效能實在太低,與高效能儲存的目標是相違背的。

業務分庫同時也帶來了成本的代價,本來 1 臺伺服器搞定的事情,現在要 3 臺,如果考慮備份,那就是 2 臺變成了 6 臺。

1.2.2 分表

將不同業務資料分散儲存到不同的資料庫伺服器,能夠支撐百萬甚至千萬使用者規模的業務,但如果業務繼續發展,同一業務的單表資料也會達到單臺數據庫伺服器的處理瓶頸。例如,淘寶的幾億使用者資料,如果全部存放在一臺資料庫伺服器的一張表中,肯定是無法滿足效能要求的,此時就需要對單表資料進行拆分。

單表資料拆分有兩種方式:垂直分表和水平分表。示意圖如下:

img

單表進行切分後,是否要將切分後的多個表分散在不同的資料庫伺服器中,可以根據實際的切分效果來確定,並不強制要求單表切分為多表後一定要分散到不同資料庫中。原因在於單表切分為多表後,新的表即使在同一個資料庫伺服器中,也可能帶來可觀的效能提升,如果效能能夠滿足業務要求,是可以不拆分到多臺資料庫伺服器的,畢竟我們在上面業務分庫的內容看到業務分庫也會引入很多複雜性的問題;如果單表拆分為多表後,單臺伺服器依然無法滿足效能要求,那就不得不再次進行業務分庫的設計了。

分表能夠有效地分散儲存壓力和帶來效能提升,但和分庫一樣,也會引入各種複雜性。

  1. 垂直分表

垂直分表適合將表中某些不常用且佔了大量空間的列拆分出去。

垂直分表引入的複雜性主要體現在表操作的數量要增加。例如,原來只要一次查詢就可以獲取 name、age、sex、nickname、description,現在需要兩次查詢,一次查詢獲取 name、age、sex,另外一次查詢獲取 nickname、description。

  1. 水平分表

水平分表適合錶行數特別大的表,有的公司要求單錶行數超過 5000 萬就必須進行分表,這個數字可以作為參考,但並不是絕對標準,關鍵還是要看錶的訪問效能。對於一些比較複雜的表,可能超過 1000 萬就要分表了;而對於一些簡單的表,即使儲存資料超過 1 億行,也可以不分表。但不管怎樣,當看到表的資料量達到千萬級別時,作為架構師就要警覺起來,因為這很可能是架構的效能瓶頸或者隱患。

水平分表相比垂直分表,會引入更多的複雜性,主要表現在下面幾個方面:

  • 路由

水平分表後,某條資料具體屬於哪個切分後的子表,需要增加路由演算法進行計算,這個演算法會引入一定的複雜性。

常見的路由演算法有:

範圍路由:選取有序的資料列(例如,整形、時間戳等)作為路由的條件,不同分段分散到不同的資料庫表中。以最常見的使用者 ID 為例,路由演算法可以按照 1000000 的範圍大小進行分段,1 ~ 999999 放到資料庫 1 的表中,1000000 ~ 1999999 放到資料庫 2 的表中,以此類推。

範圍路由設計的複雜點主要體現在分段大小的選取上,分段太小會導致切分後子表數量過多,增加維護複雜度;分段太大可能會導致單表依然存在效能問題,一般建議分段大小在 100 萬至 2000 萬之間,具體需要根據業務選取合適的分段大小。

範圍路由的優點是可以隨著資料的增加平滑地擴充新的表。例如,現在的使用者是 100 萬,如果增加到 1000 萬,只需要增加新的表就可以了,原有的資料不需要動。

範圍路由的一個比較隱含的缺點是分佈不均勻,假如按照 1000 萬來進行分表,有可能某個分段實際儲存的資料量只有 1000 條,而另外一個分段實際儲存的資料量有 900 萬條。

**Hash 路由:**選取某個列(或者某幾個列組合也可以)的值進行 Hash 運算,然後根據 Hash 結果分散到不同的資料庫表中。同樣以使用者 ID 為例,假如我們一開始就規劃了 10 個數據庫表,路由演算法可以簡單地用 user_id % 10 的值來表示資料所屬的資料庫表編號,ID 為 985 的使用者放到編號為 5 的子表中,ID 為 10086 的使用者放到編號為 6 的字表中。

Hash 路由設計的複雜點主要體現在初始表數量的選取上,表數量太多維護比較麻煩,表數量太少又可能導致單表效能存在問題。而用了 Hash 路由後,增加字表數量是非常麻煩的,所有資料都要重分佈。

Hash 路由的優缺點和範圍路由基本相反,Hash 路由的優點是表分佈比較均勻,缺點是擴充新的表很麻煩,所有資料都要重分佈。

配置路由:配置路由就是路由表,用一張獨立的表來記錄路由資訊。同樣以使用者 ID 為例,我們新增一張 user_router 表,這個表包含 user_id 和 table_id 兩列,根據 user_id 就可以查詢對應的 table_id。

配置路由設計簡單,使用起來非常靈活,尤其是在擴充表的時候,只需要遷移指定的資料,然後修改路由表就可以了。

配置路由的缺點就是必須多查詢一次,會影響整體效能;而且路由表本身如果太大(例如,幾億條資料),效能同樣可能成為瓶頸,如果我們再次將路由表分庫分表,則又面臨一個死迴圈式的路由演算法選擇問題。

  • join 操作

水平分表後,資料分散在多個表中,如果需要與其他表進行 join 查詢,需要在業務程式碼或者資料庫中介軟體中進行多次 join 查詢,然後將結果合併。

  • count() 操作

水平分表後,雖然物理上資料分散到多個表中,但某些業務邏輯上還是會將這些表當作一個表來處理。例如,獲取記錄總數用於分頁或者展示,水平分表前用一個 count() 就能完成的操作,在分表後就沒那麼簡單了。常見的處理方式有下面兩種:

count() 相加:具體做法是在業務程式碼或者資料庫中介軟體中對每個表進行 count() 操作,然後將結果相加。這種方式實現簡單,缺點就是效能比較低。例如,水平分表後切分為 20 張表,則要進行 20 次 count(*) 操作,如果序列的話,可能需要幾秒鐘才能得到結果。

記錄數表:具體做法是新建一張表,假如表名為“記錄數表”,包含 table_name、row_count 兩個欄位,每次插入或者刪除子表資料成功後,都更新“記錄數表”。

這種方式獲取表記錄數的效能要大大優於 count() 相加的方式,因為只需要一次簡單查詢就可以獲取資料。缺點是複雜度增加不少,對子表的操作要同步操作“記錄數表”,如果有一個業務邏輯遺漏了,資料就會不一致;且針對“記錄數表”的操作和針對子表的操作無法放在同一事務中進行處理,異常的情況下會出現操作子表成功了而操作記錄數表失敗,同樣會導致資料不一致。

此外,記錄數表的方式也增加了資料庫的寫壓力,因為每次針對子表的 insert 和 delete 操作都要 update 記錄數表,所以對於一些不要求記錄數實時保持精確的業務,也可以通過後臺定時更新記錄數表。定時更新實際上就是“count() 相加”和“記錄數表”的結合,即定時通過 count() 相加計算表的記錄數,然後更新記錄數表中的資料。

  • order by 操作

水平分表後,資料分散到多個子表中,排序操作無法在資料庫中完成,只能由業務程式碼或者資料庫中介軟體分別查詢每個子表中的資料,然後彙總進行排序。

總結分庫分表優化過程:

1.做硬體優化,例如從機械硬碟改成使用固態硬碟,當然固態硬碟不適合伺服器使用,只是舉個例子
2.先做資料庫伺服器的調優操作,例如增加索引,oracle有很多的引數調整;
3.引入快取技術,例如Redis,減少資料庫壓力
4.程式與資料庫表優化,重構,例如根據業務邏輯對程式邏輯做優化,減少不必要的查詢;
5.在這些操作都不能大幅度優化效能的情況下,不能滿足將來的發展,再考慮分庫分表,也要有預估性。

二.高效能NoSQL

常見的 NoSQL 方案分為 4 類。

  • K-V 儲存:解決關係資料庫無法儲存資料結構的問題,以 Redis 為代表。
  • 文件資料庫:解決關係資料庫強 schema 約束的問題,以 MongoDB 為代表。
  • 列式資料庫:解決關係資料庫大資料場景下的 I/O 問題,以 HBase 為代表。
  • 全文搜尋引擎:解決關係資料庫的全文搜尋效能問題,以 Elasticsearch 為代表。K-V 儲存

2.1 K-V 儲存

K-V 儲存的全稱是 Key-Value 儲存,其中 Key 是資料的標識,和關係資料庫中的主鍵含義一樣,Value 就是具體的資料。Redis 是 K-V 儲存的典型代表,它是一款開源(基於 BSD 許可)的高效能 K-V 快取和儲存系統。Redis 的 Value 是具體的資料結構,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被稱為資料結構伺服器。

Redis 的缺點主要體現在並不支援完整的 ACID 事務,Redis 雖然提供事務功能,但 Redis 的事務和關係資料庫的事務不可同日而語,Redis 的事務只能保證隔離性和一致性(I 和 C),無法保證原子性和永續性(A 和 D)。

2.2 文件資料庫

為了解決關係資料庫 schema 帶來的問題,文件資料庫應運而生。文件資料庫最大的特點就是 no-schema,可以儲存和讀取任意的資料。目前絕大部分文件資料庫儲存的資料格式是 JSON(或者 BSON),因為 JSON 資料是自描述的,無須在使用前定義欄位,讀取一個 JSON 中不存在的欄位也不會導致 SQL 那樣的語法錯誤。

文件資料庫的 no-schema 特性,給業務開發帶來了幾個明顯的優勢。

  1. 新增欄位簡單

  2. 歷史資料不會出錯

  3. 可以很容易儲存複雜資料

文件資料庫的這個特點,特別適合電商和遊戲這類的業務場景。

文件資料庫 no-schema 的特性帶來的這些優勢也是有代價的,最主要的代價就是不支援事務;另外一個缺點就是無法實現關係資料庫的 join 操作。

2.3 列式資料庫

顧名思義,列式資料庫就是按照列來儲存資料的資料庫,與之對應的傳統關係資料庫被稱為“行式資料庫”,因為關係資料庫是按照行來儲存資料的。

關係資料庫按照行式來儲存資料,主要有以下幾個優勢:

  • 業務同時讀取多個列時效率高,因為這些列都是按行儲存在一起的,一次磁碟操作就能夠把一行資料中的各個列都讀取到記憶體中。
  • 能夠一次性完成對一行中的多個列的寫操作,保證了針對行資料寫操作的原子性和一致性;否則如果採用列儲存,可能會出現某次寫操作,有的列成功了,有的列失敗了,導致資料不一致。

基於上述列式儲存的優缺點,一般將列式儲存應用在離線的大資料分析和統計場景中,因為這種場景主要是針對部分列單列進行操作,且資料寫入後就無須再更新刪除。

2.4 全文搜尋引擎

傳統的關係型資料庫通過索引來達到快速查詢的目的,但是在全文搜尋的業務場景下,索引也無能為力,主要體現在:

  • 全文搜尋的條件可以隨意排列組合,如果通過索引來滿足,則索引的數量會非常多。
  • 全文搜尋的模糊匹配方式,索引無法滿足,只能用 like 查詢,而 like 查詢是整表掃描,效率非常低。

全文搜尋引擎的技術原理被稱為“倒排索引”(Inverted index),也常被稱為反向索引、置入檔案或反向檔案,是一種索引方法,其基本原理是建立單詞到文件的索引。

三.高效能負載均衡

高效能叢集的複雜性主要體現在需要增加一個任務分配器,以及為任務選擇一個合適的任務分配演算法。而實際上任務分配並不只是考慮計算單元的負載均衡,不同的任務分配演算法目標是不一樣的,有的基於負載考慮,有的基於效能(吞吐量、響應時間)考慮,有的基於業務考慮。負載均衡不只是為了計算單元的負載達到均衡狀態。

3.1 分類及架構

3.1.1 DNS 負載均衡

DNS 是最簡單也是最常見的負載均衡方式,一般用來實現地理級別的均衡。例如,北方的使用者訪問北京的機房,南方的使用者訪問深圳的機房。DNS 負載均衡的本質是 DNS 解析同一個域名可以返回不同的 IP 地址。

DNS 負載均衡實現簡單、成本低,但也存在粒度太粗、負載均衡演算法少等缺點。仔細分析一下優缺點,其優點有:

簡單、成本低:負載均衡工作交給 DNS 伺服器處理,無須自己開發或者維護負載均衡裝置。

就近訪問,提升訪問速度:DNS 解析時可以根據請求來源 IP,解析成距離使用者最近的伺服器地址,可以加快訪問速度,改善效能。

缺點有:

更新不及時:DNS 快取的時間比較長,修改 DNS 配置後,由於快取的原因,還是有很多使用者會繼續訪問修改前的 IP,這樣的訪問會失敗,達不到負載均衡的目的,並且也影響使用者正常使用業務。

擴充套件性差:DNS 負載均衡的控制權在域名商那裡,無法根據業務特點針對其做更多的定製化功能和擴充套件特性。分配策略比較簡單:DNS 負載均衡支援的演算法少;不能區分伺服器的差異(不能根據系統與服務的狀態來判斷負載);也無法感知後端伺服器的狀態。

3.1.2 硬體負載均衡

硬體負載均衡是通過單獨的硬體裝置來實現負載均衡功能,這類裝置和路由器、交換機類似,可以理解為一個用於負載均衡的基礎網路裝置。

硬體負載均衡的優點是:

  • 功能強大:全面支援各層級的負載均衡,支援全面的負載均衡演算法,支援全域性負載均衡。
  • 效能強大:對比一下,軟體負載均衡支援到 10 萬級併發已經很厲害了,硬體負載均衡可以支援 100 萬以上的併發。
  • 穩定性高:商用硬體負載均衡,經過了良好的嚴格測試,經過大規模使用,穩定性高。
  • 支援安全防護:硬體均衡裝置除具備負載均衡功能外,還具備防火牆、防 DDoS 攻擊等安全功能。

硬體負載均衡的缺點是:

  • 價格昂貴:最普通的一臺 F5 就是一臺“馬 6”,好一點的就是“Q7”了。
  • 擴充套件能力差:硬體裝置,可以根據業務進行配置,但無法進行擴充套件和定製。軟體負載均衡

3.1.3 軟體負載均衡

軟體負載均衡通過負載均衡軟體來實現負載均衡功能,常見的有 Nginx 和 LVS,其中 Nginx 是軟體的 7 層負載均衡,LVS 是 Linux 核心的 4 層負載均衡。4 層和 7 層的區別就在於協議和靈活性,Nginx 支援 HTTP、E-mail 協議;而 LVS 是 4 層負載均衡,和協議無關,幾乎所有應用都可以做,例如,聊天、資料庫等。

軟體負載均衡的優點:

  • 簡單:無論是部署還是維護都比較簡單。
  • 便宜:只要買個 Linux 伺服器,裝上軟體即可。
  • 靈活:4 層和 7 層負載均衡可以根據業務進行選擇;
  • 也可以根據業務進行比較方便的擴充套件,例如,可以通過 Nginx 的外掛來實現業務的定製化功能。

其實下面的缺點都是和硬體負載均衡相比的,並不是說軟體負載均衡沒法用。

  • 效能一般:一個 Nginx 大約能支撐 5 萬併發。功能沒有硬體負載均衡那麼強大。
  • 一般不具備防火牆和防 DDoS 攻擊等安全功能。

3.1.4 負載均衡典型架構

前面我們介紹了 3 種常見的負載均衡機制:DNS 負載均衡、硬體負載均衡、軟體負載均衡,每種方式都有一些優缺點,但並不意味著在實際應用中只能基於它們的優缺點進行非此即彼的選擇,反而是基於它們的優缺點進行組合使用。具體來說,組合的基本原則為:

  • DNS 負載均衡用於實現地理級別的負載均衡;
  • 硬體負載均衡用於實現叢集級別的負載均衡;
  • 軟體負載均衡用於實現機器級別的負載均衡。

3.2 負載均衡演算法

載均衡演算法數量較多,而且可以根據一些業務特性進行定製開發,拋開細節上的差異,根據演算法期望達到的目的,大體上可以分為下面幾類。

  • 任務平分類(輪詢、加權輪詢):負載均衡系統將收到的任務平均分配給伺服器進行處理,這裡的“平均”可以是絕對數量的平均,也可以是比例或者權重上的平均。

  • 負載均衡類(負載最低優先):負載均衡系統根據伺服器的負載來進行分配,這裡的負載並不一定是通常意義上我們說的“CPU 負載”,而是系統當前的壓力,可以用 CPU 負載來衡量,也可以用連線數、I/O 使用率、網絡卡吞吐量等來衡量系統的壓力。

  • 效能最優類:負載均衡系統根據伺服器的響應時間來進行任務分配,優先將新任務分配給響應最快的伺服器。Hash 類:負載均衡系統根據任務中的某些關鍵資訊進行 。
    體上可以分為下面幾類。

  • 任務平分類(輪詢、加權輪詢):負載均衡系統將收到的任務平均分配給伺服器進行處理,這裡的“平均”可以是絕對數量的平均,也可以是比例或者權重上的平均。

  • 負載均衡類(負載最低優先):負載均衡系統根據伺服器的負載來進行分配,這裡的負載並不一定是通常意義上我們說的“CPU 負載”,而是系統當前的壓力,可以用 CPU 負載來衡量,也可以用連線數、I/O 使用率、網絡卡吞吐量等來衡量系統的壓力。

  • 效能最優類:負載均衡系統根據伺服器的響應時間來進行任務分配,優先將新任務分配給響應最快的伺服器。Hash 類:負載均衡系統根據任務中的某些關鍵資訊進行 。

  • Hash 運算,將相同 Hash 值的請求分配到同一臺伺服器上。常見的有源地址 Hash、目標地址 Hash、session id hash、使用者 ID Hash