微信终端跨平台组件 Mars 系列(三)连接超时与IPPort排序
前言
Mars 是微信官方的終端基礎組件,是一個使用 C++ 編寫的業務無關、跨平臺的基礎組件。目前在微信 Android、iOS、Windows、Mac、WP 等多個平臺中使用。Mars 主要包括以下幾個獨立的部分:
-
COMM:基礎庫,包括socket、線程、消息隊列、協程等基礎工具;
-
XLOG:通用日志模塊,充分考慮移動終端的特點,提供高性能、高可用、安全性、容錯性的日志功能;(詳情點擊:高性能日志模塊xlog)
-
SDT:網絡診斷模塊;
-
STN:信令傳輸網絡模塊,負責終端與服務器的小數據信令通道。包含了微信終端在移動網絡上的大量優化經驗與成果,經歷了微信海量用戶的考驗。
Mars 系列開始,將為大家介紹 STN(信令傳輸網絡模塊)。由于 STN 的復雜性,該模塊將被分解為多個篇章進行介紹。本文主要介紹微信中關于 socket 連接及 IP&Port 選擇的思考與設計。
你需要知道的TCP連接
TCP 協議應該是目前使用的最廣泛的傳輸層協議,它提供了可靠的端到端的傳輸,為應用的設計節省了大量的工作。TCP 建立連接的”三次握手”與連接終止的“四次揮手”也廣為人知。在這簡單的 connect 調用中,還能做怎樣的思考與設計呢?
int connect(int sockfd, const struct *addr, socklen_t addrlen)
連接的超時重傳
超時與重傳是 TCP 協議最核心的部分,在不穩定的移動網絡中,超時重傳的設計尤為重要。在連接建立的過程中,由于網絡本身的不可靠特性,不可避免的需要重傳的機制來保障可靠服務。在《TCP/IP詳解 卷1》的描述中,在大多數 BSD 實現中,若主動 connect 方沒有收到 SYN 的回應,會在第6秒發送第2個 SYN 進行重試,第3個 SYN 則是與第2個間隔24秒。在第75秒還沒有收到回應,則 connect 調用返回 ETIMEOUT。
這就意味著,在不能立刻確認失敗(例如 unreachable 等)的情況下,需要75秒的時間,才能獲得結果。如果真相并不是用戶的網絡不可用,而是某臺服務器故障、繁忙、網絡不穩定等因素,那75秒的時間只能嘗試1個 IP&Port 資源,對于大多數移動應用而言,是不可接受的。我們需要更積極的超時重傳機制!!!
然而,我們并不能修改 TCP 的協議棧,我們只能在應用層進行干預,設計應用層的超時機制。說干就干,這個時候你是否已經在構思新的、應用層的連接超時重傳機制了呢?應用層的超時重傳,典型做法就是提前結束 connect 的阻塞調用,使用新的 IP&Port 資源進行 connect 重試。但是,我們應該選擇怎樣的連接超時值呢?4秒?10秒?20秒?30秒?不同的應用場景會有不同的選擇。我們來看一下常見的幾種場景:
-
連不同 or 網絡不可用等
-
服務器繁忙 or 中間路由故障等
-
基站繁忙 or 連接信號弱 or 丟包率高等
在第一種場景中,連接超時設置不會帶來什么區別。在第二種場景中,部分服務器資源或路由不可用,我們希望連接超時能稍微短一些,使得我們能盡快的發現故障,并且通過更換 IP&Port 的方式獲得可用資源或路由路徑。而第三種場景則是在移動網絡中經常遇到的弱網絡的場景。在這種場景中,我們更換 IP&Port 資源也是無效的,因此希望連接超時能相對長一些,進行更多的TCP層的重傳。(當然,也不是超時越長越好,后面的分析可以看到很多等待時長是效果低微的)
不同的場景對連接超時有不同的需求,然而,我們在程序中并沒有很好的方法來區分這些場景。在進行連接超時這個閾值的選擇前,我們先來看看,當前主流的 android、iOS 操作系統的連接設計。android 的 TCP 層連接超時重傳如下圖所示(測試機型為 nexus5,android 4.4)。超時間隔依次為(1,2,4,8,16),第5次重試后32秒返回 ETIMEOUT,總用時63秒。超時設置符合 Linux 的常規設置。
但在不同的機型中,偶爾會出現差異性。如下圖 android 抓包(三星 android 4.4)。
iOS 的 connect 超時重傳如下圖所示。超時間隔依次為(1,1,1,1,1,2,4,8,16,32),總共是67s。
經過 tcpdump 的調研分析后,我們發現:
在 iOS 系統中對 connect 的超時重傳進行了一定的修改,在 connect 初期使用更積極的策略,以適應移動網絡的不穩定特征。而在 android 系統中,connect 超時重傳則使用了較為“懶惰”、適用于有線網絡的超時重傳間隔;
不管什么平臺,連接總超時時長都需要1分鐘左右,這個時長在大多數移動應用中,都是不符合用戶體驗要求的;
連接的初始階段,TCP 超時重傳會更積極一些,越到后面,重傳間隔越大。
因此,在實際的連接超時設置上,我們根據不同的系統特征,結合應用能接受的“用戶體驗”范圍,可以設置不同的連接超時間隔。例如在 iOS 系統中,由于采用了較為積極的超時間隔,我們可以將 connect 調用的超時設置為10s。在10s內,iOS 會自動進行6次的重發。在 android 系統中,系統會在第7秒發起第3次重發,之后需要在第15秒才會重發。在不同的用戶體驗要求下,應用可以將 connect 的調用超時設置為不同的值。例如也可以設置為10s(意味著給第3次重發3s的等待時間),從而避免無效的等待時長。同時通過更換 IP&Port 后,重新調用 connect 操作的方式,來獲得更積極的重發策略,更快的查找到可用的 IP&Port 組合。
連接的終止
“四次揮手”的連接終止協議已經口熟能詳。過程如下圖所示。需要關注的是,圖中主動關閉的一方會進入 TIME_WAIT 狀態,在此狀態中通常將停留2倍的 MSL 時長。MSL 時長在不同的操作系統中有不同的設置,通常在30秒到60秒。TIME_WAIT 的數量太多會導致耗盡主動關閉方的 socket 端口和句柄,導致無法再發起新的連接,進而嚴重影響主動關閉方的并發性能。雖然在實際的使用中,可以通過 tcp_tw_recycle,tcp_tw_reuse,tcp_max_tw_buckets 等方式緩解該問題,但也會帶來一些副作用。最好的解決方案是在協議的設計上,盡量的由終端來發起關閉的操作,避免服務器的大量 TIME_WAIT 狀態。例如,使用長連接避免頻繁的關閉;在短連接的協議設計上,務必加上終止標記(例如 http 頭部加上 content-length )使得可以由終端來發起關閉的操作。
串行連接 VS 并發連接 VS 復合連接
在上述的連接超時策略中,我們選擇10秒的連接超時。這就意味著我們需要10秒的時間來確認一個 IP&Port 組合的 connect 超時。當我們有多個 IP&Port 資源時,遍歷的效率偏低。那我們是否能設置 connect 的超時為更短呢?例如4秒。我們知道移動互聯網具有不穩定的特征,超時時間設置過短,會導致在弱網絡的情況下,connect 總是失敗,導致不可用。串行連接的策略在超時選擇上,由于需要兼顧高性能與高可用的設計目標,使得該策略是一個相對“慢”的連接策略。
與此相應,我們會想到并發連接的策略。并發連接,同時發起對N個 IP&Port 的連接調用,可以讓我們第一時間發現可用的連接,并且還順帶發現了 connect 最快的 IP&Port 配置。并發連接可以一舉解決了“高性能”、“高可用”的設計目標,看起來很完美。然而,這個時候,服務端的同學“跳”起來了。在并發連接的策略下,服務器需要提供的連接能力是串行連接的N倍,對服務器連接資源是極大的浪費。同時,并發連接是否會引起連接資源的競爭,從而影響網絡正常用戶的常規體驗,也是個未知的因素。
讓我們來回顧串行連接與并行連接的優缺點。
串行連接
-
資源占用少
-
無服務器負載問題
-
超時選擇困難
-
最慢可用
并行連接
-
網絡資源競爭
-
服務器負載高
-
最快可用
那么,有沒有一種策略,能同時滿足高性能、高可用、低負載的目標呢?在微信的連接設計中,我們使用了”復合連接“的策略。如下圖所示。
初始階段,應用發起對 IP1 &Port1 的 connect 調用。在第4秒的時候,如果第一個 connect 還沒有返回,則發起對 IP2 &Port2 的 connect 調用。以此類推,直至發起了5組 IP&Port 的 connect 調用。?
對比串行連接與并行連接,復合連接有以下特點:
-
常規情況下,服務器負載與串行連接策略相同,實現了低負載的目標;
-
異常情況下,每4s發起新(IP,Port)組合的 connect 調用,使得應用可以快速的查找可用 IP&Port,實現高性能的目標;
-
在超時時間的選擇上,復合方式的“并發”已經實現了高性能、低負載的目標,因此在超時時間的選擇上可以相對寬松,以保障高可用為重。
綜合對比,復合連接能夠維持低資源消耗的情況下,能同時實現低負載、高性能、高可用的目標。
微信 IP&Port 排序算法的演進
在建立連接的調用中,除了超時時間的設置外,IP&Port是連接的最重要參數。IP&Port 的排序、選擇對于 connect 的性能也是有著重大的影響。本節主要討論在已知 IP 列表、Port 列表的情況下,如何排序、組合的問題,而不討論如何獲得就近接入等問題。
IP&Port 的組成
在微信中,IP有多種來源類型。優先級從上而下分別為:
-
WXDNS IP
-
DNS IP
-
Auth IP
-
Hardcode IP
WXDNS IP 是通過微信自建的 DNS 服務獲得的IP列表,自建 DNS 對防劫持、有效期控制等有重要作用。DNS IP 則是通過常規的 DNS 解析獲得的 IP 列表。Auth IP 是微信動態下發的保底IP列表。而Hardcode IP 則是最終的保底IP列表。總體而言,分為常規IP列表、保底IP列表兩個類別。WXDNS IP、DNS IP 為常規列表,Auth IP,Hardcode IP 為保底列表。同時,在組成實際使用的 IP&Port 列表時,由于 WXDNS 與 DNS 的功能近似,因此通常只出現其中一種類型的IP列表。Auth IP 與Hardcode IP 的功能近似,也是同時只能出現兩者中的一種類型。?
在 Port 的選擇上,微信服務在常規情況下提供2個端口,預防端口被封鎖的情況。特別情況下,可以通過配置下發進行端口更新。
IP&Port排序算法(一):隨機組合排序算法
每個TCP連接都是以 IP&Port 的組合為唯一標識。在 IP&Port 的選擇上,我們初步歸納為2個目標:
-
高可用:盡快的找到可用的 IP&Port 資源
-
高性能:優先使用質量好的 IP&Port
-
負載均衡:IP的排序算法不帶任何偏向因子,避免造成人為的負載不均衡
在微信早期的排序選擇上,我們使用了一種隨機組合的排序算法。即將 WXDNS or DNS IP 列表與 Port 列表進行組合,組合后的結果進行隨機排序。在隨機排序的結果列表中,使用下述步驟進行排序:
選取IP1+Port1;
選取IP2+Port2,盡量使得IP1與IP2不相等,Port1與Port2不相等;
選取IP3+Port3,盡量使得IP3與IP1、IP2都不相等,Port3與Port1、Port2都不相等;
以此類推,形成常規列表。
同理,使用 Auth IP or Hardcode IP 列表與 Port 列表的組合,我們按照相同算法生成另外一份保底列表,并將保底列表排序在常規列表的后面,從而組成完整的 IP&Port 列表。隨機組合排序的算法有著以下的特點:
-
高性能:每一次嘗試都盡量使用完全不同的資源,使得能最快的發現可用資源;
-
初始隨機,從而避免列表順序的固化;
-
保底列表在最后,形成最后的保護屏障;
-
在不同的網絡下,維護著不同的資源列表。
在使用中,如果發現 IP&Port 訪問失敗,則在列表中 ban 掉該資源。這里有個小優化,即當 IP1&Port1 的上一次訪問成功時,需要連續失敗2次才 ban 該資源。目的是為了減小偶然的網絡抖動造成的影響。
隨機組合排序算法的設計初衷,是為了以最快的速度嘗試不同的資源組合,從而快速尋找到可用的資源。然而,在微信的實際使用中,卻發現這種算法存在著諸多的問題。例如:
-
網絡不可用或網絡較大波動情況下,列表被ban的速度較快;
-
Auth IP or Hardcode IP 列表太容易被訪問到:隨著常規資源陸續被ban,保底資源總是會被訪問到,造成對保底資源的訪問量大。保底資源是為了微信服務這不符合保底資源的設計初衷。
-
當引入復合連接策略后,IP資源不足。這是因為 ban 的策略簡單粗暴的丟棄失敗的 IP,導致 IP 資源越來越少;
-
每次緩存超時或列表輪空后,對于新列表沒有經驗信息可用
在隨機組合排序算法的基礎上,為了解決遇到的新問題,微信使用了新的“以史為鑒”的算法。
IP&Port 排序算法(二):以史為鑒
由于復合連接的引入,在每次復合連接的嘗試中,微信可以偽“并發”的對N個 IP&Port 進行 connect(微信中目前N=5)。簡單的ban丟棄的策略會使得 IP 資源越來越少。 針對這個特點,我們對IP&Port算法進行了以下修改:
-
初始資源列表分為兩類列表:常規列表,保底列表,分別使用方案(一)隨機組合排序算法生成初始順序;
-
對每次復合連接使用的列表,規定5個資源的組成是4個常規資源+1個保底資源,并且保底資源在最后(完全無法獲取常規資源的情況除外)。這種資源組成方式一方面解決了“保底資源太容易被訪問到”的問題,一方面也保障了保底資源的作用;
-
在不同網絡中,分別記錄每個 IP&Port 的使用情況,并根據使用記錄進行評分、排序;
-
區分連續記錄:對每個 IP&Port 的更新,10秒內的連續成功或失敗,不進行使用情況的記錄。這種處理方式一方面是為了避免網絡不可用或網絡出現較大波動時,IP資源被過快的錯誤標記;一方面也避免失敗歷史被快速的覆蓋;
-
最近的8條使用記錄中,如果有超過3條失敗記錄,且最新一次失敗記錄時間為10分鐘內,則本次排序ban該記錄。這種處理方式的目的是避免歷史分數較高的 IP&Port 在突然出現故障時很難被排序算法排除的問題;
-
無歷史的記錄使用隨機評分排序。
通過上述方法,我們保證了保底資源不會被輕易訪問到,解決了列表被快速標記的問題,同時也保證了歷史記錄好的資源在出現故障時也能被快速替換。
IP&Port 排序算法(三):遺忘歷史
“以史為鑒”的方案在微信中使用了一段時間,看起來運行良好。直至某一天,微信的部分服務集群出現了故障。雖然微信客戶端快速的切換到可用的服務器資源,但當故障服務器恢復后,微信客戶端卻遲遲沒有分流到已恢復服務的集群,導致部分微信服務器負載過高,而部分微信服務器卻負載較低的情況。通過分析,發現“以史為鑒”的排序方案存在著一些問題:
-
初始階段排在前面的資源容易獲得較多的成功記錄,從而分數始終維持在較高的水平;
-
出災情況下,故障機器由于有失敗記錄,使得很難獲得“被原諒”的機會,從而也很難更新使用歷史;
-
采用了無歷史記錄隨機評分,破壞了原有的“相鄰記錄盡量不相同”的隨機性設計;
因此,好的 IP&Port 排序算法,不僅應該快速的發現可用的資源,使得在出災情況下能快速的響應,同時,也應該具備一定的“遺忘性”、“容災性”,使得災情恢復后能較快的發現“災情恢復”這一事實,并且進行重排序,使得服務器資源得到更合理的使用。在綜合考慮“以史為鑒”和“遺忘歷史”后,新的 方案具有以下特征:
-
內存歷史、文件歷史雙層記錄歷史:反映資源使用的近期情況及歷史情況;
-
初始化狀態:每次進程重啟或網絡切換后,從文件歷史中“壓縮”出內存歷史作為初始狀態;
-
旁路檢測:額外更新歷史的渠道,更有助于挑選高性能的資源,并且幫助“災情恢復”的資源獲得使用的機會;
-
文件歷史的遺忘性:文件歷史每24小時強制刷新,避免高分數的記錄長期“占有”隊列;
-
無歷史、有歷史的混合排序。
具體實現查看 Mars 源代碼中的 simple_ipport_sort。
總結
連接是信令傳輸的前提,一個簡單的連接操作蘊含著不少的優化空間。在連接超時的選擇上,我們要兼顧性能與可用性,過短的連接超時可能導致弱網絡下的低可用性,但過長的連接超時又影響用戶體驗。在 STN 中,我們結合系統本身的 TCP 連接重傳特性,進行了相應的設計考量。即使如此,串行的連接方案仍然不能滿足高性能的需求。并發連接的方案獲得高性能的同時,也帶來了服務器負載劇增的損失。綜合考慮下,STN 使用了“復合連接”的方案,獲得高性能的同時,也保證通常情況下的服務器低負載。
IP&Port 是連接的最重要資源,IP&Port 的排序選擇是連接過程的重要部分。在微信的實際使用中,我們依次使用了“隨機組合”、“以史為鑒”、“遺忘歷史”三種方案,綜合的考慮了查找性能、移動互聯網的不穩定性、容災及容災恢復等。
連接超時、連接策略及 IP&Port 排序是連接的是三個重要組成部分,相關的方案也隨著微信實踐在不斷的發展中。相信在不同的應用場景中,我們可能會遇到更多的不同問題及需求。隨著Mars的開源,也能有機會參考、吸收其他應用中的實戰經驗,使得網絡優化持續的深入。
關注 Mars , 來 Github 給我們 star 吧
https://github.com/Tencent/mars
查看 Mars 項目源碼,請點擊[閱讀原文]。
?
https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286458&idx=1&sn=320f690faa4f97f7a49a291d4de174a9&chksm=8334c3b8b4434aae904b6d590027b100283ef175938610805dd33ca53f004bd3c56040b11fa6#rd
總結
以上是生活随笔為你收集整理的微信终端跨平台组件 Mars 系列(三)连接超时与IPPort排序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信终端跨平台组件 mars 系列(二)
- 下一篇: 深入理解 Java 锁与线程阻塞