【51CTO.com原創稿件】程式裸奔在伺服器,沒有經過任何的處理(至於怎麼處理,這裡先賣個關子,待會兒給大家慢慢道來......),當大流量來臨後,會發生什麼呢?
關鍵時刻服務崩潰,損失了一個億
有程式設計師說,對程式進行調優?調優能解決問題碼?調優肯定可以解決一部分問題,但是調優僅僅可以緩解程式出現問題,並不能消滅問題。
各位試想一下,當系統面臨大流量衝擊的時候,CPU 被打滿了(請參考下圖所示):
可以發現 CPU 爆滿,佔比幾乎達到 100%,對於系統來說是一個很危險的訊號,隨時都有可能崩潰掉。
記憶體被打滿了(如下圖),這個使用程式都可能出現崩潰:
這樣的場景就不要調優能解決的了,這涉及到你的可利用的資源就這些,超越了資源使用的極限了,服務肯定扛不住。
可以發現記憶體平均佔用已經超過了 80%,甚至有時候佔用達到了接近 100%,這對於系統來說很危險,隨時面臨崩潰的風險。
那麼這個服務該如何解決呢?至於如何解決這些問題,那就是一個非常大的課題,接下來我儘可能的透過有限的篇幅來描述清楚,這個問題該如何解決,如何在高併發,大流量環境下解保證系統平穩,高效的執行。
高併發下系統如何平穩執行
①流量集中,CPU 癱瘓
啟動服務:執行 deploy-shop.sh 啟動指令碼,啟動你的服務即可。啟動過程中可以檢視日誌:tail -f t.log,觀察是否啟動成功。
壓力測試:服務啟動後,我們可以給定 10w 個樣本進行壓力測試,來觀察服務在壓力測試情況下會發生什麼問題。
top 指令查詢,觀察 CPU 指標:
可以看見 load average 最終飆到 10 以上,說明 CPU 已經出現了嚴重的阻塞現象,如果不能及時解決,線上系統就會出現很嚴重的問題。
觀察 TPS:
最終結果,就是導致程式程式大量的錯誤;這些錯誤還有持續變高的趨勢,這對於線上系統來說可以不是一件好事情。
②搞不好,出現 OOM
啟動指令碼 2:使用指令碼:deploy2.sh 啟動專案。
這個指令碼中限定了專案啟動的記憶體大小為:500M。
- Java 堆溢位 (java.lang.OutofMemoryError:Java heap space),記憶體溢位 out of memory:是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現 out of memory。
- PermGen space(永久區內部不足)。
- 記憶體洩露 memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。
- StackOverflowError(虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間)。
- 直接崩潰。
壓力測試:
監控記憶體:使用 JProfiler 監控 JVM 記憶體情況是一種比較好的有效的手段,可以觀察記憶體溢位,記憶體洩露等等異常情況。
可以發現記憶體一直處於飆升狀態,甚至一度達到了 100%,最後甚至程式直接卡死,走不下去了,程式直接崩潰掉了。
檢視日誌:
接下來,我們去看一下日誌,我們發現直接 OOM 了,甚至程式錯誤率 100%,直接崩潰。
③服務雪崩效應
在微服務架構中通常會有多個服務層呼叫,基礎服務的故障可能會導致級聯故障,進而造成整個系統不可用的情況,這種現象被稱為服務雪崩效應。
服務雪崩效應是一種因“服務提供者”的不可用導致“服務消費者”的不可用,並將不可用逐漸放大的過程。
上圖所示,如果服務 T 因為高併發宕機,會影響到服務 U,而服務 U 又關聯了 3 條線,最終將導致整個系統崩潰,也就是災難性雪崩效應。
造成的原因:
- 服務提供者不可用(硬體故障,Bug,大量請求)
- 重試加大流量(使用者重試不斷髮送請求,程式碼邏輯重試)
- 服務呼叫者不可用(同步等待造成資源耗盡)
有什麼好的解決方案呢?
①服務降級
超時降級、資源不足時(執行緒或訊號量)降級,降級後可以配合降級介面返回託底資料。
實現一個 fallback 方法,當請求後端服務出現異常的時候,可以使用 fallback 方法返回的值。
當伺服器壓力劇增的情況下,根據實際業務情況及流量,對一些服務和頁面有策略的不處理或換種簡單的方式處理,從而釋放伺服器資源以保證核心交易正常運作或高效運作。
某電商網站在搞活動時,活動期間壓力太大,如果再進行下去,整個系統有可能掛掉,這個時候可以釋放掉一些資源,將一些不那麼重要的服務採取降級措施,比如登入、註冊。
登入服務停掉之後就不會有更多的使用者搶購,同時釋放了一些資源,登入、註冊服務就算停掉了也不影響商品搶購。
降級策略:當觸發服務降級後,新的交易再次到達時,我們該如何來處理這些請求呢?
從微服務架構全域性的視角來看,我們通常有以下是幾種常用的降級處理方案:
- 頁面降級:視覺化介面禁用點選按鈕、調整靜態頁面。
- 延遲服務:如定時任務延遲處理、訊息入 MQ 後延遲處理。
- 寫降級:直接禁止相關寫操作的服務請求。
- 讀降級:直接禁止相關讀的服務請求。
- 快取降級:使用快取方式來降級部分讀頻繁的服務介面。
針對後端程式碼層面的降級處理策略,則我們通常使用以下幾種處理措施進行降級處理:
- 拋異常
- 返回 NULL
- 呼叫 Mock 資料
- 呼叫 Fallback 處理邏輯
②服務超時
為每次請求設定一個比較短的超時時間,不管這次請求是否能夠成功,短時間內這個執行緒就會被釋放,那麼這個服務就不會那麼容易被底層服務拖死了。
此時,只要服務T達到閾值,就對服務 T 進行限流,至少服務不會被服務 T 拖死了。大大提高了我們服務的穩定性。
③服務限流
如果 B 伺服器最大能承載的 TPS 為 1000,那麼我們可以對服務設定一個閾值 800, 那麼當 TPS 的請求超過 800 的時候,就立即對其進行限流,防止服務被大流量的沖垮。
④服務隔離
服務隔離:服務隔離的方式叫做倉壁模式。那麼什麼叫做倉壁模式呢???
可以看一下輪船的設計,底層設計的時候把輪船分為一個一個的格子,當一個,或者多個格子進水了,不影響這個輪船的運轉。
當然也輪船最後沉沒了,那是因為進水太多了,或者超過了容錯的極限,因此這個輪船沉沒了。
同樣的道理,服務也可以參考這種設計,使用倉壁模式,實現服務隔離,從而使得服務具有更好的穩定性,健壯性。
那麼服務中服務如何實現隔離呢??通常情況下,服務隔離我們都採用執行緒池隔離,那麼什麼是執行緒池隔離呢??搞清楚這個問題之前,我們先看看不做隔離會發生什麼??
如下圖所示,我們直接使用 Tomcat 本身的執行緒開始工作,服務分別會呼叫 使用者服務,推薦服務,積分服務,當推薦服務不可用,或者資源耗盡,那麼就會影響整個服務的正常運轉。
因此這對服務來說,是一個及其不穩定的因素,那麼如果使用隔離術,給他隔離呢??
此時當推薦服務資源耗盡,CPU 阻塞,執行緒池無可用連線,也不會影響整個服務出現崩潰,推薦服務僅僅是因為資源耗盡,而拒接連線。
限制呼叫分散式服務的資源使用,某一個呼叫的服務出現問題不會影響其他服務呼叫。
類似的隔離技術還有很多:
程序隔離:說白了就是叢集部署,當其中一個服務宕機,透過 Nginx 負載均衡來件故障容錯。
叢集隔離:所謂的叢集隔離就是對相同的服務部署多個叢集組,來達到叢集隔離的目標。
如下圖所示:
隨著呼叫方的增多,當秒殺服務被刷會影響到其他服務的穩定性,此時應該考慮為秒殺提供單獨的服務叢集,即為服務分組,從而當某一個分組出現問題不會影響到其他分組,從而實現了故障隔離。
如下圖所示:
機房隔離:隨著對系統可用性的要求,會進行多機房部署,每個機房的服務都有自己的服務分組,本機房的服務應該只調用本機房服務,不進行跨機房呼叫。
其中一個機房服務發生問題時可以透過 DNS/負載均衡將請求全部切到另一個機房;或者考慮服務能自動重試其他機房的服務從而提升系統可用性。
讀寫隔離:透過主從模式將讀和寫叢集分離,讀服務只從從 Redis 叢集獲取資料,當主 Redis 叢集出現問題時,從 Redis 叢集還是可用的,從而不影響使用者訪問;而當從 Redis 叢集出現問題時可以進行其他叢集的重試。
動靜隔離:把靜態資源放入 Nginx,CDN 服務。達到動靜隔離。防止有頁面直接載入大量靜態資源。因為訪問量大,導致網路頻寬打滿,導致卡死,出現不可用。
熱點隔離:秒殺、搶購屬於非常合適的熱點例子;對於這種熱點是能提前知道的,所以可以將秒殺和搶購做成獨立系統或服務進行隔離,從而保證秒殺/搶購流程出現問題不影響主流程。
還存在一些熱點可能是因為價格或突發事件引起的;對於讀熱點我使用多級快取搞定;而寫熱點我們一般透過快取+佇列模式削峰。
資源隔離:最常見的資源如磁碟、CPU、網路;對於寶貴的資源都會存在競爭問題。(實在不行單獨部署一個服務,也可使用容器技術,或者虛擬機器的方式實現資源隔離)
⑤服務熔斷
服務熔斷是一種斷路器模式,斷路器的作用類似於我們家用的保險絲,當某服務出現不可用或響應超時的情況時,為了防止整個系統出現雪崩,暫時停止對該服務的呼叫。
當失敗率(如因網路故障/超時造成的失敗率高)達到閥值自動觸發降級,熔斷器觸發的快速失敗會進行快速恢復。
服務限流:胃只有那麼大,想吃很多好吃的但是裝不下,所以只能打飯打少點。
服務降級:吃的太多了,別人沒啥吃了,媽媽只好給你少打點飯,給兄弟們分點。
服務熔斷:打翻你的飯碗,你直接別吃了。
C 對 S 熔斷後,那麼原本需要呼叫 S 實現的邏輯怎麼辦呢?C 可以使用 Mock 資料、快取資料、預設資料等替代,或者乾脆就是拋異常返回錯誤資訊。
⑥服務快取
提供了請求快取:
當大流量請求來臨的時候,使用快取也是非常有效減輕服務壓力的重要手段。
當我們使用相同的請求不停的重複請求伺服器資源的時候,此時我們就不要從快取中命中資料,從而到達減輕服務壓力,增加服務穩定性的目的,防止因為資源耗盡而出現服務崩潰。
⑦請求合併
請求合併減少 HTTP 請求:資源合併與壓縮減少 HTTP 請求主要的兩個最佳化點是減少 HTTP 請求的數量和減少請求資源的大小。
HTTP 協議是無狀態的應用層協議,意味著每次 HTTP 請求都需要建立通訊鏈路、進行資料傳輸,而在伺服器端,每個 HTTP 都需要啟動獨立的執行緒去處理。
這些通訊和服務的開銷都很昂貴,減少 HTTP 請求的數量和減少請求資源的大小可有效提高訪問效能。
減少 HTTP 的主要手段是合併 CSS、合併 JavaScript、合併圖片。將瀏覽器一次訪問需要的 JavaScript 和 CSS 合併成一個檔案,這樣瀏覽器就只需要一次請求。
圖片也可以合併,多張圖片合併成一張,如果每張圖片都有不同的超連結,可透過 CSS 偏移響應滑鼠點選操作,構造不同的 URL。將圖片 base64,這樣也可以減少請求。
在微服務架構中,我們將一個專案拆分成很多個獨立的模組,這些獨立的模組透過遠端。
呼叫來互相配合工作,但是,在高併發情況下,通訊次數的增加會導致總的通訊時間增加,同時,執行緒池的資源也是有限的,高併發環境會導致有大量的執行緒處於等待狀態,進而導致響應延遲,為了解決這些問題,我們採用請求合併。
當然請求合併也不是萬能的解決的方案,只能說我們在這些方案中選擇一個折中的處理方案來進行處理相關的問題。
總結
好了,以上就是保證系統如何在流量洪峰中穩定,平穩的執行的解決方案了。
當然上面所有的都是從理論的角度來闡述如何解決這些問題,如何想要進一步對其進行落地,那麼就可以藉助一些框架(例如:spring cloud Alibaba sentinel)元件,來幫助我們實現系統的升級。
作者:JackHu
簡介:水滴健康基礎架構資深技術專家
編輯:陶家龍
【51CTO原創稿件,合作站點轉載請註明原文作者和出處為51CTO.com】