天天看點

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

在上一篇文章中,詳細分析了設計一個千萬級并發架構所需要思考的問題,以及解決方案。

在這一片文章中,我們主要分析如何在職場足夠使用者數量的情況下,同步提升架構的性能降低平均響應時間。

如何降低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等待的開銷,在多核處理器下提高處理器的使用效率可以對資料進行分區,然後每個處理器處理不同的資料。
  • 池化技術,減少頻繁建立資料庫連接配接的性能損耗。

    每次進行資料庫操作之前,先建立連接配接然後再進行資料庫操作,最後釋放連接配接。這個過程涉及到網絡通信的延時,頻繁建立連接配接對象和銷毀對象的性能開銷等,當請求量較大時,這塊帶來的性能影響非常大。

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

磁盤資料通路優化

對于磁盤的操作,無非就是讀和寫。

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

  • 磁盤的頁緩存,可以借助緩存 I/O ,充分利用系統緩存,降低實際 I/O 的次數。
  • 順序讀寫,可以用追加寫代替随機寫,減少尋址開銷,加快 I/O 寫的速度。
  • SSD代替HDD,固态硬碟的I/O效率遠遠高于機械硬碟。
  • 在需要頻繁讀寫同一塊磁盤空間時,可以用 mmap (記憶體映射,)代替 read/write,減少記憶體的拷貝次數
  • 在需要同步寫的場景中,盡量将寫請求合并,而不是讓每個請求都同步寫入磁盤,即可以用 fsync() 取代 O_SYNC

合理利用記憶體

充分利用記憶體緩存,把一些經常通路的資料和對象儲存在記憶體中,這樣可以避免重複加載或者避免資料庫通路帶來的性能損耗。

遠端服務調用,影響到IO性能的因素有。

  • 遠端調用等待傳回結果的阻塞
    • 異步通信
  • 網絡通信的耗時
    • 内網通信
    • 增加網絡帶寬
  • 遠端服務通信的穩定性

異步化架構

微服務中的邏輯複雜處理時間長的情況,在高并發量下,導緻服務線程消耗盡,不能再建立線程處理請求。對這種情況的優化,除了在程式上不斷調優(資料庫調優,算法調優,緩存等等),可以考慮在架構上做些調整,先傳回結果給用戶端,讓使用者可以繼續使用用戶端的其他操作,再把服務端的複雜邏輯處理子產品做異步化處理。這種異步化處理的方式适合于用戶端對處理結果不敏感不要求實時的情況,比如群發郵件、群發消息等。

異步化設計的解決方案: 多線程、MQ。

應用服務的拆分

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

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

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

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

如何拆分服務

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

但是實際在實施的時候,會發現拆分存在一些邊界性問題,比如有些資料模型可以存在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. 業務支援名額:對于新的業務需求功能開發,在保障品質的前提下,開發效率提升一倍,開發資源和周期降低一半。

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

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

微服務化架構帶來的問題

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

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

以上這些由于微服務帶來的複雜性,導緻了應用配置、業務配置,都被散落到各處,是以分布式配置中心的需求也出現了。最後,系統分散部署以後,所有的調用都跨了程序,我們還需要有能線上上做鍊路跟蹤,性能監控的一套技術,來協助我們時刻了解系統内部的狀态和名額,讓我們能夠随時對系統進行分析和幹預。

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

整體架構圖

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

  • 接入層,外部請求到内部系統之間的關口,所有請求都必須經過api 網關。
  • 應用層,也叫聚合層,為相關業務提供聚合接口,它會調用中台服務進行組裝。
  • 中台服務,也是業務服務層,以業務為緯度提供業務相關的接口。中台的本質是為整個架構提供複用的能力,比如評論系統,在咕泡雲課堂和Gper社群都需要,那麼這個時候評論系統為了設計得更加可複用性,就不能耦合雲課堂或者Gper社群定制化的需求,那麼作為設計評論中台的人,就不需要做非常深度的思考,如何提供一種針對不同場景都能複用的能力。

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

    服務商為客戶(開發者)提供整合雲後端的服務,如提供檔案存儲、資料存儲、推送服務、身份驗證服務等功能,以幫助開發者快速開發應用。
阿裡P8面試官:如何設計一個扛住千萬級并發的架構(超級詳細)-續

了解什麼是高并發

總結一下什麼是高并發。

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

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

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

高并發中的宏觀名額

一個滿足高并發系統,不是一味追求高性能,至少需要滿足三個宏觀層面的目标:

  • 高性能,性能展現了系統的并行處理能力,在有限的硬體投入下,提高性能意味着節省成本。同時,性能也反映了使用者體驗,響應時間分别是 100 毫秒和 1 秒,給使用者的感受是完全不同的。
  • 高可用,表示系統可以正常服務的時間。一個全年不停機、無故障;另一個隔三差五出現上事故、當機,使用者肯定選擇前者。另外,如果系統隻能做到 90%可用,也會大大拖累業務。
  • 高擴充,表示系統的擴充能力,流量高峰時能否在短時間内完成擴容,更平穩地承接峰值流量,比如雙 11 活動、明星離婚等熱點事件。
阿裡P8面試官:如何設計一個扛住千萬級并發的架構(超級詳細)-續

微觀名額

性能名額

通過性能名額可以度量目前存在的性能問題,同時作為性能優化的評估依據。一般來說,會采用一段時間内的接口響應時間作為名額。

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

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

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

可用性名額

高可用性是指系統具有較高的無故障運作能力,可用性 = 平均故障時間 / 系統總運作時間,一般使用幾個 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學架構]公衆号,擷取更多精品原創

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

繼續閱讀