HTTP 鏈接管理進化論

文章同步於: Github/Blog
鏈接管理是一個 HTTP 的關鍵話題:打開和保持鏈接在很大程度上影響着網站和 Web 應用程序的性能。在 HTTP/1.x 裏有好些個模型: 短鏈接(short-lived connections), 持久鏈接(persistent connections), 和 HTTP 管道(HTTP pipelining)

HTTP 的傳輸協議主要依賴於 TCP 來提供從客戶端到服務器端之間的鏈接。在早期,HTTP 使用一個簡單的模型來處理這樣的鏈接—— 短鏈接。這些鏈接的生命週期是短暫的:每發起一個請求時都會建立一個新的鏈接,並在收到應答時當即關閉。html

這個簡單的模型對性能有先天的限制:打開每個 TCP 鏈接都是至關耗費資源的操做。客戶端和服務器端之間須要交換好些個消息。當請求發起時,網絡延遲和帶寬都會對性能形成影響。現代瀏覽器每每要發起不少次請求(十幾個或者更多)才能拿到所需的完整信息,證實了這個早期模型的效率低下。java

有兩個新的模型在 HTTP/1.1 誕生了。首先是長鏈接模型,它會保持鏈接去完成屢次連續的請求,減小了不斷從新打開鏈接的時間。而後是 HTTP Pipelining,它還要更先進一些,多個連續的請求甚至都不用等待當即返回就能夠被髮送,這樣就減小了耗費在網絡延遲上的時間。git

image

要注意的一個重點是 HTTP 的鏈接管理適用於兩個連續節點之間的鏈接,如 hop-by-hop,而不是 end-to-end。當模型用於從客戶端到第一個代理服務器的鏈接和從代理服務器到目標服務器之間的鏈接時(或者任意中間代理)效果多是不同的。HTTP 協議頭受不一樣鏈接模型的影響,好比 ConnectionKeep-Alive,就是 hop-by-hop 協議頭,它們的值是能夠被中間節點修改的。github

短鏈接(short-lived connections)

HTTP 最先期的模型,也是 HTTP/1.0 的默認模型,是短鏈接。每個 HTTP 請求都由它本身獨立的鏈接完成;這意味着發起每個 HTTP 請求以前都會有一次 TCP 握手,並且是接二連三的。算法

TCP 協議握手自己就是耗費時間的,因此 TCP 能夠保持更多的熱鏈接來適應負載。短鏈接破壞了 TCP 具有的能力,新的冷鏈接下降了其性能。segmentfault

這是 HTTP/1.0 的默認模型(若是沒有指定 Connection 協議頭,或者是值被設置爲 close)。而在 HTTP/1.1 中,只有當 Connection 被設置爲 close 時纔會用到這個模型。瀏覽器

持久鏈接(persistent connections)

短鏈接有兩個比較大的問題:建立新鏈接耗費的時間尤其明顯,另外 TCP 鏈接的性能只有在該鏈接被使用一段時間後(熱鏈接)才能獲得改善。爲了緩解這些問題,持久鏈接(persistent connections) 的概念便被設計出來了,甚至在 HTTP/1.1 以前。或者這被稱之爲一個 keep-alive 鏈接。安全

HTTP/1.1(以及 HTTP/1.0 的各類加強版本)容許 HTTP 設備在事務處理結束以後將 TCP 鏈接保持在打開狀態,以便爲將來的 HTTP 請求重用現存的鏈接。在事務處理結束以後仍然保持在打開狀態的 TCP 鏈接被稱爲持久鏈接。非持久鏈接會在每一個事務結束以後關閉。持久鏈接會在不一樣事務之間保持打開狀態,直到客戶端或服務器決定將其關閉爲止。服務器

一個 持久鏈接 會保持一段時間,重複用於發送一系列請求,節省了新建 TCP 鏈接握手的時間,還能夠利用 TCP 的性能加強能力。固然這個鏈接也不會一直保留着:鏈接在空閒一段時間後會被關閉(服務器可使用 Keep-Alive 協議頭來指定一個最小的鏈接保持時間)。網絡

重用已對目標服務器打開的空閒持久鏈接,就能夠避開緩慢的鏈接創建階段。並且, 已經打開的鏈接還能夠避免 慢啓動擁塞適應階段,以便更快速地進行數據的傳輸。

