CodeKarle:推特系統設計面試

banq發表於2022-01-27

一個典型的面試問題:“你將如何設計一個像 Twitter 這樣的系統”。
 
讓我們看一下開始的要求。
功能要求
  • 推文 - 應該允許您釋出文字、影像、影片、連結等
  • Re-tweet - 應該允許你分享某人的推文
  • 跟隨 - 這將是一種定向關係。如果我們跟隨巴拉克奧巴馬,他不必跟隨我們回來
  • 搜尋


非功能性要求
  • 重讀 - twitter 的讀寫比非常高,所以我們的系統應該能夠支援這種模式
  • 快速渲染
  • 快速推文。
  • 延遲是可以接受的——從前面的兩個 NFR 中,我們可以理解系統應該是高可用的並且具有非常低的延遲。因此,當我們說延遲沒問題時,我們的意思是幾秒鐘後就可以收到有關其他人的推文的通知,但內容的呈現應該幾乎是即時的。
  • 可擴充套件- 平均每天在 Twitter 上每秒有 5k+ 條推文出現。在高峰時段,它很容易翻倍。這些只是推文,正如我們已經討論過的那樣,推特的讀寫比率非常高,也就是說,這些推文的讀取次數會更高。這是每秒大量的請求。

 
那麼我們如何設計一個系統來滿足我們所有的功能需求而不影響效能呢?在討論整體架構之前,讓我們將使用者分成不同的類別。這些類別中的每一個都將以稍微不同的方式處理。
  1. 知名使用者:知名使用者通常是擁有大量粉絲的名人、運動員、政治家或商界領袖
  2. 活躍使用者:這些是在過去幾個小時或幾天內訪問過系統的使用者。在我們的討論中,我們會將過去三天訪問過 Twitter 的人視為活躍使用者。
  3. 實時使用者:這些是目前正在使用系統的活躍使用者的子集,類似於 Facebook 或 WhatsApp 上的線上使用者。
  4. 被動使用者:這些是擁有活動帳戶但在過去三天內未訪問系統的使用者。
  5. 非活動使用者:可以說這些是“已刪除”的帳戶。我們並沒有真正刪除任何帳戶,它更像是軟刪除,但就使用者而言,該帳戶不再存在。

現在,為簡單起見,讓我們將整個架構分為三個流程。我們將分別檢視系統的入職流程、推文流程以及搜尋和分析方面。
 

註冊服務
我們有一個使用者服務,它將在我們的系統中儲存所有與使用者相關的資訊,併為登入、註冊和任何其他需要使用者相關資訊的內部服務提供端點,提供GET APIs來獲取使用者的ID或電子郵件,POST APIs來新增或編輯使用者資訊,以及批次GET APIs來獲取多個使用者的資訊。這個使用者服務位於一個MySQL資料庫的使用者資料庫之上。我們在這裡使用MySQL,因為我們有有限數量的使用者,而且資料是非常有關係的。另外,使用者資料庫將主要用於寫,而與使用者細節相關的讀將由Redis快取提供,它是使用者資料庫的一個影像。當使用者服務收到一個帶有使用者ID的GET請求時,它將首先在Redis快取中查詢,如果該使用者存在於Redis中,它將返回響應。否則,它將從使用者資料庫中獲取資訊,儲存在Redis中,然後響應客戶端。
 

使用者關注的流程
與 "關注 "相關的請求將由Graph Service提供服務,Graph Service建立了一個使用者在系統中的連線網路。Graph Service將提供API來新增關注連結,獲得被某個使用者ID關注的使用者或關注某個使用者ID的使用者。該Graph Service位於使用者Graph資料庫之上,該資料庫也是一個MySQL資料庫。同樣,關注連結不會經常變化,所以在Redis中快取這些資訊是有意義的。現在在關注流程中,我們可以快取兩個資訊--誰是某個使用者的關注者,以及誰是該使用者的關注者。與使用者服務類似,當Graph Service收到一個獲取請求時,它將首先在Redis中進行查詢。如果Redis有這些資訊,它就會對使用者作出回應。否則,它將從圖資料庫中獲取資訊,並將其儲存在Redis中,然後回應給使用者。
現在,有一些事情我們可以根據使用者與Twitter的互動得出結論,比如他們的興趣等等。因此,當此類事件發生時,分析服務會將這些事件放在Kafka中。

