IM服務器:開發一個高並發的IM服務器難在哪


IM服務器要實現的最基本功能就是消息的轉發。——好像是一句廢話!

這就意味着IM服務器要為每個登錄用戶創建一個與該用戶信息相關的內存上下文,為方便描述我們在這里稱之為:user_context。user_context中一般包含這些基本信息:用戶id、昵稱、peer端的ip和端口,以及最重要的用於通信的socket。

用戶連接上線時,需要malloc一個user_context塊,用於存儲上述信息,用戶斷開連接時,需要free這個user_context塊。

IM服務器要隨時維護這張user_context列表,這張表我們在這里稱之為:list_user_context。這張表非常重要,im服務器要根據這張表進行消息的轉發。如果100個用戶登錄,list_user_context表中就有100個元素,10萬個用戶就有10萬個元素,用戶間聊天時,IM服務器就需要反復查詢list_user_context,從而確定轉發的消息要發送到哪個用戶的機器上。

舉個例子:用戶A發消息給用戶B,基本流程如下:
1、A將消息發送給IM服務器;
2、IM服務器解析消息,獲取該消息的接收人為B;
3、IM服務器查詢list_user_context表,找到B的user_context(里面有B的連接通道socket);
4、IM服務器將A的消息轉發給B;

正常流程都沒有問題,我們說下特殊的情況(注意不是異常情況):

【特殊情況一】

A在發送消息給B時,B突然退出客戶端程序。
此時IM服務器接收到2個來自IO層的事件:
事件1:A發給B的聊天數據
事件2:B的掉線通知

這兩個事件會觸發IM服務器進行如下兩個操作,
操作1:查詢list_user_context表,找到B的user_context(一個指向該內存的指針),並准備轉發A的消息。
操作2:查詢list_user_context表,找到B的user_context,從表中移除並准備釋放指向該內存的上下文。

這兩個操作可能是在不同的線程中執行,實際上在IOCP這種完全異步的模型下,這種可能的幾率非常大。這時候B的user_context所在的內存區就是“臨界區”,操作不當就會導致訪問“野指針”,從而導致整個IM服務器掛掉。當然你可以給list_user_context表加把鎖,加鎖可以減少訪問野指針的幾率但還是無法完全避免這種情況的發生。

如果IM服務器先執行釋放操作,也就是“操作2”,則是安全的,當“操作1”執行時,由於查找不到B的user_context,就會認為B已離線,並放棄發送操作。但如果“操作1”先執行,IM服務器首先獲得了指向B的user_context指針,剛准備發送數據時,CPU的時間片切換到了“操作2”上,並把B的user_context釋放,之后,CPU時間片又切換到“操作1”上,此時im server會訪問之前 查到的B的user_context內存區,這時訪問異常,服務器程序崩潰。這種幾率看似很小,但在高並發且聊天繁忙時,還是會發生。注意這種情況不是異常情況,而是在真實的業務場景中會實實在在並且經常發生的情況。

當然,你可以將鎖的范圍擴大,也就是從“臨界區”數據訪問擴大到操作層面上,也就是將整個發送操作和釋放操作進行加鎖,從而確保CPU在時間片切換時仍能保證讀、寫、刪除等操作的原子性。這種方式雖然安全了,但顯然會讓你的服務器從底層IOCP的完全異步,退化為一個業務層面上的完全同步。
如果1萬人同時聊天的話,其結果將是災難性的。如果是群聊的話,就會更加復雜,如果A所在的群有100人,這就意味着IM服務器要將消息轉發給群中的其他99人。這99人可能會在此時發生各種情況,比如某些人突然退出或者突然退群。

【特殊情況二】

