QQ是怎樣創造出來的?——解密好友系統的設計

本篇介紹筆者接觸的第一個後臺系統,從自身見聞出發,所以涉及的內容相對比較基礎,後臺大牛請自覺略過。面試

什麼是好友系統?redis

簡單的說,好友系統是維護用戶好友關係的系統。咱們最熟悉的好友系統案例當屬QQ,實際上QQ是一款即時通信工具,憑着好友系統沉澱了海量的好友關係鏈,從而鑄就了一個堅如盤石的商業帝國。好友系統的重要性可見一斑。數據庫

熟悉互聯網產品的人都知道,當產品有了必定的用戶量,每每會開發一個好友系統。其主要目的是增長用戶粘性(有了好友就會常來)或者增長社區活躍度(有了好友就會多交流)。後端

而個人後臺開發生涯就是從這樣一個系統開始的。緩存

那時候,好友系統對於咱們團隊大部分人來講,都是一個全新的事物,由於咱們大部分人都是應屆生。整個系統的架構天然不是咱們一羣黃毛小孩所能創造。當年的架構圖已經找不到了,可是憑着一點記憶和多年來的經驗積累,仍是能夠把當年的架構勾勒出來。服務器

 

如圖,好友系統的架構是常見的3層結構,包括接入層、邏輯層和數據層。網絡

咱們先從數據層講起。session

由於咱們對QQ太熟悉了,咱們能夠很容易地列出好友系統的數據主要包括用戶資料、好友關係鏈、消息(聊天消息和系統消息)、在線狀態等。架構

互聯網產品每每要面對海量的請求併發,傳統的關係型數據庫比較難知足讀寫需求。在存儲中,通常是讀多寫少的數據纔會使用MySQL等關係型數據庫,並且每每還須要增長緩存來保證性能;NoSQL(Not Only SQL)應該是目前的主流。併發

對於好友系統,用戶資料和好友關係鏈都使用了kv存儲,而消息使用公司自研的tlist(能夠用redis的list替代),在線狀態下面再介紹。

接着是邏輯層

在這個系統中複雜度最高的應該是消息服務(而這個服務我並無參與開發[捂臉])。

消息服務中,消息按類型分爲聊天消息和系統消息(系統消息包括加好友消息、全局tips推送等),按狀態分爲在線消息和離線消息。在實現中,維護3種list:聊天消息、系統消息和離線消息。聊天消息是兩個用戶共享的,系統消息和離線消息每一個用戶獨佔。當用戶在線時,聊天消息和系統消息是直接發送的;若是用戶離線,就把消息往離線消息list存入一份,等用戶再次登陸時拉取。

這樣看來,消息服務並不複雜?其實否則,系統設計中常規的流程設計每每是比較簡單的,可是對於互聯網產品,異常狀況纔是常態,當把各類異常狀況都考慮進來時,系統就會很是複雜。

這個例子中,消息發送丟包是一種異常狀況,怎麼保證在丟包狀況下,還能正常運行就是一個不小的問題。

常見的解決方法是收包方回覆確認包,發送方若是沒收到確認包就重發。可是確認包又可能丟包,那又能夠給確認包增長一個確認包,這是一個永無止境的確認。

解決方法能夠參考TCP的重傳機制。那問題來了,咱們爲何不用TCP呢?由於TCP仍是比較慢的,聊天消息的可靠性沒有交易數據要求那麼高,丟幾條消息並不會形成嚴重後果,可是若是用戶每次發送消息後都要等好久才能被收到,那體驗是不好的。

一個比較折中的方案是,收包方回覆確認包,若是發送方在必定時間內沒有收到確認就重發;若是收包方收到兩個相同的包(自定義seq同樣),去重便可。

一個面試題引起的討論:

面試時我經常會問候選人一個問題:在分佈式系統中怎樣實現一個用戶同時只能有一個終端在線(用戶在兩個地方前後登陸帳號,後一次登陸能夠把前一次登陸踢下線)?這是互聯網產品中很是基礎的一個功能,考察的是候選人基本的架構設計能力。

設計要先從接入服務器(下稱接口機)提及。接口機是好友系統對外的窗口,主要功能是維護用戶鏈接、登陸鑑權、加解密數據和向後端服務透傳數據等。用戶鏈接好友系統,首先是鏈接到接口機,鑑權成功後,接口機會在內存中維護用戶session,後續的操做都是基於session進行。

如圖所示,用戶若是嘗試登陸兩次,接口機經過session就能夠將第一次的登陸踢下線,從而保證只有一個終端在線。

問題解決了嗎?

沒有。由於實際系統確定不會只有一臺接口機,在多臺接口的狀況下,上面的方法就不可行了。由於每一個接口機只能維護部分用戶的session,因此若是用戶前後鏈接到不一樣的接口機,就會形成用戶多處登陸的問題。

 

天然能夠想到,解決的方法就是要維護一個用戶狀態的全局視圖。在咱們的好友系統中,稱爲在線狀態服務。

在線狀態服務,顧名思義就是維護用戶的在線狀態(登陸時間、接口機IP等)的服務。用戶登陸和退出會經過接口機觸發這裏的狀態變動。由於登陸包和退出包均可能丟包,因此心跳包也用做在線狀態維護(收到一次心跳標記爲在線,收不到n次心跳標記爲離線)。

一種經常使用的方法是,採用bitmap存儲在線狀態,具體是指在內存中分配一塊空間,32位機器上的天然數一共有4294967296個,若是用一個bit來表示一個用戶ID(例如QQ號),1表明在線,0表明離線,那麼把所有天然數存儲在內存只要4294967296 / (8 * 1024 * 1024) = 512MB(8bit = 1Byte)。固然,實現中也能夠根據須要給每一個用戶分配更多的bit。

因而,踢下線功能如圖所示。

 

用戶登陸的時候,接口機首先查找本機上是否有session,若是有則更新session,接着給在線狀態服務發送登陸包,在線狀態服務檢查用戶是否已經在線,若是在線則更新狀態信息,並向上次登陸的接口機IP發送踢下線包;接口機在收到踢下線包時會檢查包中的用戶ID是否存在session,若是存在則給客戶端發送踢下線包並刪除session。

在實際中,踢下線功能還有不少細節問題須要注意。

又回到用戶前後登陸同一臺接口機的狀況:

 

圖中踢下線流程是正確的,可是若是步驟10和13調換了順序(在UDP傳輸中是常見的)會發生什麼?你們能夠本身推演一下,後到的踢下線包會把第二次登陸的A’踢下線了。這不是咱們指望的。怎麼辦呢?

解決方法分幾個細節,①接口機在收到13號登陸成功包時,先將session A替換成session A’,而後給客戶端A發生踢下線包(避免多處存活致使互相踢下線);②踢下線包中必須包含除用戶ID外的其餘標識信息,session的惟一標識應該是ID+XXX的形式(我最開始採用的是ID+LoginTime),XXX是爲了區分某次的登陸;③接口機在收到踢下線包的時候只要判斷ID+XXX是否吻合來決定是否給客戶端發踢下線包。

現實狀況,問題老是千奇百怪的,好在辦法總比問題多。

好比我在項目中遇到過接口機和在線狀態服務時間漂移(差幾秒)的狀況。這樣踢下線的惟一標識就不能是用戶ID+LoginTime的形式了。能夠爲每次的登陸生成一個惟一的UUID解決。相似的問題還有不少,再也不贅述。

總結一下,本篇主要介紹了好友系統的總體架構和部分模塊的實現方式。分佈式系統中各個模塊的實現其實並不難,難點主要在於應對複雜網絡環境帶來的問題(如丟包、時延等)和服務器異常帶來的問題(如爲了應對服務器宕機會增長服務器冗餘度,進而又會引起其它問題)。

好友系統雖然簡單,但麻雀雖小五臟俱全,架構設計的各類技術基本都有涉及。例如分層結構、負載均衡、平行擴展、容災、服務發現、服務器開發框架等方面,後面我會在各個不一樣的項目中介紹這些技術,敬請期待。

相關文章
相關標籤/搜索