在如今的網絡環境下,高並發的場景無處不在,特別在面試如何解決高並發是一個躲不過的問題,即使生產環境達不到那么高的qps但是也應該給自己留條后路來應對日后可能發生的高並發場景,不用匆忙的加班加點的進行重構。
在應對日常高並發場景常常會有這么幾個方法:
-
- 集群&負載均衡SLB
- 讀寫分離&分庫分表
- 緩存
- 異步隊列(RabbitMQ)
- 分布式系統、微服務
接下來就由淺入深分別來介紹下這幾個方法是怎么應用到服務器並且解決高並發的,首先我們先來看下最原始的也是最簡單的服務器與應用程序關系。
圖1
如圖1所示在一台服務器上承載了數據庫、文件系統、應用程序的所有功能,這就導致即使低qps的情況下服務器的內存或者cpu占比都非常高,用過sqlserver的同僚們都知道為了達到最高效快速的數據查詢、存儲及運算支持sql server默認會盡可能的占用內存及CPU來達到自己的目的,從而導致我們的應用程序在處理一些運算或者請求量相對升高時應用程序就會變得非常慢,這時候我們就該考慮升級我們現有的服務器了,當然最高效也是最便捷的方式是升級硬件(cpu、內存、硬盤),這也是最容易達到瓶頸的畢竟一台服務的硬件也是有瓶頸的而且費用也是相當相當高昂的,一般情況下我們會選擇我們最開始提到解決高並發方法中分布式來升級我們圖1的單一服務器系統架構。
圖2
如圖2所示我們由一台服務器轉為三台服務器互相協作的方式來處理每次請求,這也是簡單的分布式系統每台服務器各司其職再也不會發生單一應用占用大量cpu或內存的情況導致請求變得緩慢,但是就圖2而言的服務器架構的承載能力也是非常有限的,當請求量上升后可能就扛不住宕機了。
一般這時候我們就要分析發生宕機的原因,從圖2便知只有服務器A或者服務器B最有可能出現問題,根據以往的經驗在請求量升高時數據庫會承載絕大部分的壓力,如果數據庫崩了那么整個應用就會處於不可用的狀態,那么為了緩解數據庫的壓力,我們很自然的就會想到利用緩存,這也是高並發場景下最常用也是最有效最簡單的方案,利用好緩存能讓你的系統的承載能力提示幾倍甚至十幾倍幾十倍。熟悉二八原則的同僚們都知道80%請求的數據都集中在20%的數據上,雖然有些誇張但是意思就是這么個意思。緩存又分為本地緩存和分布式緩存,本着分布式的原則,我們一般都會選用分布式緩存同時也是為后期做分布式集群打下基礎。
圖3
如圖3所示在圖2的基礎上增加了一台緩存服務器D來儲存我們的緩存數據,一般我們會采用redis來存放緩存數據,至於memcache現在應用的頻率是非常低的。現在當請求到達應用程序時會優先訪問緩存服務器D,若存在緩存數據就直接返回給客戶端如果不存在緩存數據才會去數據庫獲取數據返回給客戶端同時將數據保存到緩存服務器D設置緩存失效時間這樣下次請求時就不用到數據庫查詢數據了從而達到減輕數據庫壓力的目的。雖然緩存能抵擋大部分的請求,但是我們也要做好防止緩存擊穿、穿透和雪崩的問題來提升系統的穩定性。
隨着業務量的增多和繁多的業務種類圖3的系統架構也會慢慢達到瓶頸支撐不住多樣化的業務需求,這時候我們就應該采用集群的方式來達到負載均衡的目的,將請求平均的分散到多台服務器來拓展應用程序的承載能力。
圖4
如圖示4所示由服務器A-1、服務器A-2共同承載用戶的請求來提高系統的承載能力也就是我們最開始說到集群,出現集群的地方必然少不了負載均衡,圖4我們由nginx來實現請求的分發來達到負載均衡的目的。在設計圖3的架構的時候我們有說到本地緩存,如果是采用本地緩存而不是分布式緩存那么系統架構就存在一個比較大的缺陷,因為一個請求過來是由nginx區分發的如果我們再用本地緩存那么在在服務器A-1和服務器A-2上可能存在大量相同的本地緩存這樣就得不償失了容易造成服務器資源的浪費嚴重的還會拖累服務器的性能,利用分布式緩存的好處在於我們不管有多少個應用服務器所有的緩存都是共享的。
圖4的服務器架構應該是目前中小型應用中最常用的,而且系統的整體承載能力也相當不錯,不過隨着業務的發展流量與日俱增,圖3的服務器架構也很難保證系統的穩定,特別是日常流量峰值的一些時段圖3的系統可能時常會面臨奔潰的危險,這時候就要重新分析各服務器的壓力承載情況了,顯而易見最可能出現問題的就是數據庫服務器,終於要對數據庫下手了,當下最有效的方法就是就寫分離,還是遵循二八原則80%的數據操作都是查詢操作。
圖5
如圖5所示在圖4的基礎上由單一的數據庫變為主從數據庫從庫負責數據的查詢操作主庫負責數據增刪改操作,但請求操作主庫后主庫將操作日志執行到從庫達到主從數據一致的目的,但是主從分離后不可能避免的一個問題就是主從數據一致性會有延遲,數據同步延遲的問題只能盡可能的減小數據延遲的時間,但對一些時效性非常高或者不能容忍數據延遲的請求只能做一些妥協,這類操作的crud都在主庫上操作這樣就避免數據延遲的問題,對一些對於數據時效性不那么嚴格的請求可以將這部分的查詢操作由從庫去承載,對於主從數據庫個人以為和應用集群是一樣的可以理解為集群數據庫只不過在請求的分發上制定了規則(主庫處理更新、從庫處理查詢)。
如圖6將一些不屬於核心業務的功能模塊從應用服務中剝離出來降低服務的時延提高服務的吞吐量,這些類似日志、郵件/短信通知、監控等等都丟到隊列中由單獨的服務從隊列中獲取數據進行處理。可能一些同僚們會想到用異步的方法去處理這些方法,但是當有大量請求時這些異步處理會占用一部分服務器的性能同時異步也會增加程序的復雜度,所以用消息隊列的方式可能應該是比較優的一種方法。
當然隊列不僅僅是如圖6所示起到一種日志收集、通知、服務解耦的作用,很多時候會用隊列來應對一些特定場景(秒殺)來達到限流的防御性目的。
如圖7,應對一些秒殺活動場景下,我們可以優先預估服務的處理處理能力然后創建令牌隊列的容量同時開啟服務器J的創建令牌服務勻速的將令牌放入令牌隊列,如果隊列滿了就丟棄。當秒殺請求到達網關時由網關先到令牌隊列獲取令牌再請求分發到對應的服務,如果令牌沒有了說明已經達到了服務的處理上限,可直接返回秒殺失敗防止服務被壓垮,達到限流的目的。
圖8可能是和我一樣的菜鳥同僚們能馬上想到的一種隊列的服務架構,請求到達網關后直接丟到消息隊列中由對應的服務去消費,執行完成后通過rpc通知網關將結果返回給前端,如果請求超時或者隊列滿了可以直接返回請求失敗,但是圖8這種方式請求鏈比較長影響響應的時間同時異步處理會增加服務的復雜度,所以圖7這種方式會更加合適一些。
至於文章開頭說的微服務先放一放,太大了講不來了~~~~~~
總結:以上就是小弟工作與學習中對高並發下服務器架構的一些理解,如果有錯誤的地方希望大佬們給予指正。