即時通訊IM技術領域提高篇

吳德寶AllenWu發表於2018-01-25

[TOC]

即時通訊IM技術領域提高篇

即時通訊IM技術領域基礎篇

接入層的伺服器程式如何升級

對於當前特定Access長連線接入服務而言

我經歷的xxx專案中的情況:

  1. Access接入層服務, tcp長連線的, 如果需要更新的話, 那不是客戶端需要重新登入 ?

    • 是的,但是可以改造,access 再剝一層出來專門維護長連線
  2. access 分為連線層和 access,前者不涉及業務,所以預期不用重啟,後者承載業務,更新重啟對連線沒有影響。後面還考慮把 push 合進 access

  3. 連線層和 access 通過共享記憶體來維護連線資訊。

對於通用接入層而言

  1. 調整接入層有狀態=>無狀態, 接入層與邏輯層嚴格分離.

  2. 無狀態的接入層,可以隨時重啟

  3. 邏輯服務層,可以通過etcd來做服務發現和註冊,方便升級和擴容

單臺伺服器維持的TCP長連線數

  1. 作業系統包含最大開啟檔案數(Max Open Files)限制, 分為系統全域性的, 和程式級的限制

    • fs.file-max
    • soft nofile/ hard nofile
  2. 每個tcp的socket連線都要佔用一定記憶體

    • 通過測試驗證和相關資料,只是保持connect,什麼也不做,每個tcp連線,大致佔用4k左右的記憶體,百萬連線,OS就需要4G以上記憶體.
    • 這裡注意還要修改net.ipv4.tcp_rmem/net.ipv4.tcp_wmem
  3. 網路限制

    • 假設百萬連線中有 20% 是活躍的, 每個連線每秒傳輸 1KB 的資料, 那麼需要的網路頻寬是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求伺服器至少是萬兆網路卡(10Gbps).
  4. 一些基本常用的sysctl的修改:

    • net.ipv4.tcp_mem = 78643200 104857600 157286400
    • net.ipv4.tcp_rmem=4096 87380 16777216
    • net.ipv4.tcp_wmem=4096 87380 16777216
    • net.ipv4.ip_local_port_range = 1024 65535
    • net.ipv4.tcp_tw_recycle=1
    • net.ipv4.tcp_tw_reuse=1
    • fs.file-max = 1048576
    • net.ipv4.ip_conntrack_max = 1048576

    n = (mempages * (PAGE_SIZE / 1024)) / 10;

    PAGE_SIZE:typically 4096 in an x86_64

    files_stat.max_files = n;

  5. epoll機制,長連線數太多,會影響效能嗎? <底層採用紅黑樹和連結串列來管理資料>

    • 這個不會影響tcp連線和效能, 哪怕epoll監控的事件再多,都OK
    • 核心除了幫我們在epoll檔案系統裡建了個file結點,在核心cache裡建了個紅黑樹用於儲存以後epoll_ctl傳來的socket外,還會再建立一個list連結串列,用於儲存準備就緒的事件,當epoll_wait呼叫時,僅僅觀察這個list連結串列裡有沒有資料即可。
  6. 實際應用中應該考慮哪些點呢?

    • 網路卡多佇列的支援, 檢視網路卡是否支援,要不然cpu不能很好處理網路資料, 這個需要好的網路卡,也消耗cpu

    • 維護tcp長連線的節點管理, 這個需要消耗cpu, 需要有對應的資料結構來進行管理

    • 實際中,還應該考慮,每秒中能夠建立連線的速度,因為百萬連線並不是一下就建立的,如果重啟了重連,那麼連線速度如何呢 ?

    • 如果這個節點掛掉了,請求的分攤處理怎麼弄?

    • 應用層對於每個連線的處理能力怎樣? 服務端對協議包的解析處理能力如何 ?

    • tcp mem 問題,沒有用到就不會分配記憶體, 但是不一定會馬上回收.

  7. 關於長連線的另外考慮點:

    • 在穩定連線情況下,長連線數這個指標,在沒有網路吞吐情況下對比,其實意義往往不大,維持連線消耗cpu資源很小,每條連線tcp協議棧會佔約4k的記憶體開銷,系統引數調整後,我們單機測試資料,最高也是可以達到單例項300w長連線。但做更高的測試,我個人感覺意義不大。

    • 實際網路環境下,單例項300w長連線,從理論上算壓力就很大:實際弱網路環境下,移動客戶端的斷線率很高,假設每秒有1000分之一的使用者斷線重連。300w長連線,每秒新建連線達到3w,這同時連入的3w使用者,要進行註冊,載入離線儲存等對內rpc呼叫,另外300w長連線的使用者心跳需要維持,假設心跳300s一次,心跳包每秒需要1w tps。單播和多播資料的轉發,廣播資料的轉發,本身也要響應內部的rpc呼叫,300w長連線情況下,gc帶來的壓力,內部介面的響應延遲能否穩定保障。這些集中在一個例項中,可用性是一個挑戰。所以線上單例項不會hold很高的長連線,實際情況也要根據接入客戶端網路狀況來決定。

  8. 注意的一點就是close_wait 過多問題,由於網路不穩定經常會導致客戶端斷連,如果服務端沒有能夠及時關閉socket,就會導致處於close_wait狀態的鏈路過多。

    • close_wait狀態的鏈路並不釋放控制程式碼和記憶體等資源,如果積壓過多可能會導致系統控制程式碼耗盡,發生“Too many open files”異常,新的客戶端無法接入,涉及建立或者開啟控制程式碼的操作都將失敗。
  9. 考慮到不同地區不同網路運營商的情況下,使用者可能因為網路限制,連線不上我們的服務或者比較慢。

    • 我們在實踐中就發現,某些網路運營商將某些埠封禁了,導致部分使用者連線不上服務。為了解決這個問題,可以提供多個ip和多個埠,客戶端在連線某個ip比較慢的情況下,可以進行輪詢,切換到一個更快的ip。
  10. TCP_NODELAY

    • 針對這個話題,Thompson認為很多在考慮微服務架構的人對TCP並沒有充分的理解。在特定的場景中,有可能會遇到延遲的ACK,它會限制鏈路上所傳送的資料包,每秒鐘只會有2-5個資料包。這是因為TCP兩個演算法所引起的死鎖:Nagle以及TCP Delayed Acknowledgement。在200-500ms的超時之後,會打破這個死鎖,但是微服務之間的通訊卻會分別受到影響。推薦的方案是使用TCP_NODELAY,它會禁用Nagle的演算法,多個更小的包可以依次傳送。按照Thompson的說法,其中的差別在5到500 req/sec。

    • tcp_nodelay 告訴nginx不要快取資料,而是一段一段的傳送--當需要及時傳送資料時,就應該給應用設定這個屬性,這樣傳送一小塊資料資訊時就不能立即得到返回值。

    • 我們發現 gRPC 的同步呼叫與 Nagle's algorithm 會產生衝突,雖然 gRPC 在程式碼中加入了 TCP_NODELAY 這個 socketopt 但在 OS X 中是沒有效果的。後來通過設定 net.inet.tcp.delayed_ack = 0 來解決,同樣我們在 linux 下也設定了 net.ipv4.tcp_low_latency = 1,這樣在 100M 頻寬下一次同步呼叫的時間在 500us 以下。而且在實際應用中,我們通過 streaming 呼叫來解決大量重複資料傳輸的問題,而不是通過反覆的同步呼叫來傳相同的資料,這樣一次寫入可以在 5us 左右。其實批量寫入一直都是一個很好的解決效能問題的方法S


