如何設計出一個“前浪微博”?

如何设计出一个“前浪微博”?

如何设计出一个“前浪微博”?

讓我們結合複雜度來源和架構設計原則,通過一個模擬的設計場景“前浪微博”,和你一起看看在實踐中究竟如何進行架構設計。

我們假想一個創業公司,名稱叫作“前浪微博”。前浪微博的業務發展很快,系統也越來越多,系統間協作的效率很低,例如:

  • 用戶發一條微博後,微博子系統需要通知審核子系統進行審核,然後通知統計子系統進行統計,再通知廣告子系統進行廣告預測,接著通知消息子系統進行消息推送……一條微博有十幾個通知,目前都是系統間通過接口調用的。每通知一個新系統,微博子系統就要設計接口、進行測試,效率很低,問題定位很麻煩,經常和其他子系統的技術人員產生分歧,微博子系統的開發人員不勝其煩。


  • 用戶等級達到 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:採用開源的 Kafka

Kafka 是成熟的開源消息隊列方案,功能強大,性能非常高,而且已經比較成熟,很多大公司都在使用。

備選方案 2:集群 + MySQL 存儲

首先考慮單服務器高性能。高性能消息讀取屬於“計算高可用”的範疇,單服務器高性能備選方案有很多種。考慮到團隊的開發語言是 Java,雖然有人覺得 C/C++ 語言更加適合寫高性能的中間件系統,但架構師綜合來看,認為無須為了語言的性能優勢而讓整個團隊切換語言,消息隊列系統繼續用 Java 開發。由於 Netty 是 Java 領域成熟的高性能網絡庫,因此架構師選擇基於 Netty 開發消息隊列系統。

由於系統設計的 QPS 是 13800,即使單機採用 Netty 來構建高性能系統,單臺服務器支撐這麼高的 QPS 還是有很大風險的,因此架構師選擇採取集群方式來滿足高性能消息讀取,集群的負載均衡算法採用簡單的輪詢即可。

同理,“高可用寫入”和“高性能讀取”一樣,可以採取集群的方式來滿足。因為消息只要寫入集群中一臺服務器就算成功寫入,因此“高可用寫入”的集群分配算法和“高性能讀取”也一樣採用輪詢,即正常情況下,客戶端將消息依次寫入不同的服務器;某臺服務器異常的情況下,客戶端直接將消息寫入下一臺正常的服務器即可。

整個系統中最複雜的是“高可用存儲”和“高可用讀取”,“高可用存儲”要求已經寫入的消息在單臺服務器宕機的情況下不丟失;“高可用讀取”要求已經寫入的消息在單臺服務器宕機的情況下可以繼續讀取。架構師第一時間想到的就是可以利用 MySQL 的主備複製功能來達到“高可用存儲“的目的,通過服務器的主備方案來達到“高可用讀取”的目的。

具體方案為:

如何设计出一个“前浪微博”?

簡單描述一下方案:

  • 採用數據分散集群的架構,集群中的服務器進行分組,每個分組存儲一部分消息數據。


  • 每個分組包含一臺主 MySQL 和一臺備 MySQL,分組內主備數據複製,分組間數據不同步。


  • 正常情況下,分組內的主服務器對外提供消息寫入和消息讀取服務,備服務器不對外提供服務;主服務器宕機的情況下,備服務器對外提供消息讀取的服務。


  • 客戶端採取輪詢的策略寫入和讀取消息。

備選方案 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:採用開源 Kafka 方案

  • 業務主管傾向於採用 Kafka 方案,因為 Kafka 已經比較成熟,各個業務團隊或多或少都瞭解過 Kafka。


  • 中間件團隊部分研發人員也支持使用 Kafka,因為使用 Kafka 能節省大量的開發投入;但部分人員認為 Kafka 可能並不適合我們的業務場景,因為 Kafka 的設計目的是為了支撐大容量的日誌消息傳輸,而我們的消息隊列是為了業務數據的可靠傳輸。


  • 運維代表提出了強烈地反對意見:首先,Kafka 是 Scala 語言編寫的,運維團隊沒有維護 Scala 語言開發的系統的經驗,出問題後很難快速處理;其次,目前運維團隊已經有一套成熟的運維體系,包括部署、監控、應急等,使用 Kafka 無法融入這套體系,需要單獨投入運維人力。


  • 測試代表也傾向於引入 Kafka,因為 Kafka 比較成熟,無須太多測試投入。