持久鏈接也仍是有缺點的;就算是在空閒狀態,它仍是會消耗服務器資源,並且在重負載時,還有可能遭受 DoS attacks 攻擊。這種場景下,可使用非持久鏈接,即儘快關閉那些空閒的鏈接,也能對性能有所提高。

HTTP/1.0 裏默認並不適用 持久鏈接。把 Connection 設置成 close 之外的其它參數均可以讓其保持 持久鏈接,一般會設置爲 retry-after

在 HTTP/1.1 裏,默認就是持久鏈接的,協議頭都不用再去聲明它(但咱們仍是會把它加上,萬一某個時候由於某種緣由要退回到 HTTP/1.0 呢)。

持久鏈接與並行鏈接配合使用多是最高效的方式。如今,不少 Web 應用程序都會打開少許的並行鏈接,其中的每個都是持久鏈接。

盲中繼(blind relay)

那些不理解 Connection 首部,並且不知道在沿着轉發鏈路將其發送出去以前,應該將該首部刪除的代理。不少老的或簡單的代理都 是 盲中繼(blind relay),它們只是將字節從一個鏈接轉發到另外一個鏈接中去,不對 Connection 首部進行特殊的處理。

image

HTTP Pipelining

默認狀況下,HTTP 請求是按順序發出的。下一個請求只有在當前請求收到應答事後纔會被髮出。因爲會受到網絡延遲和帶寬的限制,在下一個請求被髮送到服務器以前,可能須要等待很長時間。

流水線是在同一條長鏈接上發出連續的請求,而不用等待應答返回。這樣能夠避免鏈接延遲。理論上講,性能還會由於兩個 HTTP 請求有可能被打包到一個 TCP 消息包中而獲得提高。就算 HTTP 請求不斷的繼續,尺寸會增長,但設置 TCP 的 最大分段大小 MSS (Maximum Segment Size) 選項,任然足夠包含一系列簡單的請求。

並非全部類型的 HTTP 請求都能用到流水線:只有 idempotent 方式,好比 GET、HEAD、PUT 和 DELETE 可以被安全的重試:若是有故障發生時,流水線的內容要能被輕易的重試。

今天,全部遵循 HTTP/1.1 的代理和服務器都應該支持流水線,雖然實際狀況中仍是有不少限制:一個很重要的緣由是,任然沒有現代瀏覽器去默認支持這個功能。

HTTP 流水線在現代瀏覽器中並非默認被啓用的:

  • Web 開發者並不能輕易的碰見和判斷那些搞怪的 代理服務器 的各類莫名其妙的行爲。
  • 正確的實現流水線式複雜的:傳輸中的資源大小,多少有效的 往返時延 RTT(Round-Trip Time) 會被用到,還有有效帶寬,流水線帶來的改善有多大的影響範圍。不知道這些的話,重要的消息可能被延遲到不重要的消息後面。這個重要性的概念甚至會演變爲影響到頁面佈局!所以 HTTP 流水線在大多數狀況下帶來的改善並不明顯。
  • 流水線受制於 隊頭阻塞 Head-of-line blocking (HOL blocking) 問題。

因爲這些緣由,流水線已經被更好的算法給代替,如 multiplexing,已經用在 HTTP/2。

HTTP/2 的長鏈接與多路複用(multiplexing)

長鏈接

在HTTP/2中,客戶端向某個域名的服務器請求頁面的過程當中,只會建立一條TCP鏈接,即便這頁面可能包含上百個資源。而以前的HTTP/1.x通常會建立6-8條TCP鏈接來請求這100多個資源。單一的鏈接應該是HTTP2的主要優點,單一的鏈接能減小TCP握手帶來的時延(若是是創建在SSL/TLS上面,HTTP2能減小不少沒必要要的SSL握手,你們都知道SSL握手很慢)。

另外咱們知道,TCP協議有個滑動窗口,有慢啓動這回事,就是說每次創建新鏈接後,數據先是慢慢地傳,而後滑動窗口慢慢變大,才能較高速度地傳,這下倒好,這條鏈接的滑動窗口剛剛變大,http1.x就創個新鏈接傳數據(這就比如人家HTTP2一直在高速上一直開着,你HTTP1.x是一輛公交車走走停停)。因爲這種緣由,讓本來就具備突發性和短時性的 HTTP 鏈接變的十分低效。