心跳相關處理

  1. 心跳其實有兩個作用

    • 心跳保證客戶端和服務端的連線保活功能,服務端以此來判斷客戶端是否還線上
    • 心跳還需要維持行動網路的GGSN.
  2. 最常見的就是每隔固定時間(如4分半)傳送心跳,但是這樣不夠智慧.

    • 心跳時間太短,消耗流量/電量,增加伺服器壓力.
    • 心跳時間太長,可能會被因為運營商的策略淘汰NAT表中的對應項而被動斷開連線
  3. 心跳演算法 (參考Android微信智慧心跳策略)

    • 維護移動網GGSN(閘道器GPRS支援節點)
      • 大部分移動無線網路運營商都在鏈路一段時間沒有資料通訊時,會淘汰 NAT 表中的對應項,造成鏈路中斷。NAT超時是影響TCP連線壽命的一個重要因素(尤其是國內),所以客戶端自動測算NAT超時時間,來動態調整心跳間隔,是一個很重要的優化點。
    • 參考微信的一套自適應心跳演算法:
      • 為了保證收訊息及時性的體驗,當app處於前臺活躍狀態時,使用固定心跳。
      • app進入後臺(或者前臺關屏)時,先用幾次最小心跳維持長連結。然後進入後臺自適應心跳計算。這樣做的目的是儘量選擇使用者不活躍的時間段,來減少心跳計算可能產生的訊息不及時收取影響。
  4. 精簡心跳包,保證一個心跳包大小在10位元組之內, 根據APP前後臺狀態調整心跳包間隔 (主要是安卓)


弱網環境下的相關處理

  1. 網路加速 cdn

    • 包括信令加速點和圖片CDN網路
  2. 協議精簡和壓縮

    • 使用壓縮演算法,對資料包進行壓縮
  3. TCP第一次通過域名連線上後,快取IP,下次進行IP直連;若下次IP連線失敗,則重新走域名連線

  4. 對於大檔案和圖片等, 使用斷點上傳和分段上傳

  5. 平衡網路延遲和頻寬的影響

    • 在包大小小於1500位元組時, 儘量合併請求包. 減少請求
  6. ip就近接入

    • ip 直連(域名轉ip)
    • 域名解析(ip庫), 域名解析的耗時在行動網路中尤其慢
    • 計算距離使用者地理位置最近的同一運營商的接入點

