支撐千萬級並發的架構師如何一步步演進的?
我們現在所看到的大型網站或者架構,都是從小的網站和簡單的架構一步步發展起來的,當然,也有一些是基於已有的分散式架構來構建的,也是看業務發展的情況而定。在架構的迭代演進的過程中,會遇到很多問題,就像升級打怪一樣,等級越高,遇到的怪獸越強。
之前有個學員問了我,什麼是架構。我是這麼回答的。比如我們要建一棟房子,那建房子之前,一定要有一個建築圖紙,這個圖紙描述了建築的形狀、內部結構、材料、設備等資訊。工程實施的時候會基於這個圖紙進行構建。軟體架構也是如此,軟體架構相當於軟體系統的一個設計圖紙,這個圖紙上描述了各個組件之間的連接方式和詳細的描述了組件之間的通訊機制。 而程式設計師在實施階段,就是將這些抽象圖紙細化為實際組件,比如具體的介面定義,類的定義等
那麼我們接下來基於純技術角度模擬一個簡單的案例來看看架構迭代帶來的問題和解決方案,通過這樣一個迭代讓大家更清晰的理解架構。整個過程,重點關注的是數據量和訪問量的變化帶來架構的變化。不具體關注業務功能
從一個電商網站開始
為了更好的理解,我們用電商網站來舉例,作為一個交易類型的網站,一定會具備
用戶(用戶註冊、用戶管理)、商品(商品展示、商品管理)、交易(下單、支付)這些功能
假如我們只需要支援這幾個基本功能,那麼我們最開始的架構應該可能是這樣的
這個地方要注意的是,各個功能模組之間是通過JVM內部的方法調用來進行交互的,而應用和資料庫之間是通過JDBC進行訪問。
單機負載告警,資料庫與應用分離
隨著網站的開放,訪問量不斷增大,那麼這個時候伺服器的負載勢必會持續升高,必須要才需一些辦法來應付。這裡先不考慮更換機器和各種軟體層面的優化,先從架構的結構上來做一些調整。我們可以把資料庫與應用從一台機器分到兩台機器
變化:
網站從一台變成了2台,這個變化對我們來說影響非常小。單機的情況下,我們應用採用JDBC的方式來和資料庫進行連接,現在資料庫與應用分開了,我們只需要在配置文件中把資料庫的地址從本機改成資料庫伺服器的ip地址就行。對於開發、測試、部署都沒有影響
調整以後我們能夠緩解當前的系統壓力,不過隨著時間的退役,訪問量繼續增大的話,我們的系統還是需要做改造
為什麼這麼分呢?從電腦本身的角度來考慮的話,一個請求的訪問到處理最終到返回,性能瓶頸只會是:CPU、文件IO、網路IO、記憶體、等因素。而一台電腦中這些緯度是有性能瓶頸的,如果某個資源消耗過多,通常會造成系統的響應速度較慢,所以增加一台機器,使得資料庫的IO和CPU資源獨佔一台機器從而增加性能。
這個地方插入一點題外話,就是簡單說一下各個資源的消耗原因。
CPU/IO/****記憶體:
-
主要是上下文的切換,因為每個CPU核心在同一時刻只能執行一個執行緒,而CPU的調度有幾種方式,比如搶佔式和輪詢等,以搶佔式為例,每個執行緒會分配一定的執行時間,當達到執行時間、執行緒中有IO阻塞或者有高優先順序的執行緒要執行時。CPU會切換執行其他執行緒。而在切換的過程中,需要存儲當前執行緒的執行狀態並恢復要執行的執行緒狀態,這個過程就是上下文切換。比如IO、鎖等待等場景下也會觸發上下文切換,當上下文切換過多時會造成內核佔用比較多的CPU。
-
文件IO,比如頻繁的日誌寫入,磁碟本身的處理速度較慢、都會造成IO性能問題
-
網路IO,頻寬不夠
-
記憶體,包括記憶體溢出、記憶體泄漏、記憶體不足
實際上不管是應用層的調優也好,還是硬體的升級也好。其實無非就是這幾個因素的調整。
應用伺服器複雜告警,如何讓應用伺服器走向集群
假如說這個時候應用伺服器的壓力變大了,根據對應用的檢測結果,可以針對性的對性能壓力大的地方進行優化。我們這裡考慮通過水平擴容來進行優化,把單機變為集群
應用伺服器從一台變為兩台,這兩個應用伺服器之間沒有直接的交互,他們都依賴資料庫對外提供服務,那麼這個時候會拋出兩個問題
- 最終用戶對應兩個應用伺服器訪問的選擇
對於這個問題,可以採用DNS解決,也可以通過負載均衡設備來解決
- session的問題?
水平和垂直擴容
對於大型的分散式架構而言,我們一直在追求一種簡單、優雅的方式來應對訪問量和數據量的增長。而這種方式通常指的是不需要改動軟體程式,僅僅通過硬體升級或者增加機器就可以解決。而這種就是分散式架構下的伸縮設計
伸縮分為垂直伸縮和水平伸縮兩種
垂直伸縮:表示通過升級或者增加單台機器的硬體來支撐訪問量以及數據量增長的方式,垂直伸縮的好處在於技術難度比較低,運營和改動成本也相對較低。但是缺點是機器性能是有瓶頸的,同時升級高性能的小型機或者大型機,成本是非常大的。這也是阿里去IOE的一個原因之一
增加CPU核心數:增加CPU後系統的服務能力能夠得到大的增長,比如響應速度、同時可以處理的執行緒數。但是引入CPU後也會帶來一些顯著的問題
-
1.鎖競爭加劇;多個執行緒同時運行訪問某個共享數據,那麼就涉及到鎖競爭,鎖競爭激烈時會導致很多執行緒都在等待鎖,所以即時增加CPU也無法讓執行緒得到更快的處理。當然這裡是有調優手段的,可以通過調優手段來降低鎖競爭*
-
2.支撐並發請求的執行緒數是固定的,那麼即時增加CPU,系統的服務能力也不會得到提升*
-
3.對於單執行緒任務,多核心CPU是沒有太大的作用的*
*增加記憶體:增加記憶體可以直接提成系統的響應速度,當然,也有可能達不到效果,就是如果JVM堆記憶體是固定的。
水平伸縮:通過增加機器來支撐訪問量及數據量增長的方式,成為水平伸縮,水平伸縮理論上來說沒有瓶頸,但是缺點是技術要求比較高,同時給運維帶來了更大的挑戰
垂直伸縮和水平伸縮都有各自的有點,我們在實際使用過程中都會對兩者做結合,一方面要考慮硬體升級的成本,一方面要考慮軟體改造的成本。
引入負載均衡設備
服務路由,基於負載均衡設備來實現
引入負載均衡器以後,會帶來session相關的問題
負載均衡演算法
輪詢(Round Robin)法
將請求按順序輪流分配到後台伺服器上,均衡的對待每一台伺服器,而不關心伺服器實際的連接數和當前的系統負載
缺點:當集群中伺服器硬體配置不同、性能差別大時,無法區別對待
隨機法
通過系統隨機函數,根據後台伺服器列表的大小值來隨機選取其中一台進行訪問。隨著調用量的增大,其實際效果越來越接近於平均分配流量到後台的每一台伺服器,也就是輪詢法的效果
優點:簡單使用,不需要額外的配置和演算法。
缺點:隨機數的特點是在數據量大到一定量時才能保證均衡,所以如果請求量有限的話,可能會達不到均衡負載的要求。
源地址哈希法
根據服務消費者請求客戶端的IP地址,通過哈希函數計算得到一個哈希值,將這個哈希值和伺服器列表的大小進行取模運算,得到的結果便是要訪問的伺服器地址的序號。採用源地址哈希法進行負載均衡,相同的IP客戶端,如果伺服器列表不變,將映射到同一個後台伺服器進行訪問。
加權輪詢(Weight Round Robin)法
不同的後台伺服器可能機器的配置和當前系統的負載並不相同,因此它們的抗壓能力也不一樣。跟配置高、負載低的機器分配更高的權重,使其能處理更多的請求,而配置低、負載高的機器,則給其分配較低的權重,降低其系統負載,加權輪詢很好的處理了這一問題,並將請求按照順序且根據權重分配給後端
最小連接數法
前面幾種方式都是通過對請求次數的合理分配最大可能提高伺服器的利用率,但是實際上,請求次數的均衡並不能代表負載的均衡。所以,引入了最小連接數法。它正是根據後端伺服器當前的連接情況,動態的選取其中當前積壓連接數最少的一台伺服器來處理當前請求,儘可能的提高後台伺服器利用率,將負載合理的分流到每一台伺服器。
session問題
我們打開一個網頁,基本上需要瀏覽器和web伺服器進行多次交互,我們都知道Http協議本身是無狀態的,這也是http協議設計的初衷,客戶端只需要簡單的向伺服器請求下載某些文件,無論是客戶端還是伺服器都沒必要記錄彼此過去的行為,每一次請求之間是獨立的,好比一個顧客和一個自動售貨機之間的關係一樣.
而實際上,我們很多的場景都需要帶有狀態的特性,因此聰明的我們引入了session+cookie機制來記住每次請求的會話。
在會話開始時,給當前會話分配一個唯一的會話標識(sessionid),然後通過cookie把這個標識告訴瀏覽器,以後在每次請求的時候,瀏覽器都會帶上這個會話標識來告訴web伺服器請求屬於哪個會話。在web伺服器上,各個會話有獨立的存儲,保存不同會話的資訊。
如果遇到禁用cookie的情況,一般的做法就是把這個會話標識放到URL的參數中。
而我們應用伺服器從一台變成兩台後,就會遇到session問題
分散式環境下的session共享
Session共享在當前這個互聯網背景下,已經不是一個新鮮的話題了,而且如何解決session共享其實也有很多非常成熟的方案
伺服器實現的session複製或session共享,這類型的共享session是和伺服器緊密相關的
我們在Web伺服器之間增加了會話數據的同步,通過同步就保證了不同Web伺服器之間Session數據的一致。一般應用容器都支援Session Replication方式
存在問題:
-
同步Session數據造成了網路頻寬的開銷。只要Session數據有變化,就需要將數據同步到所有其他機器上,機器越多,同步帶來的網路頻寬開銷就越大。
-
每台Web伺服器都要保存所有Session數據,如果整個集群的Session數據很多(很多人同時訪問網站)的話,每台機器用於保存Session數據的內容佔用會很嚴重。
這個方案是靠應用容器來完成Session的複製從而解決Session的問題的,應用本身並不關心這個事情。這個方案不適合集群機器數多的場景。
利用成熟的技術做session複製,比如12306使用的gemfire,比如常見的記憶體資料庫如Redis
Session數據不保存到本機而且存放到一個集中存儲的地方,修改Session也是發生在集中存儲的地方。Web伺服器使用Session從集中存儲的地方讀取。這樣保證了不同Web伺服器讀取到的Session數據都是一樣的。存儲Session的具體方式可以是資料庫
存在問題:
-
讀寫Session數據引入了網路操作,這相對於本機的數據讀取來說,問題就在於存在時延和不穩定性,不過我們的通訊基本都是發生在內網,問題不大。
-
如果集中存儲Session的機器或者集群有問題,就會影響到我們的應用。
相對於Session Replication,當Web伺服器數量比較大、Session數比較多的時候,這個集中存儲方案的優勢是非常明顯的。
將session維護在客戶端
很容易想到就是利用cookie,但是客戶端存在風險,數據不安全,而且可以存放的數據量比較小,所以將session維護在客戶端還要對session中的資訊加密。
我們的Session數據放到Cookie中,然後在Web伺服器上從Cookie中生成對應的Session數據。這就好比我們每次都把自己的碗筷帶在身上,這樣去那家飯店就可以隨意選擇了。相對前面的集中存儲方案,不會依賴外部的存儲系統,也就不存在從外部系統獲取、寫入Session數據的網路時延、不穩定性了。
存在問題:
安全性。Session數據本來都是服務端數據,而這個方案是讓這些服務端數據到了外部網路及客戶端,因此存在安全性上的問題。我們可以對寫入的Cookie的Session數據做加密,不過對於安全來說,物理上不能接觸才是安全的。
資料庫壓力變大,讀寫分離吧
隨著業務的繼續增長,數據量和訪問量持續增加。對於大型網站來說,有不少業務是讀多寫少,這個情況也會直接回饋到資料庫上。那麼對於這種情況來說,我們可以考慮採用讀寫分離的方式來優化資料庫的壓力
這個結構的變化會帶來兩個問題
- 數據如何同步
我們希望通過讀庫來分擔主庫上讀的壓力,那麼首先需要解決的是怎麼複製到讀庫的問題。資料庫系統一般都提供了數據複製的功能,我們可以直接使用資料庫系統自身的機制。不同的資料庫系統有不同的支援,比如Mysql支援Master+slave的結構提供數據複製機制
- 應用對數據源如何路由
對於應用來說,增加一個讀庫對結構變化產生了一定的影響,也就是我們的應用需要根據不同的情況來選擇不同的資料庫源
搜索引擎其實是一個讀庫
搜索引擎其實可以理解成一個讀庫,我們的商品存儲在資料庫中,而網站需要提供用戶實時檢索的功能,尤其是在商品搜索這塊。對於這樣的讀請求,如果全部走讀庫,其實性能也會存在一個瓶頸。而使用搜索引擎,不僅僅能大大提高檢索速度。還能減輕讀資料庫的壓力
而搜索引擎最重要的工作,就是需要根據被搜索的數據來構建索引,而隨著被搜索的數據的變化,索引也需要相應變化。
搜索集群的使用方式和讀庫的使用方式是一樣的,只是構建索引的過程基本都是需要我們自己來實現。可以從兩個緯度對搜索引擎構建索引的方式進行規劃,一個是按照全量/增量劃分。一種是按照實時/非實時劃分。
全量方式用於第一次建立索引,可能是新建,也可能是重建。而增量的方式是在全量的基礎上持續更新索引。
實時和非實時提現在索引更新的時間上,實時是最好的,非實時主要考慮到對數據源頭的保護
總的來說,搜索引擎技術解決了站內搜索時某些場景下的讀的問題,提供了更好的查詢效率。
加速數據讀取的利器-快取及分散式存儲
在大型網站中,基本上就是在解決存儲和計算的問題,所以存儲是一個很重要的支撐系統。網站建設初期我們都是從關係型資料庫開始的,而且很多時候為了方便,我們會把一些業務邏輯放在資料庫裡面去做,比如觸發器、存儲過程。雖然在前期能夠很方便的解決問題,但是在未來的發展過程中會帶來很多的麻煩,比如數據量大了以後,要做分庫分表操作等. 同時,業務發展到一定的體量以後,對存儲的需求不能完全通過關係型資料庫來滿足
分散式文件系統
對一些圖片、大文本,使用資料庫就不合適了,所以我們會採用分散式文件系統來實現文件存儲,分散式文件系統有很多產品、比如淘寶的TFS、google的GFS。還有開源的HDFS
NoSQL
NoSQL 我們可以理解成Not Only SQL、或者是No SQL。 兩種意思都是為了表達在大型網站中,關係型資料庫可以解決大部分問題,但是對於不同內容的特徵、訪問特徵、事務特徵等對存儲的要求是不一樣的。NoSQL是定位於是文件系統和SQL關係型資料庫之間的範疇。
數據快取都是為了更好的服務
大型網站內部都會用到一些數據快取,主要用於分擔資料庫的讀的壓力,快取系統一般是用來保存和查詢鍵值對的。應用系統中一般會把熱點數據放入到快取,而快取的填充也應該是由應用系統完成。如果數據不存在,則從資料庫獨處數據後放入快取。隨著時間的推移,當快取容量不夠需要清除數據時,最近不被訪問的數據就會被清理掉。還有一種方式就是在資料庫的數據發生變化後,主動把數據放入到快取系統中,這樣的好處是數據變化時能夠及時更新快取的數據,不會造成讀取失效
頁面快取
除了數據快取外,我們還可以對頁面做快取,數據快取可以加速應用在響應請求時的數據讀取數度,但是最終展示給用戶的還是頁面,有些動態產生的頁面或者訪問量特別高的頁面,我們會對頁面或者內容做一些快取。
彌補關係型資料庫的不足,引入分散式存儲
我們應用最多的主要還是關係型資料庫,但是在有些場景中,關係型資料庫不是很合適。所以我們會引入分散式存儲系統,比如redis、mongoDB、cassandra、HBase等。
根據不同的場景和數據結構類型,選擇合適的分散式存儲系統可以極大提高性能。分散式系統通過集群提供一個高容量、高並發訪問、數據冗餘融債的支援。
讀寫分離後,資料庫又遇到瓶頸
通過讀寫分離以及在某些場景用分散式存儲系統替換關係型資料庫的方式,能夠降低主庫的壓力,解決數據存儲方面的問題,不過隨著業務的發展,我們的主庫也會遇到瓶頸。推演到現在,我們的網站各個模組:交易、商品、用戶數據都還是存儲在一個資料庫。儘管增加了快取、讀寫分離的方式,但是資料庫的壓力仍然在持續增加,因此我們可以對數據垂直拆分和水平拆分來解決資料庫壓力問題
專庫專用,數據垂直拆分
垂直拆分的意思是把資料庫中不同的業務數據拆分到不同的資料庫中,那麼根據我們推演的例子,把用戶、交易、商品的數據分開
不同業務的數據從原來的一個資料庫拆分到了多個資料庫中,那麼就需要考慮到如何處理原來單機跨業務的事務
-
使用分散式事務解決
-
去掉事務或者不追求強事務的支援
對數據進行垂直拆分後,解決了把所有業務數據放在一個資料庫中的壓力問題,並且也可以根據不同業務的特點進行更多的優化
垂直拆分後,遇到瓶頸,數據水平拆分
與垂直拆分對應的還有數據水平拆分,數據水平拆分就是把同一個表的數據拆分到兩個資料庫中,產生數據水平拆分的原因是某個業務的數據表的數據量或者更新量達到了單個資料庫的瓶頸,這個時候就可以把表拆到兩個或者多個資料庫中。
數據水平拆分與讀寫分離的區別是,讀寫分離解決的是讀壓力大的問題,對於數據量大或者更新量大的情況並不起作用。
數據水平拆分與數據垂直拆分的區別是,垂直拆分是把不同的表拆分到不同的資料庫,而水平拆分是把同一個表拆分到不同的資料庫中。
我們可以進一步把用戶表拆分到兩個資料庫中,它們擁有結構一模一樣的用戶表,而且每個庫中的用戶表都只涵蓋了一部分的用戶,兩個資料庫的用戶和在一起就相當於沒有拆分之前的用戶表
水平拆分帶來的影響
-
sql路由問題,需要根據一個條件來決定當前請求發到那個資料庫中
-
主鍵的處理,不能採用自增id,需要全局id
由於同一個業務的數據被拆分到不同的資料庫,因此涉及到一些查詢需要跨兩個資料庫獲取,如果數據量太大並且需要分頁,就比較難處理了
資料庫問題解決後,應用面對的挑戰
前面講的讀寫分離、分散式存儲、數據垂直拆分和水平拆分都是解決數據方面的問題,接下來我們要看看應用方面的變化
隨著業務的發展,應用的功能會越來越多,應用也會越來越大,我們需要思考如何不讓應用持續變大,這就需要把應用拆開,從一個應用變為兩個甚至是多個。
第一種方式
根據業務的特性把應用拆分,在我們的例子中,主要業務功能分三個部分、用戶、商品、交易。我們可以把原來的一個應用拆成分別以交易和商品為主的兩個應用,對於交易和商品都會有設計使用用戶的地方,我們讓這兩個系統自己完成涉及用戶的工作,而類似用戶註冊、登錄等基礎的用戶工作,可以暫時交給兩個系統之一來完成
我們還可以按照用戶註冊、用戶登錄、用戶資訊維護等再拆分,變成三個系統,不過這樣拆分後在不同系統中會有一些相似的程式碼,比如用戶相關的程式碼,如何能夠保障這部分程式碼的一致以及如何對其他模組提供復用也是需要解決的問題。而且,這樣拆分出來的新系統之間沒有直接的相互調用
服務化的道路
我們在來看一下服務化的做法,我們把應用分為三層,處於最上端的是web系統,用於完成不同的業務功能,處於中間的是一些服務中心,不同的服務中心提供不同的業務服務;處於最下層的則是業務的資料庫
與之前相比有幾個重要的變化,首先業務功能之間的訪問不僅僅是單機內部的方法調用,還引入了遠程的服務調用。其次,共享程式碼不再是散落在不同的應用中,這些實現被放在各個服務中心。最後,資料庫的連接也發生了一些變化,我們把資料庫的交互工作放到了服務中心,讓前端的web應用更加註重與瀏覽器的交互工作,而不必過多關注業務邏輯的事情。鏈接資料庫的任務交給響應的業務服務中心了,這樣可以降低資料庫的連接數。
而服務中心不僅把一些可以共用的程式碼集中管理,而且還使得這些程式碼變得更好維護。
服務化的方式會帶來很多好處,首先,從結構上來看,系統架構更加清晰了,比原本的架構更加立體。從穩定性上來看,一些散落在多個應用系統中的程式碼變成了服務並且由專門的團隊進行統一維護,一方面可以提高程式碼的品質,另一方面由於基礎核心模組相對穩定,修改和發布的頻次相對於業務系統來說會少很多,這也會提高整個架構的穩定性。最後,更加底層的資源由服務層統一管理,結構更加清晰,對於團隊開發效率來說有比較大的提高
服務化的方式,對於研發也會有很大的影響,以前的開發模式是幾個大團隊負責幾個大應用,隨著服務化的落地,我們的應用數量會飛速增長,系統內部的依賴關係也會變的錯綜複雜,同時團隊也進行了拆分,每個小團隊專註於某個具體的服務或者應用上,迭代效率也會更高
版權聲明:本部落格所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自
Mic帶你學架構
!
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術乾貨!