作者:周志傑
Mars 是微信官方的終端基礎元件,是一個使用 C++ 編寫的業務無關、跨平台的基礎元件。目前在微信 Android、iOS、Windows、Mac、WP 等多個平台中使用。Mars 主要包括以下幾個獨立的部分:
COMM:基礎庫,包括socket、線程、消息隊列、協程等基礎工具;
XLOG:通用日志子產品,充分考慮移動終端的特點,提供高性能、高可用、安全性、容錯性的日志功能;(詳情點選:高性能日志子產品xlog)
SDT:網絡診斷子產品;
STN:信令傳輸網絡子產品,負責終端與伺服器的小資料信令通道。包含了微信終端在移動網絡上的大量優化經驗與成果,經曆了微信海量使用者的考驗。
Mars 系列開始,将為大家介紹 STN(信令傳輸網絡子產品)。由于 STN 的複雜性,該子產品将被分解為多個篇章進行介紹。本文主要介紹微信中關于 socket 連接配接及 IP&Port 選擇的思考與設計。
TCP 協定應該是目前使用的最廣泛的傳輸層協定,它提供了可靠的端到端的傳輸,為應用的設計節省了大量的工作。TCP 建立連接配接的”三次握手”與連接配接終止的“四次揮手”也廣為人知。在這簡單的 connect 調用中,還能做怎樣的思考與設計呢?
逾時與重傳是 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 )使得可以由終端來發起關閉的操作。
在上述的連接配接逾時政策中,我們選擇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 的排序、選擇對于 connect 的性能也是有着重大的影響。本節主要讨論在已知 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個端口,預防端口被封鎖的情況。特别情況下,可以通過配置下發進行端口更新。
每個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 資源越來越少;
每次緩存逾時或清單輪空後,對于新清單沒有經驗資訊可用
在随機組合排序算法的基礎上,為了解決遇到的新問題,微信使用了新的“以史為鑒”的算法。
由于複合連接配接的引入,在每次複合連接配接的嘗試中,微信可以僞“并發”的對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 排序算法,不僅應該快速的發現可用的資源,使得在出災情況下能快速的響應,同時,也應該具備一定的“遺忘性”、“容災性”,使得災情恢複後能較快的發現“災情恢複”這一事實,并且進行重排序,使得伺服器資源得到更合理的使用。在綜合考慮“以史為鑒”和“遺忘曆史”後,新的 方案具有以下特征:
記憶體曆史、檔案曆史雙層記錄曆史:反映資源使用的近期情況及曆史情況;
初始化狀态:每次程序重新開機或網絡切換後,從檔案曆史中“壓縮”出記憶體曆史作為初始狀态;
旁路檢測:額外更新曆史的管道,更有助于挑選高性能的資源,并且幫助“災情恢複”的資源獲得使用的機會;
檔案曆史的遺忘性:檔案曆史每24小時強制重新整理,避免高分數的記錄長期“占有”隊列;
無曆史、有曆史的混合排序。
具體實作檢視 Mars 源代碼中的 simple_ipport_sort。
連接配接是信令傳輸的前提,一個簡單的連接配接操作蘊含着不少的優化空間。在連接配接逾時的選擇上,我們要兼顧性能與可用性,過短的連接配接逾時可能導緻弱網絡下的低可用性,但過長的連接配接逾時又影響使用者體驗。在 STN 中,我們結合系統本身的 TCP 連接配接重傳特性,進行了相應的設計考量。即使如此,串行的連接配接方案仍然不能滿足高性能的需求。并發連接配接的方案獲得高性能的同時,也帶來了伺服器負載劇增的損失。綜合考慮下,STN 使用了“複合連接配接”的方案,獲得高性能的同時,也保證通常情況下的伺服器低負載。
IP&Port 是連接配接的最重要資源,IP&Port 的排序選擇是連接配接過程的重要部分。在微信的實際使用中,我們依次使用了“随機組合”、“以史為鑒”、“遺忘曆史”三種方案,綜合的考慮了查找性能、移動網際網路的不穩定性、容災及容災恢複等。
連接配接逾時、連接配接政策及 IP&Port 排序是連接配接的是三個重要組成部分,相關的方案也随着微信實踐在不斷的發展中。相信在不同的應用場景中,我們可能會遇到更多的不同問題及需求。随着Mars的開源,也能有機會參考、吸收其他應用中的實戰經驗,使得網絡優化持續的深入。
關注 Mars,來 Github 給我們 star 吧
本文來源于:WeMobileDev 微信公衆号
相關推薦
微信終端跨平台元件 mars 系列(一):高性能日志子產品xlog
微信終端跨平台元件 mars 系列(二):信令傳輸逾時設計