對于一個即時通信服務器來說,在用戶量少的時候,一臺服務器就足以提供所有的服務。而這種架構也最簡單,舉個例子,用戶A與用戶B互為好友,A向B發消息,服務器接收到消息時,解析出接收消息的人,直接轉發給B即可。可是當用戶數量越來越多時,一臺服務器已經無法所有用戶的需求,這時就要進行服務擴容,進行分布式部署
如圖所示,不同的用戶可能登錄到不同的服務器上,那么用戶A給用戶B發消息時,服務器收到消息,首先判斷B是否也登錄在本服務器上,如果是,那么直接轉發消息即可。如果B不在本服務器上,那應該往哪里轉發這條消息呢?最簡單的做法就是向服務器集群中的其他服務器廣播這條消息,對于每個收到這條消息的服務器,首先判斷消息的目的用戶是否登錄在自己身上,如果不是,直接忽略該消息。如果是,那么向目的用戶轉發該消息。固然,這種暴力粗獷的做法是最簡單直接的,但是會產生很多無效的消息轉發,對于服務器性能產生很大的影響。曾看過蘑菇街開源的即時通信軟件Teamtalk的代碼,服務器就是這種實現方式。其服務器架構如下:
不同的msg服務器連接到同一臺route server上,所有msg服務器之間的轉發全部通過route server。這無疑會加重route server的負載。即時msg server部署的再多,根據木桶理論,一個系統的性能是由其最薄弱的環節所決定的。所以也注定這樣的架構,其系統容量也是有限的。那么如何改善這種系統呢,很明顯服務器之間的消息轉發不能直接全部廣播,而應該有一套明確的路由系統,即服務器在轉發消息時,應該知道這條消息應該轉發到哪一臺服務器,這樣就不需要每條消息都在所有服務器之間廣播了。
那么如何實現這樣一套路由系統呢?
簡單的做法是,每個用戶上線時,通過其連接的msg server向其他所有msg server廣播自己的登錄信息,告知其他服務器自己登錄在哪臺服務器上面。這樣當某個用戶向其好友發消息時,首先通過好友id查看其登錄的msg server。如果好友與自己是同一臺服務器,那么直接轉發即可;如果不是,服務器向route server發送轉發該消息,并且帶上目標msg server的id.這樣route server 收到消息后,解析出目標的msg server,進行一次轉發即可,省去了大量的廣播消息。這種方式雖然解決了廣播消息的問題,但是在每臺msg server上都要保存所有用戶的路由信息。當所有用戶都登錄時,幾乎就退化成了單點模型,msg server肯定承受不了。
那么如何解決這個問題呢?試想一下,既然所有的msg server上都保存著同樣的路由信息,那么我們可以把這些數據從msg server剝離出來,存在一個單獨的服務器上,供msg server查詢。我們暫且把這個服務器叫做route info server(路由信息服務器).對于一個用戶要存儲的數據為
{
userid,
msgserverid
}
假設這兩個數據都是32Byte,那么存儲一億個用戶需要的內存32B*10^8=3.2G。目前好點的服務器都有50G的內存,很顯然內存不是問題。那么就剩訪問量的問題。如果所有的msg server都從這一臺服務器上讀取數據, 肯定會影響整個系統的性能。所以路由信息服務器不能采用這種單點模型。考慮到這種路由信息的特點,很明顯是一種讀多寫少的數據。一個用戶只有在登錄的時候才會寫一次路由信息,其他時候就是轉發消息的時候讀取路由信息了。那么可以采用類似數據庫的主備模型,主服務器用來寫路由信息,備服務器用于查詢路由信息。而且可以設置多臺備服務器,分擔msg server的讀壓力。其實我們也可以使用一些成熟的緩存系統來完成路由信息服務器的功能,比如redis. redis擁有現成的主備方案,只是像這種通用的緩存服務器在存儲數據時,消耗的內存會大些。研究過redis源碼的,大多都能理解。其key value都存儲在redisobject的結構體當中,有一些附加的信息,所以比自己寫一個這樣的服務所消耗的內存肯定會大些。
OK,說完了路由信息服務器,我們再回到msg server上來。那么msg server在轉發消息時,首先根據目的用戶的id 到路由信息服務器上查找其所在的msg server用于消息轉發。但是這也會存在一個,每次轉發消息時,都要查詢一次路由信息,這無疑會影響消息的轉發速度,而且也會增大路由信息服務器的訪問壓力。如果在發送消息之后,將路由信息保存到本地,那么下次發送消息,就無需再去路由信息服務器重復查詢了。但是也不能把所有的路由全部保存到本地,那樣又會嚴重消耗msg server的內存。于是,就有我們想到一種折中的方案,使用一個lru的緩存隊列,在需要保存新的路由信息時,首先查看緩存隊列是否已滿,如果未滿,直接插入到隊首,如果隊列已滿,淘汰到隊尾的數據。緩存列隊大小可根據內存大小靈活設置。考慮到在我們平時在使用qq時,大部分人都登錄著,但是發消息的人并不多。對于路由信息,在其首次轉發消息是,從路由信息服務器查詢一次路由,在其整個回話過程中,路由信息都緩存在本地。在其會話結束后,將最近最久未使用的路由數據淘汰出去,這種做法再考慮到內存使用的同時,又大大減少了服務器的訪問次數,算是一種較好的折中方案. 在完成了路由信息系統之后,route server也可以進行水平擴展,route server要做的僅僅是轉發消息,并不需要存儲數據,擴展起來非常方便。最終的系統架構如下:
總結:
1. 本文所描述的即時通信服務器架構,著重討論的是消息如何路由的問題,但這并不代表一個完整的即時通信服務器系統,諸如注冊,登錄,離線消息,文件等功能這些都未在本文的討論范圍之類
2. 本文中所提的方案也是一種設想,并未真正進行實現,肯定也有很多細節問題沒有考慮到。歡迎大家留言討論
3. 對于本文所提的系統,可稱之為一個服務集群。而像qq這樣數量用戶的系統,在全國分布了很多個集群。本文所討論的也僅僅局限于一個集群內的通信設計,而集群之間的通信又如何通信呢。每個集群的路由數據,如果全同步到其他集群,這種做法顯然不是最優。如果有更好的想法,也歡迎留言討論
文章列表