為什麼http下載不是直接下載而是一點一點地加快速度?如果直接下載會有什麼後果?

這是一道騰訊的面試題。請問,這個和tcp的擁塞控制有關還是處於安全的考慮?我看到的一種說法是:Http的這種下載方式可以降低帶寬的壓力,如果一開始便以最大帶寬去下載,而客戶端中途因某種原因造成下載中斷,這顯然是對帶寬的浪費,也會成為攻擊的一種方式。麻煩大神詳細解釋一下!謝謝


TCP擁塞控制演算法造成的。這個機制叫做slow start。

TCP congestion control
TCP Slow Start


簡單說,當用戶向伺服器建立連接時,沒人知道兩者之間有效帶寬能有多大——可能是1G的光纖,也可能是坑爹的2G手機網路……

可採用的策略無非兩種:
一是默認鏈路速率很大,一開始用能支持的最大流量發;發現對方接不過來再嘗試更小的流量;
二是默認鏈路速率很小,用最低流量發;發完發現對方響應很快、沒有丟包,就嘗試加大速率發;沒丟再加大;若出現擁塞,切換到擁塞控制演算法繼續嘗試,直到找到鏈路允許的最大速率(具體做法較為複雜,有很多種演算法。不同演算法適應的網路也頗有不同;而且還一直在增加更新更好的演算法)。

前者容易造成資源浪費,萬一存在網路攻擊還會急劇放大攻擊的威脅能力;後者就好的多。

不要被slow start這個名字給誤導了,它的意思是從低速開始試探流量,並不是「慢慢增加下載速率」。其實相關演算法要求儘快探明鏈路速率上限、同時還能應對各種意外情況方為最佳。


——————————————————————————

想說清楚slow start的精確過程並不容易,需要很多基礎知識。

首先我們知道,TCP是「保證數據依序正確到達」的通信協議。換句話說,它不能像UDP那樣,一個報文發出去就完事,不管對方有沒有收到、或者是什麼時候收到的;相反,它要保證報文被對方收到、且內容不出錯、順序也不出錯。

要保證這個,那麼至少就必須在本地拷貝一份報文;萬一對方沒收到,就利用本地備份重傳。不然難道你能告訴用戶:先別發你的第22997個位元組,我把你的第21886個位元組弄丟了,你能再發一次嗎?

同樣的,既然需要知道對方有沒有收到,那麼對方在收到數據後,就需要簽一張收條——術語叫ACK(Acknowledgement (data networks));只有拿到對方的收條了,才可以在緩衝區中刪除那段數據(從發包、到接到ACK,這個時間就叫做Round-trip delay time,簡稱RTT。其實你理解成ping值也可以,原理是一樣的)。

不僅如此,前面提到過,TCP要保證「數據順序」;如果第11886個位元組還沒收到、第32700個位元組已經發了;兩者之間的數據已經超過緩衝區大小了,這第11886個位元組還怎麼「按順序」返回給用戶?

所以,用戶一次最多允許發「緩衝區大小」個數據;超出就會提示socket忙;只有最前面開始的順序N個位元組已經收到ACK的時候,才可以再發N個位元組出去。

這個「緩衝區」,術語就叫「TCP window」(TCP congestion control);其中,收發兩個方向的TCP windows size是彼此獨立的;通信雙方還可以在每個方向上協商TCP window的大小(事實上,每個方向上還區分為發送者的cwnd和接收者的rwnd,兩者大小也不總是相等的)。

正因為必須頭部的連續若干位元組成功發送並收到ACK才可以刪除它、然後往後續N個位元組的內容,這就使得TCP Window看起來好像在一系列數據上面「滑動」:經過確認的數據從一端「滑」出窗口,更多的新數據才可以從另一端「滑」入窗口。這就是著名的「滑窗演算法」。

基於滑窗演算法的特點,再考慮數據發出到收到ACK所需的時間(RTT),鏈路速率上限大約為:
(TCP window size) / RTT

可見,只要控制住了TCP window的大小,也就控制住了數據傳輸速率。


過去,TCP window size最大值為65535位元組;那麼若線路ping值為100ms,則最大傳輸速率為:65535/100ms,即大約0.624M位元組每秒。

這個TCP window size的限制,在現在越來越「瘋狂」的網路帶寬面前、尤其是在跨海電纜/衛星通信等高延遲領域,已經越來越力不從心了。所以後來的RFC1323允許縮放TCP window,最高可達1G。

這裡有更為詳細的討論:
The Cable Guy: TCP Receive Window Auto-Tuning

回到原問題上。

如果一開始就使用最大的TCP window size(按舊時的64K計算),那麼:
1、如果有人攻擊你,開許多許多鏈接但並不傳輸數據;那麼他開1000個鏈接,哪怕僅考慮一個發送緩衝區,你也得支付64M內存;他弄1000台肉雞每台開1000個鏈接,你就至少得能輕易分配64G內存才頂得住——面對現在黑客們動輒控制十萬計肉雞的現實,這就是分分鐘死在協議層的節奏。
2、鏈路中的其他設備要替你轉發數據包;如果對方接收不及,這些數據就會擁塞在鏈路中間的交換機/路由器等設備上;如果它們的緩衝隊列滿了,就會丟棄後續的數據包;然後你就需要大量重傳數據了——這樣不僅浪費流量,而且重傳還是一個很重要的、藉以調整網路參數的指標;這種無意義的重傳肯定會打亂設備上的各種參數,從而需要更複雜的演算法和更長的時間,才能重新穩定網路。

所以,這裡才需要slow start這個機制,從極小的TCP window size開始嘗試(1、2、10等)。

嘗試機制大概是:每收到一個ACK包,就把TCP window size加1;那麼理論上,一個RTT周期內,TCP window size就會翻倍(但協議允許接收者延遲發送ACK——類似用一個ACK表示「從上次確認的序號9開始,直到15號均已收到」;所以一個RTT後,TCP window size未必一定能準確的翻倍)。

(請注意:TCP window size是以位元組為單位的;這個玩意兒和每次發出的數據包大小是脫鉤的。或者說,你往socket里一次寫1個位元組,未必就會對應一個數據包;往socket一次寫40960個位元組,那麼肯定不會一起放在一個數據包里給你一次發出去;ACK是每若干個包[實際是一到兩個]返回一次,但這一次相當於對相關包中的所有位元組都做了ACK。這裡可參考TCP Nagle演算法以及MTU相關資料加強理解。因為這些和主題相關性不大,就不展開了)。

前面提到過,鏈路傳輸速率的理論上限 = TCP window size / RTT;那麼TCP window size翻倍就相當於鏈路傳輸速率翻倍了。
但是,不同網路的RTT是不同的。如果你的網路延遲是100ms,那麼你的初始傳輸速率自然就是4ms網路的1/25,然後你還需要100ms的時間(4ms延遲網路的25倍)、才能讓傳輸速率增加一倍!

另一個方面,當你的網路響應極慢(100ms)時,達到同樣的傳輸速率,你的TCP window size反而要比響應快的網路大若干倍(比如,可能比4ms的網路大25倍)。

或者,如果網路中間某個地方做了限速,那麼它就會採用一定的演算法來防止你短時間發送太多的數據;這些演算法往往會用到緩衝區;這些緩衝區的存在也會影響到RTT(當發送速度接近限速上限時,數據就可能在緩衝區滯留更久)——你的機器可能同時發出了幾十、幾百甚至幾千個TCP鏈接,這些鏈接都會爭搶帶寬;有的時候,你的機器還需要和另外許多台設備爭用網路,這都可能造成RTT增大。


所以你看,slow start演算法其實很「雞賊」的:它利用網路本身的特性(RTT)來為不同用戶提供不同的基礎傳輸速率和不同的傳輸速率增加速度——這很容易理解,ping值大的機器要麼距離很遠、要麼和很多機器爭奪不大的出口帶寬:所以「基於RTT的歧視」自然而然就達到了「依據對方網路狀況定製傳輸速率調整方案」的效果。

換句話說,slow start是通過線性增加TCP window size和利用網路本身的RTT來「被動限速」的;所以對不同網路,初始傳輸速率是變數、每秒增加的傳輸速率也是變數——從時間上看,的確是每個RTT周期增長一倍傳輸速率的「指數率增長」;但從細節上看,它其實是隨著每個ACK包平滑增加傳輸速率(根本上說,這個演算法壓根就不知道何謂「傳輸速率」,它只是在盡量快速的擴大TCP windows size而已:窗口大小配合鏈路本身的RTT,自然形成了某種事實上的限速機制)。


特別需要注意的是:這裡有個細節,就是TCP window size每加1,都需要一個來自接收者的ACK為前提,並不是盲目累加的

所以這裡可不是什麼簡單的「(每周期)流量加倍」——那樣從盲目加大流量、到發現問題,時間間隔就太長了。


另外,請注意演算法是「逐漸增大TCP windows size」,並不是「不停的計算/跟蹤網路傳輸速度然後增加它」,後者是不可行的。

比如對被限速的高速網路來說,你是一個極短的周期傳100個位元組然後發現它能支持1Gbps對呢、還是連續測量幾十個長周期、傳一百萬個位元組後發現它被限速在1Mbps對?
如果網速很低,傳100萬個位元組就要若干分鐘呢?

這種演算法有辦法實現嗎?實現起來得有多複雜?能保證在複雜的網路狀態下仍有可靠表現嗎?

很明顯,這種想當然的演算法看起來很合理,但根本就不是真實發生的過程,也不具絲毫可行性。


slow start會在如下三種情況出現時停止:
1、出現丟包

一個常識是,高速-低速鏈路交界處必然需要一個緩衝區;出現丟包就說明這個緩衝區已經被填滿了、開始主動丟棄之後的數據包了。這是數據傳輸速率達到上限的一個標誌。

2、達到接收者TCP window size的限制

為了簡化討論,前面一直都沒有區分發送者的CWND和接收者的RWND;事實上,真正決定鏈路速率上限的是RWND——很容易理解,你發再快,我接不了,你有什麼辦法?

RWND可以由操作系統根據自身網路狀況自動調優;也可以由應用根據自身情況指定——比如對http伺服器來說,它的用戶一般只接收信息、偶然會有少量上傳信息;所以它的RWND經常指定為最大8K。這已經足夠發送http請求或者發帖、貼圖使用了。

當發送者的CWND大小等於RWND時,slow start結束。

不僅如此。發送者可以發送多少信息,是依靠上一個ACK發來的、關於RWND還有多少空閑空間決定的。
假如接收者的應用死鎖了,那麼它就不會從RWND中把數據拿出去,RWND自然就一直保持滿狀態;那麼當發送者發現ACK報告的、接收者的RWND沒有空閑時,它就連一個多餘的位元組都不會發。

實際上的slow start過程就成了:逐漸增大CWND,直到它逼近RWND——CWND控制著實時數據發送速率,而RWND則控制了網速上限。

這就使得slow start很少會把信息發送速率提高到遠超鏈路允許上限的程度。

3、達到slow start threshold

slow start threshold指出現丟包/超時時,min(CWND, RWND)大小的一半。

它是擁塞控制演算法和slow start演算法的分界點:當CWND小於ssthresh時,切換slow start,大於ssthresh則切換到擁塞控制演算法。

這裡要配合特定的擁塞控制演算法才好深入討論,就不多說了。

一旦出現如上三種情況之一,控制權就會切換到其它擁塞控制/避免演算法上(TCP congestion control)。

標準的擁塞控制/避免演算法有很多很多種,分別適應不同的網路情況,而且一直在增加中(比如說,適用於衛星網路的演算法就和光纖網大不一樣;之前還針對手機3G網路做過個偏底層的小東西,就和擁塞控制演算法較過不少勁,幸好後來還是把它招安了)。

這些演算法可以更精確的使得傳輸速率逼近鏈路允許的上限,但增長速率較慢,每個RTT可能僅為TCP window size加1——特別的,當丟包出現時,某些擁塞控制演算法會直接把CWND設置到當前CWND的一半,並不一定會利用slow start演算法反覆迭代。


總之,slow start一點都不「slow」。實際上,它是一個「瘋狂」的、指數增加傳輸速率的演算法;而且還會基於網路本身的特質,自動選擇不同的起始速率和速率增長速度。


在回答這個問題之前,先來思考另外一個問題。

如果給你一個黑盒子,類似路由器的設備,有一個入口,有一個出口,入口和出口連接有可以發送和接收流量的測試儀器:

Traffic sender ---&> black box ---&> Traffic receiver

假設入口、出口的帶寬足夠大到無限,測試儀器可以發送任意速率的數據,現在讓你測試這個黑盒子的最大發送速率,用什麼方法可以最快得到這個最大發送速率?

我有兩種方法:

方法一:從小到大,指數增長

最初發送1Mbps,沒丟包,發2Mbps,沒丟包,發4M,沒丟包…512M也沒丟包,1024M開始丟包了。

發 512 + (1024-512)/2= 768,沒有丟包

發 768 + (1024 -768)/2 = 896 沒有丟包

繼續發 896 + (1024 -896)/2 = 960 丟包

繼續發 896 + (960-896)/2 = 928 沒有丟包

繼續發 928 + (960-928)/2 = 944 丟包

繼續發 928 + (944-928)/2 = 936 沒有丟包

繼續發 936 + (944-936)/2 = 940 丟包

繼續發 936 + (940-936)/2 = 938 沒有丟包

繼續發 938 + (940-938)/2 = 939 沒有丟包

可以得到最大發送速率為939 Mbps

方法二:從大到小,指數減半收斂

方法與一類似,只是從大到小,最後也會找到最大發送速率為 939 Mbps。


方法一從 1M 到1024M發送速率翻倍的過程就是TCP slow start 過程,此過程可以以最快的速度摸清網路(黑盒子)的最大上限(開始丟包),然後以網路不丟包對應的速率為基準線(即方法一中512M),以線性步長(每次增加一個packet,而不是翻倍增長)逼近最高發送速率,即這裡的939 Mbps。從 512M-939M的收斂過程有點類似TCP congestion avoidance,但是又不完全相同,因為一方的發送速率不僅受制於己方的 send_window_size,還要受制於對方的adertised_window_size

回到這個問題的答案,如果採用方法二,將會有絕大多數的流量會被丟棄,造成網路資源的浪費,如果每個主機都採用類似演算法,網路將永遠處於擁塞狀態。

而採用方法一,開始時候主機都小心翼翼(slow start)發很小的流量來探測網路帶寬的峰值,很顯然這個過程大多數流量都會被轉發,只有到達峰值會被丟棄超過峰值的部分,然後啟用congestion avoidance 演算法小幅度逼近最大傳輸速率。一句白話:TCP傳輸的各種演算法不就是以最快、丟的最少的方式探尋到網路的最大傳輸速率嗎? Yes,I think so!


http是基於tcp的,tcp層有流量控制所以才是一點點開始增大。
所以直接說tcp層為什麼要流量控制好了。
但是tcp(傳輸層)下邊還有數據鏈路層,從這裡開始就做了流量控制。
所以要回到問題:數據鏈路層為什麼要做流量控制上。
(因為我在數據鏈路層就控制了,http、tcp都是基於我的,不可能脫離流量控制)

主要原因是:信道是公用的,發生衝突時會導致傳輸數據無效,所以做流量控制是為了減少信道衝突帶來的通信損耗。
數據在信道里是沒有其他方式存儲的,就像我跟你打電話,我需要先問你在不在(小信息),在跟你吐槽(大信息)。
如果我直接吐槽你又沒在,信息你沒收到,還浪費了我的cpu等資源。

另外:能把信道塞滿了用,我覺得沒有什麼理由不塞滿,提高利用率是終極目標之一。
之所以不塞滿,是因為有以上這些客觀條件(信道衝突等)限制。


tcp滑動窗口


推薦閱讀:

既然 BBR 想要解決的是 bufferbloat 問題,那麼為什麼路由器 buffer 要做那麼多?
TCP連接中a連b和b連a是一碼事嗎?
QQ 為什麼以 UDP 協議為主,以 TCP 協議為輔?
tcp協議可靠嗎? 怎麼知道自己發出的消息已經被是否被成功接收?
tcp上傳文件時的ack變化?

TAG:計算機網路 | HTTP | TCPIP | TCP | Web伺服器 |