支撐千萬級併發的架構師如何一步步演進的?

跟著Mic學架構發表於2021-11-11

我們現在所看到的大型網站或者架構,都是從小的網站和簡單的架構一步步發展起來的,當然,也有一些是基於已有的分散式架構來構建的,也是看業務發展的情況而定。在架構的迭代演進的過程中,會遇到很多問題,就像升級打怪一樣,等級越高,遇到的怪獸越強。

之前有個學員問了我,什麼是架構。我是這麼回答的。比如我們要建一棟房子,那建房子之前,一定要有一個建築圖紙,這個圖紙描述了建築的形狀、內部結構、材料、裝置等資訊。工程實施的時候會基於這個圖紙進行構建。軟體架構也是如此,軟體架構相當於軟體系統的一個設計圖紙,這個圖紙上描述了各個元件之間的連線方式和詳細的描述了元件之間的通訊機制。 而程式設計師在實施階段,就是將這些抽象圖紙細化為實際元件,比如具體的介面定義,類的定義等

那麼我們接下來基於純技術角度模擬一個簡單的案例來看看架構迭代帶來的問題和解決方案,通過這樣一個迭代讓大家更清晰的理解架構。整個過程,重點關注的是資料量和訪問量的變化帶來架構的變化。不具體關注業務功能

從一個電商網站開始

為了更好的理解,我們用電商網站來舉例,作為一個交易型別的網站,一定會具備

使用者(使用者註冊、使用者管理)、商品(商品展示、商品管理)、交易(下單、支付)這些功能

假如我們只需要支援這幾個基本功能,那麼我們最開始的架構應該可能是這樣的

image-20200605231630780

這個地方要注意的是,各個功能模組之間是通過JVM內部的方法呼叫來進行互動的,而應用和資料庫之間是通過JDBC進行訪問。

單機負載告警,資料庫與應用分離

隨著網站的開放,訪問量不斷增大,那麼這個時候伺服器的負載勢必會持續升高,必須要才需一些辦法來應付。這裡先不考慮更換機器和各種軟體層面的優化,先從架構的結構上來做一些調整。我們可以把資料庫與應用從一臺機器分到兩臺機器

image-20200605231758944

變化:

網站從一臺變成了2臺,這個變化對我們來說影響非常小。單機的情況下,我們應用採用JDBC的方式來和資料庫進行連線,現在資料庫與應用分開了,我們只需要在配置檔案中把資料庫的地址從本機改成資料庫伺服器的ip地址就行。對於開發、測試、部署都沒有影響

調整以後我們能夠緩解當前的系統壓力,不過隨著時間的退役,訪問量繼續增大的話,我們的系統還是需要做改造

為什麼這麼分呢?從計算機本身的角度來考慮的話,一個請求的訪問到處理最終到返回,效能瓶頸只會是:CPU、檔案IO、網路IO、記憶體、等因素。而一臺計算機中這些緯度是有效能瓶頸的,如果某個資源消耗過多,通常會造成系統的響應速度較慢,所以增加一臺機器,使得資料庫的IO和CPU資源獨佔一臺機器從而增加效能。

這個地方插入一點題外話,就是簡單說一下各個資源的消耗原因。

CPU/IO/記憶體

  1. 主要是上下文的切換,因為每個CPU核心在同一時刻只能執行一個執行緒,而CPU的排程有幾種方式,比如搶佔式和輪詢等,以搶佔式為例,每個執行緒會分配一定的執行時間,當達到執行時間、執行緒中有IO阻塞或者有高優先順序的執行緒要執行時。CPU會切換執行其他執行緒。而在切換的過程中,需要儲存當前執行緒的執行狀態並恢復要執行的執行緒狀態,這個過程就是上下文切換。比如IO、鎖等待等場景下也會觸發上下文切換,當上下文切換過多時會造成核心佔用比較多的CPU。
  2. 檔案IO,比如頻繁的日誌寫入,磁碟本身的處理速度較慢、都會造成IO效能問題
  3. 網路IO,頻寬不夠
  4. 記憶體,包括記憶體溢位、記憶體洩漏、記憶體不足

實際上不管是應用層的調優也好,還是硬體的升級也好。其實無非就是這幾個因素的調整。

應用伺服器複雜告警,如何讓應用伺服器走向叢集

假如說這個時候應用伺服器的壓力變大了,根據對應用的檢測結果,可以針對性的對效能壓力大的地方進行優化。我們這裡考慮通過水平擴容來進行優化,把單機變為叢集

image-20200605231912895

應用伺服器從一臺變為兩臺,這兩個應用伺服器之間沒有直接的互動,他們都依賴資料庫對外提供服務,那麼這個時候會丟擲兩個問題

  1. 終端使用者對應兩個應用伺服器訪問的選擇

對於這個問題,可以採用DNS解決,也可以通過負載均衡裝置來解決

  1. session的問題?

水平和垂直擴容

對於大型的分散式架構而言,我們一直在追求一種簡單、優雅的方式來應對訪問量和資料量的增長。而這種方式通常指的是不需要改動軟體程式,僅僅通過硬體升級或者增加機器就可以解決。而這種就是分散式架構下的伸縮設計

伸縮分為垂直伸縮和水平伸縮兩種

垂直伸縮:表示通過升級或者增加單臺機器的硬體來支撐訪問量以及資料量增長的方式,垂直伸縮的好處在於技術難度比較低,運營和改動成本也相對較低。但是缺點是機器效能是有瓶頸的,同時升級高效能的小型機或者大型機,成本是非常大的。這也是阿里去IOE的一個原因之一

增加CPU核心數:增加CPU後系統的服務能力能夠得到大的增長,比如響應速度、同時可以處理的執行緒數。但是引入CPU後也會帶來一些顯著的問題

  • 1.鎖競爭加劇;多個執行緒同時執行訪問某個共享資料,那麼就涉及到鎖競爭,鎖競爭激烈時會導致很多執行緒都在等待鎖,所以即時增加CPU也無法讓執行緒得到更快的處理。當然這裡是有調優手段的,可以通過調優手段來降低鎖競爭*
  • 2.支撐併發請求的執行緒數是固定的,那麼即時增加CPU,系統的服務能力也不會得到提升*
  • 3.對於單執行緒任務,多核心CPU是沒有太大的作用的*

*增加記憶體:增加記憶體可以直接提成系統的響應速度,當然,也有可能達不到效果,就是如果JVM堆記憶體是固定的。

水平伸縮:通過增加機器來支撐訪問量及資料量增長的方式,成為水平伸縮,水平伸縮理論上來說沒有瓶頸,但是缺點是技術要求比較高,同時給運維帶來了更大的挑戰

垂直伸縮和水平伸縮都有各自的有點,我們在實際使用過程中都會對兩者做結合,一方面要考慮硬體升級的成本,一方面要考慮軟體改造的成本。

引入負載均衡裝置

服務路由,基於負載均衡裝置來實現

image-20200605232035544

引入負載均衡器以後,會帶來session相關的問題

負載均衡演算法

輪詢(Round Robin)法

將請求按順序輪流分配到後臺伺服器上,均衡的對待每一臺伺服器,而不關心伺服器實際的連線數和當前的系統負載

缺點:當叢集中伺服器硬體配置不同、效能差別大時,無法區別對待

隨機法

通過系統隨機函式,根據後臺伺服器列表的大小值來隨機選取其中一臺進行訪問。隨著呼叫量的增大,其實際效果越來越接近於平均分配流量到後臺的每一臺伺服器,也就是輪詢法的效果

優點:簡單使用,不需要額外的配置和演算法。

缺點:隨機數的特點是在資料量大到一定量時才能保證均衡,所以如果請求量有限的話,可能會達不到均衡負載的要求。

源地址雜湊法

根據服務消費者請求客戶端的IP地址,通過雜湊函式計算得到一個雜湊值,將這個雜湊值和伺服器列表的大小進行取模運算,得到的結果便是要訪問的伺服器地址的序號。採用源地址雜湊法進行負載均衡,相同的IP客戶端,如果伺服器列表不變,將對映到同一個後臺伺服器進行訪問。

加權輪詢(Weight Round Robin)法

不同的後臺伺服器可能機器的配置和當前系統的負載並不相同,因此它們的抗壓能力也不一樣。跟配置高、負載低的機器分配更高的權重,使其能處理更多的請求,而配置低、負載高的機器,則給其分配較低的權重,降低其系統負載,加權輪詢很好的處理了這一問題,並將請求按照順序且根據權重分配給後端

最小連線數法

前面幾種方式都是通過對請求次數的合理分配最大可能提高伺服器的利用率,但是實際上,請求次數的均衡並不能代表負載的均衡。所以,引入了最小連線數法。它正是根據後端伺服器當前的連線情況,動態的選取其中當前積壓連線數最少的一臺伺服器來處理當前請求,儘可能的提高後臺伺服器利用率,將負載合理的分流到每一臺伺服器。

session問題

我們開啟一個網頁,基本上需要瀏覽器和web伺服器進行多次互動,我們都知道Http協議本身是無狀態的,這也是http協議設計的初衷,客戶端只需要簡單的向伺服器請求下載某些檔案,無論是客戶端還是伺服器都沒必要記錄彼此過去的行為,每一次請求之間是獨立的,好比一個顧客和一個自動售貨機之間的關係一樣.

而實際上,我們很多的場景都需要帶有狀態的特性,因此聰明的我們引入了session+cookie機制來記住每次請求的會話。

在會話開始時,給當前會話分配一個唯一的會話標識(sessionid),然後通過cookie把這個標識告訴瀏覽器,以後在每次請求的時候,瀏覽器都會帶上這個會話標識來告訴web伺服器請求屬於哪個會話。在web伺服器上,各個會話有獨立的儲存,儲存不同會話的資訊。

如果遇到禁用cookie的情況,一般的做法就是把這個會話標識放到URL的引數中。

image-20200605232112456

而我們應用伺服器從一臺變成兩臺後,就會遇到session問題

分散式環境下的session共享

Session共享在當前這個網際網路背景下,已經不是一個新鮮的話題了,而且如何解決session共享其實也有很多非常成熟的方案

伺服器實現的session複製或session共享,這型別的共享session是和伺服器緊密相關的

我們在Web伺服器之間增加了會話資料的同步,通過同步就保證了不同Web伺服器之間Session資料的一致。一般應用容器都支援Session Replication方式

存在問題:

  1. 同步Session資料造成了網路頻寬的開銷。只要Session資料有變化,就需要將資料同步到所有其他機器上,機器越多,同步帶來的網路頻寬開銷就越大。
  2. 每臺Web伺服器都要儲存所有Session資料,如果整個叢集的Session資料很多(很多人同時訪問網站)的話,每臺機器用於儲存Session資料的內容佔用會很嚴重。

這個方案是靠應用容器來完成Session的複製從而解決Session的問題的,應用本身並不關心這個事情。這個方案不適合叢集機器數多的場景。

利用成熟的技術做session複製,比如12306使用的gemfire,比如常見的記憶體資料庫如Redis

image-20200605232210095

Session資料不儲存到本機而且存放到一個集中儲存的地方,修改Session也是發生在集中儲存的地方。Web伺服器使用Session從集中儲存的地方讀取。這樣保證了不同Web伺服器讀取到的Session資料都是一樣的。儲存Session的具體方式可以是資料庫