因此,HTTP2中用一條單一的長鏈接,避免了建立多個TCP鏈接帶來的網絡開銷,提升了吞吐量。

幀(frame)

HTTP/2 是基於幀(frame)的協議。採用分幀是爲了將重要信息都封裝起來, 讓協議的解析方能夠輕鬆閱讀、解析並還原信息。幀(frame) 是HTTP/2中數據傳輸的最小單位,所以幀不只要細分表達HTTP/1.x中的各個部份,也優化了HTTP/1.x表達得很差的地方,同時還增長了HTTP/1.x表達不了的方式。

HTTP/2 幀結構以下:
image

流(Stream)

HTTP/2 規範對流(stream)的定義是:HTTP/2 鏈接上獨立的、雙向的幀序列交換。你能夠將流看做在鏈接上的一系列幀,它們構成了單獨的 HTTP 請求和響應。若是客戶端想要發出請求,它會開啓一個新的流。而後,服務器將在這個流上回復。這與 h1 的請求 / 響應流程相似,重要的區別在於,由於有分幀,因此多個請求和響應能夠交錯,而不會互相阻塞。流 ID(幀首部的第 6~9 字節)用來標識幀所屬的流。

特色以下:

  • 一個HTTP/2鏈接可同時保持多個打開的流,任一端點交換幀
  • 流可被客戶端或服務器單獨或共享建立和使用
  • 流可被任一端關閉
  • 在流內發送和接收數據都要按照順序
  • 流的標識符天然數表示,1~2^31-1區間,有建立流的終端分配
  • 流與流之間邏輯上是並行、獨立存在

image

多路複用(multiplexing)

就是說在一個TCP鏈接上,咱們能夠向對方不斷髮送一個個的消息,這裏每個消息當作是一幀,而每一幀有個stream identifier 的字段標明這一幀屬於哪一個 ,而後在對方接收時,根據 stream identifier 拼接每一個 的全部幀組成一整塊數據。咱們把 HTTP/1.x 每一個請求都看成一個 ,那麼請求化成多個流,請求響應數據切成多個幀,不一樣流中的幀交錯地發送給對方,這就是HTTP/2中的 多路複用

image

從上圖咱們能夠留意到:

  • 不一樣的流在交錯發送;
  • HEADERS 幀在 DATA 幀前面;
  • 流的ID都是奇數,說明是由客戶端發起的,這是標準規定的,那麼服務端發起的就是偶數了。

多路複用讓HTTP鏈接變得很廉價,只須要建立一個新流便可,這不須要多少時間,而在 HTTP/1.x 時代卻要經歷三次握手時間或者隊首阻塞等問題。並且建立新流默認是無限制的,也就是能夠無限制的並行請求下載。不過,HTTP/2 仍是提供了 SETTINGS_MAX_CONCURRENT_STREAMS 字段在 SETTINGS 幀 上設置,能夠限制併發流數目,標準上建議不要低於 100 以保證性能。

實際的傳輸多是這樣的:
image

只看到 幀(Frame),沒有 流(Stream)嘛。

須要抽象化一些,就好理解了:

  • 每個幀可看作是一個學生,流能夠認爲是組(流標識符爲幀的屬性值),一個班級(一個鏈接)內學生被分爲若干個小組,每個小組分配不一樣的具體任務。
  • HTTP/1.x 一次請求-響應,創建一個鏈接,用完關閉;每個小組任務都須要創建一個班級,多個小組任務多個班級,1:1比例
  • HTTP/1.1 Pipeling 解決方式爲,若干個小組任務排隊串行化單線程處理,後面小組任務等待前面小組任務完成才能得到執行機會,一旦有任務處理超時等,後續任務只能被阻塞,毫無辦法,也就是人們常說的線頭阻塞
  • HTTP/2 多個小組任務可同時並行(嚴格意義上是併發)在班級內執行。一旦某個小組任務耗時嚴重,但不會影響到其它小組任務正常執行
  • 針對一個班級資源維護要比多個班級資源維護經濟多了,這也是多路複用出現的緣由。

參考

相關文章
相關標籤/搜索