[轉]阿里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等待的開銷,在多核處理器下提高處理器的使用效率可以對數據進行分區,然後每個處理器處理不同的數據。
  • 池化技術,減少頻繁創建數據庫連接的性能損耗。

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

數據存儲

磁盤數據訪問優化

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

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

  • 磁盤的頁緩存,可以藉助緩存 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學架構]公衆號,獲取更多精品原創

 

轉自  https://www.cnblogs.com/mic112/p/15420129.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章