高並發,我把握不住啊!
慎入,作者高並發搞得少(沒搞過),這裡面水太深,什麼高並發,大流量的東西都是虛擬的,作者還太年輕,沒有那個經歷,把握不住。系統只有幾QPS,開心快樂就行,不PK,文明PK。
我關注的大佬更新了,在乾貨文章的下面有這麼一小條:
我承認我有賭的成分,點進去一看,果然是廣告。說真的,內容看起來還是很有吸引力的,但是貧窮阻止了我消費的衝動。
作為一個高並發的門外漢,嘗試結合學過的課程和一些網上的資料來整理一下對於高並發的認識。——實戰是不可能實戰的,只能動動嘴皮這樣子。
什麼是高並發
高並發
指的是系統同時處理很多請求。
高並發
是一個結果導向的東西,例如,常見的高並發場景有:淘寶的雙11、春運時的搶票、微博大V的熱點新聞等,這些典型場景並不是陡然出世,而是隨著業務發展的發展而逐漸出現。像2020年淘寶雙11全球狂歡季,訂單創建峰值達到了驚人的58.3萬筆/秒,4年前的2016年,這個數字大概是四分之一,再往前四年,這個數據不可考,但是肯定就沒這麼誇張了。
高並發
的業務場景出現了,隨之而來的就是要支援這個高並發
業務場景的架構——技術要為業務服務,業務倒逼技術發展。高並發
的架構也不是某個天才冥思苦想或者靈機一動,這個過程是隨著業務的發展而演進。用一個比喻,先有了秋名山,才到了老司機。
那到底多大並發才算高並發呢?
這個本身是沒有具體標準的事情,只看數據是不行的,要結合具體的場景。不能說10W QPS的秒殺是高並發,而1W QPS的資訊流就不是高並發。資訊流場景涉及複雜的推薦模型和各種人工策略,它的業務邏輯可能比秒殺場景複雜10倍不止。業務場景不一樣,執行複雜度不一樣,單看並發量也沒有意義。
總結就是,高並發無定勢,是要和具體的業務場景相結合的。無高並發場景,無高並發架構。
高並發目標
宏觀目標
高並發絕不意味著只追求高性能。從宏觀角度看,高並發系統設計的目標有三個:高性能、高可用,以及高可擴展。就是所謂的「三高」,三高不是孤立的,而是相互支撐的。
1、高性能:性能體現了系統的並行處理能力,在有限的硬體投入下,提高性能意味著節省成本。同時,性能也反映了用戶體驗,響應時間分別是100毫秒和1秒,給用戶的感受是完全不同的。
2、高可用:表示系統可以正常服務的時間。一個全年不停機、無故障;另一個隔三差五齣線上事故、宕機,用戶肯定選擇前者。另外,如果系統只能做到90%可用,也會大大拖累業務。
3、高擴展:表示系統的擴展能力,流量高峰時能否在短時間內完成擴容,更平穩地承接峰值流量,比如雙11活動、明星離婚等熱點事件。
這3個目標是需要通盤考慮的,因為它們互相關聯、甚至也會相互影響。
比如說:考慮系統的擴展能力,你需要將服務設計成無狀態的,這種集群設計保證了高擴展性,其實也間接提升了系統的性能和可用性。
再比如說:為了保證可用性,通常會對服務介面進行超時設置,以防大量執行緒阻塞在慢請求上造成系統雪崩,那超時時間設置成多少合理呢?一般,我們會參考依賴服務的性能表現進行設置。
具體目標
性能指標
性能指標通過性能指標可以度量目前存在的性能問題,也是高並發主要關注的指標,性能和流量方面常用的一些指標有
- QPS/TPS/HPS:QPS是每秒查詢數,TPS是每秒事務數,HPS是每秒HTTP請求數。最常用的指標是QPS。
需要注意的是,並發數和QPS是不同的概念,並發數是指系統同時能處理的請求數量,反應了系統的負載能力。
-
響應時間:從請求發出到收到響應花費的時間,例如一個系統處理一個HTTP請求需要100ms,這個100ms就是系統的響應時間。
-
平均響應時間:最常用,但是缺陷很明顯,對於慢請求不敏感。比如 1 萬次請求,其中 9900 次是 1ms,100 次是 100ms,則平均響應時間為 1.99ms,雖然平均耗時僅增加了 0.99ms,但是 1%請求的響應時間已經增加了 100 倍。
-
TP90、TP99 等分位值:將響應時間按照從小到大排序,TP90 表示排在第 90 分位的響應時間, 分位值越大,對慢請求越敏感。
-
RPS(吞吐量):單位時間內處理的請求量,通常由QPS和並發數決定。
通常,設定性能目標時會兼顧吞吐量和響應時間,比如這樣表述:在每秒 1 萬次請求下,AVG 控制在 50ms 以下,TP99 控制在 100ms 以下。對於高並發系統,AVG 和 TP 分位值必須同時要考慮。另外,從用戶體驗角度來看,200 毫秒被認為是第一個分界點,用戶感覺不到延遲,1 秒是第二個分界點,用戶能感受到延遲,但是可以接受。
因此,對於一個健康的高並發系統,TP99 應該控制在 200 毫秒以內,TP999 或者 TP9999 應該控制在 1 秒以內。 -
PV:綜合瀏覽量,即頁面瀏覽量或者點擊量,一個訪客在24小時內訪問的頁面數量。
-
UV:獨立訪客 ,即一定時間範圍內相同訪客多次訪問網站,只計算為一個獨立的訪客。
-
頻寬: 計算頻寬大小需要關注兩個指標,峰值流量和頁面的平均大小。
日網站頻寬可以使用下面的公式來粗略計算:
日網站頻寬=pv/統計時間(換算到秒)*平均頁面大小(單位kB)*8
峰值一般是平均值的倍數;
QPS不等於並發連接數,QPS是每秒HTTP請求數量,並發連接數是系統同時處理的請求數量:
可用性指標
高可用性是指系統具有較高的無故障運行能力,可用性 = 平均故障時間 / 系統總運行時間,一般使用幾個 9 來描述系統的可用性。
對於大多數系統。2個9是基本可用(如果達不到開發和運維可能就要被祭天了),3個9是較高可用,4個9是具有自動恢復能力的高可用。要想達到3個9和4個9很困難,可用性影響因素非常多,很難控制,需要過硬的技術、大量的設備資金投入,工程師要具備責任心,甚至還要點運氣。
可擴展性指標
面對突發流量,不可能臨時改造架構,最快的方式就是增加機器來線性提高系統的處理能力。
對於業務集群或者基礎組件來說,擴展性 = 性能提升比例 / 機器增加比例,理想的擴展能力是:資源增加幾倍,性能提升幾倍。通常來說,擴展能力要維持在 70%以上。
但是從高並發系統的整體架構角度來看,擴展的目標不僅僅是把服務設計成無狀態就行了,因為當流量增加 10 倍,業務服務可以快速擴容 10 倍,但是資料庫可能就成為了新的瓶頸。
像 MySQL 這種有狀態的存儲服務通常是擴展的技術難點,如果架構上沒提前做好規劃(垂直和水平拆分),就會涉及到大量數據的遷移。
我們需要站在整體架構的角度,而不僅僅是業務伺服器的角度來考慮系統的擴展性 。所以說,資料庫、快取、依賴的第三方、負載均衡、交換機頻寬等等都是系統擴展時需要考慮的因素。我們要知 道系統並發到了某一個量級之後,哪一個因素會成為我們的瓶頸點,從而針對性地進行擴展。
高並發架構演進
誰不是生下來就是老司機,架構也不是架起來就支援高並發。我們來看一個經典的架構演進的例子——淘寶,真實詮釋了「好的架構是進化來的,不是設計來的」。
以下是來自《淘寶技術這十年》描述的淘寶2003—2012年的架構演進。
個人網站
初代淘寶的團隊人員只有十來個,而且面臨千載難逢的商業機會,所以要求上線的時間越快越好(實際用了不到一個月),那麼淘寶的這些牛人是怎麼做到的呢?
——買一個。
初代淘寶買了這樣一個架構的網站: LAMP(Linux+Apache+MySQL+PHP)。整個系統的架構如下:
最後開發的網站是這樣的:
由於商品搜索比較佔用資料庫資源,後來還引入了阿里巴巴的搜索引擎iSearch。
Oracle/支付寶/旺旺
淘寶飛速發展,流量和交易量迅速提升,給技術帶來了新的問題——MySQL抗不住了。怎麼辦?要搞點事情嗎?沒有,淘寶買了Oracle資料庫,當然這個也考慮到團隊里有Oracle大牛的原因。
替換了資料庫之後的架構:
比較有意思的,當時由於買不起
商用的連接池,所以用了一個開源的連接池代理服務SQLRelay,這個代理服務經常會死鎖,怎麼解決呢?人肉運維,工程師24小時待命,出現問題趕緊重啟
SQL Relay服務。😂😂
後來為了優化存儲,又買了NAS(Network Attached Storage,網路附屬存儲),NetApp 的 NAS 存儲作為了資料庫的存儲設備,加上 Oracle RAC(Real Application Clusters,實時應用集群)來實現負載均衡。
Java 時代 1.0
2004年,淘寶已經運行了一年的時間,上面提到的SQLRelay的問題解決不了,資料庫必須要用Oracle,所以決定更換開發語言。
在不拖慢現有業務發展的情況下,平滑更換整體的架構,對當時的淘寶仍然是個有挑戰性的事情。所以怎麼辦?淘寶的解決方案是請了Sun公司的大佬。
當時,由於struts1.x存在很多問題,所以淘寶自研了一套MVC框架。Sun當時在推EJB,所以這套架構里也引入了EJB。
Java 時代 2.0
在之前,淘寶的架構的架構主要思路還是「買」,隨著業務的發展,到了2005 年,「買」已經很難解決問題了,需要對整個架構進行調整和優化,需要綜合考慮容量、性能、成本的問題。
在Java時代2.0,主要做了對數據分庫、放棄EJB、引入Spring、加入快取、加入CDN等。
Java時代3.0
Java時代3.0的最大特點就是淘寶開始從商用轉為「自研」,開始真正創造自己的核心技術,例如快取存儲引擎Tair,分散式存儲系統TFS。搜索引擎iSearch也進行了升級。引入了自研技術的淘寶架構:
分散式時代1.0
到了2008年的時候,淘寶的業務進一步發展。
整個主站系統的容量已經到了瓶頸,商品數在1億個以上,PV在2.5億個以上,會員數超過了 5000萬個。這時Oracle的連接池數量都不夠用了,資料庫的容量到了極限,即使上層系統加機器也無法繼續擴容,我們只有把底層的基礎服務繼續拆分,從底層開始擴容,上層才能擴展,這才能容納以後三五年的增長。
淘寶開始對業務模組逐步拆分和服務化改造。例如拆分出了商品中心、商品中心等等。同時引入了一些自研的中間件,如分散式資料庫中間件,分散式消息中間件等等。
《淘寶技術這十年》這本書只描述到了2012年,也就是分散式時代。上圖是根據參考【8】畫的一張圖。
轉眼理2012又快過了十年,這十年,阿里巴巴逐漸進入極盛時代,技術上也是風起雲湧,才人輩出。粒度更細的微服務、隔離差距的容器化技術、快速伸縮的雲平台技術…… 如果《淘寶技術這十年》的作者能再寫一個十年,一定也是非常精彩。
按照參考【10】,接下來的淘寶服務化開始逐漸演進到雲平台架構,由於資料實在難找,而且這時候以淘寶的體量,內部的架構複雜度足以寫一本書了。所以接下來的架構演進參考服務端高並發分散式架構演進之路,是一個牛人以淘寶為模擬對象進行的架構演進,雖然不是淘寶真正的架構技術演進,但也很值得借鑒。
在這裡我們略過了微服務架構——分散式時代2.0,微服務本身是更細粒度、更輕量級的服務化,這裡插入一個關於微服務很有意思的說法——馬丁老哥老被人說設計的東西不符合面向服務的概念,於是他就自己發明創造了一個靈活的微服務理論,以後再有人說:馬老師,你又不遵循微服務架構設計的原則了。嗯,你說哪一點不符合,我立馬去改微服務的理論。
容器化時代
前最流行的容器化技術是Docker,最流行的容器管理服務是Kubernetes(K8S),應用/服務可以打包為Docker鏡像,通過K8S來動態分發和部署鏡像。Docker鏡像可理解為一個能運行你的應用/服務的最小的作業系統,裡面放著應用/服務的運行程式碼,運行環境根據實際的需要設置好。把整個「作業系統」打包為一個鏡像後,就可以分發到需要部署相關服務的機器上,直接啟動Docker鏡像就可以把服務起起來,使服務的部署和運維變得簡單。
在大促的之前,可以在現有的機器集群上劃分出伺服器來啟動Docker鏡像,增強服務的性能,大促過後就可以關閉鏡像,對機器上的其他服務不造成影響。
雲平台時代
在服務化的時候,淘寶已經演進到了雲平台架構。
所謂的雲平台,就是把海量機器資源,通過統一的資源管理,抽象為一個資源整體,在之上可按需動態申請硬體資源(如CPU、記憶體、網路等),並且之上提供通用的作業系統,提供常用的技術組件(如Hadoop技術棧,MPP資料庫等)供用戶使用,甚至提供開發好的應用,用戶不需要關係應用內部使用了什麼技術,就能夠解決需求(如音影片轉碼服務、郵件服務、個人部落格等)。
簡單總結一下:高並發的架構某種程度上是逼出來的,一般人誰能想到淘寶當年拋棄php是因為解決不了資料庫連接池的問題。架構演進就像是西湖的水——西湖的水,工程師的淚,說起來容易,裡面究竟滅了多少火,填了多少坑。我們外人看到的平湖秋波,裡面水很深🐶。
高並發架構實現
想讓系統抗住更多的並發,主要就是兩個方向:
-
縱向擴展:
1、提升單機的硬體性能:通過增加記憶體、 CPU核數、存儲容量、或者將磁碟 升級成SSD等堆硬體的方式來提升
2、提升單機的軟體性能:使用快取減少IO次數,使用並發或者非同步的方式增加吞吐量。
-
橫向擴展:單機性能總會存在極限,所以最終還需要引入橫向擴展,通過集群部署以進一步提高並發處理能力。
1、做好分層架構:這是橫向擴展的前提,因為高並發系統往往業務複雜,通過分層處理可以簡化複雜問題,更容易做到橫向擴展。
2、各層進行水平擴展:無狀態水平擴容,有狀態做分片路由。業務集群通常能設計成無狀態的,而資料庫和快取往往是有狀態的,因此需要設計分區鍵做好存儲分片,當然也可以通過主從同步、讀寫分離的方案提升讀性能。
用一個比喻,你要去打十個大漢,你大概是打不過的,最好的結果就是他們打不倒你——吊起來打。所以這時候就得想辦法了。第一個辦法就是努力鍛煉,然後全副武裝,也許還有點希望,這就是縱向擴展;第二個辦法,不行,你一看對面人多,你就叫了十九個兄弟,然後你們二十個打他們十個,唉,這下看上去能打的過了,這就是橫向擴展;還有第三個不常用的辦法,你找個門把住,每次就放一個大漢進來,打倒一個再放下一個,這個就是削峰限流的做法。
我們看一下一個大概的支援三高的典型架構:
接下來,我們從上往下,看一下,各層的一些關鍵技術。
網路層
多機器
堆機器不是萬能的,不堆機器是萬萬不能的。
我們努力地升級改造架構,最後讓我們提供的服務能夠快速橫向擴展。橫向擴展的基礎同樣是要有一定數量的、一定性能的機器。
還是上面哪個比喻,你要打十個大漢,等你努力練成了葉師傅,你突然發現對面的孩子都長大了,人數×2,這時候你還是得叫兄弟。
一般狗大戶大廠在全國各地都有機房,可能光北京就有兩個,把不同地方的請求分到不同的機房,再分到不同的集群,再分到不同的機器,這麼一勻,就在服務能扛的範疇之內了。我們大概來看一下,怎麼估算所需機器的數量。
- 通過QPS和PV計算部署伺服器的台數
單台伺服器每天PV計算:
公式2:每天總PV = QPS * 3600 * 8
伺服器計算:
- 峰值QPS和機器計算公式
原理:每天80%的訪問集中在20%的時間裡,這20%時間叫做峰值時間
公式:( 總PV數 * 80% ) / ( 每天秒數 * 20% ) = 峰值時間每秒請求數(QPS)
機器:峰值時間每秒QPS / 單台機器的QPS = 需要的機器。
一般有大流量業務的公司都實現了多機房,包括同城多機房、跨城多機房、跨國多機房等。為了保證可用性,財大氣粗的公司會預備大量的冗餘,一般會保證機器數是計算峰值所需機器數的兩倍。需要節約成本的,也可以考慮當前流行的雲平台,之前熱點事件的時候,微博就從阿里雲租了不少雲伺服器。
DNS
DNS是請求分發的第一個關口,實現的是地理級別的均衡。dns-server對一個域名配置了多個解析ip,每次DNS解析請求來訪問dns-server。通常會返回離用戶距離比較近的ip,用戶再去訪問ip。例如,北京的用戶訪問北京的機房,南京的用戶訪問南京的資源。
一般不會使用DNS來做機器級別的負載均衡,因為造不起,IP資源實在太寶貴了,例如百度搜索可能需要數萬台機器,不可能給每個機器都配置公網IP。一般只會有有限的公網IP的節點,然後再在這些節點上做機器級別的負載均衡,這樣各個機房的機器只需要配置區域網IP就行了。
DNS負載均衡的優點是通用(全球通用)、成本低(申請域名,註冊DNS即可)。
缺點也比較明顯,主要體現在:
-
DNS 快取的時間比較長,即使將某台業務機器從 DNS 伺服器上刪除,由於快取的原因,還是有很多用戶會繼續訪問已經被刪除的機器。
-
DNS 不夠靈活。DNS 不能感知後端伺服器的狀態,只能根據配置策略進行負載均衡,無法做到更加靈活的負載均衡策略。比如說某台機器的配置比其他機器要好很多,理論上來說應該多分配一些請求給它,但 DNS 無法做到這一點。
所以對於時延和故障敏感的業務,有實力的公司可能會嘗試實現HTTP-DNS的功能,即使用HTTP 協議實現一個私有的 DNS 系統。HTTP-DNS 主要應用在通過 App 提供服務的業務上,因為在 App 端可以實現靈活的伺服器訪問策略,如果是 Web 業務,實現起來就比較麻煩一些,因為 URL 的解析是由瀏覽器來完成的,只有 Javascript 的訪問可以像 App 那樣實現比較靈活的控制。
CDN
CDN是為了解決用戶網路訪問時的「最後一公里」效應,本質是一種「以空間換時間」的加速策略,即將內容快取在離用戶最近的地方,用戶訪問的是快取的內容,而不是站點實時訪問的內容。
由於CDN部署在網路運營商的機房,這些運營商又是終端用戶的網路提供商,因此用戶請求路由的第一跳就到達了CDN伺服器,當CDN中存在瀏覽器請求的資源時,從CDN直接返回給瀏覽器,最短路徑返迴響應,加快用戶訪問速度。
下面是簡單的CDN請求流程示意圖:
CDN能夠快取的一般是靜態資源,如圖片、文件、CSS、Script腳本、靜態網頁等,但是這些文件訪問頻度很高,將其快取在CDN可極大改善網頁的打開速度。
反向代理層
我們把這一層叫反向代理層,也可以叫接入層、或者負載層。這一層是流量的入口,是系統抗並發很關鍵的一層。
還是那個比喻,還是你打十個大漢,這次你叫了十九個兄弟,理想的情況是你們兩個打對面一個,但是你由於太激動,沖在了最前面,結果瞬間被十個大漢暴打……
反向代理會對流量進行分發,保證最終落到每個服務上的流量是服務能扛的範圍之內。
Nginx、LVS、F5
DNS 用於實現地理級別的負載均衡,而 Nginx、 LVS、 F5 用於同一地點內機器級別的負載均衡。其中 Nginx 是軟體的 7 層負載均衡,LVS 是內核的 4 層負載均衡,F5 是硬體的 4 層負載均衡。
軟體和硬體的區別就在於性能,硬體遠遠高於軟體,Ngxin 的性能是萬級,一般的 Linux 伺服器上裝個 Nginx 大概能到 5 萬 / 秒;LVS 的性能是十萬級,據說可達到 80萬 / 秒;F5 性能是百萬級,從 200 萬 / 秒到 800 萬 / 秒都有。
硬體雖然性能高,但是單台硬體的成本也很高,一台最便宜的 F5 都是幾十萬,但是如果按照同等請求量級來計算成本的話,實際上硬體負載均衡設備可能會更便宜,例如假設每秒處理 100 萬請求,用一台 F5 就夠了,但用 Nginx, 可能要 20 台,這樣折算下來用 F5 的成本反而低。因此通常情況下,如果性能要求不高,可以用軟體負載均衡;如果性能要求很髙,推薦用硬體負載均衡。
4 層和 7 層的區別就在於協議和靈活性。Nginx 支援 HTTP、 E-mail 協議,而 LVS 和 F5 是 4層負載均衡,和協議無關,幾乎所有應用都可以做,例如聊天、資料庫等。目前很多雲服務商都已經提供了負載均衡的產品,例如阿里雲的 SLB、UCIoud 的 ULB 等,中小公司直接購買即可。
對於開發而言,一般只需要關注到Nginx這一層面就行了。
負載均衡典型架構
像上面提到的負載均衡機制,在使用中,可以組合使用。
DNS負載均衡用於實現地理級別的負載均衡,硬體件負載均衡用於實現集群級別的負載均衡;軟體負載均衡用於實現機器級別的負載均衡。
整個系統的負載均衡分為三層。
- 地理級別負載均衡:www.xxx.com 部署在北京、廣州、上海三個機房,當用戶訪問時,DNS 會根據用戶的地理位置來決定返回哪個機房的 IP,圖中返回了廣州機房的 IP 地址,這樣用戶就訪問到廣州機房了。
- 集群級別負載均衡:廣州機房的負載均衡用的是 F5 設備,F5 收到用戶請求後,進行集群級別的負載均衡,將用戶請求發給 3 個本地集群中的一個,我們假設 F5 將用戶請求發給了 「廣州集群 2」 。
- 機器級別的負載均衡:廣州集群 2 的負載均衡用的是 Nginx, Nginx 收到用戶請求後,將用戶請求發送給集群裡面的某台伺服器,伺服器處理用戶的業務請求並返回業務響應。
Nginx負載均衡
我們主要關心是Nginx這一層的負載,通常LVS 和 F5這兩層都是由網路運維工程師管控。
對於負載均衡我們主要關心的幾個方面如下:
-
上游伺服器配置:使用 upstream server配置上游伺服器
-
負載均衡演算法:配置多個上游伺服器時的負載均衡機制。
-
失敗重試機制:配置當超時或上游伺服器不存活時,是否需要重試其他上游伺服器。
-
伺服器心跳檢查:上游伺服器的健康檢查/心跳檢查。
upstream server中文直接翻譯是上游伺服器,意思就是負載均衡伺服器設置,就是被nginx代理最後真實訪問的伺服器。
負載均衡演算法
負載均衡演算法數量較多,Nginx主要支援以下幾種負載均衡演算法:
1、輪詢(默認)
每個請求按時間順序逐一分配到不同的後端服務,如果後端某台伺服器死機,自動剔除故障系統,使用戶訪問不受影響。
2、weight(輪詢權值)
weight的值越大分配到的訪問概率越高,主要用於後端每台伺服器性能不均衡的情況下。或者僅僅為在主從的情況下設置不同的權值,達到合理有效的地利用主機資源。
3、ip_hash
每個請求按訪問IP的哈希結果分配,使來自同一個IP的訪客固定訪問一台後端伺服器,並且可以有效解決動態網頁存在的session共享問題。
4、fair
比 weight、ip_hash更加智慧的負載均衡演算法,fair演算法可以根據頁面大小和載入時間長短智慧地進行負載均衡,也就是根據後端伺服器的響應時間 來分配請求,響應時間短的優先分配。Nginx本身不支援fair,如果需要這種調度演算法,則必須安裝upstream_fair模組。
5、url_hash
按訪問的URL的哈希結果來分配請求,使每個URL定向到一台後端伺服器,可以進一步提高後端快取伺服器的效率。Nginx本身不支援url_hash,如果需要這種調度演算法,則必須安裝Nginx的hash軟體包。
失敗重試
Nginx關於失敗重試主要有兩部分配置,upstream server 和 proxy_pass。
通過配置上游伺服器的 max_fails和 fail_timeout,來指定每個上游伺服器,當fail_timeout時間內失敗了max_fail次請求,則認為該上游伺服器不可用/不存活,然後將會摘掉該上游伺服器,fail_timeout時間後會再次將該伺服器加入到存活上游伺服器列表進行重試。
健康檢查
Nginx 對上游伺服器的健康檢查默認採用的是惰性策略,Nginx 商業版提供了healthcheck 進 行 主 動 健 康 檢 查 。當 然 也 可 以 集 成 nginx_upstream_check_module( //github.com/yaoweibin/nginx_upstream_check module ) 模組來進行主動健康檢查。
nginx_upstream_check_module 支援 TCP 心跳和 HTTP 心跳來實現健康檢查。
流量控制
流量分發
流量分發就不多說了,上面已經講了,是接入層的基本功能。
流量切換
我聽朋友說過一個有意思的事情,他們公司將流量從一個機房切到另一個機房,結果翻車,所有工程師運維平台一片飄紅,全公司集體圍觀,運維團隊就很丟面子。
流量切換就是在某些情況下,比如機房故障、光纖被挖斷、伺服器故障故障情況,或者灰度發布、A/B等運維測試場景,需要將流量切到不同的機房、伺服器等等。
就像我們上面提到的負載均衡典型架構,不同層級的負載負責切換不同層級的流量。
- DNS:切換機房入口。
- HttpDNS:主要 APP 場景下,在客戶端分配好流量入口,繞過運營商 LocalDNS並實現更精準流量調度。
- LVS/HaProxy:切換故障的 Nginx 接入層。
- Nginx:切換故障的應用層。
另外,有些應用為了更方便切換,還可以在 Nginx 接入層做切換,通過 Nginx 進行一些流量切換,而沒有通過如 LVS/HaProxy 做切換。
限流
限流是保證系統可用的一個重要手段,防止超負荷的流量直接打在服務上,限流演算法主要有令牌桶、漏桶。
可以在很多層面做限流,例如服務層網關限流、消息隊列限流、Redis限流,這些主要是業務上的限流。
這裡我們主要討論的是接入層的限流,直接在流量入口上限流。
對於 Nginx接入層限流可以使用 Nginx自帶的兩個模組:連接數限流模組 ngx_http_limit_conn_module
和漏桶演算法實現的請求限流模組 ngx_http_limit_req_moduleo
還可以使用 OpenResty提供的 Lua限流模組 ua-resty–limit-traffic應對更複雜的限流場景。
limmit_conn用來對某個 key 對應的總的網路連接數進行限流,可以按照如 IP、域名維度進行限流。limit_req用來對某個 key對應的請求的平均速率進行限流,有兩種用法:平滑模式(delay ) 和允許突發模式(nodelay )。
流量過濾
很多時候,一個網站有很多流量是爬蟲流量,或者直接是惡意的流量。
可以在接入層,對請求的參數進行校驗,如果參數校驗不合法,則直接拒絕請求,或者把請求打到專門用來處理非法請求的服務。
最簡單的是使用Nginx,實際場景可能會使用OpenResty,對爬蟲 user-agent 過濾和一些惡意IP (通過統計 IP 訪問量來配置閾值),將它們分流到固定分組,這種情況會存在一定程度的誤殺,因為公司的公網 IP —般情況下是同一個,大家使用同一個公網出口 IP 訪問網站,因此,可以考慮 IP+Cookie 的方式,在用戶瀏覽器種植標識用戶身份的唯一 Cookie。訪問服務前先種植 Cookie, 訪問服務時驗證該 Cookie, 如果沒有或者不正確,則可以考慮分流到固定分組,或者提示輸入驗證碼後訪問。
降級
降級也是保證高可用的一把利劍,降級的思路是「棄車保帥」,在眼看著不能保證全局可用的情況下,拋棄或者限制一些不重要的服務。
降級一般分為多個層級,例如在應用層進行降級,通過配置中心設置降級的閾值,一旦達到閾值,根據不同的降級策略進行降級。
也可以把降級開關前置到接入層,在接入層配置功能降級開發,然後根據情況行自動/人工降級。後端應用服務出問題時,通過接入層降級,可以避免無謂的流量再打到後端服務,從而給應用服務有足夠的時間恢復服務。
Web層
經過一系列的負載均衡,用戶終於請求到了web層的服務。web服務開發完成,經過部署,運行在web伺服器中給用戶提供服務。
集群
一般會根據業務模組,來劃分不同的服務,一個服務部署多個實例組成集群。
為了隔離故障,可以再將集群進行分組,這樣一個分組出現問題,也不會影響其它分組。像比較常問的秒殺,通常會將秒殺的服務集群和普通的服務集群進行隔離。
能做到集群化部署的三個要點是無狀態、拆分、服務化。
- 無狀態:設計的應用是無狀態的,那麼應用比較容易進行水平擴展。
- 拆分:設計初期可以不用拆分,但是後期訪問量大的時候,就可以考慮按功能拆分系統。拆分的維度也比較靈活,根據實際情況來選擇,例如根據系統維度、功能維度、讀寫維度、AOP 維度、模組維度等等。
- 服務化:拆分更多的是設計,服務化是落地,服務化一般都得服務治理的問題。除了最基本的遠程調用,還得考慮負載均衡、服務發現、服務隔離、服務限流、服務訪問黑白名單等。甚至還有細節需要考慮,如超時時間、重試機制、服務路由、故障補償等。
Web伺服器
獨立開發一個成熟的 Web 伺服器,成本非常高,況且業界又有那麼多成熟的開源 Web 伺服器,所以互聯網行業基本上都是 “拿來主義” ,挑選一個流行的開源伺服器即可。大一點的公司,可能會在開源伺服器的基礎上,結合自己的業務特點做二次開發,例如淘寶的 Tengine,但一般公司基本上只需要將開源伺服器摸透,優化一下參數,調整一下配置就差不多了。
伺服器的選擇主要和開發語言相關,例如,Java 的有 Tomcat、JBoss、Resin 等,PHP/Python 的用 Nginx。
Web伺服器的性能之類的一般不會成為瓶頸,例如Java最流行的Web伺服器Tomcat默認配置的最大請求數是 150,但是沒有關係,集群部署就行了。
容器
容器是最近幾年才開始火起來的,其中以 Docker 為代表,在 BAT 級別的公司已經有較多的應用。
容器化可以說給運維帶來了革命性的變化。Docker 啟動快,幾乎不佔資源,隨時啟動和停止,基於Docker 打造自動化運維、智慧化運維逐漸成為主流方式。
容器化技術也天生適合當前流行的微服務,容器將微服務進程和應用程式隔離到更小的實例里,使用更少的資源,更快捷地部署。結合容器編排技術,可以更方便快速地搭建服務高可用集群。
服務層
開發框架
一般,互聯網公司都會指定一個大的技術方向,然後使用統一的開發框架。例如,Java 相關的開發框架 SSH、SpringBoot, Ruby 的 Ruby on Rails, PHP 的 ThinkPHP, Python 的Django 等。
框架的選擇,有一個總的原則:優選成熟的框架,避免盲目追逐新技術!
對於一般的螺絲工而言,所做的主要工作都是在這個開發框架之下。對於開發語言和框架的使用,一定要充分了解和利用語言和框架的特性。
以Java為例,在作者的開發中,涉及到一個加密解密的服務調用,服務提供方利用了JNI的技術——簡單說就是C語言編寫程式碼,提供api供Java調用,彌補了Java相對沒那麼底層的劣勢,大大提高了運算的速度。
在服務開發這個日常工作的層面,可以做到這些事情來提高性能:
- 並發處理,通過多執行緒將串列邏輯並行化。
- 減少IO次數,比如資料庫和快取的批量讀寫、RPC的批量介面支援、或者通過冗餘數據的方式幹掉RPC調用。
- 減少IO時的數據包大小,包括採用輕量級的通訊協議、合適的數據結構、去掉介面中的多餘欄位、減少快取key的大小、壓縮快取value等。
- 程式邏輯優化,比如將大概率阻斷執行流程的判斷邏輯前置、For循環的計算邏輯優化,或者採用更高效的演算法
- 各種池化技術的使用和池大小的設置,包括HTTP請求池、執行緒池(考慮CPU密集型還是IO密集型設置核心參數)、資料庫和Redis連接池等。
- JVM優化,包括新生代和老年代的大小、GC演算法的選擇等,儘可能減少GC頻率和耗時。
- 鎖選擇,讀多寫少的場景用樂觀鎖,或者考慮通過分段鎖的方式減少鎖衝突。
可以通過這些事情來提高可用性:
- 設置合適的超時時間、重試次數及機制,必要時要及時降級,返回兜底數據等,防止把服務提方供打崩
- 防重設計:通過防重key、防重表等方式實現防重
- 冪等設計:在介面層面實現冪等設計
服務中心
當系統數量不多的時候,系統間的調用一般都是直接通過配置文件記錄在各系統內部的,但當系統數量多了以後,這種方式就存在問題了。
比如說總共有 10 個系統依賴 A 系統的 X 介面,A 系統實現了一個新介面 Y, 能夠更好地提供原有 X 介面的功能,如果要讓已有的 10 個系統都切換到 Y 介面,則這 10 個系統的幾十上百台器的配置都要修改,然後重啟,可想而知這個效率是很低的。
服務中心的實現主要採用服務名字系統。
- 服務務名字系統 (Service Name System)
看到這個翻譯,相信你會立刻聯想到 DNS, 即 Domain Name System。沒錯,兩者的性質是基本類似的。
DNS 的作用將域名解析為 IP 地址,主要原因是我們記不住太多的數字 IP, 域名就容易記住。服務名字系統是為了將 Service 名稱解析為 “host + port + 介面名稱” ,但是和 DNS一樣,真正發起請求的還是請求方。
在微服務的架構下,實現這個功能的稱之為註冊中心,例如在Java語言體系下,開源的註冊中心有Nacos、Ecuraka等。
配置中心
配置中心就是集中管理各個服務的配置。
在服務不多的時候,各個服務各自管理自己的配置,沒有問題,但是當服務成百上千,再各行其政,就是一個比較頭疼的事。
所以將配置中心抽象成公共的組件,集中配置多個系統,操作效率高。
在微服務架構體系下,配置中心的開源方案有SpringCloud的SpringCloud Config、阿里的Nacos等。
服務框架
服務拆分最直接的影響就是本地調用的服務變成了遠程調用,服務消費者A需要通過註冊中心去查詢服務提供者B的地址,然後發起調用,這個看似簡單的過程就可能會遇到下面幾種情況,比如:
- 註冊中心宕機;
- 服務提供者B有節點宕機;
- 服務消費者A和註冊中心之間的網路不通;
- 服務提供者B和註冊中心之間的網路不通;
- 服務消費者A和服務提供者B之間的網路不通;
- 服務提供者B有些節點性能變慢;
- 服務提供者B短時間內出現問題。
怎麼去保證服務消費者成功調用服務生產者?這就是服務治理框架要解決的問題。
在Java語言體系下,目前流行的服務治理框架有SpringCloud和Dubbo。
以SpringCloud為例:
- Feign封裝RestTemplate實現http請求方式的遠程調用
- Feign封裝Ribbon實現客戶端負載均衡
- Euraka集群部署實現註冊中心高可用
- 註冊中心心跳監測,更新服務可用狀態
- 集成Hystrix實現熔斷機制
- Zuul作為API 網關 ,提供路由轉發、請求過濾等功能
- Config實現分散式配置管理
- Sluth實現調用鏈路跟蹤
- 集成ELK,通過Kafka隊列將日誌非同步寫入Elasticsearch,通過Kibana可視化查看
SpringCloud是一整套完整微服務解決方案,被稱為「SpringCloud 全家桶」。這裡只是簡單地介紹一下。
Dubbo主要提供了最基礎的RPC功能。
不過SpringCloud的RPC採用了HTTP協議,可能性能會差一些。
利好的是,「SpringCloud2.0」——SpringCloud Alibaba流行了起來,Dubbo也可以完美地融入SpringCloud的生態。
消息隊列
消息隊列在高性能、高擴展、高可用的架構中扮演著很重要的角色。
消息隊列是用來解耦一些不需要同步調用的服務或者訂閱一些自己系統關心的變化。使用消息隊列可以實現服務解耦(一對多消費)、非同步處理、流量削峰/緩衝等。
服務解耦
服務解耦可以降低服務間耦合,提高系統系統的擴展性。
例如一個訂單服務,有多個下游,如果不用消息隊列,那麼訂單服務就要調用多個下游。如果需求要再加下游,那麼訂單服務就得添加調用新下流的功能,這就比較煩。
引入消息隊列之後,訂單服務就可以直接把訂單相關消息塞到消息隊列中,下游系統只管訂閱就行了。
非同步處理
非同步處理可以降低響應時間,提高系統性能。
隨著業務的發展項目的請求鏈路越來越長,這樣一來導致的後果就是響應時間變長,有些操作其實不用同步處理,這時候就可以考慮採用非同步的方式了。
流量削峰/緩衝
流量削峰/緩衝可以提高系統的可用性。
我們前面提到了接入層的限流,在服務層的限流可以通過消息隊列來實現。網關的請求先放入消息隊列中,後端服務儘可能去消息隊列中消費請求。超時的請求可以直接返回錯誤,也可以在消息隊列中等待。
消息隊列系統基本功能的實現比較簡單,但要做到高性能、高可用、消息時序性、消息事務性則比較難。業界已經有很多成熟的開源實現方案,如果要求不高,基本上拿來用即可,例如,RocketMQ、Kafka、ActiveMQ 等。
但如果業務對消息的可靠性、時序、事務性要求較高時,則要深入研究這些開源方案,提前考慮可能會遇到的問題,例如消息重複消費、消息丟失、消息堆積等等。
平台層
當業務規模比較小、系統複雜度不高時,運維、測試、數據分析、管理等支撐功能主要由各系統或者團隊獨立完成。隨著業務規模越來越大,系統複雜度越來越高,子系統數量越來越多,如果繼續採取各自為政的方式來實現這些支撐功能,會發現重複工作非常多。所以就會自然地把相關功能抽離出來,作為公共的服務,避免重複造輪子,減少不規範帶來的溝通和協作成本。
平台層是服務化思維下的產物。將公共的一些功能拆分出來,讓相關的業務服務只專註於自己的業務,這樣有利於明確服務的職責,方便服務擴展。
同時一些公共的平台,也有利於各個服務之間的統籌,例如數據平台,可以對數據進行聚合,某個服務以前需要一些整合一些數據可能要調用多個上游服務,但是引入數據平台以後,只需要從數據平台取數據就可以了,可以降低服務的響應時間。
運維平台
運維平台核心的職責分為四大塊:配置、部署、監控、應急,每個職責對應系統生命周期的一個階段,如下圖所示:
- 部署:主要負責將系統發布到線上。例如,包管理、灰度發布管理、回滾等。
- 監控:主要負責收集系統上線運行後的相關數據並進行監控,以便及時發現問題。
- 應急:主要負責系統出故障後的處理。例如,停止程式、下線故障機器、切換 IP 等。
運維平台的核心設計要素是「四化”——標準化、平台化、自動化、可視化。
-
標準化:要制定運維標準,規範配置管理、部署流程、監控指標、應急能力等,各系統按照運維標準來
實現,避免不同的系統不同的處理方式。
-
平台化:傳統的手工運維方式需要投入大量人力,效率低,容易出錯,因此需要在運維標準化的基礎上,
將運維的相關操作都集成到運維平台中,通過運維平台來完成運維工作。
-
自動化:傳統手工運維方式效率低下的一個主要原因就是要執行大量重複的操作,運維平台可以將這些重
復操作固化下來,由系統自動完成。
-
可視化:運維平台有非常多的數據,如果全部通過人工去查詢數據再來判斷,則效率很低,可視化的主要目的就是為了提升數據查看效率。
測試平台
測試平台核心的職責當然就是測試了,包括單元測試、集成測試、介面測試、性能測試等,都可以在測試平台來完成。
測試平台的核心目的是提升測試效率,從而提升產品品質,其設計關鍵就是自動化。
數據平台
數據平台的核心職責主要包括三部分:數據管理、數據分析和數據應用。每一部分又包含更多的細分領域,詳細的數據平台架構如下圖所示:
- 數據管理
數據管理包含數據採集、數據存儲、數據訪問和數據安全四個核心職責,是數據平台的基礎功能。
- 數據採集:從業務系統搜集各類數據。例如,日誌、用戶行為、業務數據等,將這些數據傳送到數據平台。
- 數據存儲:將從業務系統採集的數據存儲到數據平台,用於後續數據分析。
- 數據訪問:負責對外提供各種協議用於讀寫數據。例如,SQL、 Hive、 Key-Value 等讀寫協議。
- 數據安全:通常情況下數據平台都是多個業務共享的,部分業務敏感數據需要加以保護,防止被其他業務讀取甚至修改,因此需要設計數據安全策略來保護數據。
- 數據分析
數據分析包括數據統計、數據挖掘、機器學習、深度學習等幾個細分領域。
-
數據挖掘:數據挖掘這個概念本身含義可以很廣,為了與機器學習和深度學習區分開,這裡的數據挖掘主要是指傳統的數據挖掘方式。例如,有經驗的數據分析人員基於數據倉庫構建一系列規則來對數據進行分析從而發現一些隱含的規律、現象、問題等,經典的數據挖掘案例就是沃爾瑪的啤酒與尿布的關聯關係的發現。
-
機器學習、深度學習:機器學習和深度學習屬於數據挖掘的一種具體實現方式,由於其實現方式與傳統的數據挖掘方式差異較大,因此數據平台在實現機器學習和深度學習時,需要針對機器學習和深度學習獨立進行設計。
- 數據應用
數據應用很廣泛,既包括在線業務,也包括離線業務。例如,推薦、廣告等屬於在線應用,報表、欺詐檢測、異常檢測等屬於離線應用。數據應用能夠發揮價值的前提是需要有 “大數據” ,只有當數據的規模達到一定程度,基於數據的分析、挖掘才能發現有價值的規律、現象、問題等。如果數據沒有達到一定規模,通常情況下做好數據統計就足夠了,尤其是很多初創企業,無須一開始就參考 BAT 來構建自己的數據平台。
管理平台
管理平台的核心職責就是許可權管理,無論是業務系統(例如,淘寶網) 、中間件系統(例如,消息隊列 Kafka) , 還是平台系統(例如,運維平台) ,都需要進行管理。如果每個系統都自己來實現許可權管理,效率太低,重複工作很多,因此需要統一的管理平台來管理所有的系統的許可權。
說到「平台」,不由地想起這幾年一會兒被人猛吹,一會兒被人唱衰的「中台」。在平台里的數據平台,其實已經和所謂的「數據中台」類似了。「中台」是個概念性的東西,具體怎麼實現,沒有統一的標準方案。作者所在的公司,也跟風建了中台,以「數據中台」為例,我們數據中台的建設主要為了數據共享和數據可視化,簡單說就是把各個業務模組的一些數據匯聚起來。說起來簡單,落地很難,數據匯聚的及時性、數據共享的快速響應……最終的解決方案是採購了阿里的一些商業化組件,花了老鼻子錢,但是效果,不能說一地雞毛,也差不多吧。
快取層
雖然我們可以通過各種手段來提升存儲系統的性能,但在某些複雜的業務場景下,單純依靠存儲系統的性能提升不夠的。
絕大部分在線業務都是讀多寫少。例如,微博、淘寶、微信這類互聯網業務,讀業務佔了整體業務量的 90%以上。以微博為例:一個明星發一條微博,可能幾千萬人來瀏覽。
如果直接從DB中取數據,有兩個問題,一個是DB查詢的速度有瓶頸,會增加系統的響應時間,一個是資料庫本身的並發瓶頸。快取就是為了彌補讀多寫少場景下存儲系統的不足。
在前面我們提到的CDN可以說是快取的一種,它快取的是靜態資源。
從整個架構來看,一般採用多級快取的架構,在不同層級對數據進行快取,來提升訪問效率。
簡單說一下整體架構和流程,快取一級一級地去讀取,沒有命中再去讀取下一級,先讀取本地快取,再去讀取分散式快取,分散式快取也沒有命中,最後就得去讀取DB。
分散式快取
為了提高快取的可用性,一般採用分散式快取。分散式快取一般採用分片實現,即將數據分散到多個實例或多台伺服器。演算法一般釆用取模和一致性哈希。
要採用不過期快取機制,可以考慮取模機制,擴容時一般是新建一個集群。
而對於可以丟失的快取數據,可以考慮一致性哈希,即使其中一個實例出問題只是丟一小部分。
對於分片實現可以考慮客戶端實現,或者使用如Twemproxy 中間件進行代理(分片對客戶端是透明的)。
如果使用 Redis, 則 可 以考慮使用 redis-cluster 分散式集群方案。
熱點本地快取
對於那些訪問非常頻繁的熱點快取,如果每次都去遠程快取系統中獲取,可能會因為訪問量太大導致遠程快取系統請求過多、負載過高或者頻寬過高等問題,最終可能導致快取響應慢,使客戶端請求超時。
一種解決方案是通過掛更多的從快取,客戶端通過負載均衡機制讀取從快取系統數據。不過也可以在客戶端所在的應用/代理層本地存儲一份,從而避免訪問遠程快取,即使像庫存這種數據,在有些應用系統中也可以進行幾秒鐘的本地快取,從而降低遠程系統的壓力。
快取的引入雖然提高了系統的性能,但同時也增加了系統的複雜度,帶來了一些運維的成本。
快取穿透
快取穿透是指快取沒有發揮作用,業務系統雖然去快取查詢數據,但快取中沒有數據,業務系統需要再次去存儲系統查詢數據,結果存儲系統也沒有數據。
快取穿透的示意圖:
一般情況下,如果存儲系統中沒有某個數據,則不會在快取中存儲相應的數據,這樣就導致用戶查詢的時候,在快取中找不到對應的數據,每次都要去存儲系統中再查詢一遍,然後返回數據不存在。快取在這個場景中並沒有起到分擔存儲系統訪問壓力的作用。
通常情況下,業務上讀取不存在的數據的請求量並不會太大,但如果出現一些異常情況,例如被黑客攻擊,故意大量訪問某些讀取不存在數據的業務,有可能會將存儲系統拖垮。
這種情況的解決辦法有兩種:
一種比較簡單,如果查詢存儲系統的數據沒有找到,則直接設置一個默認值(可以是空值,也可以是具體的值) 存到快取中,這樣第二次讀取快取時就會獲取到默認值,而不會繼續訪問存儲系統。
一種需要引入布隆過濾器,它的原理也很簡單就是利用高效的數據結構和演算法,快速判斷出查詢的Key是否在資料庫中存在,不存在直接返回空,存在就去查了DB,刷新KV再返回值。
快取擊穿
快取擊穿和快取穿透也有點難以區分,快取穿透表示的是快取和資料庫中都沒有數據,快取擊穿表示快取中沒有數據而資料庫中有數據。快取擊穿是某個熱點的key失效,大並發集中對其進行請求,就會造成大量請求讀快取沒讀到數據,從而導致高並發訪問資料庫,引起資料庫壓力劇增。這種現象就叫做快取擊穿。
快取擊穿示意圖:
關鍵在於某個熱點的key失效了,導致大並發集中打在資料庫上。所以要從兩個方面解決,第一是否可以考慮熱點key不設置過期時間,第二是否可以考慮降低打在資料庫上的請求數量。
主要有兩個解決辦法:
-
利用互斥鎖保證同一時刻只有一個客戶端可以查詢底層資料庫的這個數據,一旦查到數據就快取至Redis內,避免其他大量請求同時穿過Redis訪問底層資料庫。這種方式會阻塞其他的執行緒,此時系統的吞吐量會下降
-
熱點數據快取永遠不過期。
永不過期有兩種方式:
- 物理不過期,針對熱點key不設置過期時間
- 邏輯過期,把過期時間存在key對應的value里,如果發現要過期了,通過一個後台的非同步執行緒進行快取的構建
快取雪崩
快取雪崩,指的是是快取不可用,或者同一時刻是大量熱點key失效。
兩種情況導致的同樣的後果就是大量的請求直接落在資料庫上,對於一個高並發的業務系統來說,幾百毫秒內可能會接到幾百上千個請求,最嚴重的後果就是直接導致資料庫宕機,可能會引起連鎖反應,導致系統崩潰。
快取雪崩的解決方案可以分為三個維度:
- 事前:
① 均勻過期:設置不同的過期時間,讓快取失效的時間盡量均勻,避免相同的過期時間導致快取雪崩,造成大量資料庫的訪問。
② 分級快取:第一級快取失效的基礎上,訪問二級快取,每一級快取的失效時間都不同。
③ 熱點數據快取永遠不過期。
④ 保證Redis快取的高可用,防止Redis宕機導致快取雪崩的問題。可以使用 Redis集群等方式來避免 Redis 全盤崩潰的情況。
- 事中:
① 互斥鎖:在快取失效後,通過互斥鎖或者隊列來控制讀數據寫快取的執行緒數量,比如某個key只允許一個執行緒查詢數據和寫快取,其他執行緒等待。這種方式會阻塞其他的執行緒,此時系統的吞吐量會下降
② 使用熔斷機制,限流降級。當流量達到一定的閾值,直接返回「系統擁擠」之類的提示,防止過多的請求打在資料庫上將資料庫擊垮,至少能保證一部分用戶是可以正常使用,其他用戶多刷新幾次也能得到結果。
- 事後:
① 開啟Redis持久化機制,儘快恢復快取數據,一旦重啟,就能從磁碟上自動載入數據恢復記憶體中的數據。
存儲層
不管是為了滿足業務發展的需要,還是為了提升自己的競爭力,關係資料庫廠商(Oracle、DB2、MySQL 等)在優化和提升單個資料庫伺服器的性能方面也做了非常多的技術優化和改進。但業務發展速度和數據增長速度,遠遠超出資料庫廠商的優化速度,尤其是互聯網業務興起之後,海量用戶加上海量數據的特點,單個資料庫伺服器已經難以滿足業務需要,必須考慮資料庫集群的方式來提升性能。
讀寫分離
讀寫分離的基本原理是將資料庫讀寫操作分散到不同的節點上,下面是其基本架構圖:
讀寫分離的基本實現是:
-
資料庫伺服器搭建主從集群,一主一從、一主多從都可以。
-
資料庫主機負責讀寫操作,從機只負責讀操作。
-
資料庫主機通過複製將數據同步到從機,每台資料庫伺服器都存儲了所有的業務數據。
-
業務伺服器將寫操作發給資料庫主機,將讀操作發給資料庫從機。
讀寫分離的實現邏輯並不複雜,但有兩個細節點將引入設計複雜度:主從複製延遲和分配機制。
複製延遲
以 MySQL 為例,主從複製延遲可能達到 1 秒,如果有大量數據同步,延遲 1 分鐘也是有可能的。
主從複製延遲會帶來一個問題:如果業務伺服器將數據寫入到資料庫主伺服器後立刻 (1 秒 內)進行讀取,此時讀操作訪問的是從機,主機還沒有將數據複製過來,到從機讀取數據是讀不到最新數據的,業務上就可能出現問題。
比如說將微博的資訊同步給審核系統,所以我們在更新完主庫之後,會將微博的 ID 寫入消息隊列,再由隊列處理機依據 ID 在從庫中 獲取微博資訊再發送給審核系統。此時如果主從資料庫存在延遲,會導致在從庫中獲取不到微博資訊,整個流程會出現異常。
解決主從複製延遲的常見方法:
- 數據的冗餘
我們可以在發送消息隊列時不僅僅發送微博 ID,而是發送隊列處理機需要的所有微博資訊,藉此避免從資料庫中重新查詢數據。
- 使用快取
我們可以在同步寫資料庫的同時,也把微博的數據寫入到快取裡面,隊列處理機在獲取微博資訊的時候會優先查詢快取,這樣也可以保證數據的一致性。
- 二次讀取
我們可以對底層資料庫訪問的API進行封裝,一次讀取從庫發現不實時之後再讀取一次,例如我們通過微博ID沒有在從庫里讀到微博,那麼第二次就直接去主庫讀取。
- 查詢主庫
我們可以把關鍵業務,或者對實時性有要求的業務讀寫操作全部指向主機,非關鍵業務或者實時性要求不高的業務採用讀寫分離。
分配機制
將讀寫操作區分開來,然後訪問不同的資料庫伺服器,一般有兩種方式:程式程式碼封裝和中間件封裝。
- 程式程式碼封裝
程式程式碼封裝指在程式碼中抽象一個數據訪問層(所以有的文章也稱這種方式為 “中間層封裝” ) ,實現讀寫操作分離和資料庫伺服器連接的管理。例如,基於 Hibernate 進行簡單封裝,就可以實現讀寫分離,基本架構是:
程式程式碼封裝的方式具備幾個特點:
-
實現簡單,而且可以根據業務做較多訂製化的功能。
-
每個程式語言都需要自己實現一次,無法通用,如果一個業務包含多個程式語言寫的多個子系統,則重複開發的工作量比較大。
-
故障情況下,如果主從發生切換,則可能需要所有系統都修改配置並重啟。
如果不想自己造輪子,也可以用開源的方案,淘寶的TDDL是比較出名的一個。
- 中間件封裝
中間件封裝指的是獨立一套系統出來,實現讀寫操作分離和資料庫伺服器連接的管理。中間件對業務伺服器提供 SQL 兼容的協議,業務伺服器無須自己進行讀寫分離。對於業務伺服器來說,訪問中間件和訪問資料庫沒有區別,事實上在業務伺服器看來,中間件就是一個資料庫伺服器。
其基本架構是:
資料庫中間件的方式具備的特點是:
- 能夠支援多種程式語言,因為資料庫中間件對業務伺服器提供的是標準 SQL 介面。
- 資料庫中間件要支援完整的 SQL 語法和資料庫伺服器的協議(例如,MySQL 客戶端和伺服器的連接協議) ,實現比較複雜,細節特別多,很容易出現 bug, 需要較長的時間才能穩定。
- 資料庫中間件自己不執行真正的讀寫操作,但所有的資料庫操作請求都要經過中間件,中間件的性能要求也很高。
- 資料庫主從切換對業務伺服器無感知,資料庫中間件可以探測資料庫伺服器的主從狀態。例如,向某個測試表寫入一條數據,成功的就是主機,失敗的就是從機。
目前開源的資料庫中間件有基於 MySQL Proxy 開發的奇虎 360 的 Atlas 、阿 里 的Cobar、基於 Cobar 開發的 Mycat 等。
分庫分表
讀寫分離分散了資料庫讀寫操作的壓力,但沒有分散存儲壓力,當數據量達到干萬甚至上億條的時候,單台資料庫伺服器的存儲能力會成為系統的瓶頸,主要體現在這幾個方面:
-
數據量太大,讀寫的性能會下降,即使有索引,索引也會變得很大,性能同樣會下降。
-
數據文件會變得很大,資料庫備份和恢復需要耗費很長時間。
-
數據文件越大,極端情況下丟失數據的風險越高(例如,機房火災導致資料庫主備機都發生故障)。
基於上述原因,單個資料庫伺服器存儲的數據量不能太大,需要控制在一定的範圍內。為了滿足業務數據存儲的需求,就需要將存儲分散到多台資料庫伺服器上。
業務分庫
業務分庫指的是按照業務模組將數據分散到不同的資料庫伺服器。例如,一個簡單的電商網站,包括用戶、商品、訂單三個業務模組,我們可以將用戶數據、商品數據、訂單數據分開放到三台不同的資料庫伺服器上,而不是將所有數據都放在一台資料庫伺服器上。
雖然業務分庫能夠分散存儲和訪問壓力,但同時也帶來了新的問題,接下來我們詳細分析一下。
- join 操作問題
業務分庫後,原本在同一個資料庫中的表分散到不同資料庫中,導致無法使用 SQL 的 join 查 詢。
例如: “查詢購買了化妝品的用戶中女性用戶的列表〃 這個功能,雖然訂單數據中有用戶的 ID資訊,但是用戶的性別數據在用戶資料庫中,如果在同一個庫中,簡單的 join 查詢就能完成;但現在數據分散在兩個不同的資料庫中,無法做 join 查詢,只能採取先從訂單資料庫中查詢購買了化妝品的用戶 ID 列表,然後再到用戶資料庫中查詢這批用戶 ID 中的女性用戶列表,這樣實現就比簡單的 join 查詢要複雜一些。
- 事務問題
原本在同一個資料庫中不同的表可以在同一個事務中修改,業務分庫後,表分散到不同的資料庫中,無法通過事務統一修改。雖然資料庫廠商提供了一些分散式事務的解決方案(例如,MySQL 的 XA) , 但性能實在太低,與高性能存儲的目標是相違背的。
例如,用戶下訂單的時候需要扣商品庫存,如果訂單數據和商品數據在同一個資料庫中,我們可訂單,如果因為訂單資料庫異常導致生成訂單失敗,業務程式又需要將商品庫存加上;而如果因為業務程式自己異常導致生成訂單失敗,則商品庫存就無法恢復了,需要人工通過曰志等方式來手工修復庫存異常。
- 成本問題
業務分庫同時也帶來了成本的代價,本來 1 台伺服器搞定的事情,現在要 3 台,如果考慮備份,那就是 2 台變成了 6 台。
基於上述原因,對於小公司初創業務,並不建議一開始就這樣拆分,主要有幾個原因:初創業務存在很大的不確定性,業務不一定能發展起來,業務開始的時候並沒有真正的存儲和訪問壓力,業務分庫並不能為業務帶來價值。業務分庫後,表之間的 join 查詢、資料庫事務無法簡單實現了。
業務分庫後,因為不同的數據要讀寫不同的資料庫,程式碼中需要增加根據數據類型映射到不同資料庫的邏輯,增加了工作量。而業務初創期間最重要的是快速實現、快速驗證,業務分庫會拖慢業務節奏。
單表拆分
將不同業務數據分散存儲到不同的資料庫伺服器,能夠支撐百萬甚至千萬用戶規模的業務,但如果業務繼續發展,同一業務的單表數據也會達到單台資料庫伺服器的處理瓶頸。例如,淘寶的幾億用戶數據,如果全部存放在一台資料庫伺服器的一張表中,肯定是無法滿足性能要求的,此時就需要對單表數據進行拆分。
單表數據拆分有兩種方式:垂直分表和水平分表。示意圖如下:
分表能夠有效地分散存儲壓力和帶來性能提升,但和分庫一樣,也會引入各種複雜性。
兩種分表方式可以用一個例子比喻,我們很多人可能都看過這麼一篇文章,怎麼把蘋果切出星星來,答案是橫著切。
- 垂直分表
垂直分表適合將表中某些不常用且佔了大量空間的列拆分出去。例如,前面示意圖中的nickname 和 desc 欄位,假設我們是一個婚戀網站,用戶在篩選其他用戶的時候,主要是用 age 和 sex 兩個欄位進行查詢,而 nickname 和 description 兩個欄位主要用於展示,一般不會在業務查詢中用到。description 本身又比較長,因此我們可以將這兩個欄位獨立到另外—張表中,這樣在查詢 age 和 sex 時,就能帶來一定的性能提升。垂直分表引入的複雜性主要體現在表操作的數量要增加。例如,原來只要一次查詢就可以獲取name、age、sex、nickname、description, 現在需要兩次查詢,—次查詢獲取 name、age、 sex, 另一次查詢獲取 nickname、desc。
不過相比接下來要講的水平分表,這個複雜性就是小巫見大巫了。
- 水平分表
水平分表適合錶行數特別大的表,有的公司要求單錶行數超過 5000 萬就必須進行分表,這個數字可以作為參考,但並不是絕對標準,關鍵還是要看錶的訪問性能。對於一些比較複雜的表,可能超過 1000 萬就要分表了;而對於一些簡單的表,即使存儲數據超過 1 億行,也可以不分表。但不管怎樣,當看到表的數據量達到干萬級別時,這很可能是架構的性能瓶頸或者隱患。
水平分表相比垂直分表,會引入更多的複雜性,主要表現在下面幾個方面:
- 路由
水平分表後,某條數據具體屬於哪個切分後的子表,需要增加路由演算法進行計算,這個演算法會引入一定的複雜性。
常見的路由演算法有:
範圍路由:選取有序的數據列 (例如,整形、時間戳等) 作為路由的條件,不同分段分散到不同的資料庫表中。以訂單 Id 為例,路由演算法可以按照 1000萬 的範圍大小進行分段。範圍路由設計的複雜點主要體現在分段大小的選取上,分段太小會導致切分後子表數量過多,增加維護複雜度;分段太大可能會導致單表依然存在性能問題,一般建議分段大小在 100 萬至2000 萬之間,具體需要根據業務選取合適的分段大小。
範圍路由的優點是可以隨著數據的增加平滑地擴充新的表。例如,現在的用戶是 100 萬,如果增加到 1000 萬,只需要增加新的表就可以了,原有的數據不需要動。範圍路由的一個比較隱含的缺點是分布不均勻,假如按照 1000 萬來進行分表,有可能某個分段實際存儲的數據量只有 1000 條,而另外一個分段實際存儲的數據量有 900 萬條。
Hash 路由:選取某個列 (或者某幾個列組合也可以) 的值進行 Hash 運算,然後根據 Hash 結果分散到不同的資料庫表中。同樣以訂單 id 為例,假如我們一開始就規划了 4個資料庫表,路由演算法可以簡單地用 id % 4 的值來表示數據所屬的資料庫表編號,id 為 12的訂單放到編號為 50的子表中,id為 13的訂單放到編號為 61的字表中。
Hash 路由設計的複雜點主要體現在初始表數量的選取上,表數量太多維護比較麻煩,表數量太少又可能導致單表性能存在問題。而用了 Hash 路由後,增加字表數量是非常麻煩的,所有數據都要重分布。
Hash 路由的優缺點和範圍路由基本相反,Hash 路由的優點是表分布比較均勻,缺點是擴充新的表很麻煩,所有數據都要重分布。
配置路由:配置路由就是路由表,用一張獨立的表來記錄路由資訊。
同樣以訂單id 為例,我們新增一張 order_router 表,這個表包含 orderjd 和 tablejd 兩列 , 根據 orderjd 就可以查詢對應的 table_id。
配置路由設計簡單,使用起來非常靈活,尤其是在擴充表的時候,只需要遷移指定的數據,然後修改路由表就可以了。
配置路由的缺點就是必須多查詢一次,會影響整體性能;而且路由表本身如果太大(例如,幾億條數據) ,性能同樣可能成為瓶頸,如果我們再次將路由表分庫分表,則又面臨一個死循環式的路由演算法選擇問題。
- join 操作
水平分表後,數據分散在多個表中,如果需要與其他表進行 join 查詢,需要在業務程式碼或者資料庫中間件中進行多次 join 查詢,然後將結果合併。
- count()操作
分表後就沒那麼簡單了。常見的處理方式有下面兩種:
count() 相加:具體做法是在業務程式碼或者資料庫中間件中對每個表進行 count操作,然後將結果相加。這種方式實現簡單,缺點就是性能比較低。例如,水平分表後切分為 20 張表,則要進行 2 0 次 count()操作,如果串列的話,可能需要幾秒鐘才能得到結果。
記錄數表:具體做法是新建一張表,假如表名為 “記錄數表」 ,包含 table_name、 row_count兩個欄位,每次插入或者刪除子表數據成功後,都更新 “記錄數表「。這種方式獲取表記錄數的性能要大大優於 count()相加的方式,因為只需要一次簡單查詢就可以獲取數據。缺點是複雜度增加不少,對子表的操作要同步操作 “記錄數表” ,如果有一個業務邏輯遺漏了,數據就會不一致;且針對 “記錄數表” 的操作和針對子表的操作無法放在同一事務中進行處理,異常的情況下會出現操作子表成功了而操作記錄數表失敗,同樣會導致數據不一致。
此外,記錄數表的方式也增加了資料庫的寫壓力,因為每次針對子表的 insert 和 delete 操作都要 update 記錄數表,所以對於一些不要求記錄數實時保持精確的業務,也可以通過後台定時更新記錄數表。定時更新實際上就是 “count()相加” 和 “記錄數表” 的結合,即定時通過count()相加計算表的記錄數,然後更新記錄數表中的數據。
- order by 操作
水平分表後,數據分散到多個子表中,排序操作無法在資料庫中完成,只能由業務程式碼或者資料庫中間件分別查詢每個子表中的數據,然後匯總進行排序。
實現方法
和資料庫讀寫分離類似,分庫分表具體的實現方式也是 “程式程式碼封裝” 和 “中間件封裝” ,但實現會更複雜。讀寫分離實現時只要識別 SQL 操作是讀操作還是寫操作,通過簡單的判斷SELECT、UPDATE、 INSERT、DELETE 幾個關鍵字就可以做到,而分庫分表的實現除了要判斷操作類型外,還要判斷 SQL 中具體需要操作的表、操作函數(例如 count 函數)、order by、group by 操作等,然後再根據不同的操作進行不同的處理。例如 order by 操作,需要先從多個庫查詢到各個庫的數據,然後再重新 order by 才能得到最終的結果。
數據異構
完成分庫分表以後,我們看到存在一些問題,除了”程式程式碼封裝” 和 “中間件封裝”之外,我們還有一種辦法,就是數據異構。數據異構就是將數據進行異地存儲,比如業務上將MySQL的數據,寫一份到Redis中,這就是實現了數據在集群中的異地存儲,也就是數據異構。
在數據量和訪問量雙高時使用數據異構是非常有效的,但增加了架構的複雜度。異構時可以通過雙寫、訂閱 MQ 或者 binlog 並解析實現。
- 雙寫:在寫入數據的時候,同時將數據寫入MySQL和異構存儲系統;
- MQ:寫入MySQL成功後,發一個mq消息,快取讀取mq消息並將消息寫入異構存儲系統;
- binlog:寫入MySQL後,快取系統x消費binlog,將變動寫入異構存儲系統。
這是一個異構的數據架構示意圖:
在圖中用到了ES搜索集群來處理搜索業務,同樣也可以我們前面提到的跨庫join的問題。
在設計異構的時候,我們可以充分利用一些流行的NoSQL資料庫。NoSQL儘管已經被證明不能取代關係型資料庫,但是在很多場景下是關係型資料庫的有力補充。
舉幾個例子,像我們熟悉的Redis這樣的KV存儲,有極高的讀寫性能,在讀寫性能有要求的場景可以使用;
Hbase、Cassandra 這樣的列式存儲資料庫。這種資料庫的特點是數據不像傳統資料庫以行為單位來存儲,而是以列來存儲,適用於一些離線數據統計的場景;
MongoDB、CouchDB 這樣的文檔型資料庫,具備 Schema Free(模式自由)的特點,數據表中的欄位可以任意擴展,可以用於數據欄位不固定的場景。
查詢維度異構
比如對於訂單庫,當對其分庫分表後,如果想按照商家維度或者按照用戶維度進行查詢,那麼是非常困難的,因此可以通過異構資料庫來解決這個問題。可以採用下圖的架構。
或者採用下圖的ES異構:
異構數據主要存儲數據之間的關係,然後通過查詢源庫查詢實際數據。不過,有時可以通過數據冗餘存儲來減少源庫查詢量或者提升查詢性能。
聚合據異構
商品詳情頁中一般包括商品基本資訊、商品屬性、商品圖片,在前端展示商品詳情頁時,是按照商品 ID 維度進行查詢,並且需要查詢 3 個甚至更多的庫才能查到所有展示數據。此時,如果其中一個庫不穩定,就會導致商品詳情頁出現問題,因此,我們把數據聚合後異構存儲到 KV 存儲集群(如存儲 JSON ), 這樣只需要一次查詢就能得到所有的展示數據。這種方式也需要系統有了一定的數據量和訪問量時再考慮。
高並發架構要點
通過前面的內容,已經差不多了解高並發的架構是一個什麼樣,接下來做一些總結和補充。
高性能要點
高可用要點
除了從技術的角度來考慮,保證高可用同樣需要良好的組織制度,來保證服務出現問題的快速恢復。
高擴展要點
1、合理的分層架構:比如上面談到的互聯網最常見的分層架構,另外還能進一步按照數據訪問層、業務邏輯層對微服務做更細粒度的分層(但是需要評估性能,會存在網路多一跳的情況)。
2、存儲層的拆分:按照業務維度做垂直拆分、按照數據特徵維度進一步做水平拆分(分庫分表)。
3、業務層的拆分:最常見的是按照業務維度拆(比如電商場景的商品服務、訂單服務等),也可以按照核心請求和非核心請求拆分,還可以按照請求源拆(比如To C和To B,APP和H5 )。
好了,攢的這一篇終於完事了,更深入學習建議閱讀書籍參考【11】。祝各位架構師能真的如江似海,把握高並發,多掙達不溜。
由於作者對於高並發的認識是從零開始,所以參考了很多經典資料!
參考與感謝:
【1】:極客時間 《從零開始學架構》
【2】:知乎問答:我沒有高並發項目經驗,但是面試的時候經常被問到高並發、性能調優方面的問題,有什麼辦法可以解決嗎?
【3】:什麼是高並發 ,詳細講解
【4】:【高並發】如何設計一個支撐高並發大流量的系統?這次我將設計思路分享給大家!
【5】:《淘寶技術這十年》
【6】:知乎問答:如何獲得高並發的經驗?
【7】:服務端高並發分散式架構演進之路
【9】:《大型網站技術架構核心原理與案例分析》
【10】:阿里技術專家:日活5億的淘寶技術發展歷程和架構經驗分享!18頁ppt詳解
【11】:《億級流量網站架構技術》
【12】:極客時間《從零開始學微服務》
【13】:面試題:如何保證消息不丟失?處理重複消息?消息有序性?消息堆積處理?
【14】:極客時間 高並發系統設計40問
【15】:《Redis深度歷險:核心原理和應用實踐》
【16】:Redis的快取雪崩、快取擊穿、快取穿透與快取預熱、快取降級
【17】:如何優雅的設計和使用快取?
【19】:數據異構