即時通信IM技術領域提升篇

[TOC]node

即時通信IM技術領域提升篇

即時通信IM技術領域基礎篇mysql

接入層的服務器程序如何升級

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

我經歷的xxx項目中的狀況:linux

  1. Access接入層服務, tcp長鏈接的, 若是須要更新的話, 那不是客戶端須要從新登陸 ?ios

    • 是的,可是能夠改造,access 再剝一層出來專門維護長鏈接
  2. access 分爲鏈接層和 access,前者不涉及業務,因此預期不用重啓,後者承載業務,更新重啓對鏈接沒有影響。後面還考慮把 push 合進 accessnginx

  3. 鏈接層和 access 經過共享內存來維護鏈接信息。golang

對於通用接入層而言

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

  2. 無狀態的接入層,能夠隨時重啓算法

  3. 邏輯服務層,能夠經過etcd來作服務發現和註冊,方便升級和擴容sql

單臺服務器維持的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)以四、八、16...(最大不超過30)數列執行,以免頻繁的斷線重連,從而減輕服務器負擔。當服務端收到正確的包時,此策略重置

  2. 有網絡但鏈接失敗的狀況下,按單位秒(s)以間隔時間爲二、二、四、四、八、八、1六、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 領域方向思考

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

【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】

個人微信公衆號
相關文章
相關標籤/搜索