先說一下IM服務器和WEB服務器在設計上的最大不同。理解這一點,就能體會到IM服務器設計上的復雜性。 WEB服務器,也就是基於HTTP協議的服務器,其業務可以抽象為:請求應答式服務, 即客戶發送請求,服務器響應請求,一問一答。即使是用POST命令上傳文件也是基於請求應答式,只不過發送請求的數據特別長而已。服務器在沒有收到請求時,不會主動發送數據給客戶端,這點非常重要,也就是說同一時間要么只有一個讀操作,要么只有一個寫操作。

“請求應答式”業務,如果放在IO層看就是讀寫同步,服務器從IO中讀完請求后開始向IO中寫響應。實際上大部分應用協議都是基於請求應答式,比如:Telnet、FTP、POP3、SMTP。。。,這種方式在業務層面處理起來比較簡單。

另一種業務模式,就是:“非請求應答式”,比如IM,讀寫之間沒有聯系,讀寫操作可能同時存在。

A在給B發送消息的時候,可能會同時收到B發來的消息,甚至還有其它人的消息,這時A要時刻保持“讀”監聽狀態,同時也會進行“寫”操作。對於IM服務器來說,既要保持對A的“讀”監聽(用於接收A發來的消息),也可能要對A進行“寫”操作 (轉發其他人發給A的消息)。

假設A和100人同時進行聊天,就意味着IM服務器可能要不停的對A的IO進行“寫”操作。即使A不發送消息,IM服務器也要保持對A的“讀”監聽。如果此時,A退出聊天客戶端程序,而此時尚有98條消息正在准備發送A。那些存在於內存中的98條消息,該如何釋放?
服務器捕獲到A離線,開始准備釋放A的user_context,此時服務器正在向A轉發群聊中來自不同用戶的消息給A(上述98條消息),這時一旦處理不當就會導致內存訪問異常,從而造成服務器崩潰。
當然這種情況下,你也可以通過加鎖來解決,但遇到的問題和上述 【特殊情況一】 一樣,你可能要鎖的不是一個數據臨界區,而是一個完整的操作,從而確保操作的原子性,避免內存訪問異常。但過多的加鎖會導致IM服務器性能大打折扣。

如果是IM服務器集群則會更加復雜,不同的用戶會登錄在不同的IM服務器上,A可能在服務器1上,B可能在服務器2上。A給B轉發消息時,可能B已從服務器2上離線。如果支持群聊的話,就更加復雜了。

舉一個極端的例子:
假設存在一個100台IM服務器的集群,你有99個好友,你和他們分別登錄在上述100台服務器上,也就是說大家彼此登錄在不同的IM服務器上。此時你要和每一位好友聊天就需要知道每位好友當前登錄在哪台IM服務器上,你給每個好友發送的消息都需要進行服務器間的轉發。如果你和他們分別建立N個群組的話,則每台IM服務器都要知道每個人所在的群組,從而進行群組消息的轉發。

總結一下,IM服務器的業務復雜就在於:用戶間會頻繁的進行交互。回到文章開頭的那句廢話:IM服務器要實現的最基本功能就是消息的轉發。正是由於消息的轉發,才會導致臨界區的存在,因為某一時刻在線的用戶,可能在你給他發送消息時,已經下線。
IM服務器編寫的難度和復雜性就源於這句廢話。 因為你編寫不是一個簡單的demo,而是要處理和解決所有可能的意外和異常情況,從而讓你的服務器健壯、可靠和穩定。

最后多說幾句:

單機並發量越高,需要的集群機器就越少,成本就越低,整個系統的復雜度也會降低。假設需要開發一個支持千萬級在線聊天的IM服務器。
如果單機支持1萬,則需要1000台IM服務器,如果單機支持10萬,則只需要100台。顯性成本就是需要多購買900台服務器,如果1台服務器價格1萬,則要多付出900萬。隱性成本就是每台服務器每年的電費或機房托管費,假設每台每年成本為1千元,則每年要多付出90萬。此外,服務器集群越多,復雜度越高,開發成本越高,運維成本也就越高。所以要盡量采用好的IO模型開發服務器端,比如linux下的epoll,windows下的iocp,從而提升單機的並發量。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM