從0到千萬級用戶億級請求微服務架構歷程

單體應用因其架構簡單、使用技術門檻低、研發快速上手、項目快速上線等特點是創業公司初級階段的必然產物。隨着平臺用戶規模的遞增,產品功能的豐富以及需求迭代的頻率也會加速,相對應的研發人數也逐步遞增,系統的性能問題、研發人員之間的協作問題、交付速度等一系列的問題就慢慢凸顯,這些問題會逐步演化成阻礙項目推進的“絆腳石”。此時微服務的出現似乎是一根救命稻草,但凡遇到系統性能、項目交付質量、項目進度等問題的時候就開始準備系統重構,認爲往微服務方向轉型就一定能解決這些面臨的問題。那麼一個在企業在單體應用架構中到底如何轉型微服務呢?在轉型之前還需要去了解下實施微服務的一些前置條件。

本文是根據潘志偉老師在 ArchSummit 全球架構師峯會上的演講整理出來的,講述瞭如何從 0 開始構建一個億級請求的系統的歷程,其中包括了服務拆分、微服務測試、容量預估以及上線的流程。穩定的系統不僅要依賴好的架構設計,而且需要對核心代碼,高頻訪問模塊精雕細琢,看似不起眼的一些小優化,長期積累起來就會有質的變化,正所謂細節決定成敗,做架構也是同樣的到底。

微服務實施的前置條件

很多技術人員在聽到企業技術架構要轉型,打算從單體架構往微服務架構轉型,得知消息後就異常的興奮,認爲自己馬上又能學到新的技術了,開始去關注到底是選型哪種技術架構,並運行框架提供的 Demo,認爲成功運行 Demo 就具備了實施微服務的條件了,等待公司一聲令下,就踏上微服務之旅了。其實這是一種典型的技術人員考慮事情的思維,通過過往的經驗來看,更重要的是在實施微服務之前全員統一思想、充分培訓、 以及工程結構標準化。

統一思想:因爲在準備實施微服務的時候,首要條件就是獲得高層的認可,因爲涉及到組織結構的調整以及後續人力資源的增補,另外在新架構上線後難免會出現問題,這個時候需要得到高層的支持。另外,在單體應用中其組織機構包括開發部、測試部、運維部、DBA 部,每個部門各司其職由高層統一指揮,看似很非常合理的組織結構,但是在項目或者迭代實際過程中會花費大量的時間去跨部門溝通,形成了孤島式功能團隊。

充分培訓:微服務架構的開發人員具備“精”、“氣”、“神”的特質,否則在後續發展階段一定會出現各種難題。“精”是指熟悉業務,熟悉選型的開發框架,而不僅限完成 demo 運行,必須要熟悉原理,最好能熟悉源碼,做到面對問題不慌,“氣”是指大家對微服務架構這件事情的思想認知一致,能夠在一個頻道上對話,“神”是指需要了解其理論知識,比如什麼是服務治理,什麼是服務自治原則,明白爲什麼需要這樣而不是那樣。

工程結構標準化:所有服務交付服務,從代碼風格比如類的命名,module 命名以及啓動方式都是一致的,減少研發人員對於未知的未知而產生的擔心。

在正式開始微服務之前,有必要了解下雲原生的 12 要素,它針對微服務的一些設計思想做了充分的歸納和總結,雲原生 12 要素的內容如下:

  • 基準代碼:一份基準代碼(Codebase),多份部署(deploy)
  • 依賴:顯式聲明依賴關係
  • 配置:在環境中存儲配置
  • 後端服務:把後端服務 (backing services) 當作附加資源
  • 構建、發佈、運行:嚴格分離構建和運行
  • 進程:以一個或多個無狀態進程運行應用
  • 端口綁定:通過端口綁定 (Port binding) 來提供服務
  • 併發:通過進程模型進行擴展
  • 易處理(快速啓動和優雅終止可最大化健壯性)
  • 環境等價:開發環境與線上環境等價
  • 日誌
  • 管理進程

其中第一條需要特別注意,它要求基準代碼或者軟件製品只允許有一份,可以部署到多個環境,不因爲環境的改變而需要重新編譯或者針對不同的環境編譯多個製品。記住,測試即交付的原則,即你測試的軟件製品和交付到生產的軟件製品是一樣的。重點強調環境配置和製品庫分離,如果是測試環境的配置,那麼軟件運行起來就是測試環境,如果是生產環境的配置,那麼軟件運行起來就是生產環境。發現很多程序員都喜歡把配置文件寫到工程裏面,例如 application-dev.properties、application-test.properties、application-prod.properties 等,然後在系統啓動的時候增加 spring.profiles.active=dev 來說明環境,其實這種做法是非常的不優雅,首先違背了雲原生 12 要素第一個條件,其次測試環境、生成環境的所有的配置信息都暴露在代碼中,容易導致信息泄露,最後增加運維部署難度,一旦環境變量標識錯誤就會導致軟件運行失敗。


總結爲三條:切實需要使用微服務來解決實際問題;組織結構思想認知一致;前期有完善系統性針對微服務的培訓。

微服務實施的具體步驟

有些人認爲使用 Dubbo 或者 SpringCloud 把系統內部接口調用換成 RPC 或者 Rest 調用,就完成了微服務改造了,其實這是隻是微服務的冰山一角,完整的去實施微服務必須從全局考慮統一規劃,包括前後端分離,服務無狀態、統一認證以及運維體系的調整等。

前後端分離:是指前端和後端的代碼分離,前端負責 HTML 頁面的編寫以及邏輯跳轉,後端負責提供數據接口給前端,前後端開發人員可以並行開發。前端對跳轉邏輯和 UI 交互負責,後端對接口的高可用負責。前端 html 層使用 VUE 框架,node.js 可以起到邏輯跳轉的控制,前後端通信採用 rest 方式,json 數據格式通信。前後端分離後的好處總結來說包含如下:

  • 各端的專家來對各自的領域進行優化,以滿足用戶體驗優化效果最優化;
  • 前後端交互界面更加清晰,採用接口方式通信,後端的接口簡潔明瞭,更容易維護;
  • 前端多渠道集成場景更容易擴展,採用統一的數據和模型,可以支撐前端的 web UI\ 移動 App 等訪問,後端服務不需要改動;

服務無狀態:是指該服務運行的實例不會在本地存執行有狀態的存儲,例如不存儲需要持久化的數據,不存儲業務上下文信息,並且多個副本對於同一個請求響應的結果是完全一致的,一般業務邏輯處理都被會定義爲無狀態服務。

記得在微服務重構的初級階段發生過一次特別有代表性的線上故障,有個研發人員負責驗證碼登陸模塊開發,把驗證碼存入了本地緩存中,由於我們開發、測試環境都是單實例部署,所以並沒有發現問題,當線上是多實例部署,所以會導致大量用戶登陸失敗的場景。這個線上故障的核心問題點在於沒有清楚的認識無狀態服務和有狀態服務的的使用場景。


統一認證:統一認證與授權是開始實施服務化的最基礎條件,也是最基礎的一項應用。在過去的單體應用中,可以基於攔截器和 Session 實現基本的登錄與鑑權。在微服務架構中,服務都被設計爲無狀態模式,顯然攔截器和 Session 模式已經不符合架構要求了,需要由統一認證服務來完成認證鑑權以及和第三方聯合登陸的要求,我們使用 token 機制來做統一認證,主要流程如下:

  • 用戶輸入用戶名和密碼提交給認證服務鑑權;
  • 認證服務驗證通過後生成 token 存入分佈式存儲;
  • 把生成的 token 返回給調用方;
  • 調用方每次請求中攜帶 token,業務系統拿到 token 請求認證服務;
  • 認證服務通過後業務系統處理業務邏輯並返回最終結果。

完成前後端分離,服務無狀態改造、統一認證處理後, 基本上完成了微服務輪廓的改造。接下來就需要去識別單體應用最需要改造成微服務的模塊,推薦是一個模塊甚至一個接口這樣的進度去拆分單體應用,而不建議一次性全部重構完畢。

服務拆分理論和原理及方法

談到微服務,議論的最多,吵架的最多的就是服務拆分問題,服務拆分是否合理直接影響到微服務架構的複雜性、穩定性以及可擴展性。然而並沒有任何一本書籍或者規範來介紹如何拆分服務,那麼如何正確的做服務的拆分? 目前各家做法也都是根據架構師經驗以及業務形態和用戶規模等因素綜合考慮。在工作中曾經遇到以下二種服務拆分的模式:

  • 一個方法一個服務:視業務規模和業務場景而定;
  • 基於代碼行數的劃分:簡單粗暴,不推薦;

有人說按方法拆分服務太過於細緻,應該要按業務功能來拆。其實當業務達到一定規模的時候,按方法拆分是一種非常有效的做法,以用戶服務舉例,在初始階段的時候,用戶服務具備了用戶的增刪改查功能,當用戶規模上升之後需要對增刪改查功能做優先級劃分。大家都知道在互聯網中流量獲客是最貴的,運營團隊通過互聯網投放廣告獲客,用戶在廣告頁上填寫手機號碼執行註冊過程,如果此時註冊失敗或者註冊過程響應時間過長,那麼這個客戶就可能流失了,但是廣告的點擊費用產生了,無形中形成了資源的浪費。所以此時需要按方法維度來拆分服務,把用戶服務拆分爲用戶註冊服務(只有註冊功能),用戶基礎服務(修改、查詢用戶信息)。

在做服務拆分的時候,每個服務的團隊人數規模也是非常重要的,人數過多可能會變成單體應用,溝通決策會緩慢,人數太少工作效率又會降低,一般來說會遵循 2 個披薩原則和康威定律:

  • 2 個披薩原則:兩個披薩原則最早是由亞馬遜 CEO 貝索斯提出的,他認爲如果兩個披薩不足以餵飽一個項目團隊,那麼這個團隊可能就顯得太大了,所以一個服務的人數劃分爲 5-7 人比較合適。因爲人數過多的項目將不利於決策的形成,而讓一個小團隊在一起做項目、開會討論,則更有利於達成共識,並能夠有效促進企業內部的創新。
  • 康威定律:你想要架構成爲什麼樣,就將團隊分成怎樣的結構。比如前後端分離的團隊,架構就是基於前後端分離。在基於微服務設計的團隊裏,一個很好的理念是自管理,團隊內部對於自己所負責的模塊高度負責,進行端對端的開發以及運維。

整個單體應用有那麼多的功能,到底哪些業務功能需要拆分,哪些業務功能又不需要拆分呢?可以遵循服務拆分的方法論:當一塊業務不依賴或極少依賴其它服務,有獨立的業務語義,爲超過 2 個或以上的其他服務或客戶端提供數據,應該被拆分成一個獨立的服務模,而且拆分的服務要具備高內聚低耦合。

關於服務拆分模式,使用比較多的是業務功能分解模式和數據庫模式,因爲容易理解而且使用起來比較簡單,效果也很好。

業務功能分解模式:判斷一個服務拆分的好壞,就看微服務拆分完成後是否具備服務的自治原則,如果把複雜單體應用改造成一個一個鬆耦合式微服務,那麼按照業務功能進行分解是最簡單的,只需把業務功能相似的模塊聚集在一起。比如:

  • 用戶管理:管理用戶相關的信息,例如註冊、修改、註銷或查詢、統計等。
  • 商品管理:管理商品的相關信息。
  • 數據庫模式:在微服務架構中,每個服務分配一套單獨的數據庫是非常理想方案,這樣就緩解了單個數據庫的壓力,也不會因爲某個數據庫的問題而導致整個系統出現問題。

微服務初始階段服務拆分不需要太細,等到業務發展起來後可以再根據子域方式來拆分,把獨立的服務再拆分成更小的服務,最後到接口級別服務。如果服務拆分的過小會導致調用鏈過長,以及引發沒有必要的分佈式事務,此時階段性的合併非常重要。做爲架構師不僅要學會拆分服務,也需要學會合並服務,需要週期性的去把拆分過小或者拆分不合理的服務要及時合併。

總得來說,在服務拆分的時候需要抓住以下重點:

  • 高內聚的拆分模式
  • 以業務爲模塊拆分
  • 以迭代頻率和改動範圍拆分
  • 階段性合併
  • 定期覆盤總結

代碼結構如何做到提高研發效率

曾經有一項調查,當一個程序員到新公司或者接手項目最怕的事情是什麼,超過 90% 的人的都認爲最怕接手其他人的項目。從心理學角度來看,這個結果非常正常,害怕是因爲對即將接手項目的未知,不清楚項目如何啓動,不清楚代碼是如何分層。大家試想看,當一個單體應用被劃分爲 N 多個服務的時候,每個服務啓動方式,代碼層次各不相同,如何去維護呢?所以微服務啓動階段,首先要做的事情就是工程結構標準化和自動化,讓研發人員的重點精力去做業務,而不是去搭建框架。因此基於 velocity 自定義了一套微服務代碼自動生成框架,研發人員設計好表結構之後,框架根據表結構自動生成服務代碼,包含 API 接口,實現類,DAO 層以及 Mybatis 的配置文件,類的名稱,包名、module 名稱都有嚴格的定義。以用戶服務爲例,生成後的代碼格式如下:

basics-userservice
basics-userservice-api
basics-userservice-business
basics-userservice-façade
basics-userservice-model
basics-userservice-service

爲了讓研發效率更快,架構更清晰,又從架構層面把代碼再拆分爲聚合服務層和原子服務層,每一層對應的功能不一樣。

  • 聚合層:收到終端請求後,聚合多個原子服務數據,按接口要求把聚合後的數據返回給終端,需要注意點是聚合層不會和數據庫交互;
  • 原子服務層:數據庫交互,實現數據的增刪改查,結合緩存和工具保障服務的高響應;要遵循單表原則,禁止 2 張以上的表做 jion 查詢,如有分庫分表,那麼對外要屏蔽具體規則。

需要說明的是,聚合層和業務比較貼近,需要了解業務更好的服務業務,和 App 端交互非常多,重點是合理設計的前後端接口,減少 App 和後端交互次數。原子服務則是關注性能,屏蔽數據庫操作,屏蔽分庫分表等操作。

最後還得提下系統日誌,日誌記錄的詳細程度直接關係到系統在出現問題時定位的速度, 同時也可以通過對記錄日誌觀察和分析統計,提前發現系統可能的風險,避免線上事故的發生。對於服務端開發人員來說,線上日誌的監控尤其重要,能夠通過日誌第一時間發現線上問題並及時解決。然而通過觀察收集後的日誌信息內容的時候才發現日誌規範這塊內容一直都沒有重視過,記日誌永遠看心情,日誌記錄的內容也是憑感覺。因此在實施微服務的之前,必須要先確定日誌的規範,爲了便於後面的日誌採集、處理和分析。例如統一約定日誌格式如下:

  • 時間|事件名稱|traceID|耗時時間|用戶 ID|設備唯一標識|設備類型|App 版本|訪問 IP|自定義參數
  • 時間:日誌產生時候系統的當前時間,格式爲 YYYY-MM-DD HH:MM:SS;
  • 事件名稱:預先定義好的枚舉值,例如 Login、Logout、search 等;
  • TraceID:當前請求的唯一標識符;
  • 耗時時間:當前事件執行完成所消耗的時間;
  • 用戶 ID:當前登陸用戶的唯一 ID,非登陸用戶爲空;
  • 設備唯一標識:當前設備的唯一標識,假如某用戶登錄前開始操作 App,這個時間記錄下設置唯一標識後,可以通該標識關聯到具體用戶;
  • 設備類型:當前設備的類型,如 Android 或者 iOS;
  • App 版本:當前訪問設置的 App 版本號;
  • 訪問 IP:當前設備所在 IP 地址;
  • 自定義參數用:自定義參數,參數之間使用 & 分割,例如 pid=108&ptag=65

工程結構、代碼框架和日誌在開發過程中最容易被忽略的,但卻非常的重要,前期合理的規劃有助於規模化推廣的時候減輕壓力,在規劃階段要重點關注以下內容:

  • 代碼未編工具先行;
  • 統一微服務工程結構;
  • 統一服務啓動方式(jar war);
  • 統一緩存調用方式(架構封裝統一提供 jar 包和底層存儲無關);
  • 統一 MQ 調用方式(架構封裝統一提供 jar,和具體 MQ 類型無關) ;
  • 統一日誌格式;
  • 統一多服務依賴調用方式 (串行調用方式、並行調用方式);
  • 統一熔斷、降級處理流程;

架構“三板斧”如何切入到微服務框架中

要提高系統穩定性,那麼好的設計、精心打磨代碼、運維監控這 3 個環境必不可少。例如在產品詳情頁這個功能上,聚合層會調用原子服務 26 個 RPC 接口,爲了降低客戶端的響應時間,在設計第一版的時候把聚合後的結果放入到分佈式緩存中,但是用戶訪問高峯期的時候分佈式緩存 QPS 非常高,會一定程度上影響系統的性能,而且一旦緩存失效又需要再次調用這些 RPC 接口,響應時間變長。爲了更精細化使用緩存,使用了二級緩存設計思路,仔細分析這些接口後發現,數據的緩存失效時間可以設置不同的時間,沒必要整體過期,接口請求的時候先默認使用本地緩存,當本地緩存失效後再調用 RPC 接口,這樣可以有效的降低 RPC 接口調用次數。

在使用緩存的時候不可避免的會遇到緩存穿透、緩存擊穿、緩存雪崩等場景,針對每種場景的時候需要使用不同的應對策略,從而保障系統的高可用性。

緩存穿透:是指查詢一個一定不存在緩存 key,由於緩存是未命中的時候需要從數據庫查詢,正常情況下查不到數據則不寫入緩存,就會導致這個不存在的數據每次請求都要到數據庫去查詢,造成緩存穿透, 有 2 個方案可以解決緩存穿透:

  • 方案 1:可以使用布隆過濾器方案,系統啓動的時候將所有存在的數據哈希到一個足夠大的 bitmap 中,當一個一定不存在的數據請求的時候,會被這個 bitmap 攔截掉,從而避免了對底層數據庫的查詢壓力
  • 方案 2:返回空值:如果一個查詢請求查詢數據庫後返回的數據爲空(不管是數據不存在,還是系統故障),仍然把這個空結果進行緩存,但它的過期時間會很短,比如 1 分鐘,但是這種方法解決不夠徹底。

緩存擊穿:緩存 key 在某個時間點過期的時候,剛好在這個時間點對這個 Key 有大量的併發請求過來,請求命中緩存失敗後會通過 DB 加載數據並回寫到緩存,這個時候大併發的請求可能會瞬間把後端 DB 壓垮,解決方案也很簡單通過加鎖的方式讀取數據,同時寫入緩存。

緩存雪崩:是指在設置緩存時使用了相同的過期時間,導致緩存在某一時刻同時失效,所有的查詢都請求到數據庫上,導致應用系統產生各種故障,這樣情況稱之爲緩存雪崩,可以通過限流的方式來限制請求數據庫的次數。

緩存的使用在一定程度上可以提高系統的 QPS,但是上線後還是發現偶爾會出現超時的問題,假設每個服務響應時間爲 50 毫秒,那麼 26*50=1300 毫秒,已經超過了設置 1 秒超時時間,爲解決偶發性超時問題,就需要把串行的調用調整爲並行調用:

- 線程池並行調用: 爲了提高接口響應時間,把之前串行調用方式修改爲把請求封裝爲各種 Future 放入線程池並行調用,最後通過 Future 的 get 方法拿到結果。這種方式暫時解決了詳情頁訪問速度的問題,但是運行一段時間後發現在併發量大的時候整個聚合層服務的 Tomcat 線程池全部消耗完,出現假死的現象。
- 服務隔離並行調用: 由於所有調用外部服務請求都在同一個線程池裏面,所以任何一個服務響應慢就會導致 Tomcat 線程池不能及時釋放,高併發情況下出現假死現象。爲解決這個問題,需要把調用外部每個服務獨立成每個線程池,線程池滿之後直接拋出異常,程序中增加各種異常判斷,來解決因爲個別服務慢導致的服務假死。
- 線程隔離服務降級: 方式 2 似乎解決了問題,但是並沒有解決根本問題。響應慢的服務仍然接收到大量請求,最終把基礎服務壓垮,需要判斷當服務異常超過一定次數之後,就直接返回設置好的返回值,而不用去調用 RPC 接口。這時候程序中存在大量判斷異常的代碼,判斷分支太多,考慮不完善接口就會出差。

在進行服務化拆分之後,系統中原有的本地調用就會變成遠程調用,這樣就引入了更多的複雜性。比如說服務 A 依賴於服務 B,這個過程中可能會出現網絡抖動、網絡異常,服務 B 變得不可用或者響應慢時,也會影響到 A 的服務性能,甚至可能會使得服務 A 佔滿整個線程池,導致這個應用上其它的服務也受影響,從而引發更嚴重的雪崩效應。所以引入了 Hystrix 或 Sentinel 做服務熔斷和降級;需要針對如下幾項做了個性化配置:

  • 錯誤率:可以設置每個服務錯誤率到達制定範圍後開始熔斷或降級;
  • 人工干預:可以人工手動干預,主動觸發降級服務;
  • 時間窗口:可配置化來設置熔斷或者降級觸發的統計時間窗口;
  • 主動告警:當接口熔斷之後,需要主動觸發短信告知當前熔斷的接口信息。

雖然服務拆分已經解決了模塊之間的耦合,大量的 RPC 調用依然存在高度的耦合,不管是串行調用還是並行調用,都需要把所依賴的服務全部調用一次。但是有些場景不需要同步給出結果的,可以引入 MQ 來降低服務調用之間的耦合。例如用戶完成註冊動作後需要調用優惠券服務發放優惠券,在調用積分服務發放積分,還需要初始化財務模塊,再計算營銷活動,最後需要把本次註冊信息上報給信息流,這種做法帶來了 2 個問題,第一個問題是整個註冊鏈路太長,容易發生失敗,第二個問題,任何依賴用戶註冊行爲的服務都需要修改服務註冊接口。但是使用 MQ 解耦就非常簡單了,只需要往 MQ 發送一個註冊的通知消息,下游業務如需要依賴註冊相關的數據,只需要訂閱註冊消息的 topic 即可,從而實現了業務的解耦。使用 MQ 的好處包括以下幾點:

  • 解耦:用戶註冊服務只需要關注註冊相關的邏輯,簡化了用戶註冊的流程;
  • 可靠投遞:消息投遞由 MQ 來保障,無需程序來保障必須調用成功;
  • 流量削峯:大流量的新用戶註冊,只需要新增用戶服務,併發流量由 MQ 來做緩衝,消費方通過消費 MQ 來完成業務邏輯;
  • 異步通信(支持同步):由於消息只需要進入 MQ 即可,完成同步轉異步的操作;
  • 提高系統吞吐、健壯性:調用鏈減少了,系統的健壯性和吞吐量提高了;

日常開發中,對於 MQ 做了如下約定:

  • 應用層必須支持消息冪等
  • 支持消息回溯
  • 支持消息重放
  • 消息的消費的機器 IP 以及消息時間

緩存、並行調用、消息隊列這些手段都使用上之後,系統的穩定性也是有了質的提升,超時現象也極少發生。隨着業務的高速發展,每週上線的功能也是逐步增多,每個業務都會需要鑑權、限流、驗籤等邏輯,需要每個聚合服務都需要根據規則自己實現一遍,權限認證每個模塊各自處理,狀態碼各種各樣,比如接口返回需要重新登陸狀態碼,有些服務返回 code=-8,有些則返回 code=-1;前端根據每個業務模塊來匹配解析不同的驗證碼,在上線的發版時候出現一些有趣的現象,很多人圍着運維在修改 Nginx 的配置,此時開發又遇到了新的瓶頸:

  • 上線新接口需要運維在 Nginx 中配置
  • 接口複用性不高
  • 權限認證每個模塊各自處理,狀態碼各種各樣
  • 限流熔斷降級各自處理
  • 開發效率不高
  • 接口文檔沒有最新的,誰負責的接口無人知道

爲此使用 Netty 框架和 Dubbo 泛化功能自研了一套網關,在網關上集成了用戶權限認證、限流、熔斷降級等功能,讓業務開發人員只關心業務實現,而無需關注非業務。

網關可以理解爲一個反向路由,它屏蔽內部細節,爲調用者提供統一入口,接收所有調用者請求,通過路由機制轉發到服務實例,同時網關也是“過濾器”集合,可以實現一系列與業務無關的橫切面功能,如安全認證、限流熔斷、日誌監控,同時網關還有如下特性:
- 協議轉換: 將不同的協議轉換成“通用協議”,然後再將通用協議轉化成本地系統能夠識別的協議,例如把 HTTP 協議統一轉換爲 Dubbo 協議。
- 鏈式處理: 消息從第一個插件流入,從最後一個插件流出,每個步驟的插件對經過的消息進行處理,整個過程形成了一個鏈條。優勢在於它將處理請求和處理步驟分開,每個處理的插件,只關心這個插件上需要做的處理操作,處理步驟和邏輯順序由“鏈”來完成。
- 異步請求: 所有的請求都會通過 API 網關訪問應用服務,無論業務量如何變化,網關的吞吐量要保持穩定狀態。

在架構層面把網關分爲接入層、分發層和監控層,網關和後端服務通信使用 Dubbo 的泛化方式,負載均衡也直接使用 Dubbo 默認的隨機策略。
接入層:負責接收客戶端請求,調度、加載和執行組件,把非法請求攔截在系統之外;

  • 分發層:將請求路由到上游服務端,並處理其返回的結果;
  • 監控層:監控日誌、生成各種運維管理報表、自動告警等;
  • 管理和監控系統主要是爲核心系統服務的,起到支撐的作用。

在通信協議上前端和網關交互採用 HTTP 方式,數據格式是 JSON 格式,並定義了一套網關接口規範,前端只需要根據接口協議封裝好報文就可以了,JSON 格式的接口協議爲

{
    "apiId" : "ACQ002",
    "requestParams": [
        {
            "id": 23,
            "username": "testUser"
        }
    ]
}

在 Dubbo 的泛化調用的時候,需要方法名、參數類型以及具體的值 Object result = genericService.$invoke(“方法名”, new Object []{“參數類型”}, new Object[]{“具體值”}),但是在前後端的接口協議中並沒有看到參數類型,其主要原因是爲了安全,在接口配置過程中會把 apiId 對應的方法名稱、參數類型寫入數據庫,網關啓動後這些配置都會加載到本地緩存,運行過程中可通過 ACQ002 找到具體的配置信息,並完成泛化調用。

所以,這裏做個總結:

  • 網關讓服務具有可複用性;
  • 多個服務調用儘可能並行化調用;
  • 本地緩存 + 遠程緩存完美搭配,提供統一調用方式;
  • 服務高可用熔斷降級必不可少,但是參數配置需要清晰;
  • MQ 的解耦和消峯功能是微服務有效搭配。

微服務後服務的測試方法

當從單體應用轉變到微服務架構的時候,測試的方式也在慢慢改變,在單體應用時候更關注單元測試,提倡單元測試代碼覆蓋率。當在微服務初期階段只有部分服務,此時 API 測試佔比非常大,會從 API 最終輸出的結果上來分析接口是否正確,因爲單元測試已經保證不了接口的正確性,真正在微服務階段會引入契約測試,來加速集成測試,因爲依賴的服務過多,服務之間協調會變動相當麻煩。

當我們在談服務的高可用性的時候,一般會從流量入口分流策略、下游服務調用、應用進程、消息服務、數據緩存、數據存儲以及系統運維策略等 7 個維度綜合來考慮。這其中涉及到開發、測試、運維等多種角色。尤其是針對測試人員而言,這些保障系統高可用的措施能否測試全面?以熔斷測試和降級測試舉例:

  • 熔斷測試:從服務的性能角度,當系統負載達到某個熔斷狀態的時候,服務是否能正確熔斷;同時,從功能角度驗證熔斷後系統的行爲是否跟預期相符;
  • 降級測試:從業務的穩定性角度,要能區分出核心業務和非核心業務,在需要降級的時候不能影響核心業務;當某個服務降級後,從功能角度驗證系統行爲是否跟預期相符。

雖然熔斷、降級在架構設計階段就規劃到系統中,但是這種業務場景在測試階段如何去驗證呢?

例如當測試工程師在針對 API 接口測試的時候,該接口依賴 X 和 Y 這 2 個服務,測試過程中所依賴的任何一個服務出現異常就會導致接口測試失敗,如果 Service-A 依賴 Service-B,Service-C,Service-X,Service-Y,那麼此時修改了 Service-X 之後其他服務是否有影響,這些在測試階段都沒有辦法去驗證,由於很多不確定因素導致微服務測試結果存在很多不確定性,這種情況下可以研發契約測試平臺,通過自定義的報文來解決多級依賴的問題。

