posted @ 2021-04-06 22:05 Jack Jiang 閱讀(230) | 評論 (0) | 編輯 收藏
posted @ 2021-03-29 22:36 Jack Jiang 閱讀(261) | 評論 (0) | 編輯 收藏
本文內容和編寫思路是基于鄧昀澤的“大規模并發IM服務架構設計”、“IM的弱網場景優化”兩文的提綱進行的,感謝鄧昀澤的無私分享。
1、引言
接上篇《一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等》,本文主要聚焦這套億級用戶的IM架構的一些比較細節但很重要的熱門問題上,比如:消息可靠性、消息有序性、數據安全性、移動端弱網問題等。
以上這些熱門IM問題每個話題其實都可以單獨成文,但限于文章篇幅,本文不會逐個問題詳細深入地探討,主要以拋磚引玉的方式引導閱讀者理解問題的關鍵,并針對問題提供專項研究文章鏈接,方便有選擇性的深入學習。希望本文能給你的IM開發帶來一些益處。

本文已同步發布于“即時通訊技術圈”公眾號,歡迎關注。公眾號上的鏈接是:點此進入。
2、系列文章
為了更好以進行內容呈現,本文拆分兩了上下兩篇。
本文是2篇文章中的第2篇:
本篇主要聚焦這套億級用戶的IM架構的一些比較細節但很重要的熱門問題上。
3、消息可靠性問題
消息的可靠性是IM系統的典型技術指標,對于用戶來說,消息能不能被可靠送達(不丟消息),是使用這套IM的信任前提。
換句話說,如果這套IM系統不能保證不丟消息,那相當于發送的每一條消息都有被丟失的概率,對于用戶而言,一定會不會“放心”地使用它,即“不信任”這套IM。
從產品經理的角度來說,有這樣的技術障礙存在,再怎么費力的推廣,最終用戶都會很快流失。所以一套IM如果不能保證消息的可靠性,那問題是很嚴重的。
PS:如果你對IM消息可靠性的問題還沒有一個直觀的映象的話,通過《零基礎IM開發入門(三):什么是IM系統的可靠性?》這篇文章可以通俗易懂的理解它。

如上圖所示,消息可靠性主要依賴2個邏輯來保障:
- 1)上行消息可靠性;
- 2)下行消息可靠性。
1)針對上行消息的可靠性,可以這樣的思路來處理:
用戶發送一個消息(假設協議叫PIMSendReq),用戶要給這個消息設定一個本地ID,然后等待服務器操作完成給發送者一個PIMSendAck(本地ID一致),告訴用戶發送成功了。
如果等待一段時間,沒收到這個ACK,說明用戶發送不成功,客戶端SDK要做重試操作。
2)針對下行消息的可靠性,可以這樣的思路來處理:
服務收到了用戶A的消息,要把這個消息推送給B、C、D 3個人。假設B臨時掉線了,那么在線推送很可能會失敗。
因此確保下行可靠性的核心是:在做推送前要把這個推送請求緩存起來。
這個緩存由存儲系統來保證,MsgWriter要維護一個(離線消息列表),用戶的一條消息,要同時寫入B、C、D的離線消息列表,B、C、D收到這個消息以后,要給存儲系統一個ACK,然后存儲系統把消息ID從離線消息列表里拿掉。
針對消息的可靠性問題,具體的解決思路還可以從另一個維度來考慮:即實時消息的可靠性和離線消息的可靠性。
有興趣可以深入讀一讀這兩篇:
而對于離線消息的可靠性來說,單聊和群聊又有很大區別,有關群聊的離線消息可靠投遞問題,可以深入讀一讀《IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞》。
4、消息有序性問題
消息的有序性問題是分布式IM系統中的另一個技術“硬骨頭”。
因為是分布式系統,客戶端和服務器的時鐘可能是不同步的。如果簡單依賴某一方的時鐘,就會出現大量的消息亂序。
比如只依賴客戶端的時鐘,A比B時間晚30分鐘。所有A給B發消息,然后B給A回復。
發送順序是:
客戶端A:“XXX”
客戶端B:“YYY”
接收方的排序就會變成:
客戶端B:“YYY”
客戶端A:“XXX”
因為A的時間晚30分鐘,所有A的消息都會排在后面。
如果只依賴服務器的時鐘,也會出現類似的問題,因為2個服務器時間可能也不一致。雖然客戶端A和客戶端B時鐘一致,但是A的消息由服務器S1處理,B的消息由服務器S2處理,也會導致同樣消息亂序。
為了解決這種問題,我的思路是通過可以做這樣一系列的操作來實現。
1)服務器時間對齊:
這部分就是后端運維的鍋了,由系統管理員來盡量保障,沒有別的招兒。
2)客戶端通過時間調校對齊服務器時間:
比如:客戶端登錄以后,拿客戶端時間和服務器時間做差值計算,發送消息的時候考慮這部分差值。
在我的im架構里,這個能把時間對齊到100ms這個級,差值再小的話就很困難了,因為協議在客戶端和服務器之間傳遞速度RTT也是不穩定的(網絡傳輸存在不可控的延遲風險嘛)。
3)消息同時帶上本地時間和服務器時間:
具體可以這樣的處理:排序的時候,對于同一個人的消息,按照消息本地時間來排;對于不同人的消息,按照服務器時間來排,這是插值排序算法。
PS:關于消息有序性的問題,顯然也不是上面這三兩句話能講的清楚,如果你想更通俗一理解它,可以讀一讀《零基礎IM開發入門(四):什么是IM系統的消息時序一致性?》。
另外:從技術實踐可行性的角度來說,《一個低成本確保IM消息時序的方法探討》、《如何保證IM實時消息的“時序性”與“一致性”?》這兩篇中的思路可以借鑒一下。
實際上,消息的排序問題,還可以從消息ID的角度去處理(也就是通過算法讓消息ID產生順序性,從而根據消息ID就能達到消息排序的目的)。
有關順序的消息ID算法問題,這兩篇非常值得借鑒:《IM消息ID技術專題(一):微信的海量IM聊天消息序列號生成實踐(算法原理篇)》、《IM消息ID技術專題(三):解密融云IM產品的聊天消息ID生成策略》,我就不廢話了。
5、消息已讀同步問題
消息的已讀未讀功能,如下圖所示:

上圖就是釘釘中的已讀未讀消息。這在企業IM場景下非常有用(因為做領導的都喜歡,你懂的)。
已讀未讀功能,對于一對一的單聊消息來說,還比較好理解:就是多加一條對應的回執息(當用戶閱讀這條消息時發回)。
但對于群聊這說,這一條消息有多少人已讀、多少人未讀,想實現這個效果,那就真的有點麻煩了。對于群聊的已讀未讀功能實現邏輯,這里就不展開了,有興趣可以讀一下這篇《IM群聊消息的已讀回執功能該怎么實現?》。
回歸到本節的主題“已讀同步”的問題,這顯示難度又進一級,因為已讀未讀回執不只是針對“賬號”,現在還要細分到“同一賬號在不同端登陸”的情況,對于已讀回執的同步邏輯來說,這就有點復雜化了。
在這里,根據我這邊IM架構的實踐經驗,提供一些思路。
具體來說就是:用戶可能有多個設備登錄同一個賬戶(比如:Web PC和移動端同時登陸),這種情況下的已讀未讀功能,就需要來實現已讀同步,否則在設備1看過的消息,設備2看到依然是未讀消息,從產品的角度來說,這就影響用戶體驗了。
對于我的im架構來說,已讀同步主要依賴2個邏輯來保證:
- 1)同步狀態維護,為用戶的每一個Session,維護一個時間戳,保存最后的讀消息時間;
- 2)如果用戶打開了某個Session,且用戶有多個設備在線,發送一條PIMSyncRead消息,通知其它設備。
6、數據安全問題
6.1 基礎
IM系統架構中的數據安全比一般系統要復雜一些,從通信的角度來說,它涉及到socket長連接通信的安全性和http短連接的兩重安全性。而隨著IM在移動端的流行,又要在安全性、性能、數據流量、用戶體驗這幾個維度上做權衡,所以想要實現一套完善的IM安全架構,要面臨的挑戰是很多的。
IM系統架構中,所謂的數據安全,主要是通信安全和內容安全。
6.2 通信安全
所謂的通信安全,這就要理解IM通信的服務組成。
目前來說,一個典型的im系統,主要由兩種通信服務組成:
- 1)socket長連接服務:技術上也就是多數人耳熟能詳的網絡通信這一塊,再細化一點也就是tcp、udp協議這一塊;
- 2)http短連接服務:也就是最常用的http rest接口那些。
對于提升長連接的安全性思路,可以深入閱讀《通俗易懂:一篇掌握即時通訊的消息傳輸安全原理》。另外,微信團隊分享的《微信新一代通信安全解決方案:基于TLS1.3的MMTLS詳解》一文,也非常有參考意義。
如果是通信安全級別更高的場景,可以參考《即時通訊安全篇(二):探討組合加密算法在IM中的應用》,文中關于組合加密算法的使用思路非常不錯。
至于短連接安全性,大家就很熟悉了,開啟https多數情況下就夠用了。如果對于https不甚了解,可以從這幾篇開始:《一文讀懂Https的安全性原理、數字證書、單項認證、雙項認證等》、《即時通訊安全篇(七):如果這樣來理解HTTPS,一篇就夠了》。
6.3 內容安全
這個可能不太好理解,上面既然實現了通信安全,那為什么還要糾結“內容安全”?
我們了解一下所謂的密碼學三大作用:加密( Encryption)、認證(Authentication),鑒定(Identification) 。
詳細來說就是:
加密:防止壞人獲取你的數據。
認證:防止壞人修改了你的數據而你卻并沒有發現。
鑒權:防止壞人假冒你的身份。
在上節中,惡意攻擊者如果在通信環節繞開或突破了“鑒權”、“認證”,那么依賴于“鑒權”、“認證”的“加密”,實際上也有可有被破解。
針對上述問題,那么我們需要對內容進行更加安全獨立的加密處理,就這是所謂的“端到端加密”(E2E)。
比如,那個號稱無法被破解的IM——Telegram,實際上就是使用了端到端加密技術。

關于端到端加密,這里就不深入探討,這里有兩篇文章有興趣地可以深入閱讀:
7、雪崩效應問題
在分布式的IM架構中,存在雪崩效應問題。
我們知道,分布式的IM架構中,為了高可用性,用戶每次登陸都是根據負載均衡算法分配到不同的服務器。那么問題就來了。
舉個例子:假設有5個機房,其中A機房故障,導致這個機房先前服務的用戶都跑去B機房。B機房不堪重負也崩潰了,A+B的用戶跑去機房C,連鎖反應會導致所有服務掛掉。
防止雪崩效應需要在服務器架構,客戶端鏈接策略上有一些配合的解決方案。服務器需要有限流能力作為基礎,主要是限制總服務用戶數和短時間鏈接用戶數。
在客戶端層面,發現服務斷開之后要有一個策略,防止大量用戶同一時間去鏈接某個服務器。
通常有2種方案:
- 1)退避:重連之間設置一個隨機的間隔;
- 2)LBS:跟服務器申請重連的新的服務器IP,然后由LBS服務去降低短時間分配到同一個服務器的用戶量。
這2種方案互不沖突,可以同時做。
8、弱網問題
8.1 弱網問題的原因
鑒于如今IM在移動端的流行,弱網是很常態的問題。電梯、火車上、開車、地鐵等等場景,都會遇到明顯的弱網問題。
那么為什么會出現弱網問題?
要回答這個問題,那就需要從無線通信的原理上去尋找答案。
因為無線通信的質量受制于很多方面的因素,比如:無線信號強弱變化快、信號干擾、通信基站分布不均、移動速度太快等等。要說清楚這個問題,那就真是三天三夜都講不完。
有興趣的讀者,一定要仔細閱讀下面這幾篇文章,類似的跨學科科譜式文章并不多見:
《IM開發者的零基礎通信技術入門(十一):為什么WiFi信號差?一文即懂!》
《IM開發者的零基礎通信技術入門(十二):上網卡頓?網絡掉線?一文即懂!》
弱網問題是移動端APP的必修課,下面這幾篇總結也值得借鑒:
《移動端IM開發者必讀(一):通俗易懂,理解移動網絡的“弱”和“慢”》
《移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結》
8.2 IM針對弱網問題的處理
對于IM來說,弱網問題并不是很復雜,核心是做好消息的重發、排序以及接收端的重試。
為了解決好弱網引發的IM問題,通常可以通過以下手段改善:
- 1)消息自動重發;
- 2)離線消息接收;
- 3)重發消息排序;
- 4)離線指令處理。
下面將逐一展開討論。
8.3 消息自動重發
坐地鐵的時候,經常遇到列車開起來以后,網絡斷開,發送消息失敗。
這時候產品有2種表現形式:
- a、直接告訴用戶發送失敗;
- b、保持發送狀態,自動重試3-5次(3分鐘)以后告訴用戶發送失敗。
顯然:自動重試失敗以后再告訴用戶發送失敗體驗要好很多。尤其是在網絡閃斷情況下,重試成功率很高,很可能用戶根本感知不到有發送失敗。
從技術上:客戶端IMSDK要把每條消息的狀態監控起來。發送消息不能簡單的調用一下網絡發送請求,而是要有一個狀態機,管理幾個狀態:初始狀態,發送中,發送失敗,發送超時。對于失敗和超時的狀態,要啟用重試機制。
這里還有一篇關于重試機制設計的討論帖子,有興趣可以看看:《完全自已開發的IM該如何設計“失敗重試”機制?》。
《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》一文中關于消息超時與重傳機制的實現思路,也可以參考一下。
8.4 離線消息接收
現代IM是沒有“在線”這個狀態的,不需要給用戶這個信息。但是從技術的層面,用戶掉線了還是要正確的去感知的。
感知方法有幾條:
- a、信令長連接狀態:如果長時間沒收到到服務器的心跳反饋,說明掉線了;
- b、網絡請求失敗次數:如果多次網絡請求失敗,說明”可能“掉線了;
- c、設備網絡狀態檢測:直接檢測網卡狀態就好,一般Android/iOS/Windows/Mac都有相應系統API。
正確檢測到網絡狀態以后,發現網絡從”斷開到恢復“的切換,要去主動拉取離線階段的消息,就可以做到弱網狀態不丟消息(從服務器的離線消息列表拉取)。
上面文字中提到的網絡狀態的確定,涉及到IM里網絡連接檢查和保活機制問題,是IM里比較頭疼的問題。
一不小心,又踩進了IM網絡保活這個坑,我就不在這里展開,有興趣一定要讀讀下面的文章:
《一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等》
8.5 重發消息排序
弱網邏輯的另一個坑是消息排序。
假如有A、B、C 3條消息,A、C發送成功,B發送的時候遇到了網絡閃斷,B觸發自動重試。
那么接收方的接收順序應該是 A B C還是A C B呢?我觀察過不同的IM產品,處理邏輯各不相同,這個大家有興趣可以去玩一下。
這個解決方法是要依賴上一篇服務架構里提到的差值排序,同一個人發出的消息,排序按消息附帶的本地時間來排。不同人的消息,按照服務器時間排序。
具體我這邊就不再得復,可以回頭看看本篇中的第四節“4、消息有序性問題”。
8.6 離線指令處理
部分指令操作的時候,網絡可能出現了問題,等網絡恢復以后,要自動同步給服務器。
舉一個例子,大家可以試試手機設置為飛行模式,然后在微信里刪除一個聯系人,看看能不能刪除。然后重新打開網絡,看看這個數據會不會同步到服務器。
類似的邏輯也適用于已讀同步等場景,離線狀態看過的信息,要正確的跟服務器同步。
8.7 小結一下
IM的弱網處理,其實相對還是比較簡單的,基本上自動重試+消息狀態就可以解決絕大部分的問題了。
一些細節處理也并不復雜,主要原因是IM的消息量比較小,網絡恢復后能快速的恢復操作。
視頻會議在弱網下的邏輯,就要復雜的多了。尤其是高丟包的弱網環境下,要盡力去保證音視頻的流暢性。
9、本文小結
《一套億級用戶的IM架構技術干貨》這期文章的上下兩篇就這么侃完了,上篇涉及到的IM架構問題倒還好,下篇一不小心又帶出了IM里的各種熱門問題“坑”,搞IM開發直是一言難盡。。。
建議IM開發的入門朋友們,如果想要系統地學習移動端IM開發的話,應該去讀一讀我整理的那篇IM開發“從入門到放棄”的文章(哈哈哈),就是這篇《新手入門一篇就夠:從零開發移動端IM》。具體我就不再展開了,不然這篇幅又要剎不住車了。。。
10、參考資料
[1] 大規模并發IM服務架構設計
[2] IM的弱網場景優化
[4] IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞
[5] IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞
[6] 即時通訊安全篇(二):探討組合加密算法在IM中的應用
[7] 微信新一代通信安全解決方案:基于TLS1.3的MMTLS詳解
附錄:更多IM開發文章匯總
《IM開發干貨分享:有贊移動端IM的組件化SDK架構設計實踐》
《IM開發寶典:史上最全,微信各種功能參數和邏輯規則資料匯總》
本文已同步發布于“即時通訊技術圈”公眾號。
▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3445-1-1.html
posted @ 2021-03-22 16:10 Jack Jiang 閱讀(206) | 評論 (0) | 編輯 收藏
1、引言
經歷過稍有些規模的IM系統開發的同行們都有體會,要想實現大規模并發IM(比如億級用戶和數十億日消息量這樣的規模),在架構設計上需要一些額外的考慮,尤其是要解決用戶高并發、服務高可用,架構和實現細節上都需要不短時間的打磨。
我在過往的工作經歷里,親手設計和實現了一套億級用戶量的IM,平臺上線并經過6年多的驗證,穩定性和可用性被驗證完全達到預期。
這套IM系統,從上線至今已6年有余,本人也已經離職創業近2年,但當初設計和開發這套系統時積累和收獲了大量的第一手實踐經驗和技術心得。
因此,想借本文把當時的架構設計經歷記錄下來,作為同行交流和參考,希望能提供一些啟發,少走彎路。

本文已同步發布于“即時通訊技術圈”公眾號,歡迎關注。公眾號上的鏈接是:點此進入。
2、系列文章
為了更好以進行內容呈現,本文拆分兩了上下兩篇。
本文是2篇文章中的第1篇:
《一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等》(本文)
《一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等(稍后發布...)》
本篇主要總結和分享這套IM架構的總體設計和服務拆分等。
3、原作者
本文基于鄧昀澤的“大規模并發IM服務架構設計”一文進行的擴展和修訂,感謝原作者的分享。

鄧昀澤:畢業于北京航空航天大學,現藍貓微會創始人兼CEO,曾就職于美團、YY語音、微軟和金山軟件等公司,有十多年研發管理經驗。
4、技術指標
在這套IM系統的架構上,技術上我們堅持高要求,經過數年的驗證,也確實達到了設計預期。
這4大技術指標是:

具體解釋就是:
- 1)高可靠:確保不丟消息;
- 2)高可用:任意機房或者服務器掛掉,不影響服務;
- 3)實時性:不管用戶在哪里,在線用戶消息在1秒內達到(我們實際是75%消息可以做到120ms);
- 4)有序性:確保用戶消息的有序性,不會出現發送和接受的亂序。
5、架構拆分
從整體架構上來說,億級用戶量的IM架構整體上偏復雜。
傳統開源的IM服務喜歡把所有服務做到1-2個服務里(Connector+Service模型),這樣帶來的問題比較嚴重。
傳統開源的IM的問題主要體現在:
- 1)服務代碼復雜,難以持續開發和運維;
- 2)單一業務邏輯出問題,可能會影響到其它邏輯,導致服務的全面不可用。
因此,我在做架構設計的時候盡量追求微服務化。即把整體架構進行分拆為子系統,然后子系統內按照業務邏輯分拆為微服務。
系統拆分如下圖:

4個子系統的職責是:
- 1)IM業務系統:服務IM相關的業務邏輯(比如好友關系、群關系、用戶信息等);
- 2)信令系統:負責用戶登錄,用戶在線狀態的維護,以及在線用戶的下行推送;
- 3)推送系統:負責消息的在線推送和離線推送;
- 4)存儲系統:負責消息和文件的存儲和查詢;
其中:信令系統和推送系統是基礎設施,不只是可以為IM業務服務,也可以承載其它類似的業務邏輯(比如客服系統)。
在部署層面:采用存儲3核心機房,信令和推送節點按需部署的方式(國內業務推薦8-10個點)。實際上我們只做了了北京3個機房,上海1個機房和香港一個機房的部署,就基本上滿足了大陸+香港的業務需求。
下面將逐個介紹這4個子系統的細節方面。
6、IM業務系統
一說到IM,很多人腦海里跳出的第一個關鍵就是“即時通信”,技術上理所當然的聯想到了socket,也就是大家成天嘴上說的:“長連接”。換句話說,很多對IM不了解或了解的不多的人,認為IM里的所有數據交互、業務往來都是通過“長連接”來實現的,這樣話,對于本文章中拆分出的“IM業務系統”就有點不理解了。
實際上,早期的IM(比如20年前的QQ、MSN、ICQ),確實所有數據基本都是通過“長連接”(也就是程序員所說的“socket”)實現。
但如今,移動端為主端的IM時代,IM系統再也不是一個條“長連接”走天下。
現在,一個典型的IM系統數據往來通常拆分成兩種服務:
- 1)socket長連接服務(也就是本文中的“推送服務”);
- 2)http短連接服務(就是最常用的http rest接口那些,也就是本文中的“IM業務系統”)。
通俗一點,也也就現在的IM系統,通常都是長、短連接配合一起實現的。

比如論壇里很多熱門技術方案都是這樣來做的,比如最典型的這兩篇:《IM單聊和群聊中的在線狀態同步應該用“推”還是“拉”?》、《IM消息送達保證機制實現(二):保證離線消息的可靠投遞》,文記里提到的“推”其實就是走的“長連接”、“拉”就上指的http短連接。
對于socket長連接服務就沒什么好說,就是大家最常理解的那樣。
IM業務系統詳細來說,就是專注處理IM相關的業務邏輯,比如:
- 1)維護用戶數據:用戶基本信息等;
- 2)維護好友關系:好友請求、好友列表、好友信息等;
- 3)維護群組信息:群創建、解散、成員管理等;
- 4)提供數據:離線拉取、歷史記錄同步;
- 5)其它邏輯:比如通過存儲和推送系統,存儲消息和發送通知;
按照微服務的原則,IM業務系統也被分拆為多個服務,比如:
- 1)GInfo服務:群組信息維護;
- 2)IM服務:處理1V1消息;
- 3)GIM服務:處理群組消息。
7、信令系統
7.1 基本情況
信令系統主要職責是3部分:

1)維護用戶在線狀態:
因為用戶規模龐大,必然是多個集群,每個集群多臺服務器為用戶提供服務。
考慮到服務器運維的復雜性,我們要假定任何一個集群,任何一個服務器都可能會掛掉,而且在這種情況下要能夠繼續為用戶提供服務。
在這種情況下,如果用戶A給用戶B發消息,我們需要知道用戶B在哪個服務器上,才能把消息正確推送給用戶B。用戶在哪個信令服務,這個信息就是在線狀態數據。
2)下行消息推送:
跟上一個職責有關,用戶在線的時候,如果有其它用戶給他發消息,那就最好不要走離線推送,而是走在線推送。
在線推送的最后一個環節,是把用戶消息推送給用戶設備,因為就需要知道用戶登錄到哪個服務器上。
3)業務分發:
信令服務不只可以處理IM請求,也可以處理其它類型的業務請求。為了處理不同的業務,就需要有分發能力。
具體做法是通過一個SVID(service id)來實現,不同的業務攜帶不同的SVID,信令服務就知道如何分發了。
用戶通過登錄服務把數據(比如IM消息)發送到信令系統,信令系統根據SVID轉發給IM系統。不管后臺有多少個業務,用戶只需要一條鏈接到信令。
7.2 服務拆分
信令系統為了實現以上這3個職責,同時要確保我們服務可平行擴展的能力和穩定性,在實際的技術實現上,我們實際上把信令服務分拆為3個服務模塊。
如下圖所示:

下面將逐個介紹這3個子服務。
7.3 Login服務
Login服務主要負責維護用戶長鏈接:
- 1)每個用戶一條鏈接到Login服務,并按時間發心跳包給Login服務;
- 2)服務定時檢查用戶鏈接狀態和心跳包,比如發現2個心跳周期都沒收到心跳,就認為用戶掉線了(有假在線問題,有興趣同學可回貼討論)。
Login服務收到用戶登錄請求以后,驗證uid/cookie,如果成功就把這個用戶的登錄信息發送給online。
此過程主要記錄的信息包含:
- 1)uid(用戶id);
- 2)Login服務器IP/Port;
- 3)Route服務器的IP/Port。
如果用戶發送IM消息,先發送到Login,Login轉發給Route,Route根據服務的類型(SVID),發現是IM協議就發送給后端的IM服務。
Login對并發要求比較高,一般要支持TCP+UDP+Websocket幾種方式,單服務可以做到10-250萬之間。從服務穩定性角度觸發,建議是控制VM的CPU/內存,單服務器以20-50萬為合適。
Login服務器本身沒有狀態,任何一個Login服務斷掉,用戶端檢測到以后重連另一個Login服務器就可以了,對整體服務可靠性基本沒有影響。
7.4 Online服務
Online服務主要負責維護用戶的在線信息:
- 1)如果用戶掉線,Online服務里信息就是空;
- 2)如果用戶在線,Online就能找到用戶登錄在哪個集群,哪個Login服務器上。
Online業務相對簡單:多個Login服務器會連接到Online,定期同步用戶登錄和離線信息。
Online主要職責是:把用戶狀態信息存儲在Redis集群里。因此也是無狀態的,任何一個Online服務掛掉,不影響整體服務能力。
如果集群規模不大,用戶規模也不大,Online服務也可以收到Login服務里去。
如果規模比較大,建議分拆出來,一方面簡化Login的邏輯復雜度,同時避免寫Redis的慢操作放在Login服務里。因為Login要同時處理50萬以上的并發鏈接,不適合在循環里嵌入慢操作。
7.5 Route服務
Route服務的設計核心,是作為信令系統跟其它子系統的交互層。Route下接Login服務,可以接受用戶業務信息(IM),也可以往用戶推送下行消息。
多個后端業務系統可以接入到Route,按照服務類型(SVID, service id)注冊。比如IM服務可以接入到Route, 注冊SVID_IM。這樣Login接收到SVID=SVID_IM的消息,轉發給Route,Route就可以根據SVID轉發給IM相關的服務。
Route簡單的根據SVID做轉發,不處理具體的業務邏輯,因此也是無狀態的。一個信令集群可以有多個Route服務,任何服務掛了不影響整體服務能力。
8、推送系統
推送系統的核心任務:是接收到給用戶發送下行消息的請求以后,去信令服務查詢用戶是否在線,如果在線走信令推送,如果不在線走離線推送(如iOS的APNS、華為推送、小米推送等)。
因為推送服務可能出現大規模并發蜂擁,比如大群激烈討論的時候,會觸發億級的TPS。因此推送服務用Kafka做了削峰。
我在實際的技術實現上,將推送系統進行了如下細分:

具體就是:
- 1)PushProxy:接受用戶的推送請求,寫入Kafka;
- 2)Kafka:緩存推送服務;
- 3)PushServer:從Kafka獲取推送請求,判斷用戶是否在線;
- 4)PushWorker:真正推送給信令或者APNS,華為推送等。
這里同樣,除了Kafka以外每個服務都是無狀態的,因為也可以實現平行擴展和容錯,任何服務掛掉不影響整體服務可用性。
9、存儲系統
存儲服務主要是負責消息的存儲和查詢,因為消息量巨大,對存儲服務的并發能力和存儲量要求巨大。
為了平衡性能、空間和成本,存儲服務按數據的熱度進行了分級和區別對待。
具體是:
- 1)短期消息(7天):存儲在Redis里;
- 2)近期消息(1-3個月):存儲在Mysql里,以備用戶實時查詢;
- 3)歷史信息:存儲在HBase里,作為歷史數據慢查詢。
同時,為了應對超大群的大量消息處理,存儲服務在實際的技術實現上,也做了比較細的分拆。
存儲服務具體拆分如下圖:

具體的業務劃分就是:
- 1)MsgProxy:負責接受IM子系統的存儲請求,寫入Kafka;
- 2)MsgWriter:從Kafka獲取寫請求,按需寫入Redis和Mysql;
- 3)MsgReader:接受用戶的消息查詢請求,從Redis,Mysql或者HBase讀數據;
- 4)運維工具:主要是數據庫的運維需求。
消息隊列(Kafka)在這里角色比較重要,因為對于高并發請求(100萬人公眾號),需要通過消息隊列來做削峰和并行。
在具體部署上:可能是3-4個MsgProxy,后端可以對應15個左右的MsgWriter。MsgWriter是比較慢的,需要同時操作多個數據庫,還要保證操作的原子性。
10、本篇小結
本篇主要總結了這套億級用戶量IM系統的總體架構設計,為了高性能和橫向擴展性,基于微信的理念將整個架構在實現上分成了4個子系統,分別是:IM業務系統、信令系統、推送系統、存儲系統。
針對這4個子系統,在實際的技術應用層上,又進行了進一步的服務拆分和細化,使得整個架構伸縮性大大增強。
—— 下篇《一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等》稍后發布,敬請期待 ——
附錄:相關文章
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》(* 力薦)
《一套原創分布式即時通訊(IM)系統理論架構方案》(* 力薦)
《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》(* 力薦)
《現代IM系統中聊天消息的同步和存儲方案探討》(* 力薦)
《一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐》(* 力薦)
《社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等》
《社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進》
《社交軟件紅包技術解密(三):微信搖一搖紅包雨背后的技術細節》
《社交軟件紅包技術解密(四):微信紅包系統是如何應對高并發的》
《社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的》
《社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐》
《社交軟件紅包技術解密(七):支付寶紅包的海量高并發技術實踐》
《從游擊隊到正規軍(一):馬蜂窩旅游網的IM系統架構演進之路》(* 力薦)
《從游擊隊到正規軍(二):馬蜂窩旅游網的IM客戶端架構演進和實踐總結》
《從游擊隊到正規軍(三):基于Go的馬蜂窩旅游網分布式IM系統技術實踐》
《瓜子IM智能客服系統的數據架構設計(整理自現場演講,有配套PPT)》
《阿里釘釘技術分享:企業級IM王者——釘釘在后端架構上的過人之處》
《一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等》
>> 更多同類文章 ……
本文已同步發布于“即時通訊技術圈”公眾號。
▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3393-1-1.html
posted @ 2021-03-15 23:07 Jack Jiang 閱讀(489) | 評論 (0) | 編輯 收藏
posted @ 2021-03-06 17:08 Jack Jiang 閱讀(307) | 評論 (0) | 編輯 收藏
posted @ 2021-03-03 13:02 Jack Jiang 閱讀(233) | 評論 (0) | 編輯 收藏
本文原題“你管這破玩意兒叫TCP?”,由閃客sun分享,轉載請聯系作者。
1、引言
網絡編程能力對于即時通訊技術開發者來說是基本功,而計算機網絡又是網絡編程的理論根基,因而深刻準確地理解計算機網絡知識顯然能夯實你的即時通訊應用的實踐品質。
本文風格類似于《網絡編程懶人入門》、《腦殘式網絡編程入門》兩個系列,但通俗又不失內涵,簡潔又不簡陋,非常適合對計算機網絡知識有向往但又有懼怕的網絡編程愛好者們閱讀,希望能給你帶來不一樣的網絡知識入門視角。
本篇將運用通俗易懂的語言,配上細致精確的圖片動畫,循序漸進地引導你理解TCP協議的主要特性和技術原理,讓TCP協議的學習不再如此枯燥和生澀,非常適合入門者閱讀。
本文已同步發布于“即時通訊技術圈”公眾號,歡迎關注。公眾號上的鏈接是:點此進入。
2、系列文章
本文是該系列文章中的第2篇:
本文主要涉及計算機網絡的傳輸層,希望讓TCP協議的學習不再枯燥和生澀。
3、初識傳輸層
你是一臺電腦,你的名字叫 A。
經過上篇《假如你來設計網絡,會怎么做?》的一番折騰,只要你知道另一位伙伴 B 的 IP 地址,且你們之間的網絡是通的,無論多遠,你都可以將一個數據包發送給你的伙伴 B。
上篇中分享的這就是物理層、數據鏈路層、網絡層這三層所做的事情。
站在第四層的你,就可以不要臉地利用下三層所做的鋪墊,隨心所欲地發送數據,而不必擔心找不到對方了。
雖然你此時還什么都沒干,但你還是給自己這一層起了個響亮的名字,叫做傳輸層。
你本以為自己所在的第四層萬事大吉,啥事沒有,但很快問題就接踵而至。
4、問題來了
前三層協議只能把數據包從一個主機搬到另外一臺主機,但是到了目的地以后,數據包具體交給哪個程序(進程)呢?
所以:你需要把通信的進程區分開來,于是就給每個進程分配一個數字編號,你給它起了一個響亮的名字:端口號。
然后:你在要發送的數據包上,增加了傳輸層的頭部:源端口號與目標端口號。
OK,這樣你將原本主機到主機的通信,升級為了進程和進程之間的通信。
你沒有意識到,你不知不覺實現了UDP協議!
當然 UDP 協議中不光有源端口和目標端口,還有數據包長度和校驗值,我們暫且略過。
就這樣,你用 UDP 協議無憂無慮地同 B 進行著通信,一直沒發生什么問題。
但很快,你發現事情變得非常復雜 ... ...
5、丟包問題
由于網絡的不可靠,數據包可能在半路丟失,而 A 和 B 卻無法察覺。
對于丟包問題,只要解決兩個事就好了。
第一個:A 怎么知道包丟了?
答案是:讓 B 告訴 A。
第二個:丟了的包怎么辦?
答案是:重傳。
于是你設計了如下方案:A 每發一個包,都必須收到來自 B 的確認(ACK),再發下一個,否則在一定時間內沒有收到確認,就重傳這個包。
你管它叫停止等待協議。
只要按照這個協議來,雖然 A 無法保證 B 一定能收到包,但 A 能夠確認 B 是否收到了包,收不到就重試,盡最大努力讓這個通信過程變得可靠,于是你們現在的通信過程又有了一個新的特征,可靠交付。
6、效率問題
停止等待雖然能解決問題,但是效率太低了。
A 原本可以在發完第一個數據包之后立刻開始發第二個數據包,但由于停止等待協議,A 必須等數據包到達了 B ,且 B 的 ACK 包又回到了 A,才可以繼續發第二個數據包。這效率慢得可不是一點兩點。
于是:你對這個過程進行了改進,采用流水線的方式,不再傻傻地等。
7、順序問題
但是網路是復雜的、不可靠的。
這導致的問題是:有的時候 A 發出去的數據包,分別走了不同的路由到達 B,可能無法保證和發送數據包時一樣的順序。
對應于我們的例子:在流水線中有多個數據包和ACK包在亂序流動,他們之間對應關系就亂掉了。
如果回到上面的停止等待協議,那么A 每收到一個包的確認(ACK)再發下一個包,那就根本不存在順序問題。但,應該有更好的辦法吧?
是的,更好的辦法就是:A 在發送的數據包中增加一個序號(seq),同時 B 要在 ACK 包上增加一個確認號(ack)。這樣不但解決了停止等待協議的效率問題,也通過這樣標序號的方式解決了順序問題。
而 B 這個確認號意味深長:比如 B 發了一個確認號為 ack = 3,它不僅僅表示 A 發送的序號為 2 的包收到了,還表示 2 之前的數據包都收到了。這種方式叫累計確認或累計應答。
注意:實際上 ack 的號是收到的最后一個數據包的序號 seq + 1,也就是告訴對方下一個應該發的序號是多少。但圖中為了便于理解,ack 就表示收到的那個序號,不必糾結。
8、流量問題
有的時候,A 發送數據包的速度太快,而 B 的接收能力不夠,但 B 卻沒有告知 A 這個情況。
怎么解決呢?
很簡單:B 告訴 A 自己的接收能力,A 根據 B 的接收能力,相應控制自己的發送速率就好了。
B 怎么告訴 A 呢?B 跟 A 說"我很強"這三個字么?那肯定不行,得有一個嚴謹的規范。
于是 B 決定:每次發送數據包給 A 時,順帶傳過來一個值,叫窗口大小(win),這個值就表示 B 的接收能力。
同理:每次 A 給 B 發包時也帶上自己的窗口大小,表示 A 的接收能力。
B 告訴了 A 自己的窗口大小值,A 怎么利用它去做 A 這邊發包的流量控制呢?
很簡單:假如 B 給 A 傳過來的窗口大小 win = 5,那 A 根據這個值,把自己要發送的數據分成這么幾類。
圖片過于清晰,就不再文字解釋了。
當 A 不斷發送數據包時,已發送的最后一個序號就往右移動,直到碰到了窗口的上邊界,此時 A 就無法繼續發包,達到了流量控制。
但是:當 A 不斷發包的同時,A 也會收到來自 B 的確認包,此時整個窗口會往右移動,因此上邊界也往右移動,A 就能發更多的數據包了。
以上都是在窗口大小不變的情況下。而 B 在發給 A 的 ACK 包中,每一個都可以重新設置一個新的窗口大小,如果 A 收到了一個新的窗口大小值,A 會隨之調整。
如果 A 收到了比原窗口值更大的窗口大小,比如 win = 6,則 A 會直接將窗口上邊界向右移動 1 個單位。
如果 A 收到了比原窗口值小的窗口大小,比如 win = 4,則 A 暫時不會改變窗口大小,更不會將窗口上邊界向左移動,而是等著 ACK 的到來,不斷將左邊界向右移動,直到窗口大小值收縮到新大小為止。
OK,終于將流量控制問題解決得差不多了,你看著上面一個個小動圖,給這個窗口起了一個更生動的名字:滑動窗口。
9、擁塞問題
但有的時候,不是 B 的接受能力不夠,而是網絡不太好,造成了網絡擁塞。
擁塞控制與流量控制有些像,但流量控制是受 B 的接收能力影響,而擁塞控制是受網絡環境的影響。
擁塞控制的解決辦法依然是通過設置一定的窗口大小。只不過,流量控制的窗口大小是 B 直接告訴 A 的,而擁塞控制的窗口大小按理說就應該是網絡環境主動告訴 A。
但網絡環境怎么可能主動告訴 A 呢?只能 A 單方面通過試探,不斷感知網絡環境的好壞,進而確定自己的擁塞窗口的大小。
擁塞窗口大小的計算有很多復雜的算法,就不在本文中展開了(有興趣可以深入閱讀《[通俗易懂]深入理解TCP協議(下):RTT、滑動窗口、擁塞處理》)。
假如擁塞窗口的大小為 cwnd,上一部分流量控制的滑動窗口的大小為 rwnd,那么窗口的右邊界受這兩個值共同的影響,需要取它倆的最小值。
窗口大小 = min(cwnd, rwnd)
含義很容易理解:當 B 的接受能力比較差時,即使網絡非常通暢,A 也需要根據 B 的接收能力限制自己的發送窗口。當網絡環境比較差時,即使 B 有很強的接收能力,A 也要根據網絡的擁塞情況來限制自己的發送窗口。正所謂受其短板的影響嘛~
10、連接問題
有的時候,B 主機的相應進程還沒有準備好或是掛掉了,A 就開始發送數據包,導致了浪費。
這個問題在于:A 在跟 B 通信之前,沒有事先確認 B 是否已經準備好,就開始發了一連串的信息。就好比你和另一個人打電話,你還沒有"喂"一下確認對方有沒有在聽,你就巴拉巴拉說了一堆。
這個問題該怎么解決呢?
地球人都知道:三次握手嘛!
- A:我準備好了(SYN)
- B:我知道了(ACK),我也準備好了(SYN)
- A:我知道了(ACK)
A 與 B 各自在內存中維護著自己的狀態變量,三次握手之后,雙方的狀態都變成了連接已建立(ESTABLISHED)。
雖然就只是發了三次數據包,并且在各自的內存中維護了狀態變量,但這么說總覺得太 low,你看這個過程相當于雙方建立連接的過程,于是你靈機一動,就叫它面向連接吧。
注意:這個連接是虛擬的,是由 A 和 B 這兩個終端共同維護的,在網絡中的設備根本就不知道連接這回事兒!
但凡事有始就有終,有了建立連接的過程,就要考慮釋放連接的過程。
這就是網絡編程中耳熟能詳的四次揮手啦!
- A:再見,我要關閉了(FIN)
- B:我知道了(ACK)。給 B 一段時間把自己的事情處理完...
- B:再見,我要關閉了(FIN)
- A:我知道了(ACK)
11、小結一下
以上講述的,就是 TCP 協議的核心思想,上面過程中需要傳輸的信息,就體現在 TCP 協議的頭部,這里放上最常見的 TCP 協議頭解讀的圖。
不知道你現在再看下面這句話,是否能理解:
TCP 是面向連接的、可靠的、基于字節流的傳輸層通信協議。
“面向連接、可靠”,這兩個詞通過上面的講述很容易理解,那什么叫做基于字節流呢?
很簡單:TCP 在建立連接時,需要告訴對方 MSS(最大報文段大小)。
也就是說:如果要發送的數據很大,在 TCP 層是需要按照 MSS 來切割成一個個的 TCP 報文段 的。
切割的時候我才不管你原來的數據表示什么意思,需要在哪里斷句啥的,我就把它當成一串毫無意義的字節,在我想要切割的地方咔嚓就來一刀,標上序號,只要接收方再根據這個序號拼成最終想要的完整數據就行了。
在我 TCP 傳輸這里,我就把它當做一個個的字節,也就是基于字節流的含義了。
12、寫在最后
一提到 TCP,可能很多人都想起被三次握手和四次揮手所支配的恐懼。
但其實你跟著本文中的思路你就會發現,三次握手與四次揮手只占 TCP 所解決的核心問題中很小的一部分,只是因為它在面試中很適合作為知識點進行考察,所以在很多人的印象中就好像 TCP 的核心就是握手和揮手似的。
本文希望你能從問題出發,真正理解 TCP 所想要解決的問題,你會發現很多原理就好像生活常識一樣順其自然,并不復雜,希望你有收獲~
最后,如果對TCP的理解仍存在疑惑,可以繼續閱讀以下精選的資料:
- 《TCP/IP詳解 - 第17章·TCP:傳輸控制協議》(* 推薦)
- 《TCP/IP詳解 - 第18章·TCP連接的建立與終止》
- 《TCP/IP詳解 - 第21章·TCP的超時與重傳》
- 《通俗易懂-深入理解TCP協議(上):理論基礎》(* 推薦)
- 《通俗易懂-深入理解TCP協議(下):RTT、滑動窗口、擁塞處理》
- 《理論經典:TCP協議的3次握手與4次揮手過程詳解》
- 《理論聯系實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》
- 《網絡編程懶人入門(一):快速理解網絡通信協議(上篇)》
- 《網絡編程懶人入門(二):快速理解網絡通信協議(下篇)》(* 推薦)
- 《網絡編程懶人入門(三):快速理解TCP協議一篇就夠》(* 推薦)
- 《腦殘式網絡編程入門(一):跟著動畫來學TCP三次握手和四次揮手》
本文已同步發布于“即時通訊技術圈”公眾號。
▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3339-1-1.html
posted @ 2021-02-24 12:47 Jack Jiang 閱讀(317) | 評論 (0) | 編輯 收藏
posted @ 2021-02-02 15:24 Jack Jiang 閱讀(227) | 評論 (0) | 編輯 收藏
本文原題“高并發高性能服務器是如何實現的”,轉載請聯系作者。
1、系列文章引言
1.1 文章目的
作為即時通訊技術的開發者來說,高性能、高并發相關的技術概念早就了然與胸,什么線程池、零拷貝、多路復用、事件驅動、epoll等等名詞信手拈來,又或許你對具有這些技術特征的技術框架比如:Java的Netty、Php的workman、Go的gnet等熟練掌握。但真正到了面視或者技術實踐過程中遇到無法釋懷的疑惑時,方知自已所掌握的不過是皮毛。
返璞歸真、回歸本質,這些技術特征背后的底層原理到底是什么?如何能通俗易懂、毫不費力真正透徹理解這些技術背后的原理,正是《從根上理解高性能、高并發》系列文章所要分享的。
1.2 文章源起
我整理了相當多有關IM、消息推送等即時通訊技術相關的資源和文章,從最開始的開源IM框架MobileIMSDK,到網絡編程經典巨著《TCP/IP詳解》的在線版本,再到IM開發綱領性文章《新手入門一篇就夠:從零開發移動端IM》,以及網絡編程由淺到深的《網絡編程懶人入門》、《腦殘式網絡編程入門》、《高性能網絡編程》、《不為人知的網絡編程》系列文章。
越往知識的深處走,越覺得對即時通訊技術了解的太少。于是后來,為了讓開發者門更好地從基礎電信技術的角度理解網絡(尤其移動網絡)特性,我跨專業收集整理了《IM開發者的零基礎通信技術入門》系列高階文章。這系列文章已然是普通即時通訊開發者的網絡通信技術知識邊界,加上之前這些網絡編程資料,解決網絡通信方面的知識盲點基本夠用了。
對于即時通訊IM這種系統的開發來說,網絡通信知識確實非常重要,但回歸到技術本質,實現網絡通信本身的這些技術特征:包括上面提到的線程池、零拷貝、多路復用、事件驅動等等,它們的本質是什么?底層原理又是怎樣?這就是整理本系列文章的目的,希望對你有用。
1.3 文章目錄
《從根上理解高性能、高并發(一):深入計算機底層,理解線程與線程池》
《從根上理解高性能、高并發(二):深入操作系統,理解I/O與零拷貝技術》
《從根上理解高性能、高并發(三):深入操作系統,徹底理解I/O多路復用》
《從根上理解高性能、高并發(四):深入操作系統,徹底理解同步與異步》
1.4 本篇概述
接上篇《從根上理解高性能、高并發(五):深入操作系統,理解高并發中的協程》,本篇是高性能、高并發系列的第6篇文章(也是完結篇)。
本篇是本系列文章的完結篇,你將能了解到,一個典型的服務器端是如何利用前5篇中講解的各單項技術從而實現高性能高并發的。
本文已同步發布于“即時通訊技術圈”公眾號,歡迎關注。公眾號上的鏈接是:點此進入。
2、本文作者
應作者要求,不提供真名,也不提供個人照片。
本文作者主要技術方向為互聯網后端、高并發高性能服務器、檢索引擎技術,網名是“碼農的荒島求生”。感謝作者的無私分享。
3、正文引言
當你在閱讀本篇文章的時候,有沒有想過,服務器是怎么把這篇文章發送給你的呢?
說起來很簡單:不就是一個用戶請求嗎?服務器根據請求從數據庫中撈出這篇文章,然后通過網絡發回去嗎。
其實有點復雜:服務器端到底是如何并行處理成千上萬個用戶請求的呢?這里面又涉及到哪些技術呢?
這篇文章就是來為你解答這個問題的。

4、多進程
歷史上最早出現也是最簡單的一種并行處理多個請求的方法就是利用多進程。
比如在Linux世界中,我們可以使用fork、exec等系統調用創建多個進程,我們可以在父進程中接收用戶的連接請求,然后創建子進程去處理用戶請求。
就像這樣:

這種方法的優點就在于:
- 1)編程簡單,非常容易理解;
- 2)由于各個進程的地址空間是相互隔離的,因此一個進程崩潰后并不會影響其它進程;
- 3)充分利用多核資源。
多進程并行處理的優點很明顯,但是缺點同樣明顯:
- 1)各個進程地址空間相互隔離,這一優點也會變成缺點,那就是進程間要想通信就會變得比較困難,你需要借助進程間通信(IPC,interprocess communications)機制,想一想你現在知道哪些進程間通信機制,然后讓你用代碼實現呢?顯然,進程間通信編程相對復雜,而且性能也是一大問題;
- 2)我們知道創建進程開銷是比線程要大的,頻繁的創建銷毀進程無疑會加重系統負擔。
幸好,除了進程,我們還有線程。

5、多線程
不是創建進程開銷大嗎?不是進程間通信困難嗎?這些對于線程來說統統不是問題。
什么?你還不了解線程,趕緊看看這篇《深入計算機底層,理解線程與線程池》,這里詳細講解了線程這個概念是怎么來的。
由于線程共享進程地址空間,因此線程間通信天然不需要借助任何通信機制,直接讀取內存就好了。
線程創建銷毀的開銷也變小了,要知道線程就像寄居蟹一樣,房子(地址空間)都是進程的,自己只是一個租客,因此非常的輕量級,創建銷毀的開銷也非常小。

我們可以為每個請求創建一個線程,即使一個線程因執行I/O操作——比如讀取數據庫等——被阻塞暫停運行也不會影響到其它線程。
就像這樣:

但線程就是完美的、包治百病的嗎,顯然,計算機世界從來沒有那么簡單。
由于線程共享進程地址空間,這在為線程間通信帶來便利的同時也帶來了無盡的麻煩。
正是由于線程間共享地址空間,因此一個線程崩潰會導致整個進程崩潰退出,同時線程間通信簡直太簡單了,簡單到線程間通信只需要直接讀取內存就可以了,也簡單到出現問題也極其容易,死鎖、線程間的同步互斥、等等,這些極容易產生bug,無數程序員寶貴的時間就有相當一部分用來解決多線程帶來的無盡問題。
雖然線程也有缺點,但是相比多進程來說,線程更有優勢,但想單純的利用多線程就能解決高并發問題也是不切實際的。
因為雖然線程創建開銷相比進程小,但依然也是有開銷的,對于動輒數萬數十萬的鏈接的高并發服務器來說,創建數萬個線程會有性能問題,這包括內存占用、線程間切換,也就是調度的開銷。
因此,我們需要進一步思考。

6、事件驅動:Event Loop
到目前為止,我們提到“并行”二字就會想到進程、線程。
但是:并行編程只能依賴這兩項技術嗎?并不是這樣的!
還有另一項并行技術廣泛應用在GUI編程以及服務器編程中,這就是近幾年非常流行的事件驅動編程:event-based concurrency。
PS:搞IM服務端開發的程序員肯定不陌生,著名的Java NIO高性能網絡編程框架Netty中EvenLoop 這個接口意味著什么(有關Netty框架的高性能原理可以讀這篇《新手入門:目前為止最透徹的的Netty高性能原理和框架架構解析》)。
大家不要覺得這是一項很難懂的技術,實際上事件驅動編程原理上非常簡單。
這一技術需要兩種原料:
- 1)event;
- 2)處理event的函數,這一函數通常被稱為event handler;
剩下的就簡單了:你只需要安靜的等待event到來就好,當event到來之后,檢查一下event的類型,并根據該類型找到對應的event處理函數,也就是event handler,然后直接調用該event handler就好了。

That's it !
以上就是事件驅動編程的全部內容,是不是很簡單!
從上面的討論可以看到:我們需要不斷的接收event然后處理event,因此我們需要一個循環(用while或者for循環都可以),這個循環被稱為Event loop。
使用偽代碼表示就是這樣:
while(true) {
event = getEvent();
handler(event);
}
Event loop中要做的事情其實是非常簡單的,只需要等待event的帶來,然后調用相應的event處理函數即可。
注意:這段代碼只需要運行在一個線程或者進程中,只需要這一個event loop就可以同時處理多個用戶請求。
有的同學可以依然不明白:為什么這樣一個event loop可以同時處理多個請求呢?
原因很簡單:對于網絡通信服務器來說,處理一個用戶請求時大部分時間其實都用在了I/O操作上,像數據庫讀寫、文件讀寫、網絡讀寫等。當一個請求到來,簡單處理之后可能就需要查詢數據庫等I/O操作,我們知道I/O是非常慢的,當發起I/O后我們大可以不用等待該I/O操作完成就可以繼續處理接下來的用戶請求。

現在你應該明白了吧:雖然上一個用戶請求還沒有處理完我們其實就可以處理下一個用戶請求了,這也是并行,這種并行就可以用事件驅動編程來處理。
這就好比餐廳服務員一樣:一個服務員不可能一直等上一個顧客下單、上菜、吃飯、買單之后才接待下一個顧客,服務員是怎么做的呢?當一個顧客下完單后直接處理下一個顧客,當顧客吃完飯后會自己回來買單結賬的。
看到了吧:同樣是一個服務員也可以同時處理多個顧客,這個服務員就相當于這里的Event loop,即使這個event loop只運行在一個線程(進程)中也可以同時處理多個用戶請求。
相信你已經對事件驅動編程有一個清晰的認知了,那么接下來的問題就是,這個事件也就是event該怎么獲取呢?
7、事件來源:IO多路復用
在《深入操作系統,徹底理解I/O多路復用》這篇文章中我們知道,在Linux/Unix世界中一切皆文件,而我們的程序都是通過文件描述符來進行I/O操作的,當然對于網絡編程中的socket也不例外。
那我們該如何同時處理多個文件描述符呢?
IO多路復用技術正是用來解決這一問題的:通過IO多路復用技術,我們一次可以監控多個文件描述,當某個“文件”(實際可能是im網絡通信中socket)可讀或者可寫的時候我們就能得到通知啦。
這樣IO多路復用技術就成了event loop的原材料供應商,源源不斷的給我們提供各種event,這樣關于event來源的問題就解決了。

當然:關于IO多路復用技術的詳細講解請參見《深入操作系統,徹底理解I/O多路復用》,本文作為綱領性文章,就不再贅述了。
至此:關于利用事件驅動來實現并發編程的所有問題都解決了嗎?event的來源問題解決了,當得到event后調用相應的handler,看上去大功告成了。
想一想還有沒有其它問題?

8、問題:阻塞式IO
現在:我們可以使用一個線程(進程)就能基于事件驅動進行并行編程,再也沒有了多線程中讓人惱火的各種鎖、同步互斥、死鎖等問題了。
但是:計算機科學中從來沒有出現過一種能解決所有問題的技術,現在沒有,在可預期的將來也不會有。
那上述方法有什么問題嗎?
不要忘了,我們event loop是運行在一個線程(進程),這雖然解決了多線程問題,但是如果在處理某個event時需要進行IO操作會怎么樣呢?
在《深入操作系統,理解I/O與零拷貝技術》一文中,我們講解了最常用的文件讀取在底層是如何實現的,程序員最常用的這種IO方式被稱為阻塞式IO。
也就是說:當我們進行IO操作,比如讀取文件時,如果文件沒有讀取完成,那么我們的程序(線程)會被阻塞而暫停執行,這在多線程中不是問題,因為操作系統還可以調度其它線程。
但是:在單線程的event loop中是有問題的,原因就在于當我們在event loop中執行阻塞式IO操作時整個線程(event loop)會被暫停運行,這時操作系統將沒有其它線程可以調度,因為系統中只有一個event loop在處理用戶請求,這樣當event loop線程被阻塞暫停運行時所有用戶請求都沒有辦法被處理。你能想象當服務器在處理其它用戶請求讀取數據庫導致你的請求被暫停嗎?

因此:在基于事件驅動編程時有一條注意事項,那就是不允許發起阻塞式IO。
有的同學可能會問,如果不能發起阻塞式IO的話,那么該怎樣進行IO操作呢?
PS:有阻塞式IO,就有非阻塞式IO。我們繼續往下討論。
9、解決方法:非阻塞式IO
為克服阻塞式IO所帶來的問題,現代操作系統開始提供一種新的發起IO請求的方法,這種方法就是異步IO。對應的,阻塞式IO就是同步IO,關于同步和異步這兩個概念可以參考《從根上理解高性能、高并發(四):深入操作系統,徹底理解同步與異步》。
異步IO時,假設調用aio_read函數(具體的異步IO API請參考具體的操作系統平臺),也就是異步讀取,當我們調用該函數后可以立即返回,并繼續其它事情,雖然此時該文件可能還沒有被讀取,這樣就不會阻塞調用線程了。此外,操作系統還會提供其它方法供調用線程來檢測IO操作是否完成。
就這樣,在操作系統的幫助下IO的阻塞調用問題也解決了。
雖然有異步IO來解決event loop可能被阻塞的問題,但是基于事件編程依然是困難的。
首先:我們提到,event loop是運行在一個線程中的,顯然一個線程是沒有辦法充分利用多核資源的,有的同學可能會說那就創建多個event loop實例不就可以了,這樣就有多個event loop線程了,但是這樣一來多線程問題又會出現。
另一點在于編程方面,在《從根上理解高性能、高并發(四):深入操作系統,徹底理解同步與異步》這篇文章中我們講到過,異步編程需要結合回調函數(這種編程方式需要把處理邏輯分為兩部分:一部分調用方自己處理,另一部分在回調函數中處理),這一編程方式的改變加重了程序員在理解上的負擔,基于事件編程的項目后期會很難擴展以及維護。
那么有沒有更好的方法呢?
要找到更好的方法,我們需要解決問題的本質,那么這個本質問題是什么呢?

11、更好的方法
為什么我們要使用異步這種難以理解的方式編程呢?
是因為:阻塞式編程雖然容易理解但會導致線程被阻塞而暫停運行。
那么聰明的你一定會問了:有沒有一種方法既能結合同步IO的簡單理解又不會因同步調用導致線程被阻塞呢?
答案是肯定的:這就是用戶態線程(user level thread),也就是大名鼎鼎的協程(關于協程請詳讀本系列的上篇《從根上理解高性能、高并發(五):深入操作系統,理解高并發中的協程》,本文就不再贅述了)。
雖然基于事件編程有這樣那樣的缺點,但是在當今的高性能高并發服務器上基于事件編程方式依然非常流行,但已經不是純粹的基于單一線程的事件驅動了,而是 event loop + multi thread + user level thread。
關于這一組合,同樣值得拿出一篇文章來講解,我們將在后續文章中詳細討論。

12、本文小結
高并發技術從最開始的多進程一路演進到當前的事件驅動,計算機技術就像生物一樣也在不斷演變進化,但不管怎樣,了解歷史才能更深刻的理解當下。希望這篇文章能對大家理解高并發服務器有所幫助。

附錄:更多高性能、高并發文章精選
《高性能網絡編程(一):單臺服務器并發TCP連接數到底可以有多少》
《高性能網絡編程(二):上一個10年,著名的C10K并發連接問題》
《高性能網絡編程(三):下一個10年,是時候考慮C10M并發問題了》
《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》
《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》
《高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型》
《以網游服務端的網絡接入層設計為例,理解實時通信的技術挑戰》
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《騰訊資深架構師干貨總結:一文讀懂大型分布式系統設計的方方面面》
本文已同步發布于“即時通訊技術圈”公眾號。
▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3315-1-1.html
posted @ 2021-01-25 16:36 Jack Jiang 閱讀(285) | 評論 (0) | 編輯 收藏
posted @ 2021-01-18 14:51 Jack Jiang 閱讀(189) | 評論 (0) | 編輯 收藏