斷線重連策略

掉線後,根據不同的狀態需要選擇不同的重連間隔。如果是本地網路出錯,並不需要定時去重連,這時只需要監聽網路狀態,等到網路恢復後重連即可。如果網路變化非常頻繁,特別是 App 處在後臺執行時,對於重連也可以加上一定的頻率控制,在保證一定訊息實時性的同時,避免造成過多的電量消耗。

  1. 斷線重連的最短間隔時間按單位秒(s)以4、8、16...(最大不超過30)數列執行,以避免頻繁的斷線重連,從而減輕伺服器負擔。當服務端收到正確的包時,此策略重置

  2. 有網路但連線失敗的情況下,按單位秒(s)以間隔時間為2、2、4、4、8、8、16、16...(最大不超過120)的數列不斷重試

  3. 重連成功後的策略機制

    • 合併部分請求,以減少一次不必要的網路請求來回的時間
    • 簡化登入後的同步請求,部分同步請求可以推遲到UI操作時進行,如群成員資訊重新整理。
  4. 在重連Timer中,為了防止雪崩效應的出現,我們在檢測到socket失效(伺服器異常),並不是立馬進行重連,而是讓客戶端隨機Sleep一段時間(或者上述其他策略)再去連線服務端,這樣就可以使不同的客戶端在服務端重啟的時候不會同時去連線,從而造成雪崩效應。


網路切換怎麼處理? 是否需要重連,是否重新登入?

  1. 一般的話,有網路切換(3g->4g->wifi->4g)就重連,重新走一遍整體流程

  2. 最好APP能以儘量少的通訊量來重新註冊伺服器, 比如不再從伺服器獲取配置資訊,從上一次拉取的伺服器配置的快取資料直接讀取(如果伺服器改變,最好能夠發一條通知給app更新)

  3. 如從wifi 切換到4G、處於地鐵、WIFI邊緣地帶等,為避免造成重連風暴(因為網路不穩定,會頻繁發起重連請求), 可以採用稍加延遲重連策略


服務端程式怎麼擴容/縮容? 水平擴充套件方案?

  1. 採用業界常用的分散式服務發現,配置方案. 如通過etcd來進行服務發現和註冊.

  2. 設計的各個模組要能獨立化部署,設計為無狀態,例如所謂的微服務, 這樣才能夠很好的做服務的升級、擴容, 保證無單點故障, 也方便灰度釋出更新

  3. 動態配置


群訊息相關

  1. 訊息是寫擴散,還是讀擴散: 群裡面每個人都寫一次相同的訊息,還是群裡面都從同一個地方讀取這條相同訊息?

    • 寫擴散: 簡單,但是群裡面每個人都要寫一遍快取.資料量有點大,而且增加網路消耗(比如寫redis的時候).

    • 讀擴算: 只寫一份到快取,拉取的時候,從這個群快取訊息裡面拉,需要增加一點邏輯處理,方便在所有群成員都拉取完後刪掉快取資料(或者過期)

  2. 傳送方式

    • 遍歷群成員,如果線上就依次傳送, 但是群成員多,群活躍的時候,可能會增大壓力.

    • 遍歷群成員, 線上人員, 服務內部流轉(rpc)的時候是否可以批量傳送?

  3. 群方式

    • 線上的,msg只有一份到db中, index還是寫擴散到cache和db中.
    • 離線的,快取中,寫擴散(msg和index),如果快取失效,則穿透到db中拉取.
  4. 對於群訊息,每條訊息都需要拉取群成員的線上狀態.如果存放在redis,拉取會太過頻繁.連線數會暴增,併發過高. 這樣可以增加一級本地快取,把連線資訊放到本地快取(通過消耗記憶體來減少網路連線和請求)


客戶端減小電量消耗策略

  1. 不能影響手機休眠,採用alarm manager觸發心跳包

  2. 儘量減少網路請求,最好能夠合併(或者一次傳送多個請求). 批量、合併資料請求/傳送

  3. 行動網路下載速度大於上傳速度,2G一次傳送資料包不要太大,3G/4G一次傳送多更省電.


訊息是如何保證可達(不丟)/唯一/保序?

  1. 訊息頭包含欄位dup, 如果是重複遞送的訊息,置位此欄位,用來判定重複遞送

  2. 服務端快取對應的msgid列表, 客戶端下發已收到的最大msgid, 服務端根據客戶端收到的最大msgid來判斷小於此id的訊息已經全部被接收.這樣保證訊息不丟.

  3. 服務端確保msgid生成器的極度高的可用性,並且遞增, 通過msgid的大小,來保證訊息的順序

詳細說明訊息防丟失機制

為了達到任意一條訊息都不丟的狀態,最簡單的方案是手機端對收到的每條訊息都給伺服器進行一次ack確認,但該方案在手機端和伺服器之間的互動過多,並且也會遇到在弱網路情況下ack丟失等問題。因此,引入sequence機制

  1. 每個使用者都有42億的sequnence空間(從1到UINT_MAX),從小到大連續分配
  2. 每個使用者的每條訊息都需要分配一個sequence
  3. 伺服器儲存有每個使用者已經分配到的最大sequence
  4. 手機端儲存有已收取訊息的最大sequence
    image.png

** 方案優點 **

  1. 根據伺服器和手機端之間sequence的差異,可以很輕鬆的實現增量下發手機端未收取下去的訊息

  2. 對於在弱網路環境差的情況,丟包情況發生概率是比較高的,此時經常會出現伺服器的回包不能到達手機端的現象。由於手機端只會在確切的收取到訊息後才會更新本地的sequence,所以即使伺服器的回包丟了,手機端等待超時後重新拿舊的sequence上伺服器收取訊息,同樣是可以正確的收取未下發的訊息。

  3. 由於手機端儲存的sequence是確認收到訊息的最大sequence,所以對於手機端每次到伺服器來收取訊息也可以認為是對上一次收取訊息的確認。一個帳號在多個手機端輪流登入的情況下,只要伺服器儲存手機端已確認的sequence,那就可以簡單的實現已確認下發的訊息不會重複下發,不同手機端之間輪流登入不會收到其他手機端已經收取到的訊息。


通訊方式(TCP/UDP/HTTP)同時使用tcp和http.

  1. IM系統的主要需求:包括賬號、關係鏈、線上狀態顯示、訊息互動(文字、圖片、語音)、實時音視訊

  2. http模式(short連結)和 tcp 模式(long 連結),分別應對狀態協議和資料傳輸協議

  3. 保持長連線的時候,用TCP. 因為需要隨時接受資訊. 要維持長連線就只能選TCP,而非UDP

  4. 獲取其他非及時性的資源的時候,採用http短連線. 為啥不全部用TCP協議呢? 用http協議有什麼好處?

    • 目前大部分功能可以通過TCP來實現.
    • 檔案上傳下載的話,就非http莫屬了
      • 支援斷點續傳和分片上傳.
    • 離線訊息用拉模式,避免 tcp 通道壓力過大,影響即時訊息下發效率
    • 大塗鴉、檔案採用儲存服務上傳,避免 tcp 通道壓力過大
  5. IM到底該用UDP還是TCP協議

    • UDP和TCP各有各的應用場景,作為IM來說,早期的IM因為服務端資源(伺服器硬體、網路頻寬等)比較昂貴且沒有更好的辦法來分擔效能負載,所以很多時候會考慮使用UDP,這其中主要是早期的QQ為代表。

    • TCP的服務端負載已經有了很好的解決方案,加之伺服器資源成本的下降,目前很多IM、訊息推送解決方案也都在使用TCP作為傳輸層協議。不過,UDP也並未排除在IM、訊息推送的解決方案之外,比如:弱網路通訊(包括跨國的高延遲網路環境)、物聯網通訊、IM中的實時音視訊通訊等等場景下,UDP依然是首選項。

    • 關於IM到底該選擇UDP還是TCP,這是個仁者見仁智者見智的問題,沒有必要過於糾結,請從您的IM整體應用場景、開發代價、部署和運營成本等方面綜合考慮,相信能找到你要的答案。


伺服器和客戶端的通訊協議選擇

  1. 常用IM協議:IM協議選擇原則一般是:易於擴充,方便覆蓋各種業務邏輯,同時又比較節約流量。後一點的需求在移動端IM上尤其重要?

    • xmpp: 協議開源,可擴充性強,在各個端(包括伺服器)有各種語言的實現,開發者接入方便。但是缺點也是不少:XML表現力弱,有太多冗餘資訊,流量大,實際使用時有大量天坑。

    • MQTT: 協議簡單,流量少,但是它並不是一個專門為IM設計的協議,多使用於推送. 需要自己在業務上實現群,好友相關等等. 適合推送業務,適合直播IM場景。

    • SIP: 多用於VOIP相關的模組,是一種文字協議. sip信令控制比較複雜

    • 私有協議: 自己實現協議.大部分主流IM APP都是是使用私有協議,一個被良好設計的私有協議一般有如下優點:高效,節約流量(一般使用二進位制協議),安全性高,難以破解。

  2. 協議設計的考量:

    • 網路資料大小——佔用頻寬,傳輸效率:雖然對單個使用者來說,資料量傳輸很小,但是對於伺服器端要承受眾多的高併發資料傳輸,必須要考慮到資料佔用頻寬,儘量不要有冗餘資料,這樣才能夠少佔用頻寬,少佔用資源,少網路IO,提高傳輸效率;

    • 網路資料安全性——敏感資料的網路安全:對於相關業務的部分資料傳輸都是敏感資料,所以必須考慮對部分傳輸資料進行加密

    • 編碼複雜度——序列化和反序列化複雜度,效率,資料結構的可擴充套件性

    • 協議通用性——大眾規範:資料型別必須是跨平臺,資料格式是通用的

  3. 常用序列化協議比較

    • 提供序列化和反序列化庫的開源協議: pb,Thrift. 擴充套件相當方便,序列化和反序列化方便

    • 文字化協議: xml,json. 序列化,反序列化容易,但是佔用體積大.

  4. 定義協議考量

    • 包資料可以考慮壓縮,減小資料包大小

    • 包資料考慮加密,保證資料安全

    • 協議裡面有些欄位uint64,可以適當調整為uint32.減小包頭大小

    • 協議頭裡面最好包含seq_num

      • 這個是為了非同步化的支援。這種訊息通道最重要的是解決通道問題,所有訊息處理不能是同步的,必須是非同步的,你發一個訊息出去,ABC三個包,你收到XYZ三個包之後,你怎麼知道它是對應的,就是對應關係的話我們怎麼處理,就是加一個ID

IM系統架構設計的重點考量點

  1. 編碼角度:採用高效的網路模型,執行緒模型,I/O處理模型,合理的資料庫設計和操作語句的優化;

  2. 垂直擴充套件:通過提高單伺服器的硬體資源或者網路資源來提高效能;

  3. 水平擴充套件:通過合理的架構設計和運維方面的負載均衡策略將負載分擔,有效提高效能;後期甚至可以考慮加入資料快取層,突破IO瓶頸;

  4. 系統的高可用性:防止單點故障;

  5. 在架構設計時做到業務處理和資料的分離,從而依賴分散式的部署使得在單點故障時能保證系統可用。

  6. 對於關鍵獨立節點可以採用雙機熱備技術進行切

  7. 資料庫資料的安全性可以通過磁碟陣列的冗餘配置和主備資料庫來解決。


TCP 擁堵解決方案

TCP的擁塞控制由4個核心演算法組成:“慢啟動”(Slow Start)、“擁塞避免”(Congestion voidance)、“快速重傳 ”(Fast Retransmit)、“快速恢復”(Fast Recovery)。


怎麼判斷kafka佇列是否滯後了?

kafka佇列,沒有滿的概念, 只有消費滯後/堆積的概念

  1. 通過offset monitor 監控對kafka進行實時監控

  2. 對於kafka

    • 本身就是一個分散式,本身就能給支援這種線性的擴充套件,所以不會面臨這種問題。

    • 你會寫資料不消費麼。


操作快取和資料庫的方案

  1. 寫: 先寫資料庫,成功後,更新快取

  2. 讀: 先讀快取, 沒有資料則穿透到db.

但是, 假如我寫資料庫成功,更新快取失敗了. 那下次讀的時候,就會讀到髒資料(資料不一致),這種情況怎麼處理?

方案:

  1. 先淘汰快取,再寫資料庫. 但是如果在併發的時候,也可能出現不一致的問題,就是假如淘汰掉快取後,還沒有及時寫入db, 這個時候來了讀請求,就會直接從db裡面讀取舊資料.

    • 因此,需要嚴格保證針對同一個資料的操作都是序列的.
  2. 由於資料庫層面的讀寫併發,引發的資料庫與快取資料不一致的問題(本質是後發生的讀請求先返回了),可能通過兩個小的改動解決:

    • 修改服務Service連線池,id取模選取服務連線,能夠保證同一個資料的讀寫都落在同一個後端服務上

    • 修改資料庫DB連線池,id取模選取DB連線,能夠保證同一個資料的讀寫在資料庫層面是序列的


資料庫分庫分表

