如何設計一個微博系統?- 4招教你搞定系統設計

2023-03-26 18:00:53

經常在面試的時候,會被問到系統設計類的題目,比如如何設計微信朋友圈、如何設計12306系統、如何設計一個搶票系統等等。如果是沒有準備過,一般都會不知所措,難以找到切入點。今天這裡碼老思會介紹一個解決系統設計類問題的通用框架,無論什麼問題,朝這幾步走,一定能找到解決辦法。

系統設計在考察什麼?

系統設計面試中,經常會被問到如何設計微信、如何設計微博、如何設計百度……我們怎麼能在如此短的時間內設計出來一個由成千上萬的碼農、PM,經年累月地迭代出來的如此優秀的產品?如果面試者這麼優秀,那還面試啥?百度、谷歌也不可能只是一個搜尋方塊而已,底下的東西複雜去了。

所以首先要明確,系統問題的答案一定不可能是全面的,面試官也不會期望我們給出一個滿分答案。所謂的系統設計面試實際上是在模擬一個場景:兩名同事在一起就一個模糊的問題,討論一番,得出一個還不錯的解決方案。

因此在這個過程中,我們需要與面試官進行充分的溝通,瞭解清楚需求,迴應面試管的各種問題,闡述我們設計的方案。最終設計的結果是開放性的,沒有標準答案;而面試官會在這個過程中,會充分挖掘我們的溝通、分析、解決問題的能力,最後給出通過與否的結論。

常見誤區

沒有方向,只提出各種關鍵詞

在面試中,常見的錯誤是面試官給出問題後,候選人就開始懟各種關鍵詞,什麼Load Balancer,Memcache,NodeJS,MongoDB,MySQL.

不是說我們不能說這些技術詞彙,但是系統設計首先我們需要搞清楚具體的需求,然後在大概確定系統架構,最後才根據場景進行技術選型,因為並不存在一個完全完美的系統設計,往往都是各方面之間的均衡,至於如何均衡,需要結合面試官的要求和具體實際來選擇。

一開始直奔最優解

系統設計考察我們如何去解決一個具體的問題,在實際工作中,我們也是先實現功能之後,再在此基礎上去進行鍼對性的優化;在面試中同樣也十分重要,一方面,面試官希望看到你從一開始設計系統,到慢慢完善的過程,另外,系統設計其實沒有最優解,往往都是各種因素之間權衡後的結果,就像CAP理論一樣,無法同時滿足,我們的系統只要能滿足面試官提出來的要求,剛好夠用就行。

悶頭設計不溝通

很多人在聽到問題之後,就開始悶頭設計,絲毫不會和麵試官進行溝通交流。這樣不僅不利於自己理解題目意圖,而且讓面試官無法瞭解你是如何一步步去解決問題的過程。

這個時候可以把面試官當做一位同事,比如你對題目不理解,可以提出問題搞清楚題目意圖;又比如你在哪個環節卡住了,也不要一直悶在那,可以大膽向面試官求助尋求提示,也能節省不少時間。

就像開始說的,系統設計沒有最優解,你的思路和解決問題的過程很重要,而這些就是通過不斷的溝通來傳遞給面試官的。

如何搞定系統設計 - 4步法

針對系統設計,這裡給大家提供一個4步法解決方案,無論是任何系統設計的題目,都可以按照這幾步去思考解決。

1. 確定問題需求

這一步主要是確定問題的需求和系統使用場景,為了搞清楚這個問題,需要和麵試官你來我往地問問題。

在做設計的同時,問面試官的要求是什麼,做出合理的假設、取捨,讓面試官看出你的思考過程,最終綜合所有的資訊完成一個還不錯的設計。不要害怕問問題。那並不會說明我們不懂,而是讓面試官理解我們的思考過程。當我們問出問題後,面試官要麼給出明確的回答;要麼讓我們自己做出假設。當需要做出假設時,我們需要把假設寫下來備用。

這個舉個例子,比如面試官讓你設計weibo,因為weibo的功能較為龐大,例如發微博、微博時間線、關注、取消關注、微博熱搜榜等等,我們無法在短短的面試時間內完成這麼多功能設計,所以這時候可以詢問面試官我們需要實現哪些功能。

比如需要實現微博時間線的功能,我們得進一步確認,整體使用者量多大,系統的QPS多大,因為這涉及到我們後續的系統設計,而且如果對於QPS特別高的情況,在後續的設計中需要針對此進行專門的擴充套件性優化。

QPS有什麼用?

  • QPS = 100,那麼用你的筆電作Web伺服器就好了;
  • QPS = 1K,一臺好點的Web 伺服器也能應付,需要考慮Single Point Failure;
  • QPS = 1m,則需要建設一個1000臺Web伺服器的叢集,並且要考慮如何Maintainance(某一臺掛了怎麼辦)。

QPS 和 伺服器/資料庫之間的關係:

  • 一臺Web Server承受量約為 1K的QPS(考慮到邏輯處理時間以及資料庫查詢的瓶頸);
  • 一臺SQL Database承受量約為 1K的QPS(如果JOIN和INDEX query比較多的話,這個值會更小);
  • 一臺 NoSQL Database (Cassandra) 約承受量是 10k 的 QPS;
  • 一臺 NoSQL Database (Memcached) 約承受量是 1M 的 QPS。

以下是一些通用的問題,可以通過詢問系統相關的問題,搞清楚面試官的意圖和系統的使用場景。

  • 系統的功能是什麼
  • 系統的目標群體是什麼
  • 系統的使用者量有多大
  • 希望每秒鐘處理多少請求?
  • 希望處理多少資料?
  • 希望的讀寫比率?
  • 系統的擴張規模怎麼樣,這涉及到後續的擴充套件

總結,在這一步,不要設計具體的系統,把問題搞清楚就行。不要害怕問問題,那並不會說明我們不懂,而是讓面試官理解我們的思考過程。

2. 完成整體設計

這一步根據系統的具體要求,我們將其劃分為幾個功能獨立的模組,然後做出一張整體的架構圖,並依據此架構圖,看是否能實現上一步所提出來的需求。

如果可能的話,設想幾個具體的例子,對著圖演練一遍。這讓我們能更堅定當前的設計,有時候還能發現一些未考慮到的邊界case。

這裡說的比較抽象,具體可以參考下面的實戰環節,來理解如何完成整體設計。

3. 深入模組設計

至此,我們已經完成了系統的整體設計,接下來需要根據面試官的要求對模組進行具體設計。

比如需要設計一個短網址的系統,上一步中可能已經把系統分為了如下三個模組:

  • 生成完整網址的hash值,並進行儲存。
  • 短網址轉換為完整url。
  • 短網址轉換的API。

這一步中我們需要對上面三個模組進行具體設計,這裡面就涉及到實際的技術選型了。下面舉個簡單的例子。

比如說生成網址的hash值,假設別名是http://tinyurl.com/<alias_hash>,alias_hash是固定長度的字串。

如果長度為 7,包含[A-Z, a-z, 0-9],則可以提供62 ^ 7 ~= 3500 億個 URL。至於3500億的網址數目是否能滿足要求,目前世界上總共有2億多個網站,平均每個網站1000個網址來計算,也是夠用的。而且後續可以引入網址過期的策略。

首先,我們將所有的對映儲存在一個資料庫中。 一個簡單的方法是使用alias_hash作為每個對映的 ID,可以生成一個長度為 7 的隨機字串。

所以我們可以先儲存<ID,URL>。 當用戶輸入一個長 URL http://www.lfeng.tech時,系統建立一個隨機的 7 個字元的字串,如abcd123作為 ID,並插入條目<"abcd123", "http://www.lfeng.tech">進入資料庫。

在執行期間,當有人存取http://tinyurl.com/abcd123時,我們通過 ID abcd123查詢並重定向到相應的 URL http://www.lfeng.tech

當然,上面的例子中只解決了生成網址的問題,還有網址的儲存、網址生成hash值之後產生碰撞如何解決等等,都需要在這個階段解決。這裡面涉及到各種儲存方案的選擇、資料庫的設計等等,後面會有專門的文章進行介紹。

4. 可延伸性設計

這是最後一步,面試官可能會針對系統中的某個模組,給出擴充套件性相關的問題,這塊的問題可以分為兩類:

  1. 當前系統的優化。因為沒有完美的系統,我們的設計的也不例外,因此這類問題需要我們認真反思系統的設計,找出其中可能的缺陷,提出具體的解決方案。
  2. 擴充套件當前系統。例如我們當前的設計能夠支撐100w 使用者,那麼當用戶數達到 1000w 時,需要如何應對等等。

這裡面可能涉及到水平擴充套件、垂直擴充套件、快取、負載均衡、資料庫的拆分的同步等等話題,後續會有專門的文章進行講解。

實戰 - 4步法解決Weibo設計

這部分我以Weibo的設計為例,帶大家過一遍如何用4步法解決實際的系統設計。

確定weibo的使用場景

因為weibo功能較多,這裡沒有面試官的提示,我們假定需要實現微博的時間線以及搜尋的功能。

基於這個設定,我們需要解決如下幾個問題:

  • 使用者發微博:服務能夠將微博推播給對應的關注者,同時可能需要相應的提醒。
  • 使用者瀏覽自己的微博時間線。
  • 使用者瀏覽主頁微博,也就是需要將使用者關注物件的微博呈現出來。
  • 使用者能夠搜尋微博。
  • 整個系統具有高可用性。

初步估算

基於上面的需求,我們進一步做出一些假設,然後計算出大概的儲存需求和QPS,因為後續的技術選型依賴於當前系統的規模。這裡我做出如下假定:

  • 假設有1億活躍使用者,每人平均每天5條微博,也就是每天5億條微博,每月150億條。
  • 每條微博的平均閱讀量是20,也就是每月3000億閱讀量。
  • 對於搜尋,假定每人每天搜尋5次,一個月也就是150億次搜尋請求。

下面進一步對儲存和QPS進行估算,

首先是儲存,每條微博至少包含如下幾個內容:

  • 微博id:8 bytes
  • 使用者id:32 bytes
  • 微博正文:140 bytes
  • 媒體檔案:10 KB (這裡只考慮媒體檔案對應的連結)
    總共10KB左右。

每月150億條微博,也就是0.14PB,每年1.68PB。

其次是QPS,這裡涉及到3個介面:

  • 發微博介面:每天5億條微博,也就是大約6000QPS
  • 閱讀微博介面:每天100億閱讀,也就是大約12萬QPS
  • 搜尋微博介面:每天每人搜尋5次,那也大約6000QPS。

這裡估算出來的資料為後續技術選型做參考。

完成weibo整體設計

根據上面的分析,這一步將主要服務拆分出來,可以分為讀微博服務、發微博服務、搜尋服務;同時還有相關的時間線服務、微博資訊服務,使用者資訊服務、使用者關係圖服務、訊息推播服務等等。考慮到服務高可用,這裡也引入了快取。

拆分好了之後,根據使用者使用場景,可以設計出如下圖所示的系統架構圖,注意到目前為止,都是粗略設計,我們只需要將服務抽象出來,完成具體的功能即可。後續步驟會對主要服務進行詳細設計。

下圖的架構中,主要實現了使用者發微博、瀏覽微博時間線和搜尋的場景。

設計核心模組

上一步我們完成了微博的架構設計,這一步從使用者場景入手,詳細設計核心模組。

使用者發微博

使用者發微博的時候,發微博服務需要完成如下幾項工作:

  • 將微博寫入到MySQL等關係型資料庫,考慮到流量較大,這裡可以引入Kafka等MQ來進行削峰。
  • 查詢使用者關係圖服務,找到該使用者的所有follower,然後把微博插入到所有follower的時間線上,注意到這裡時間線的資訊都是存放在快取中的。
  • 將微博資料匯入到搜尋叢集中,提供給後續搜尋使用。
  • 將微博的媒體檔案存放到物件儲存中。
  • 呼叫訊息推播服務,將訊息推播到所有的followers。這裡同樣可以採用一個MQ來進行非同步訊息推播。

對於每個使用者,我們可以用一個Redis的list,來存放所有該使用者關注物件的微博資訊,類似如下:

第N條微博 第N+1條微博 第N+2條微博
8 bytes 8 bytes 1 byte 8 bytes 8 bytes 1 byte 8 bytes 8 bytes 1 byte
weibo_id user_id meta weibo_id user_id meta weibo_id user_id meta

後續的時間線服務,可以根據這個列表生成使用者的時間線微博。

使用者瀏覽微博時間線

當用戶瀏覽主頁時間線時,微博時間線服務會完成如下的工作:

  • 從上面設計的Redis list中拿到時間線上所有的微博id和使用者id,可以在O(1)時間內完成。
  • 查詢微博資訊服務,來獲取這些微博的詳細資訊,這些資訊可以存放在快取中,O(N)時間內可以完成。
  • 查詢使用者資訊服務,來獲取每條微博對應使用者的詳細資訊,同樣也是O(N)時間完成。

注意到這裡有一個特殊的情形,就是使用者瀏覽自己的微博列表,對於這種情況,如果頻率不是很高的情況,可以直接從MySQL中取出使用者所有的微博即可。

使用者搜尋微博

當用戶搜尋微博時,會發生下面的事情:

  • 搜尋服務首先會進行預處理,包括對輸入文字的分詞、正則化、詞語糾錯等等處理。
  • 接著將處理好的結果組裝成查詢語句,在叢集中完成對應的查詢,獲取搜尋結果。
  • 最後根據使用者的設定,可能需要對結果進行排序、聚合等等,最後將處理好的結果返回給使用者。

系統擴充套件設計

在做系統擴充套件設計之前,我們可以依據下面幾個步驟,找出系統中可能存在的瓶頸,然後進行鍼對性優化。

(1)對各個模組進行benckmark測試,並記錄對應的響應時間等重要資料;
(2)綜合各種資料,找到系統的瓶頸所在;
(3)解決瓶頸問題,並在各種可選方案之間的做權衡,就像開頭所說,沒有完美的系統。

下面針對我們剛才設計的微博系統,可能的瓶頸存在於下面幾個地方:

  • 微博伺服器的入口。這裡承受了最大的流量,因此可以引入負載均衡進緩解。
  • 發微博服務。可以看到這裡需要和大量的服務進行互動,在流量很大的情況下,很容易成為整個系統的瓶頸,因此可以考慮將其進行水平擴充套件,或者將發微博服務進行進一步拆分,拆成各個小元件之後,再進行單獨優化。
  • MySQL伺服器。這裡儲存著使用者資訊、微博資訊等,也承載了很大的流量,可以考慮將讀寫進行分離,同時引入主從伺服器來保證高可用。
  • 讀微博服務。在某些熱點事件時,讀微博服務會接收巨大的流量,可以引入相應的手段對讀服務進行自動擴充套件,比如K8s的水平擴充套件等,方便應對各種突發情況。

下面是進一步優化之後的系統架構圖:

針對這個系統,我們還可以進一步優化,以下是幾個思路,也可以自己思考看看,

  • 對於有大量粉絲的使用者,比如很多明星;在現有架構上明星每次都會將微博利用資料傳輸服務,發到每個粉絲的時間線上去,這樣其實會造成大量的流量,針對這種情況,可以將這些明星使用者單獨處理,每當粉絲在重新整理主頁的時候,會根據這些粉絲的時間線以及明星的微博資訊,合併來生成粉絲的主頁時間線,避免了不必要的流量浪費。
  • 只儲存最近一段時間的微博資料在快取中, 這主要是為了節省叢集空間,並且一般熱點微博都是最近的微博。
  • 只儲存活躍使用者的主頁時間線在快取中,對於過去一段時間內,比如30天內未活躍的使用者,我們只有在該使用者首次瀏覽的時候,從DB中將資料load出來組成時間線。

擴充套件的方向不止上面說的這幾點,大家可以從快取(CDN,使用者快取、伺服器快取)、非同步(MQ、微服務等等)、資料庫調優等方向去思考,看如何提升整體效能。這些相關內容我會在後續文章中仔細講述,歡迎關注【碼老思】,後續文章敬請期待。


參考:

可以關注公眾號【碼老思】,一時間獲取最通俗易懂的原創技術乾貨。