阿里P8面試官:如何設計一個扛住千萬級併發的架構(超級詳細)-續

跟著Mic學架構發表於2021-10-18

在上一篇文章中,詳細分析了設計一個千萬級併發架構所需要思考的問題,以及解決方案。
在這一片文章中,我們主要分析如何在職場足夠使用者數量的情況下,同步提升架構的效能降低平均響應時間。

如何降低RT的值

繼續看上面這個圖,一個請求只有等到tomcat容器中的應用執行完成才能返回,而請求在執行過程中會做什麼事情呢?

  • 查詢資料庫
  • 訪問磁碟資料
  • 進行記憶體運算
  • 呼叫遠端服務

這些操作每一個步驟都會消耗時間,當前客戶端的請求只有等到這些操作都完成之後才能返回,所以降低RT的方法,就是優化業務邏輯的處理。

資料庫瓶頸的優化

當18000個請求進入到服務端並且被接收後,開始執行業務邏輯處理,那麼必然會查詢資料庫。

每個請求至少都有一次查詢資料庫的操作,多的需要查詢3~5次以上,我們假設按照3次來計算,那麼每秒會對資料庫形成54000個請求,假設一臺資料庫伺服器每秒支撐10000個請求(影響資料庫的請求數量有很多因素,比如資料庫表的資料量、資料庫伺服器本身的系統效能、查詢語句的複雜度),那麼需要6臺資料庫伺服器才能支撐每秒10000個請求。

除此之外,資料庫層面還有涉及到其他的優化方案。

  • 首先是Mysql的最大連線數設定,大家可能遇到過MySQL: ERROR 1040: Too many connections這樣的問題,原因就是訪問量過高,連線數耗盡了。

    show variables like '%max_connections%';
    

    如果伺服器的併發連線請求量比較大,建議調高此值,以增加並行連線數量,當然這建立在機器能支撐的情況下,因為如果連線數越多,介於MySQL會為每個連線提供連線緩衝區,就會開銷越多的記憶體,所以要適當調整該值,不能盲目提高設值。

  • 資料表資料量過大,比如達到幾千萬甚至上億,這種情況下sql的優化已經毫無意義了,因為這麼大的資料量查詢必然會涉及到運算。

    • 可以快取來解決讀請求併發過高的問題,一般來說對於資料庫的讀寫請求也都遵循2/8法則,在每秒54000個請求中,大概有43200左右是讀請求,這些讀請求中基本上90%都是可以通過快取來解決。

    • 分庫分表,減少單表資料量,單表資料量少了,那麼查詢效能就自然得到了有效的提升

    • 讀寫分離,避免事務操作對查詢操作帶來的效能影響

      • 寫操作本身耗費資源

        資料庫寫操作為IO寫入,寫入過程中通常會涉及唯一性校驗、建索引、索引排序等操作,對資源消耗比較大。一次寫操作的響應時間往往是讀操作的幾倍甚至幾十倍。

      • 鎖爭用

        寫操作很多時候需要加鎖,包括表級鎖、行級鎖等,這類鎖都是排他鎖,一個會話佔據排它鎖之後,其他會話是不能讀取資料的,這會會極大影響資料讀取效能。

        所以MYSQL部署往往會採用讀寫分離方式,主庫用來寫入資料及部分時效性要求很高的讀操作,從庫用來承接大部分讀操作,這樣資料庫整體效能能夠得到大幅提升。

  • 不同型別的資料採用不同的儲存庫,

    • MongoDB nosql 文件化儲存
    • Redis nosql key-value儲存
    • HBase nosql, 列式儲存,其實本質上有點類似於key-value資料庫。
    • cassandra,Cassandra 是一個來自 Apache 的分散式資料庫,具有高度可擴充套件性,可用於管理大量的結構化資料
    • TIDB,是PingCAP公司自主設計、研發的開源分散式關係型資料庫,是一款同時支援線上事務處理與線上分析處理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分散式資料庫產品

為什麼把mysql資料庫中的資料放redis快取中能提升效能?

  1. Redis儲存的是k-v格式的資料。時間複雜度是O(1),常數階,而mysql引擎的底層實現是B+TREE,時間複雜度是O(logn)是對數階的。Redis會比Mysql快一點點。
  2. Mysql資料儲存是儲存在表中,查詢資料時要先對錶進行全域性掃描或根據索引查詢,這涉及到磁碟的查詢,磁碟查詢如果是單點查詢可能會快點,但是順序查詢就比較慢。而redis不用這麼麻煩,本身就是儲存在記憶體中,會根據資料在記憶體的位置直接取出。
  3. Redis是單執行緒的多路複用IO,單執行緒避免了執行緒切換的開銷,而多路複用IO避免了IO等待的開銷,在多核處理器下提高處理器的使用效率可以對資料進行分割槽,然後每個處理器處理不同的資料。
  • 池化技術,減少頻繁建立資料庫連線的效能損耗。

    每次進行資料庫操作之前,先建立連線然後再進行資料庫操作,最後釋放連線。這個過程涉及到網路通訊的延時,頻繁建立連線物件和銷燬物件的效能開銷等,當請求量較大時,這塊帶來的效能影響非常大。

資料儲存

磁碟資料訪問優化

對於磁碟的操作,無非就是讀和寫。

比如對於做交易系統的場景來說,一般會設計到對賬檔案的解析和寫入。而對於磁碟的操作,優化方式無非就是

  • 磁碟的頁快取,可以藉助快取 I/O ,充分利用系統快取,降低實際 I/O 的次數。

  • 順序讀寫,可以用追加寫代替隨機寫,減少定址開銷,加快 I/O 寫的速度。

  • SSD代替HDD,固態硬碟的I/O效率遠遠高於機械硬碟。

  • 在需要頻繁讀寫同一塊磁碟空間時,可以用 mmap (記憶體對映,)代替 read/write,減少記憶體的拷貝次數

  • 在需要同步寫的場景中,儘量將寫請求合併,而不是讓每個請求都同步寫入磁碟,即可以用 fsync() 取代 O_SYNC

合理利用記憶體

充分利用記憶體快取,把一些經常訪問的資料和物件儲存在記憶體中,這樣可以避免重複載入或者避免資料庫訪問帶來的效能損耗。

呼叫遠端服務

遠端服務呼叫,影響到IO效能的因素有。

  • 遠端呼叫等待返回結果的阻塞
    • 非同步通訊
  • 網路通訊的耗時
    • 內網通訊
    • 增加網路頻寬
  • 遠端服務通訊的穩定性

非同步化架構

微服務中的邏輯複雜處理時間長的情況,在高併發量下,導致服務執行緒消耗盡,不能再建立執行緒處理請求。對這種情況的優化,除了在程式上不斷調優(資料庫調優,演算法調優,快取等等),可以考慮在架構上做些調整,先返回結果給客戶端,讓使用者可以繼續使用客戶端的其他操作,再把服務端的複雜邏輯處理模組做非同步化處理。這種非同步化處理的方式適合於客戶端對處理結果不敏感不要求實時的情況,比如群發郵件、群發訊息等。

非同步化設計的解決方案: 多執行緒、MQ。

應用服務的拆分

除了上述的手段之外,業務系統往微服務化拆分也非常有必要,原因是:

  • 隨著業務的發展,應用程式本身的複雜度會不斷增加,同樣會產生熵增現象。
  • 業務系統的功能越來越多,參與開發迭代的人員也越多,多個人維護一個非常龐大的專案,很容易出現問題。
  • 單個應用系統很難實現橫向擴容,並且由於伺服器資源有限,導致所有的請求都集中請求到某個伺服器節點,造成資源消耗過大,使得系統不穩定
  • 測試、部署成本越來越高
  • .....

其實,最終要的是,單個應用在效能上的瓶頸很難突破,也就是說如果我們要支援18000QPS,單個服務節點肯定無法支撐,所以服務拆分的好處,就是可以利用多個計算機階段組成一個大規模的分散式計算網路,通過網路通訊的方式完成一整套業務邏輯。

img

如何拆分服務

如何拆分服務,這個問題看起來簡單,很多同學會說,直接按照業務拆分啊。

但是實際在實施的時候,會發現拆分存在一些邊界性問題,比如有些資料模型可以存在A模組,也可以存在B模組,這個時候怎麼劃分呢?另外,服務拆分的粒度應該怎麼劃分?

一般來說,服務的拆分是按照業務來實現的,然後基於DDD來指導微服務的邊界劃分。領域驅動就是一套方法論,通過領域驅動設計方法論來定義領域模型,從而確定業務邊界和應用邊界,保證業務模型和程式碼模型的一致性。不管是DDD還是微服務,都要遵循軟體設計的基本原則:高內聚低耦合。服務內部高內聚,服務之間低耦合,實際上一個領域服務對應了一個功能集合,這些功能一定是有一些共性的。比如,訂單服務,那麼建立訂單、修改訂單、查詢訂單列表,領域的邊界越清晰,功能也就越內聚,服務之間的耦合性也就越低。

服務拆分還需要根據當前技術團隊和公司所處的狀態來進行。

如果是初創團隊,不需要過分的追求微服務,否則會導致業務邏輯過於分散,技術架構太過負載,再加上團隊的基礎設施還不夠完善,導致整個交付的時間拉長,對公司的發展來說會造成較大的影響。所以在做服務拆分的時候還需要考慮幾個因素。

  • 當前公司業務所處領域的市場性質,如果是市場較為敏感的專案,前期應該是先出來東西,然後再去迭代和優化。
  • 開發團隊的成熟度,團隊技術能否能夠承接。
  • 基礎能力是否足夠,比如Devops、運維、測試自動化等基礎能力。 團隊是否有能力來支撐大量服務例項執行帶來的運維複雜度,是否可以做好服務的監控。
  • 測試團隊的執行效率,如果測試團隊不能支援自動化測試、自動迴歸、壓力測試等手段來提高測試效率,那必然會帶來測試工作量的大幅度提升從而導致專案上線週期延期

如果是針對一個老的系統進行改造,那可能涉及到的風險和問題更多,所以要開始著手改動之前,需要考慮幾個步驟:拆分前準備階段,設計拆分改造方案,實施拆分計劃

  • 拆分之前,先梳理好當前的整個架構,以及各個模組的依賴關係,還有介面

    準備階段主要是梳理清楚了依賴關係和介面,就可以思考如何來拆,第一刀切在哪兒裡,即能達到快速把一個複雜單體系統變成兩個更小系統的目標,又能對系統的現有業務影響最小。要儘量避免構建出一個分散式的單體應用,一個包含了一大堆互相之間緊耦合的服務,卻又必須部署在一起的所謂分散式系統。沒分析清楚就強行拆,可能就一不小心剪斷了大動脈,立馬搞出來一個 A 類大故障,後患無窮。

  • 不同階段拆分要點不同,每個階段的關注點要聚焦

    拆分本身可以分成三個階段,核心業務和非業務部分的拆分、核心業務的調整設計、核心業務內部的拆分。

    • 第一階段將核心業務瘦身,把非核心的部分切開,減少需要處理的系統大小;

    • 第二階段。重新按照微服務設計核心業務部分;

    • 第三階段把核心業務部分重構設計落地。

    拆分的方式也有三個:程式碼拆分、部署拆分、資料拆分。

另外,每個階段需要聚焦到一兩個具體的目標,否則目標太多反而很難把一件事兒做通透。例如某個系統的微服務拆分,制定瞭如下的幾個目標:

  1. 效能指標(吞吐和延遲):核心交易吞吐提升一倍以上(TPS:1000->10000),A 業務延遲降低一半(Latency:250ms->125ms),B 業務延遲降低一半(Latency:70ms->35ms)。
  2. 穩定性指標(可用性,故障恢復時間):可用性>=99.99%,A 類故障恢復時間<=15 分鐘,季度次數<=1 次。
  3. 質量指標:編寫完善的產品需求文件、設計文件、部署運維文件,核心交易部分程式碼 90%以上單測覆蓋率和 100%的自動化測試用例和場景覆蓋,實現可持續的效能測試基準環境和長期持續效能優化機制。
  4. 擴充套件性指標:完成程式碼、部署、執行時和資料多個維度的合理拆分,對於核心系統重構後的各塊業務和交易模組、以及對應的各個資料儲存,都可以隨時通過增加機器資源實現伸縮擴充套件。
  5. 可維護性指標:建立全面完善的監控指標、特別是全鏈路的實時效能指標資料,覆蓋所有關鍵業務和狀態,縮短監控報警響應處置時間,配合運維團隊實現容量規劃和管理,出現問題時可以在一分鐘內拉起系統或者回滾到上一個可用版本(啟動時間<=1 分鐘)。
  6. 易用性指標,通過重構實現新的 API 介面既合理又簡單,極大的滿足各個層面使用者的使用和需要,客戶滿意度持續上升。
  7. 業務支援指標:對於新的業務需求功能開發,在保障質量的前提下,開發效率提升一倍,開發資源和週期降低一半。

當然,不要期望一次性完成所有目標,每一個階段可以選擇一個兩個優先順序高的目標進行執行。

img

微服務化架構帶來的問題

微服務架構首先是一個分散式的架構,其次我們要暴露和提供業務服務能力,然後我們需要考慮圍繞這些業務能力的各種非功能性的能力。這些分散在各處的服務本身需要被管理起來,並且對服務的呼叫方透明,這樣就有了服務的註冊發現的功能需求。

同樣地,每個服務可能部署了多臺機器多個例項,所以,我們需要有路由和定址的能力,做負載均衡,提升系統的擴充套件能力。有了這麼多對外提供的不同服務介面,我們一樣需要有一種機制對他們進行統一的接入控制,並把一些非業務的策略做到這個接入層,比如許可權相關的,這就是服務閘道器。同時我們發現隨著業務的發展和一些特定的運營活動,比如秒殺大促,流量會出現十倍以上的激增,這時候我們就需要考慮系統容量,服務間的強弱依賴關係,做服務降級、熔斷,系統過載保護等措施。

以上這些由於微服務帶來的複雜性,導致了應用配置、業務配置,都被散落到各處,所以分散式配置中心的需求也出現了。最後,系統分散部署以後,所有的呼叫都跨了程式,我們還需要有能線上上做鏈路跟蹤,效能監控的一套技術,來協助我們時刻了解系統內部的狀態和指標,讓我們能夠隨時對系統進行分析和干預。

image-20210624133950124

整體架構圖

基於上述從微觀到巨集觀的整體分析,我們基本上能夠設計出一個整體的架構圖。

  • 接入層,外部請求到內部系統之間的關口,所有請求都必須經過api 閘道器。

  • 應用層,也叫聚合層,為相關業務提供聚合介面,它會呼叫中臺服務進行組裝。

  • 中臺服務,也是業務服務層,以業務為緯度提供業務相關的介面。中臺的本質是為整個架構提供複用的能力,比如評論系統,在咕泡雲課堂和Gper社群都需要,那麼這個時候評論系統為了設計得更加可複用性,就不能耦合雲課堂或者Gper社群定製化的需求,那麼作為設計評論中臺的人,就不需要做非常深度的思考,如何提供一種針對不同場景都能複用的能力。

    你會發現,當這個服務做到機制的時候,就變成了一個baas服務。

    服務商客戶(開發者)提供整合雲後端的服務,如提供檔案儲存、資料儲存、推送服務、身份驗證服務等功能,以幫助開發者快速開發應用。

image-20210624152616146

瞭解什麼是高併發

總結一下什麼是高併發。

高併發並沒有一個具體的定義,高併發主要是形容突發流量較高的場景。

如果面試的過程中,或者在實際工作中,你們領導或者面試官問你一個如何設計承接千萬級流量的系統時,你應該要按照我說的方法去進行逐一分析。

  • 一定要形成可以量化的資料指標,比如QPS、DAU、總使用者數、TPS、訪問峰值
  • 針對這些資料情況,開始去設計整個架構方案
  • 接著落地執行

高併發中的巨集觀指標