現在,還記得我們的直播使用者嗎?假設U1是一個關注U2的實時使用者,U2發了一些推特。因為U1是直播,所以U1立即得到通知是合理的。這是透過使用者直播Websocket服務實現的。這個服務與所有的直播使用者保持著開放的連線,每當有直播使用者需要被通知的事件發生時,就會透過這個服務發生。現在,基於使用者與這個服務的互動,我們也可以跟蹤使用者線上的時間,當互動停止時,我們可以得出結論,使用者不再是活的了。當使用者下線時,透過websocket服務,一個事件將被髮射到Kafka,Kafka將進一步與使用者服務互動,並在Redis中儲存使用者最後的活動時間,其他系統可以使用這些資訊來相應地修改他們的行為。
 

Tweet流程
現在,一條推特可以包含文字、圖片、影片或連結。我們有一個叫做資產服務的東西,它負責上傳和顯示一條推文中的所有多媒體內容。我們已經在Netflix的設計文章中討論了資產服務的細節,如果你有興趣,可以去看看。

現在,我們知道推特有140個字元的限制,可以包括文字和連結。由於這個限制,我們不能在我們的推文中釋出巨大的URL。這就是URL縮短器服務的作用。我們不打算討論這項服務的工作細節,但我們已經在我們的Tiny URL文章中討論過了,所以請務必檢視。現在我們已經處理了連結,剩下的就是儲存推文的文字,並在需要時獲取它。這就是推文攝取服務的作用。當使用者試圖釋出一條推文並點選提交按鈕時,它會呼叫推文攝取服務,將推文儲存在一個永久的資料儲存中。我們在這裡使用Cassandra,因為我們每天都會有大量的推文進來,而我們在這裡需要的查詢模式正是Cassandra最擅長的。要了解更多關於我們為什麼使用資料庫解決方案的資訊,請檢視我們關於選擇最佳儲存解決方案的文章。
現在,推文攝取服務,顧名思義,只負責釋出推文,不提供任何GET API來獲取推文。一旦有推文被髮布,推文攝取服務就會向Kafka傳送一個事件,說某某使用者釋出了一條推文ID。現在,在我們的Cassandra上面,有一個Tweet服務,它將提供API,透過tweet ID或使用者ID獲取tweet。

現在,讓我們快速看一下使用者方面的情況。在讀取流程中,一個使用者可以有一個使用者時間線,即來自該使用者的推文,或者一個主頁時間線,即來自使用者所關注的人的推文。現在,一個使用者可能有一個巨大的他們所關注的使用者列表,如果我們在顯示時間線之前在執行時進行所有的查詢,就會降低渲染速度。因此,我們對使用者的時間線進行快取。我們將預先計算活躍使用者的時間線,並將其快取在Redis中,所以活躍使用者可以立即看到他們的時間線。這可以透過一個叫做Tweet處理器的東西來實現。

如前所述,當一條tweet被髮布時,一個事件被觸發到Kafka。Kafka將該事件傳達給推文處理器,併為所有需要被通知到這條推文的使用者建立時間線並進行快取。為了找出需要被通知這一變化的關注者,推特服務與Graph Service進行互動。假設使用者U1,其次是使用者U2、U3和U4釋出了一條tweet T1,那麼tweet處理器將用tweet T1更新U2、U3和U4的時間線,並更新cache。

現在,我們只快取了活躍使用者的時間線。當一個被動的使用者,比如說P1,登入到系統時會發生什麼?這就是時間線服務的作用。請求將到達時間線服務,時間線服務將與使用者服務互動,以識別P1是一個主動使用者還是一個被動使用者。現在,由於P1是一個被動使用者,它的時間線沒有被快取在Redis中。現在,時間線服務將與Graph Service對話,以找到P1所關注的使用者列表,然後查詢推特服務以獲取所有這些使用者的推文,將它們快取在Redis中,並回復給客戶端。

現在我們已經看到了主動和被動使用者的行為。我們將如何為我們的實時使用者最佳化流程?正如我們之前所討論的,當一條tweet被成功釋出時,一個事件將被髮送到Kafka。然後,Kafka將與推特處理器對話,後者為活躍使用者建立時間線並將其儲存在Redis中。但在這裡,如果推文處理器發現需要更新的使用者之一是一個實時使用者,那麼它將向Kafka傳送一個事件,而Kafka現在將與我們之前簡單討論過的實時websocket服務進行互動。這個websocket服務現在將嚮應用程式傳送一個通知,並更新時間線。