資料庫為什麼要分庫分表? 什麼情況下分庫分表 ?

  1. 解決磁碟系統最大檔案限制

  2. 減少增量資料寫入時的鎖 對查詢的影響,減少長時間查詢造成的表鎖,影響寫入操作等鎖競爭的情況. (表鎖和行鎖) . 避免單張表間產生的鎖競爭,節省排隊的時間開支,增加呑吐量

  3. 由於單表數量下降,常見的查詢操作由於減少了需要掃描的記錄,使得單表單次查詢所需的檢索行數變少,減少了磁碟IO,時延變短

  4. 一臺伺服器的資源(CPU、磁碟、記憶體、IO等)是有限的,最終資料庫所能承載的資料量、資料處理能力都將遭遇瓶頸。分庫的目的是降低單臺伺服器負載,切分原則是根據業務緊密程度拆分,缺點是跨資料庫無法聯表查詢

  5. 當資料量超大的時候,B-Tree索引的作用就沒那麼明顯了。如果資料量巨大,將產生大量隨機I/O,同時資料庫的響應時間將大到不可接受的程度。

    • 資料量超大的時候,B-TREE的樹深度會變深,從根節點到葉子節點要經過的IO次數也會增大。當IO層數超過4層之後,就會變得很慢,其實4層IO,儲存的資料都是TB級別的了,除非你的資料型別都是INT等小型別的。也不能說BTREE不起作用,只是說作用沒那麼明顯了。
    • 資料量巨大,就一定是隨機IO嗎?這不一定的,如果都是主鍵查詢,10E條記錄都可以很快返回結果。當用二級索引來查詢的時候,就變成隨機IO了,響應時間是會變慢,這也要看資料的分佈。另外他也沒說儲存介質,如果用SSD盤,隨機IO比SAS的強100倍,效能也是不錯的

Golang的goroutine

  1. goroutine都是使用者態的排程, 協程切換隻是簡單地改變執行函式棧,不涉及核心態與使用者態轉化, 上下文切換的開銷比較小.

  2. 建立一個goroutine需要大概2k(V1.4)左右的棧空間.

  3. go有搶佔式排程:如果一個Goroutine一直佔用CPU,長時間沒有被排程過,就會被runtime搶佔掉

是不是表示,在記憶體夠用的條件下, 建立一定量(比如,30w,50w)的goroutine, 不會因為cpu排程原因導致效能下降太多?

  1. 如果系統裡面goroutine太多, 可能原因之一就是因為每個goroutine處理時間過長,那麼就需要檢視為啥處理耗時較長.

  2. 給出大概資料,24核,64G的伺服器上,在QoS為message at least,純粹推,訊息體256B~1kB情況下,單個例項100w實際使用者(200w+)協程,峰值可以達到2~5w的QPS...記憶體可以穩定在25G左右,gc時間在200~800ms左右(還有優化空間)。 (來自360訊息系統分享)

長連線接入層的net連線管理

長連線接入層的net連線很多,一般單臺伺服器可以有幾十萬、甚至上百萬,那麼怎麼管理這些連線 ? 後端資料來了, 怎麼快速找到這個請求對應的連線呢 ? 連線和使用者如何對應

管理tcp長連線

  1. 一個連線結構. 包含tcp連線資訊,上次通訊時間, 加解密sharekey, clientaddr. 還包含一個使用者結構

    • 使用者結構裡面包含uid, deviceid. name ,version ...., 還包含上面的這個連線, 兩者一一對應.

    • 不用map來管理, 而是把tcp連線資訊和user資訊來進行一一對應,如果map的話,幾百萬可能查詢起來比較慢.

    • 登入請求的時候,可以根據這個tcp連線資訊,獲取user資訊,但是此時user資訊基本沒有填充什麼資料,所以就需要根據登入來填充user資訊結構. 關鍵是: 在當前Access接入服務裡面,會有一個useMap,會把uid和user資訊對應起來,可以用來判斷此uid,是否在本例項上登入過

    • 返回資料的時候, 可以根據這個uid,來獲取對應的user結構,然後通過這個結構可以獲取對應的tcp 連線資訊, 可以進行傳送資訊.

  2. 另外,登入登出的時候,會有另外的連線資訊(uid/topic/protoType/addr...) 新增刪除到使用者中心

    • 登入成功:UseAddConn
    • 登出下線:UserDelConn
    • 這裡的連線資訊,供其他遠端服務呼叫,如Oracle.
  3. 如果有多個Access接入層, 每個接入層都會有一個useMap結構.

    • 如果多個終端登入同一個賬號,而且在不同的Access,那麼就不能通過useMap來踢出,就需要上步說的使用者中心來管理踢出
    • 多個Access,意味著多個useMap,那麼就需要保證,從某個Access下發的請求,一定會回到當前Access. 怎麼保證呢? 把當前Access的ip:addr一直下發下去,然後返回的時候,根據下發的Access的ip:addr來回到對應的Access.
    • 然後根據uid,來獲取當前uid對應的user結構和tcp連線結構.

資料結構: map/hash(紅黑樹)

管理收發異常,請求迴應ack, 超時

  1. 利用map資料結構, 傳送(publish)完訊息後,立即通過msgid和uid,把對應的訊息體新增到map結構.

  2. 收到迴應後,刪除對應的map結構.

  3. 超時後,重新提交OfflineDeliver. 然後刪除對應的map結構.

非同步,併發的時候,rpc 框架,怎麼知道哪個請求是哪個的呢 ?

  1. client執行緒每次通過socket呼叫一次遠端介面前,生成一個唯一的ID,即requestID(requestID必需保證在一個Socket連線裡面是唯一的),一般常常使用AtomicLong從0開始累計數字生成唯一ID,或者利用時間戳來生成唯一ID.

  2. grpc 也需要服務發現. grpc服務可能有一個例項. 2個, 甚至多個? 可能某個服務會掛掉/當機. 可以利用zookeeper來管理.

  3. 同步 RPC 呼叫一直會阻塞直到從服務端獲得一個應答,這與 RPC 希望的抽象最為接近。另一方面網路內部是非同步的,並且在許多場景下能夠在不阻塞當前執行緒的情況下啟動 RPC 是非常有用的。 在多數語言裡,gRPC 程式設計介面同時支援同步和非同步的特點。

  4. gRPC 允許客戶端在呼叫一個遠端方法前指定一個最後期限值。這個值指定了在客戶端可以等待服務端多長時間來應答,超過這個時間值 RPC 將結束並返回DEADLINE_EXCEEDED錯誤。在服務端可以查詢這個期限值來看是否一個特定的方法已經過期,或者還剩多長時間來完成這個方法。 各語言來指定一個截止時間的方式是不同的

服務效能方面的考慮點

  1. 編碼角度:

    • 採用高效的網路模型,執行緒模型,I/O處理模型,合理的資料庫設計和操作語句的優化;
  2. 垂直擴充套件:

    • 通過提高單伺服器的硬體資源或者網路資源來提高效能;
  3. 水平擴充套件:

    • 通過合理的架構設計和運維方面的負載均衡策略將負載分擔,有效提高效能;後期甚至可以考慮加入資料快取層,突破IO瓶頸;
  4. 系統的高可用性:

    • 防止單點故障;
  5. 在架構設計時做到業務處理和資料的分離,從而依賴分散式的部署使得在單點故障時能保證系統可用。

  6. 對於關鍵獨立節點可以採用雙機熱備技術進行切換。

  7. 資料庫資料的安全性可以通過磁碟陣列的冗餘配置和主備資料庫來解決。


伺服器的瓶頸分析

通過壓測得知gRPC是瓶頸影響因素之一,為啥是grpc? 為啥消耗cpu? 怎麼解決? 網路一定不會影響吞吐.

  1. 採用uarmy 方式. 可以考慮採用streaming方式. 批量傳送,提高效率

    • uarmy方式一對一,併發增大的時候,連線數會增大

    • streaming方式的話,就是合併多個請求(批量打包請求/響應), 減少網路互動, 減少連線

    • 做過streaming 的壓測,效能說比 unary 高一倍還多

  2. 一般伺服器都會有個拋物線規律, 隨著併發數的增大,會逐漸消耗並跑滿(cpu/記憶體/網路頻寬/磁碟io), 隨之帶來的就是響應時間變慢(時延Latency變成長),而qps/吞吐量也上不去.

  3. 對於grpc 而言, 併發數增多後,能看到實際效果就是延遲增大,有部分請求的一次請求響應時間達到了5s左右(ACCESS/PUSH), 這樣說明時延太長, qps/吞吐量 = 併發數/響應時間. 響應時間太長,吞吐當然上不去.

    • 為啥響應時間這麼長了? 是因為cpu跑滿了麼?

    • 還有一個原因倒是響應慢,那就是最終請求會到Oracle服務, 而oracle會請求資料資源(cache/db), oracle的設計中請求資源的併發增多(連線數也增多),導致請求資源的時延增長,因此返回到上級grpc的呼叫也會增大時延.

    • 因此關鍵最終又回到了 cpu/記憶體/網路頻寬/磁碟io這裡了

  4. rpc 而言, 連線數增多了,會導致:

    • 類似tcp長連線一樣, 每個連線肯定要分配一定的記憶體

    • 要同時處理這麼多連線,每個連線都有相應的事務, cpu的處理能力要強

  5. 後來經過調查我們發現 gRPC 的同步呼叫與 Nagle's algorithm 會產生衝突,雖然 gRPC 在程式碼中加入了 TCP_NODELAY 這個 socketopt 但在 OS X 中是沒有效果的。後來通過設定 net.inet.tcp.delayed_ack = 0 來解決,同樣我們在 linux 下也設定了 net.ipv4.tcp_low_latency = 1,這樣在 100M 頻寬下一次同步呼叫的時間在 500us 以下。而且在實際應用中,我們通過 streaming 呼叫來解決大量重複資料傳輸的問題,而不是通過反覆的同步呼叫來傳相同的資料,這樣一次寫入可以在 5us 左右。其實批量寫入一直都是一個很好的解決效能問題的方法


如何快速接入服務端的接入層

如果伺服器在北京, 客戶端在廣州, 如何能夠快速接入? 除了走cdn還有其他方式沒 ?

  1. 如果只有一個資料中心, 暫時除了cdn加速, 沒有其他方法.

  2. 如果有兩個資料中心, 可以採取就近原則,但是需要兩個資料中心的資料進行同步

  3. 就近接入:就是利用DNS服務找到離使用者最近的機器,從而達到最短路徑提供服務

怎麼提高在IM領域的能力 ?

  1. 要能在不壓測的情況下,就能夠預估出系統能夠支援的qps. 要能夠粗略估算出一次db的請求耗時多久, 一次redis的請求耗時多少, 一次rpc呼叫的請求耗時多少?

    • 系統中有哪些是比較耗時,比較消耗cpu的.
  2. 所有系統, 一定都是分為幾層, 從上層到底層, 每一步的請求是如何的? 在每個層耗時咋樣?

    • 系統有沒有引入其他資源

    • 效能瓶頸無法是cpu/io.

    • db查詢慢,是為啥慢? 慢一定有原因的?

      • 查詢一條sql語句的時間大致在0.2-0.5ms(在表資料量不大的情況下, 是否根據索引id來查詢,區別不大.)
    • 單臺機, qps為8k, 是比較少的. qps: 8k, 那麼平均請求響應時間: 1/8ms=0.125ms, qps為8k, 那麼5臺機器, qps就是4w, 同時10w人線上, 收發算一個qps的話,那麼qps減半, 那就是2w qps, 10w同時線上, 每個人3-4s發一次訊息, 需要qps到3w.

    • 之前測試redis的時候, 有測試過,如果併發太高,會導致拉取redis耗時較長,超過3s左右.

    • 正常情況下,一個人傳送一條訊息需要耗時至少5s左右(6-8個字).

  3. 要深入提高IM技術, 就必須要能夠學會分析效能, 找到效能瓶頸, 並解決掉.

    • 還要看別人如微信的一些做法
  4. 架構都是逐步改造的, 每個階段有每個階段的架構, 一般架構,初始都是三層/四層架構. 然後開始改造, 改造第一階段都是拆分服務,按邏輯拆分,按業務拆分, 合併資源請求,減少併發數,減少連線數.

  5. 要經常關注一些大資料, 比如註冊使用者數, 日活, 月活, 留存. 要對資料敏感, 為什麼一直不變, 為什麼突然增高, 峰值是多少? 目前能抗住多少 ?

  6. 關注系統效能指標,cpu,記憶體,網路,磁碟等資料, 經常觀測, 看看有沒有異常, 做到提前發現問題,而不是等到問題出現了再進行解決, 就是是出現問題了再進行解決, 也要保證解決時間是分鐘級別的.

    • 完全理解系統底層工具的含義,如sar,iosta,dstat,vmstat,top等,這些資料要經常觀察,經常看

    • 保證整套系統中所涉及的各個部分都是白盒的

      • 依賴的其他服務是誰負責,部署情況,在哪個機房

      • 使用的資源情況,redis記憶體多大 ? mysql 資料庫多少? 表多少? 主從怎麼分佈 ? 對於訊息: 一主兩從,32庫,32表. 對於好友資料:一主一從,128表. 對於朋友圈,按月分表.

總結&如何思考問題和提升自我能力

  1. 如果沒有自己思考,現有的東西都是合理的, 這顯然是不行的.

    • 每看一個東西,都要思考, 這個東西合不合理? 是否可以優化? 有哪些類似的? 要想如果怎麼樣

    • 例如:剛開始接觸xxx專案的時候,覺得這個架構不錯,覺得不用優化了,但是後面需要大規模推廣後,xxx就提出了一些優化點, 通過量級的提高,暴露了一些問題

      • 併發大後,mysql慢請求問題

      • 併發大後,請求資源併發太多,連線數太多問題,因此需要合併資源請求

      • Access接入層長連線的問題, Access接入層服務升級不方便, 因此需要拆分Access長連線,提升穩定性.方便服務升級.

  2. 除了熟悉程式碼框架外, 一定還要深入到細節, 比如golang的底層優化, 系統級別的優化.

  3. 圍繞im領域思考問題和量級, 當前的量級是什麼級別,然後需要考慮更高階別要做的事情.

    • 當前級別為w級別的時候, 就要考慮十萬級別該做的事,十萬級別後,就要考慮百萬, 不一定要馬上做,但是一定要先想,先考慮,目前效能如何,怎麼擴充套件? 怎麼重構?
  4. 瞭解業界相關技術方案, 瞭解別人踩過的坑. 用來後續量大了後,可以提供更好的技術方法和架構, 往資深im/im高階方向發展, 不僅僅限於xxx專案. 要能夠圍繞整個IM 領域方向思考

    • 業界的架構, 技術方案, 選型, 都需要先了解.

相關文章