契約測試 ,又稱之爲消費者驅動的契約測試 (Consumer-Driven Contracts,簡稱 CDC),契約測試最開始的概念由 Martin Fowler 提出,根據消費者驅動契約,可以將服務分爲消費者端和生產者端,而消費者驅動的契約測試的核心思想在於是從消費者業務實現的角度出發,由消費者自己來定義需要的數據格式以及交互細節,並驅動生成一份契約文件。然後生產者則根據契約文件來實現自己的邏輯,並在持續集成環境中持續驗證。契約測試核心原則是以消費者提出接口契約,交由服務提供方實現,並以測試用例對契約結果進行約束,所以服務提供方在滿足測試用例的情況下可以自行更改接口或架構實現而不影響消費者。

以 Dubbo 框架爲例來構建契約測試,使用 Dubbo 自定義 Filter 中可以方便的獲取到所調用方法的接口名稱、方法名以及參數,可以根據接口名稱 + 方法名的組合來定位。例如:

public class  ContractTestFilter  implements  Filter { public  Result  invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {      Result result = null; // 定義一個返回對象;      String interfaceName = invoker.getUrl().getPath(); // 獲取接口服務名稱      String methodName = invocation.getMethodName(); // 獲取接口方法名稱     Object[] arguments = invocation.getArguments(); // 獲取參數集合  String resp = httpPost(url, jsonObj.toString());
            responeJSON = JSONObject.parseObject(resp);
            Object rst = null;
            //isPact=1表示啓用契約測試
            if (responeJSON.getInteger("isPact") == 1) {                 
                result = new RpcResult(); // 定義返回結果
                ((RpcResult) result).setValue(rst);
                ((RpcResult) result).setException(null);
            } else {
                result = invoker.invoke(invocation); // 調用真實接口
                // 然後把接口信息存到契約測試平臺
                jsonObj.put("returnValue", JSON.toJSON(result.getValue()));
                httpPost(CONTRACT_TEST_SERVER + "/test/pactAdd.do", jsonObj.toString());
            }
        return result;       } 

契約測試總體流程說明如下:Consumer 端請求 Provider 的之前,會先執行 ContractTestFilter ,使用 HTTPPost 方式是發送一個 JSON 數據格式的 post 請求,包含 interfaceName、methodName、arguments 發送到契約測試平臺,測試平臺根據傳遞參數組合成查詢條件來查詢該接口是否啓用了契約測試。如果啓用了契約測試,則直接返回設定的返回值,否則返回 false,Consumer 根據返回的結果再去調用 Provider 所提供的服務,把具體結果返回給調用方,同時再把返回結果和入參封裝成 JSON 數據格式發送到契約測試平臺,完成接口數據採集。

隨着註冊用戶量的逐步增多,平臺所提供多元化且豐富功能以及運營使用了有效的用戶觸達機制,用戶活躍度上升非常快,此時平臺的穩定性越來越重要。雖然我們在架構層面做了熔斷、降級、多級緩存等措施,但是在過去的一段時間裏還是發生了幾次大的線上故障,我們也針對這些事故做了詳細的覆盤分析,出現事故的原因包括:磁盤寫滿、CPU 打滿、服務響應慢甚至超時、數據庫響應慢等等,分析過程中發現架構中雖然包括了熔斷、降級等手段,但是當事故發生的時候似乎這些機制都沒有生效。比如磁盤寫滿、CPU 打滿等故障在設計階段根本未考慮到,服務響應慢或者超時這個現象研發雖然可以通過硬編碼在開發階段來模擬,但是迭代正式提測後,測試是否需要測試架構中的熔斷降級策略呢?如果需要測試那誰來協助測試模擬各種異常情況?

爲了解決這個難題,我們參考了互聯網公司的通用方法,通過模擬調用延遲、服務不可用、機器資源滿載等,查看發生故障的節點或實例是否被自動隔離、下線,流量調度是否正確,預案是否有效,同時觀察系統整體的 QPS 或 RT 是否受影響。在此基礎上可以緩慢增加故障節點範圍,驗證上游服務限流降級、熔斷等是否有效。通過模擬故障節點增加到請求服務超時,估算系統容錯紅線,衡量系統容錯能力在生產環境的分佈式系統中進行一些試驗,用以考驗系統在動盪環境下的健壯性,從而增強對系統穩定運行的信心,稱之爲混沌工程,ChaosBlade 是阿里巴巴開源的一款遵循混沌工程實驗原理,提供豐富故障場景實現,幫助分佈式系統提升容錯性和可恢復性的混沌工程工具。

例如需要做一次針對數據庫慢 SQL 對業務影響範圍的試驗,可以按照以下方式來執行:

  • 設定場景:數據庫調用延遲
  • 監控指標:慢 SQL 數,告警信息
  • 期望假設:慢 SQL 數增加,釘釘羣收到慢 SQL 告警
  • 混沌實驗:對 user-provider 注入調用 user 數據庫延遲故障
  • 監控指標:慢 SQL 數增加,釘釘羣收到告警,符合預期
blade create mysql delay
--time 1000
--database demo
--table user
--sqltype select
--effect-percent 50

當在 user-provider 所在服務器上執行命令後,客戶端正常發起調用,此時數據庫慢 SQL 數會增加,消費端出現調用超時,會觸發相關告警。

在單體應用中不可能發送的異常在分佈式環境下會成爲常態,超時、重試、服務異常這些對於測試人員來說都是挑戰。任何場景在測試環境不驗證通過就會變成線上生產事故,總體來說在測試階段需要注意以下內容:

  • 測試人員思想的改變,接口有數據返回不一定就是正確的
  • 丟棄 E2E 的思想,更多的去看服務和接口
  • 想要線上穩定,混沌工程必須動起來
  • 線上的事故或者 Bug 必須要定期覆盤和總結

服務上線流程以及容量預估

互聯網做促銷活動是非常正常的一件事情,當一次活動即將上線的時候,業務系統往往會被問到一些資源的問題,例如需要多少服務器,現有機器能否支撐當前的業務增長等,“拍胸脯”保證的方式還有幾個人能信服?我們需要科學而又嚴謹的評估,通過評估結果來確定需要的資源數量。在容量評估之前我們需要先了解下網站訪問量的常用衡量標準:

  • UV:獨立訪客;
  • PV:綜合瀏覽量;
  • 併發量:系統同時處理的請求數;
  • 響應時間:一般取平均響應時間;
  • QPS:每秒鐘處理的請求數,QPS = 併發量 / 平均響應時間;
  • 帶寬:PV / 統計時間(換算到秒)平均頁面大小(單位 KB)* 8

例如:運營需要做一次活動,通過短信和 PUSH 的方式觸達用戶,推送約 6500W 用戶預計 2 個小時內推送結束,根據這樣的活動規模,相關資源估算如下:

錯誤估算容量估算: 推送用戶量 /(2 x 60 x 60)=9028
單機正常 QPS=1000(水位 60% 極限)=600
因此需要服務器 9028/600=15 臺,根據測試大概需要 15 臺機器。
正確容量估算: 需要考慮短信、PUSH 的歷史轉化情況,不能單純看用戶數,通過歷史數據來看,短信點擊率在 10%,PUSH 點擊率在 6%,合計約 16% 的點擊率,一般 2 個小時內是高峯期,每個用戶約點擊 8-10 個頁面。

點擊用戶數:6500w * 16%=1040W
2 小時 =2x60x60=7200s
QPS=1040W*/7200=1444
單機 QPS=1000(水位 60% 極限)=600
1444/600=2.4

預估需要新增 2.4 臺服務器,實際上需要適當增加一些緩衝,通過綜合計算再額外增加 3 臺服務器即可。

從預期 15 臺變成最終的 3 臺,中間有如此大的差距,最主要的原因是沒有考慮短信和 PUSH 的轉化率,這些在平時的運營過程中需要積累數據,切不可拍腦袋給出數字,在我們估算容量的時候這點千萬要注意。

線上監控如何做?

遵循“誰構建,誰運維”這一理念,服務上線只是完成了構建的環節,更重要的是線上的運維和監控,需要提供服務狀態上報的機制。舉例來說,當某一個時刻 A 接口響應超時,需要追蹤到具體時刻系統的整體負載情況以及 A 接口的調用量趨勢,以及服務 A 接口的數據庫 CPU 佔用率,慢 SQL 情況等。所以從解決問題角度來看,在服務監控上需要監控 QPS,錯誤返回數等維度,數據庫監控上需要監控連接數、CPU 佔用率、慢 SQL 等等。從系統的穩定性來看,在線上監控需要針對服務狀態、數據庫、硬件等信息監控。

服務監控:

  • QPS
  • 錯誤返回數
  • 接口請求次數的 top
  • 接口 95th,99th 請求時間 top
  • JVM 堆內存 / 非堆內存
  • JVM 線程數
  • GC 的暫停時間和次數
  • Tomcat 的活躍線程
  • API 自己上報的業務數據

數據庫監控:

  • 數據庫連接數
  • 數據庫 CPU
  • 慢 SQL
  • 全表掃描 SQL

硬件監控:

  • CPU 使用率和負載
  • 磁盤空間
  • 網絡流量

微服務實施總結

  • 實施微服務有難度,如非必須,不要輕易使用微服務;
  • 微服務的目的是解決研發的矛盾以及企業規劃相關,不要爲了微服務而微服務;
  • 思想認知一致,充分的溝通和培訓是必不可少的;
  • 所有微服務都是不可信賴的;
  • 監控、監控、監控一切可監控的內容。

【活動推薦】
目前我們正在策劃2020年下半年在深圳舉辦的ArchSummit架構師峯會,設置了“服務治理架構演化”,“基於DDD的微服務架構設計”,“數據中臺建設”,“軟件定義基礎架構”等專題,講師正在邀請當中。對於會議話題感興趣的,可以點擊官網查看詳情

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