存在問題:

  1. 讀寫Session資料引入了網路操作,這相對於本機的資料讀取來說,問題就在於存在時延和不穩定性,不過我們的通訊基本都是發生在內網,問題不大。
  2. 如果集中儲存Session的機器或者叢集有問題,就會影響到我們的應用。

相對於Session Replication,當Web伺服器數量比較大、Session數比較多的時候,這個集中儲存方案的優勢是非常明顯的。

將session維護在客戶端

很容易想到就是利用cookie,但是客戶端存在風險,資料不安全,而且可以存放的資料量比較小,所以將session維護在客戶端還要對session中的資訊加密。

我們的Session資料放到Cookie中,然後在Web伺服器上從Cookie中生成對應的Session資料。這就好比我們每次都把自己的碗筷帶在身上,這樣去那家飯店就可以隨意選擇了。相對前面的集中儲存方案,不會依賴外部的儲存系統,也就不存在從外部系統獲取、寫入Session資料的網路時延、不穩定性了。

存在問題:

安全性。Session資料本來都是服務端資料,而這個方案是讓這些服務端資料到了外部網路及客戶端,因此存在安全性上的問題。我們可以對寫入的Cookie的Session資料做加密,不過對於安全來說,物理上不能接觸才是安全的。

資料庫壓力變大,讀寫分離吧

隨著業務的繼續增長,資料量和訪問量持續增加。對於大型網站來說,有不少業務是讀多寫少,這個情況也會直接反饋到資料庫上。那麼對於這種情況來說,我們可以考慮採用讀寫分離的方式來優化資料庫的壓力

image-20200605232258797

這個結構的變化會帶來兩個問題

  1. 資料如何同步

我們希望通過讀庫來分擔主庫上讀的壓力,那麼首先需要解決的是怎麼複製到讀庫的問題。資料庫系統一般都提供了資料複製的功能,我們可以直接使用資料庫系統自身的機制。不同的資料庫系統有不同的支援,比如Mysql支援Master+slave的結構提供資料複製機制

  1. 應用對資料來源如何路由

對於應用來說,增加一個讀庫對結構變化產生了一定的影響,也就是我們的應用需要根據不同的情況來選擇不同的資料庫源

搜尋引擎其實是一個讀庫

搜尋引擎其實可以理解成一個讀庫,我們的商品儲存在資料庫中,而網站需要提供使用者實時檢索的功能,尤其是在商品搜尋這塊。對於這樣的讀請求,如果全部走讀庫,其實效能也會存在一個瓶頸。而使用搜尋引擎,不僅僅能大大提高檢索速度。還能減輕讀資料庫的壓力

而搜尋引擎最重要的工作,就是需要根據被搜尋的資料來構建索引,而隨著被搜尋的資料的變化,索引也需要相應變化。

image-20200605232320136

搜尋叢集的使用方式和讀庫的使用方式是一樣的,只是構建索引的過程基本都是需要我們自己來實現。可以從兩個緯度對搜尋引擎構建索引的方式進行規劃,一個是按照全量/增量劃分。一種是按照實時/非實時劃分。

全量方式用於第一次建立索引,可能是新建,也可能是重建。而增量的方式是在全量的基礎上持續更新索引。

實時和非實時提現在索引更新的時間上,實時是最好的,非實時主要考慮到對資料來源頭的保護

總的來說,搜尋引擎技術解決了站內搜尋時某些場景下的讀的問題,提供了更好的查詢效率。

加速資料讀取的利器-快取及分散式儲存

在大型網站中,基本上就是在解決儲存和計算的問題,所以儲存是一個很重要的支撐系統。網站建設初期我們都是從關係型資料庫開始的,而且很多時候為了方便,我們會把一些業務邏輯放在資料庫裡面去做,比如觸發器、儲存過程。雖然在前期能夠很方便的解決問題,但是在未來的發展過程中會帶來很多的麻煩,比如資料量大了以後,要做分庫分表操作等. 同時,業務發展到一定的體量以後,對儲存的需求不能完全通過關係型資料庫來滿足

分散式檔案系統

對一些圖片、大文字,使用資料庫就不合適了,所以我們會採用分散式檔案系統來實現檔案儲存,分散式檔案系統有很多產品、比如淘寶的TFS、google的GFS。還有開源的HDFS

NoSQL

NoSQL 我們可以理解成Not Only SQL、或者是No SQL。 兩種意思都是為了表達在大型網站中,關係型資料庫可以解決大部分問題,但是對於不同內容的特徵、訪問特徵、事務特徵等對儲存的要求是不一樣的。NoSQL是定位於是檔案系統和SQL關係型資料庫之間的範疇。

資料快取都是為了更好的服務

大型網站內部都會用到一些資料快取,主要用於分擔資料庫的讀的壓力,快取系統一般是用來儲存和查詢鍵值對的。應用系統中一般會把熱點資料放入到快取,而快取的填充也應該是由應用系統完成。如果資料不存在,則從資料庫獨處資料後放入快取。隨著時間的推移,當快取容量不夠需要清除資料時,最近不被訪問的資料就會被清理掉。還有一種方式就是在資料庫的資料發生變化後,主動把資料放入到快取系統中,這樣的好處是資料變化時能夠及時更新快取的資料,不會造成讀取失效

image-20200605232359062

頁面快取

除了資料快取外,我們還可以對頁面做快取,資料快取可以加速應用在響應請求時的資料讀取數度,但是最終展示給使用者的還是頁面,有些動態產生的頁面或者訪問量特別高的頁面,我們會對頁面或者內容做一些快取。

彌補關係型資料庫的不足,引入分散式儲存

我們應用最多的主要還是關係型資料庫,但是在有些場景中,關係型資料庫不是很合適。所以我們會引入分散式儲存系統,比如redis、mongoDB、cassandra、HBase等。

根據不同的場景和資料結構型別,選擇合適的分散式儲存系統可以極大提高效能。分散式系統通過叢集提供一個高容量、高併發訪問、資料冗餘融債的支援。

image-20200605232438961

讀寫分離後,資料庫又遇到瓶頸

通過讀寫分離以及在某些場景用分散式儲存系統替換關係型資料庫的方式,能夠降低主庫的壓力,解決資料儲存方面的問題,不過隨著業務的發展,我們的主庫也會遇到瓶頸。推演到現在,我們的網站各個模組:交易、商品、使用者資料都還是儲存在一個資料庫。儘管增加了快取、讀寫分離的方式,但是資料庫的壓力仍然在持續增加,因此我們可以對資料垂直拆分和水平拆分來解決資料庫壓力問題

專庫專用,資料垂直拆分

垂直拆分的意思是把資料庫中不同的業務資料拆分到不同的資料庫中,那麼根據我們推演的例子,把使用者、交易、商品的資料分開

image-20200605232517496

不同業務的資料從原來的一個資料庫拆分到了多個資料庫中,那麼就需要考慮到如何處理原來單機跨業務的事務

  1. 使用分散式事務解決
  2. 去掉事務或者不追求強事務的支援

對資料進行垂直拆分後,解決了把所有業務資料放在一個資料庫中的壓力問題,並且也可以根據不同業務的特點進行更多的優化

垂直拆分後,遇到瓶頸,資料水平拆分

與垂直拆分對應的還有資料水平拆分,資料水平拆分就是把同一個表的資料拆分到兩個資料庫中,產生資料水平拆分的原因是某個業務的資料表的資料量或者更新量達到了單個資料庫的瓶頸,這個時候就可以把表拆到兩個或者多個資料庫中。

資料水平拆分與讀寫分離的區別是,讀寫分離解決的是讀壓力大的問題,對於資料量大或者更新量大的情況並不起作用。

資料水平拆分與資料垂直拆分的區別是,垂直拆分是把不同的表拆分到不同的資料庫,而水平拆分是把同一個表拆分到不同的資料庫中。

我們可以進一步把使用者表拆分到兩個資料庫中,它們擁有結構一模一樣的使用者表,而且每個庫中的使用者表都只涵蓋了一部分的使用者,兩個資料庫的使用者和在一起就相當於沒有拆分之前的使用者表

image-20200605232544106

水平拆分帶來的影響

  1. sql路由問題,需要根據一個條件來決定當前請求發到那個資料庫中
  2. 主鍵的處理,不能採用自增id,需要全域性id

由於同一個業務的資料被拆分到不同的資料庫,因此涉及到一些查詢需要跨兩個資料庫獲取,如果資料量太大並且需要分頁,就比較難處理了

資料庫問題解決後,應用面對的挑戰

前面講的讀寫分離、分散式儲存、資料垂直拆分和水平拆分都是解決資料方面的問題,接下來我們要看看應用方面的變化

隨著業務的發展,應用的功能會越來越多,應用也會越來越大,我們需要思考如何不讓應用持續變大,這就需要把應用拆開,從一個應用變為兩個甚至是多個。

第一種方式

根據業務的特性把應用拆分,在我們的例子中,主要業務功能分三個部分、使用者、商品、交易。我們可以把原來的一個應用拆成分別以交易和商品為主的兩個應用,對於交易和商品都會有設計使用使用者的地方,我們讓這兩個系統自己完成涉及使用者的工作,而類似使用者註冊、登入等基礎的使用者工作,可以暫時交給兩個系統之一來完成

image-20200605232613647

我們還可以按照使用者註冊、使用者登入、使用者資訊維護等再拆分,變成三個系統,不過這樣拆分後在不同系統中會有一些相似的程式碼,比如使用者相關的程式碼,如何能夠保障這部分程式碼的一致以及如何對其他模組提供複用也是需要解決的問題。而且,這樣拆分出來的新系統之間沒有直接的相互呼叫

服務化的道路

我們在來看一下服務化的做法,我們把應用分為三層,處於最上端的是web系統,用於完成不同的業務功能,處於中間的是一些服務中心,不同的服務中心提供不同的業務服務;處於最下層的則是業務的資料庫

image-20200605232641743

與之前相比有幾個重要的變化,首先業務功能之間的訪問不僅僅是單機內部的方法呼叫,還引入了遠端的服務呼叫。其次,共享程式碼不再是散落在不同的應用中,這些實現被放在各個服務中心。最後,資料庫的連線也發生了一些變化,我們把資料庫的互動工作放到了服務中心,讓前端的web應用更加註重與瀏覽器的互動工作,而不必過多關注業務邏輯的事情。連結資料庫的任務交給響應的業務服務中心了,這樣可以降低資料庫的連線數。

而服務中心不僅把一些可以共用的程式碼集中管理,而且還使得這些程式碼變得更好維護。

服務化的方式會帶來很多好處,首先,從結構上來看,系統架構更加清晰了,比原本的架構更加立體。從穩定性上來看,一些散落在多個應用系統中的程式碼變成了服務並且由專門的團隊進行統一維護,一方面可以提高程式碼的質量,另一方面由於基礎核心模組相對穩定,修改和釋出的頻次相對於業務系統來說會少很多,這也會提高整個架構的穩定性。最後,更加底層的資源由服務層統一管理,結構更加清晰,對於團隊開發效率來說有比較大的提高

服務化的方式,對於研發也會有很大的影響,以前的開發模式是幾個大團隊負責幾個大應用,隨著服務化的落地,我們的應用數量會飛速增長,系統內部的依賴關係也會變的錯綜複雜,同時團隊也進行了拆分,每個小團隊專注於某個具體的服務或者應用上,迭代效率也會更高

版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注同名微信公眾號獲取更多技術乾貨!

相關文章