因此,現在我們的系統可以成功地釋出不同型別的推文,並有一些內建的最佳化,以某種不同的方式處理主動、被動和直播使用者。但它仍然是一個相當低效的系統。為什麼呢?因為我們完全忘記了我們的著名使用者 如果唐納德-川普有7500萬粉絲,那麼每次川普推特上的東西,我們的系統都需要進行7500萬次更新。而這僅僅是一個使用者的一條推特。所以這個流程對我們的著名使用者來說是行不通的。

Redis快取只會在預先計算的時間線中快取非著名使用者的推文。時間線服務知道Redis只儲存正常使用者的推文。它與Graph Service互動,以獲得我們當前使用者(例如U1)所關注的著名使用者列表,並從推文服務中獲取他們的推文。然後,它將在Redis中更新這些推文,並新增一個時間戳,表明時間線的最後更新時間。當U1提出下一個請求時,它會檢查Redis中針對U1的時間戳是否是幾分鐘前的。如果是的話,它將再次查詢tweet服務。但如果時間戳是最近的,Redis將直接回復給應用程式。

現在我們已經處理了主動、被動、活生生的和著名的使用者。至於不活躍的使用者,他們已經是停用的賬戶,所以我們不需要擔心他們。

現在,如果一個著名的使用者關注另一個著名的使用者,比方說唐納德-川普和埃隆-馬斯克,會發生什麼?如果唐納德-川普發推文,埃隆-馬斯克應該立即得到通知,即使其他非著名使用者沒有被通知。這又是由推特處理器處理的。鳴叫處理器,當它從Kafa收到一個關於著名使用者的新鳴叫的事件時,比方說,唐納德-川普,更新關注川普的著名使用者的快取。

現在,這看起來是一個相當有效的系統,但也有一些瓶頸。比如Cassandra--它將承受巨大的負載,Redis--它需要有效地擴充套件,因為它完全儲存在RAM中,還有Kafka--它又將收到瘋狂的事件量。因此,我們需要確保這些元件是可以橫向擴充套件的,對於Redis來說,不要儲存舊的資料,這只是不必要地佔用了記憶體。
 

現在來看看搜尋和分析!

還記得我們在上一節討論的推文攝取服務嗎?當一條tweet被新增到系統中時,它會向Kafka發射一個事件。一個監聽Kafka的搜尋消費者將所有這些傳入的推文儲存到Elasticsearch資料庫中。現在,當使用者在搜尋UI中搜尋一個字串時,它與搜尋服務對話。搜尋服務將與彈性搜尋對話,獲取結果,並回復給使用者。

現在,假設一個事件發生了,人們在Twitter上發推特或搜尋它,那麼可以肯定的是,更多的人會去搜尋它。現在我們不應該在elasticsearch上一次又一次地查詢同樣的東西。一旦搜尋服務從elasticsearch獲得一些結果,它將把它們儲存在Redis中,生存時間為2-3分鐘。現在,當使用者搜尋某些東西時,搜尋服務將首先在Redis中查詢。如果在Redis中找到了資料,它將回饋給使用者,否則,搜尋服務將查詢elasticsearch,獲得資料,將其儲存在Redis中,並回饋給使用者。這大大降低了elasticsearch的負載。

讓我們再次回到我們的Kafka。將有一個連線到Kafka的spark流消費者,它將跟蹤趨勢關鍵詞,並將它們傳達給Trends服務。這可以進一步連線到一個趨勢UI,使這些資料視覺化。我們不需要為這些資訊建立永久的資料儲存,因為趨勢是暫時的,但我們可以使用Redis作為短期儲存的快取。

現在你一定注意到我們的設計中大量使用了Redis。現在,儘管Redis是一個記憶體解決方案,但仍有一個選項可以將資料儲存到磁碟。因此,在發生故障的情況下,如果一些機器壞了,你仍然有資料儲存在磁碟上,以使它有更多的容錯性。

現在,除了趨勢之外,還有一些其他的分析可以進行,比如印度人在談論什麼。為此,我們將把所有傳入的推文傾倒在一個Hadoop叢集中,這可以為查詢提供動力,如被轉發最多的帖子等。我們還可以在Hadoop叢集上執行一個每週的cron作業,它將拉入我們的被動使用者的資訊,並向他們傳送每週的通訊,包括一些他們可能感興趣的最新推文。這可以透過執行一些簡單的ML演算法來實現,這些演算法可以根據使用者以前的搜尋和閱讀來判斷推文的相關性。新聞簡報可以透過一個通知服務來傳送,該服務可以與使用者服務對話,以獲取使用者的電子郵件ID。
 

相關文章