以「前浪微博」場景爲例,談談架構設計流程四步曲
讓咱們結合複雜度來源和架構設計原則,經過一個模擬的設計場景「前浪微博」,和你一塊兒看看在實踐中究竟如何進行架構設計。
咱們假想一個創業公司,名稱叫做「前浪微博」。前浪微博的業務發展很快,系統也愈來愈多,系統間協做的效率很低,例如:
- 用戶發一條微博後,微博子系統須要通知審覈子系統進行審覈,而後通知統計子系統進行統計,再通知廣告子系統進行廣告預測,接着通知消息子系統進行消息推送……一條微博有十幾個通知,目前都是系統間經過接口調用的。每通知一個新系統,微博子系統就要設計接口、進行測試,效率很低,問題定位很麻煩,常常和其餘子系統的技術人員產生分岐,微博子系統的開發人員不勝其煩。
- 用戶等級達到 VIP 後,等級子系統要通知福利子系統進行獎品發放,要通知客服子系統安排專屬服務人員,要通知商品子系統進行商品打折處理……等級子系統的開發人員也是不勝其煩。
新來的架構師在梳理這些問題時,結合本身的經驗,敏銳地發現了這些問題背後的根源在於架構上各業務子系統強耦合,而消息隊列系統正好能夠完成子系統的解耦,因而提議要引入消息隊列系統。通過一分析二討論三開會四彙報五審批等一系列操做後,消息隊列系統終於立項了。其餘背景信息還有:
- 中間件團隊規模不大,大約 6 人左右。
- 中間件團隊熟悉 Java 語言,但有一個新同事 C/C++ 很牛。
- 開發平臺是 Linux,數據庫是 MySQL。
- 目前整個業務系統是單機房部署,沒有雙機房。
架構設計第 1 步:識別複雜度
架構設計的本質目的是爲了解決軟件系統的複雜性,因此在設計架構時,首先就要分析系統的複雜性。
針對前浪微博的消息隊列系統,採用「排查法」來分析複雜度,具體分析過程是:
1. 這個消息隊列是否須要高性能
咱們假設前浪微博系統用戶天天發送 1000 萬條微博,那麼微博子系統一天會產生 1000 萬條消息,咱們再假設平均一條消息有 10 個子系統讀取,那麼其餘子系統讀取的消息大約是 1 億次。
1000 萬和 1 億看起來很嚇人,但對於架構師來講,關注的不是一天的數據,而是 1 秒的數據,即 TPS 和 QPS。咱們將數據按照秒來計算,一天內平均每秒寫入消息數爲 115 條,每秒讀取的消息數是 1150 條;再考慮系統的讀寫並非徹底平均的,設計的目標應該以峯值來計算。峯值通常取平均值的 3 倍,那麼消息隊列系統的 TPS 是 345,QPS 是 3450,這個量級的數據意味着並不要求高性能。
雖然根據當前業務規模計算的性能要求並不高,但業務會增加,所以系統設計須要考慮必定的性能餘量。因爲如今的基數較低,爲了預留必定的系統容量應對後續業務的發展,咱們將設計目標設定爲峯值的 4 倍,所以最終的性能要求是:TPS 爲 1380,QPS 爲 13800。TPS 爲 1380 並不高,但 QPS 爲 13800 已經比較高了,所以高性能讀取是複雜度之一。
注意,這裏的設計目標設定爲峯值的 4 倍是根據業務發展速度來預估的,不是固定爲 4 倍,不一樣的業務能夠是 2 倍,也能夠是 8 倍,但通常不要設定在 10 倍以上,更不要一上來就按照 100 倍預估。
2. 這個消息隊列是否須要高可用性
對於微博子系統來講,若是消息丟了,致使沒有審覈,而後觸犯了國家法律法規,則是很是嚴重的事情;對於等級子系統來講,若是用戶達到相應等級後,系統沒有給他獎品和專屬服務,則 VIP 用戶會很不滿意,致使用戶流失從而損失收入,雖然也比較關鍵,但沒有審覈子系統丟消息那麼嚴重。
綜合來看,消息隊列須要高可用性,包括消息寫入、消息存儲、消息讀取都須要保證高可用性。
3. 這個消息隊列是否須要高可擴展性
消息隊列的功能很明確,基本無須擴展,所以可擴展性不是這個消息隊列的複雜度關鍵。
爲了方便理解,這裏我只排查「高性能」「高可用」「擴展性」這 3 個複雜度,在實際應用中,不一樣的公司或者團隊,可能還有一些其餘方面的複雜度分析。例如,金融系統可能須要考慮安全性,有的公司會考慮成本等。
綜合分析下來,消息隊列的複雜性主要體如今這幾個方面:高性能消息讀取、高可用消息寫入、高可用消息存儲、高可用消息讀取。
架構設計第 2 步:設計備選方案
肯定了系統面臨的主要複雜度問題後,方案設計就有了明確的目標,咱們就能夠開始真正進行架構方案設計了。
1. 備選方案 1:採用開源的 Kafka
Kafka 是成熟的開源消息隊列方案,功能強大,性能很是高,並且已經比較成熟,不少大公司都在使用。
2. 備選方案 2:集羣 + MySQL 存儲
首先考慮單服務器高性能。高性能消息讀取屬於「計算高可用」的範疇,單服務器高性能備選方案有不少種。考慮到團隊的開發語言是 Java,雖然有人以爲 C/C++ 語言更加適合寫高性能的中間件系統,但架構師綜合來看,認爲無須爲了語言的性能優點而讓整個團隊切換語言,消息隊列系統繼續用 Java 開發。因爲 Netty 是 Java 領域成熟的高性能網絡庫,所以架構師選擇基於 Netty 開發消息隊列系統。
因爲系統設計的 QPS 是 13800,即便單機採用 Netty 來構建高性能系統,單臺服務器支撐這麼高的 QPS 仍是有很大風險的,所以架構師選擇採起集羣方式來知足高性能消息讀取,集羣的負載均衡算法採用簡單的輪詢便可。
同理,「高可用寫入」和「高性能讀取」同樣,能夠採起集羣的方式來知足。由於消息只要寫入集羣中一臺服務器就算成功寫入,所以「高可用寫入」的集羣分配算法和「高性能讀取」也同樣採用輪詢,即正常狀況下,客戶端將消息依次寫入不一樣的服務器;某臺服務器異常的狀況下,客戶端直接將消息寫入下一臺正常的服務器便可。
整個系統中最複雜的是「高可用存儲」和「高可用讀取」,「高可用存儲」要求已經寫入的消息在單臺服務器宕機的狀況下不丟失;「高可用讀取」要求已經寫入的消息在單臺服務器宕機的狀況下能夠繼續讀取。架構師第一時間想到的就是能夠利用 MySQL 的主備複製功能來達到「高可用存儲「的目的,經過服務器的主備方案來達到「高可用讀取」的目的。
具體方案:
簡單描述一下方案:
- 採用數據分散集羣的架構,集羣中的服務器進行分組,每一個分組存儲一部分消息數據。
- 每一個分組包含一臺主 MySQL 和一臺備 MySQL,分組內主備數據複製,分組間數據不一樣步。
- 正常狀況下,分組內的主服務器對外提供消息寫入和消息讀取服務,備服務器不對外提供服務;主服務器宕機的狀況下,備服務器對外提供消息讀取的服務。
- 客戶端採起輪詢的策略寫入和讀取消息。
3. 備選方案 3:集羣 + 自研存儲方案
在備選方案 2 的基礎上,將 MySQL 存儲替換爲自研實現存儲方案,由於 MySQL 的關係型數據庫的特色並非很契合消息隊列的數據特色,參考 Kafka 的作法,能夠本身實現一套文件存儲和複製方案(此處省略具體的方案描述,實際設計時須要給出方案)。
能夠看出,高性能消息讀取單機系統設計這部分時並無多個備選方案可選,備選方案 2 和備選方案 3 都採起基於 Netty 的網絡庫,用 Java 語言開發,緣由就在於團隊的 Java 背景約束了備選的範圍。一般狀況下,成熟的團隊不會輕易改變技術棧,反而是新成立的技術團隊更加傾向於採用新技術。
上面簡單地給出了 3 個備選方案用來示範如何操做,實踐中要比上述方案複雜一些。架構師的技術儲備越豐富、經驗越多,備選方案也會更多,從而才能更好地設計備選方案。例如,開源方案選擇可能就包括 Kafka、ActiveMQ、RabbitMQ;集羣方案的存儲既能夠考慮用 MySQL,也能夠考慮用 HBase,還能夠考慮用 Redis 與 MySQL 結合等;自研文件系統也能夠有多個,能夠參考 Kafka,也能夠參考 LevelDB,還能夠參考 HBase 等。限於篇幅,這裏就不一一展開了。
架構設計第 3 步:評估和選擇備選方案
在完成備選方案設計後,如何挑選出最終的方案也是一個很大的挑戰。有時候咱們要挑選最簡單的方案,有時候要挑選最優秀的方案,有時候要挑選最熟悉的方案,甚至有時候真的要領導拍板。所以關鍵問題是:這裏的「有時候」到底應該怎麼判斷?
個人答案就是「360 度環評」! 具體的操做方式爲:列出咱們須要關注的質量屬性點,而後分別從這些質量屬性的維度去評估每一個方案,再綜合挑選適合當時狀況的最優方案。
針對上期提出的 3 個備選方案,架構師組織了備選方案評審會議,參加的人有研發、測試、運維、還有幾個核心業務的主管。
1. 備選方案 1:採用開源 Kafka 方案
- 業務主管傾向於採用 Kafka 方案,由於 Kafka 已經比較成熟,各個業務團隊或多或少都瞭解過 Kafka。
- 中間件團隊部分研發人員也支持使用 Kafka,由於使用 Kafka 能節省大量的開發投入;但部分人員認爲 Kafka 可能並不適合咱們的業務場景,由於 Kafka 的設計目的是爲了支撐大容量的日誌消息傳輸,而咱們的消息隊列是爲了業務數據的可靠傳輸。
- 運維表明提出了強烈的反對意見:首先,Kafka 是 Scala 語言編寫的,運維團隊沒有維護 Scala 語言開發的系統的經驗,出問題後很難快速處理;其次,目前運維團隊已經有一套成熟的運維體系,包括部署、監控、應急等,使用 Kafka 沒法融入這套體系,須要單獨投入運維人力。
- 測試表明也傾向於引入 Kafka,由於 Kafka 比較成熟,無須太多測試投入。
2. 備選方案 2:集羣 + MySQL 存儲
- 中間件團隊的研發人員認爲這個方案比較簡單,但部分研發人員對於這個方案的性能持懷疑態度,畢竟使用 MySQL 來存儲消息數據,性能確定不如使用文件系統;而且有的研發人員擔憂作這樣的方案是否會影響中間件團隊的技術聲譽,畢竟用 MySQL 來作消息隊列,看起來比較「土」、比較另類。
- 運維表明贊同這個方案,由於這個方案能夠融入到現有的運維體系中,並且使用 MySQL 存儲數據,可靠性有保證,運維團隊也有豐富的 MySQL 運維經驗;但運維團隊認爲這個方案的成本比較高,一個數據分組就須要 4 臺機器(2 臺服務器 + 2 臺數據庫)。
- 測試表明認爲這個方案測試人力投入較大,包括功能測試、性能測試、可靠性測試等都須要大量地投入人力。
- 業務主管對這個方案既不願定也不否認,由於反正都不是業務團隊來投入人力來開發,系統維護也是中間件團隊負責,對業務團隊來講,只要保證消息隊列系統穩定和可靠便可。
3. 備選方案 3:集羣 + 自研存儲系統
- 中間件團隊部分研發人員認爲這是一個很好的方案,既可以展示中間件團隊的技術實力,性能上相比 MySQL 也要高;但另外的研發人員認爲這個方案複雜度過高,按照目前的團隊人力和技術實力,要作到穩定可靠的存儲系統,須要耗時較長的迭代,這個過程當中消息隊列系統可能由於存儲出現嚴重問題,例如文件損壞致使丟失大量數據。
- 運維表明不太同意這個方案,由於運維以前遇到過幾回相似的存儲系統故障致使數據丟失的問題,損失慘重。例如,MongoDB 丟數據、Tokyo Tyrant 丟數據沒法恢復等。運維團隊並不相信目前的中間件團隊的技術實力足以支撐本身研發一個存儲系統(這讓中間件團隊的人員感受有點不爽)。
- 測試表明贊同運維表明的意見,而且自研存儲系統的測試難度也很高,投入也很大。
- 業務主管對自研存儲系統也持保留意見,由於從歷史經驗來看,新系統上線確定有 bug,而存儲系統出 bug 是最嚴重的,一旦出 bug 致使大量消息丟失,對系統的影響會嚴重。
針對 3 個備選方案的討論初步完成後,架構師列出了 3 個方案的 360 度環評表:
列出這個表格後,沒法一眼看出具體哪一個方案更合適,因而你們都把目光投向架構師,決策的壓力如今集中在架構師身上了。
架構師通過思考後,給出了最終選擇備選方案 2,緣由有:
- 排除備選方案 1 的主要緣由是可運維性,由於再成熟的系統,上線後均可能出問題,若是出問題沒法快速解決,則沒法知足業務的需求;而且 Kafka 的主要設計目標是高性能日誌傳輸,而咱們的消息隊列設計的主要目標是業務消息的可靠傳輸。
- 排除備選方案 3 的主要緣由是複雜度,目前團隊技術實力和人員規模(總共 6 人,還有其餘中間件系統須要開發和維護)沒法支撐自研存儲系統(參考架構設計原則 2:簡單原則)。
- 備選方案 2 的優勢就是複雜度不高,也能夠很好地融入現有運維體系,可靠性也有保障。
針對備選方案 2 的缺點,架構師解釋是:
- 備選方案 2 的第一個缺點是性能,業務目前須要的性能並非很是高,方案 2 可以知足,即便後面性能需求增長,方案 2 的數據分組方案也可以平行擴展進行支撐(參考架構設計原則 3:演化原則)。
- 備選方案 2 的第二個缺點是成本,一個分組就須要 4 臺機器,支撐目前的業務需求可能須要 12 臺服務器,但實際上備機(包括服務器和數據庫)主要用做備份,能夠和其餘系統並行部署在同一臺機器上。
- 備選方案 2 的第三個缺點是技術上看起來並不很優越,但咱們的設計目的不是爲了證實本身(參考架構設計原則 1:合適原則),而是更快更好地知足業務需求。
最後,你們針對一些細節再次討論後,肯定了選擇備選方案 2。
經過「前浪微博」這個案例咱們能夠看出,備選方案的選擇和不少因素相關,並不僅僅考慮性能高低、技術是否優越這些純技術因素。業務的需求特色、運維團隊的經驗、已有的技術體系、團隊人員的技術水平都會影響備選方案的選擇。所以,一樣是上述 3 個備選方案,有的團隊會選擇引入 Kafka,有的會選擇自研存儲系統。
架構設計第 4 步:詳細方案設計
完成備選方案的設計和選擇後,咱們終於能夠長出一口氣,由於整個架構設計最難的一步已經完成了,但總體方案還沒有完成,架構師還需繼續努力。接下來咱們須要再接再勵,將最終肯定的備選方案進行細化,使得備選方案變成一個能夠落地的設計方案。
雖然咱們在「前浪微博」消息隊列的架構設計挑選了備選方案 2 做爲最終方案,但備選方案設計階段的方案粒度還比較粗,沒法真正指導開發人員進行後續的設計和開發,所以須要在備選方案的基礎上進一步細化。
下面我列出一些備選方案 2 典型的須要細化的點供參考,有興趣的同窗能夠本身嘗試細化更多的設計點。
1. 細化設計點 1:數據庫表如何設計?
- 數據庫設計兩類表,一類是日誌表,用於消息寫入時快速存儲到 MySQL 中;另外一類是消息表,每一個消息隊列一張表。
- 業務系統發佈消息時,首先寫入到日誌表,日誌表寫入成功就表明消息寫入成功;後臺線程再從日誌表中讀取消息寫入記錄,將消息內容寫入到消息表中。
- 業務系統讀取消息時,從消息表中讀取。
- 日誌表表名爲 MQ_LOG,包含的字段:日誌 ID、發佈者信息、發佈時間、隊列名稱、消息內容。
- 消息表表名就是隊列名稱,包含的字段:消息 ID(遞增生成)、消息內容、消息發佈時間、消息發佈者。
- 日誌表須要及時清除已經寫入消息表的日誌數據,消息表最多保存 30 天的消息數據。
2. 細化設計點 2:數據如何複製?
直接採用 MySQL 主從複製便可,只複製消息存儲表,不復制日誌表。
3. 細化設計點 3:主備服務器如何倒換?
採用 ZooKeeper 來作主備決策,主備服務器都鏈接到 ZooKeeper 創建本身的節點,主服務器的路徑規則爲「/MQ/server/ 分區編號 /master」,備機爲「/MQ/server/ 分區編號 /slave」,節點類型爲 EPHEMERAL。
備機監聽主機的節點消息,當發現主服務器節點斷連後,備服務器修改本身的狀態,對外提供消息讀取服務。
4. 細化設計點 4:業務服務器如何寫入消息?
- 消息隊列系統設計兩個角色:生產者和消費者,每一個角色都有惟一的名稱。
- 消息隊列系統提供 SDK 供各業務系統調用,SDK 從配置中讀取全部消息隊列系統的服務器信息,SDK 採起輪詢算法發起消息寫入請求給主服務器。若是某個主服務器無響應或者返回錯誤,SDK 將發起請求發送到下一臺服務器。
5. 細化設計點 5:業務服務器如何讀取消息?
- 消息隊列系統提供 SDK 供各業務系統調用,SDK 從配置中讀取全部消息隊列系統的服務器信息,輪流向全部服務器發起消息讀取請求。
- 消息隊列服務器須要記錄每一個消費者的消費狀態,即當前消費者已經讀取到了哪條消息,當收到消息讀取請求時,返回下一條未被讀取的消息給消費者。
6. 細化設計點 6:業務服務器和消息隊列服務器之間的通訊協議如何設計?
考慮到消息隊列系統後續可能會對接多種不一樣編程語言編寫的系統,爲了提高兼容性,傳輸協議用 TCP,數據格式爲 ProtocolBuffer。
固然還有更多設計細節就再也不一一列舉,所以這還不是一個完整的設計方案,我但願能夠經過這些具體實例來講明細化方案具體如何去作。