備選方案 2:集群 + MySQL 存儲

  • 中間件團隊的研發人員認為這個方案比較簡單,但部分研發人員對於這個方案的性能持懷疑態度,畢竟使用 MySQL 來存儲消息數據,性能肯定不如使用文件系統;並且有的研發人員擔心做這樣的方案是否會影響中間件團隊的技術聲譽,畢竟用 MySQL 來做消息隊列,看起來比較“土”、比較另類。


  • 運維代表贊同這個方案,因為這個方案可以融入到現有的運維體系中,而且使用 MySQL 存儲數據,可靠性有保證,運維團隊也有豐富的 MySQL 運維經驗;但運維團隊認為這個方案的成本比較高,一個數據分組就需要 4 臺機器(2 臺服務器 + 2 臺數據庫)。


  • 測試代表認為這個方案測試人力投入較大,包括功能測試、性能測試、可靠性測試等都需要大量地投入人力。


  • 業務主管對這個方案既不肯定也不否定,因為反正都不是業務團隊來投入人力來開發,系統維護也是中間件團隊負責,對業務團隊來說,只要保證消息隊列系統穩定和可靠即可。

備選方案 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:數據庫表如何設計?

  • 數據庫設計兩類表,一類是日誌表,用於消息寫入時快速存儲到 MySQL 中;另一類是消息表,每個消息隊列一張表。


  • 業務系統發佈消息時,首先寫入到日誌表,日誌表寫入成功就代表消息寫入成功;後臺線程再從日誌表中讀取消息寫入記錄,將消息內容寫入到消息表中。


  • 業務系統讀取消息時,從消息表中讀取。


  • 日誌表表名為 MQ_LOG,包含的字段:日誌 ID、發佈者信息、發佈時間、隊列名稱、消息內容。


  • 消息表表名就是隊列名稱,包含的字段:消息 ID(遞增生成)、消息內容、消息發佈時間、消息發佈者。


  • 日誌表需要及時清除已經寫入消息表的日誌數據,消息表最多保存 30 天的消息數據。

細化設計點 2:數據如何複製?

直接採用 MySQL 主從複製即可,只複製消息存儲表,不復制日誌表。

細化設計點 3:主備服務器如何倒換?

採用 ZooKeeper 來做主備決策,主備服務器都連接到 ZooKeeper 建立自己的節點,主服務器的路徑規則為“/MQ/server/ 分區編號 /master”,備機為“/MQ/server/ 分區編號 /slave”,節點類型為 EPHEMERAL。

備機監聽主機的節點消息,當發現主服務器節點斷連後,備服務器修改自己的狀態,對外提供消息讀取服務。

細化設計點 4:業務服務器如何寫入消息?

  • 消息隊列系統設計兩個角色:生產者和消費者,每個角色都有唯一的名稱。


  • 消息隊列系統提供 SDK 供各業務系統調用,SDK 從配置中讀取所有消息隊列系統的服務器信息,SDK 採取輪詢算法發起消息寫入請求給主服務器。如果某個主服務器無響應或者返回錯誤,SDK 將發起請求發送到下一臺服務器。

細化設計點 5:業務服務器如何讀取消息?

  • 消息隊列系統提供 SDK 供各業務系統調用,SDK 從配置中讀取所有消息隊列系統的服務器信息,輪流向所有服務器發起消息讀取請求。


  • 消息隊列服務器需要記錄每個消費者的消費狀態,即當前消費者已經讀取到了哪條消息,當收到消息讀取請求時,返回下一條未被讀取的消息給消費者。

細化設計點 6:業務服務器和消息隊列服務器之間的通信協議如何設計?

考慮到消息隊列系統後續可能會對接多種不同編程語言編寫的系統,為了提升兼容性,傳輸協議用 TCP,數據格式為 ProtocolBuffer。

當然還有更多設計細節就不再一一列舉,因此這還不是一個完整的設計方案,我希望可以通過這些具體實例來說明細化方案具體如何去做。


分享到:


相關文章: