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


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

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

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

從一個電商網站開始

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

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

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

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帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟着Mic學架構」公眾號公眾號獲取更多技術干貨!


免責聲明!

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



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