一個滿足高併發系統,不是一味追求高效能,至少需要滿足三個巨集觀層面的目標:

  • 高效能,效能體現了系統的並行處理能力,在有限的硬體投入下,提高效能意味著節省成本。同時,效能也反映了使用者體驗,響應時間分別是 100 毫秒和 1 秒,給使用者的感受是完全不同的。
  • 高可用,表示系統可以正常服務的時間。一個全年不停機、無故障;另一個隔三差五出現上事故、當機,使用者肯定選擇前者。另外,如果系統只能做到 90%可用,也會大大拖累業務。
  • 高擴充套件,表示系統的擴充套件能力,流量高峰時能否在短時間內完成擴容,更平穩地承接峰值流量,比如雙 11 活動、明星離婚等熱點事件。

image-20210624211728937

微觀指標

效能指標

通過效能指標可以度量目前存在的效能問題,同時作為效能優化的評估依據。一般來說,會採用一段時間內的介面響應時間作為指標。

1、平均響應時間:最常用,但是缺陷很明顯,對於慢請求不敏感。比如 1 萬次請求,其中 9900 次是 1ms,100 次是 100ms,則平均響應時間為 1.99ms,雖然平均耗時僅增加了 0.99ms,但是 1%請求的響應時間已經增加了 100 倍。

2、TP90、TP99 等分位值:將響應時間按照從小到大排序,TP90 表示排在第 90 分位的響應時間, 分位值越大,對慢請求越敏感。

img

可用性指標

高可用性是指系統具有較高的無故障執行能力,可用性 = 平均故障時間 / 系統總執行時間,一般使用幾個 9 來描述系統的可用性。

對於高併發系統來說,最基本的要求是:保證 3 個 9 或者 4 個 9。原因很簡單,如果你只能做到 2 個 9,意味著有 1%的故障時間,像一些大公司每年動輒千億以上的 GMV 或者收入,1%就是 10 億級別的業務影響。

可擴充套件性指標

面對突發流量,不可能臨時改造架構,最快的方式就是增加機器來線性提高系統的處理能力。

對於業務叢集或者基礎元件來說,擴充套件性 = 效能提升比例 / 機器增加比例,理想的擴充套件能力是:資源增加幾倍,效能提升幾倍。通常來說,擴充套件能力要維持在 70%以上。

但是從高併發系統的整體架構角度來看,擴充套件的目標不僅僅是把服務設計成無狀態就行了,因為當流量增加 10 倍,業務服務可以快速擴容 10 倍,但是資料庫可能就成為了新的瓶頸。

像 MySQL 這種有狀態的儲存服務通常是擴充套件的技術難點,如果架構上沒提前做好規劃(垂直和水平拆分),就會涉及到大量資料的遷移。

因此,高擴充套件性需要考慮:服務叢集、資料庫、快取和訊息佇列等中介軟體、負載均衡、頻寬、依賴的第三方等,當併發達到某一個量級後,上述每個因素都可能成為擴充套件的瓶頸點。

實踐方案

通用設計方法

縱向擴充套件(scale-up)

它的目標是提升單機的處理能力,方案又包括:

1、提升單機的硬體效能:通過增加記憶體、CPU 核數、儲存容量、或者將磁碟升級成 SSD 等堆硬體的方式來提升。

2、提升單機的軟體效能:使用快取減少 IO 次數,使用併發或者非同步的方式增加吞吐量。

橫向擴充套件(scale-out)

因為單機效能總會存在極限,所以最終還需要引入橫向擴充套件,通過叢集部署以進一步提高併發處理能力,又包括以下 2 個方向:

1、做好分層架構:這是橫向擴充套件的提前,因為高併發系統往往業務複雜,通過分層處理可以簡化複雜問題,更容易做到橫向擴充套件。

2、各層進行水平擴充套件:無狀態水平擴容,有狀態做分片路由。業務叢集通常能設計成無狀態的,而資料庫和快取往往是有狀態的,因此需要設計分割槽鍵做好儲存分片,當然也可以通過主從同步、讀寫分離的方案提升讀效能。

高效能實踐方案

1、叢集部署,通過負載均衡減輕單機壓力。

2、多級快取,包括靜態資料使用 CDN、本地快取、分散式快取等,以及對快取場景中的熱點 key、快取穿透、快取併發、資料一致性等問題的處理。

3、分庫分表和索引優化,以及藉助搜尋引擎解決複雜查詢問題。

4、考慮 NoSQL 資料庫的使用,比如 HBase、TiDB 等,但是團隊必須熟悉這些元件,且有較強的運維能力。

5、非同步化,將次要流程通過多執行緒、MQ、甚至延時任務進行非同步處理。

6、限流,需要先考慮業務是否允許限流(比如秒殺場景是允許的),包括前端限流、Nginx 接入層的限流、服務端的限流。

7、對流量進行削峰填谷,通過 MQ 承接流量。

8、併發處理,通過多執行緒將序列邏輯並行化。

9、預計算,比如搶紅包場景,可以提前計算好紅包金額快取起來,發紅包時直接使用即可。

10、快取預熱,通過非同步任務提前預熱資料到本地快取或者分散式快取中。

11、減少 IO 次數,比如資料庫和快取的批量讀寫、RPC 的批量介面支援、或者通過冗餘資料的方式幹掉 RPC 呼叫。

12、減少 IO 時的資料包大小,包括採用輕量級的通訊協議、合適的資料結構、去掉介面中的多餘欄位、減少快取 key 的大小、壓縮快取 value 等。

13、程式邏輯優化,比如將大概率阻斷執行流程的判斷邏輯前置、For 迴圈的計算邏輯優化,或者採用更高效的演算法。

14、各種池化技術的使用和池大小的設定,包括 HTTP 請求池、執行緒池(考慮 CPU 密集型還是 IO 密集型設定核心引數)、資料庫和 Redis 連線池等。

15、JVM 優化,包括新生代和老年代的大小、GC 演算法的選擇等,儘可能減少 GC 頻率和耗時。

16、鎖選擇,讀多寫少的場景用樂觀鎖,或者考慮通過分段鎖的方式減少鎖衝突。

高可用實踐方案

1、對等節點的故障轉移,Nginx 和服務治理框架均支援一個節點失敗後訪問另一個節點。

2、非對等節點的故障轉移,通過心跳檢測並實施主備切換(比如 redis 的哨兵模式或者叢集模式、MySQL 的主從切換等)。

3、介面層面的超時設定、重試策略和冪等設計。

4、降級處理:保證核心服務,犧牲非核心服務,必要時進行熔斷;或者核心鏈路出問題時,有備選鏈路。

5、限流處理:對超過系統處理能力的請求直接拒絕或者返回錯誤碼。

6、MQ 場景的訊息可靠性保證,包括 producer 端的重試機制、broker 側的持久化、consumer 端的 ack 機制等。

7、灰度釋出,能支援按機器維度進行小流量部署,觀察系統日誌和業務指標,等執行平穩後再推全量。

8、監控報警:全方位的監控體系,包括最基礎的 CPU、記憶體、磁碟、網路的監控,以及 Web 伺服器、JVM、資料庫、各類中介軟體的監控和業務指標的監控。

9、災備演練:類似當前的“混沌工程”,對系統進行一些破壞性手段,觀察區域性故障是否會引起可用性問題。

高可用的方案主要從冗餘、取捨、系統運維 3 個方向考慮,同時需要有配套的值班機制和故障處理流程,當出現線上問題時,可及時跟進處理。

高擴充套件的實踐方案

1、合理的分層架構:比如上面談到的網際網路最常見的分層架構,另外還能進一步按照資料訪問層、業務邏輯層對微服務做更細粒度的分層(但是需要評估效能,會存在網路多一跳的情況)。

2、儲存層的拆分:按照業務維度做垂直拆分、按照資料特徵維度進一步做水平拆分(分庫分表)。

3、業務層的拆分:最常見的是按照業務維度拆(比如電商場景的商品服務、訂單服務等),也可以按照核心介面和非核心介面拆,還可以按照請求去拆(比如 To C 和 To B,APP 和 H5)。
關注[跟著Mic學架構]公眾號,獲取更多精品原創

相關文章