http2-stream-optima-prioritation

談及http/2,你們都會認爲多播、服務器推送是最重要的。但是優先級調度同樣很是關鍵。儘管客戶端請求能夠經過多播一次發出,服務器相應也能夠經過多播,推送提升帶寬利用率,然而,不分主次的使用,極可能會致使頁面加載時間更長。由於高優先級的資源本應優先傳遞,卻由於多播而必須和低優先級資源競爭,致使總體延誤。css

曾有人以spdy協議作過實驗[1],準備:
1. 服務器啓用spdy
2. 客戶端關閉 webkit的資源加載調度特性( ResourceLoadScheduler), 不在節流,而是馬上發送所有請求html

這樣,原本由客戶端作的節流以便調度優先級,轉化爲由支持SPDY服務器來作調度。加上多播和壓縮,指望是會帶來性能提高的。然而事與願違:這樣作的結果比起單純的HTTP其實是更慢了。細究發現,實驗服務器(ngnix beta)並無作優先級管理。目前,nginx的SPDY並不完善的,卻被貿然的相對普遍的採用。nginx

爲何說http/2,卻以spdy舉例?由於spdy就是http/2的前身,而且一直並行演化中。git

動機

基於http實現的browser,採用的是對低優先級資源節流的作法,避免在高優先級資源沒有完成前被低優先級資源擠佔了帶寬。github

這樣作是合理的,可是也帶來了問題——就是在啓動鏈接到文檔的dom裝入之間對帶寬的利用不足:即便關鍵資源準備時間很長,網絡再空閒,期間也不能夠傳遞其餘低優先級資源。web

爲了充分利用帶寬,考慮到http2是多播的,這樣的想法就是合理的了:客戶端解析html後獲知的所有資源,設定好資源(流)優先級,一次性發出給服務器。這樣,優先級的調度就到了服務器一側:再也不由客戶端經過節流的方式來體現優先級,而是把優先級做爲一個提示,發給服務器,由服務器來完成最終的優先級調度。算法

服務器的作法能夠有不少。簡單幼稚的一個可行實現,就是按照優先級信息排出隊列,次序作出響應便可。固然,http/2的實際作法採用的是依賴樹算法(後面會細化此算法)。瀏覽器

儘管優先級信息看起來應該是一個表明優先級高低的數字字段,可是這樣的認識其實是一個誤解。優先級信息不是一個數字字段,而是3個字段。認識到這個誤解的可能存在對於理解如下內容是很是重要的。服務器

http2 客戶端發送新建的流和現有流的依賴關係和依賴權重建議給服務器,以及對現有流調整依賴關係和依賴權重的建議給服務器,由服務器根據這些信息來構建依賴流的關係樹,而後由這個依賴樹來決定爲每一個流肯定發送次序以及分配計算資源,從而完成對流定優先級的效果。網絡

比起直接了當的單一優先級數值,採用依賴流+權重的方式,能夠更好的表達瀏覽器渲染約束須要的依賴關係。渲染一個頁面自己就是一個資源傳遞依賴的過程。好比腳本會阻塞html的解析,最終的佈局依賴於外部的樣式表。

依賴樹的構建

和優先級有關的Frame有兩種,分別爲 HEADER,PRIORITATION 。新建流的終端能夠在HEADER中包含優先級信息來對流標記優先級。對於已存在的流,PRIORITATION能夠用來改變流優先級。

這些字段是:

  • E : 1位的標記用於標識流依賴是不是專用的。可選。
  • Stream Dependency : StreamID。31位流 .可選。
  • Weight : 流的8位權重標記。添加一個1-256的值來存儲流的權重。

對於HEADER,PRIORITATION而言,這3個字段都是可選的,標誌 Flags:PRIORITY 設置代表此3個字段存在與否。

能夠經過Stream Dependency 指定依賴另一個流。當指定了一個依賴流,流會做爲一個子流加入依賴樹內。好比:B流,C流依賴於A流(左下圖),若是D流經過HEADER frame建立,並同時指定依賴A流,那麼依賴樹就會變成右下圖那樣,b,d,c的次序不重要。

A                 A
      / \      ==>      /|\
     B   C             B D C

E 爲專有標記。若是這個標記被設置,就意味着當前流將會獨自佔有父親流,原本的兄弟流要成爲當前流的子流。以上圖爲例,若是D流設置了E標誌,那麼D獨自佔有A流爲父親,B,C 則成爲D的子流。

A
       A                 |
      / \      ==>       D
     B   C              / \
                       B   C

每一個依賴都會指定Weight權重,這個權重用來做爲和其餘兄弟流(一樣依賴一個流的其餘流)分配資源的籌碼。 有同一個父親的子流們應該按照權重值按比例分配資源。好比B依賴A權重4,C依賴A權重12,若是A已經完成,理想狀況下,B獲得它的1/4資源,C獲得它的3/4的資源。

依賴、併發、優先級調整的案例

假設一個index.html

<html>
  <body>
  <script src="a.js"></script>
  <img src="a.jpg" width="100" height="100"/>
  <img src="b.jpg" width="100" height="100"/>
  <link rel="stylesheet" type="text/css" href="style.css">
  </body>

依賴於a.js

document.write('<script src="b.js"></script>');

依賴於b.js:

document.write('<div>blocker</div>');

依賴於style.css:

div {
    border: 1px solid #000;
  }

這樣的狀況,現在是如何傳輸的呢?index.html被接收而後解析,文檔解析器(Document Parser)會發現a.js,發出請求而且被阻塞;接下來,探測解析器(Speculative Parser ),繼續探測發現a.jpg,b.jpg,style.css 並向服務器發出資源請求,而後a.js得到並被解析執行,發現依賴b.js,而後發出b.js資源請求,而後文檔解析器再次被阻塞... 。如圖:

client                             server
    |--------------get index.html------> |
    |<----index.html         ----------- |
    |--------------get a.js      ------> |
    |--------------get a.jpg     ------> |
    |--------------get b.jpg     ------> |
    |--------------get style.css ------> |
    |<----a.js      -------------------- |
    |--------------get b.js      ------> |
    |<----a.jpg     -------------------- |
    |<----b.jpg     -------------------- |
    |<----style.css -------------------- |
    |<----b.js      -------------------- |

分析完畢,結論也就出來了:很是低效。惟有style.css,b.js完成,整個頁面才完成加載,而關鍵的b.js由於被其餘不重要的資源(img)競爭而傳遞緩慢。

採用了優先級幀(PRIORITITION),依賴樹算法,併發技術的http2,有可能改善這樣狀況,只要適合加入 PRIORITITION 幀,按照依賴算法計算優先級便可。

client                             server
    |--------------1.get index.html----------------> |
    |<----index.html ------------------------------- |
    |--------------2.get a.js      (parent 1)-----> |
    |--------------3.get a.jpg     (parent 2)------> |
    |--------------4.get b.jpg    (parent 2) ------> |
    |--------------5.get style.css(parent 2 E)-----> |
    |<----a.js                  -------------------- |
    |--------------6.get b.js(parent 2 E)    ------> |
    |<----b.js       ------------------------------- |
    |<----style.css  ------------------------------- |
    |<----a.jpg      ------------------------------- |
    |<----b.jpg      ------------------------------- |

當1.get.index.html的時候,依賴樹爲

index.html ->root

當2.get a.js 時,在HEADER 幀內指定流依賴id爲 1 (parent 1),所以,流2依賴於流1,依賴樹爲:

a.js -> index.html ->root

相應的,3.get a.jpg

a.jpg -> a.js -> index.html ->root

相應的,4.get b.jpg

a.jpg ->|a.js -> index.html ->root
  b.jpg ->|

相應的,5.get style.css(parent 2 E),E技術前面提到的字段E ,表示獨佔父親流。

a.jpg    ->|style.css->a.js -> index.html ->root
  b.jpg    ->|

相應的,6.get b.js(parent 2 E),E表示獨佔。

a.jpg    ->|style.css->b.js->a.js -> index.html ->root
  b.jpg    ->|

這樣,就能夠在分析(動態)發現更高優先級資源的時候,調整依賴樹,從而調整優先級次序,保證瀏覽器馬上需求的資源能夠沒必要和晚點要的資源發生帶寬競爭。

Webkit 的傳統資源優先級方法:代碼實例

webkit的ResourceLoadSchedule,就是首先載入影響繪製的html,js,css,第一次繪製完畢,dom加載完成,才放出更多的資源(如img)請求。

https://github.com/adobe/chromium/blob/master/content/browser/renderer_host/resource_dispatcher_host_impl.cc 

net::RequestPriority DetermineRequestPriority(ResourceType::Type type) {

  switch (type) {
    case ResourceType::MAIN_FRAME:
    case ResourceType::SUB_FRAME:
      return net::HIGHEST;
    case ResourceType::STYLESHEET:
    case ResourceType::SCRIPT:
    case ResourceType::FONT_RESOURCE:
      return net::MEDIUM;
    case ResourceType::SUB_RESOURCE:
    case ResourceType::OBJECT:
    case ResourceType::MEDIA:
    case ResourceType::WORKER:
    case ResourceType::SHARED_WORKER:
    case ResourceType::XHR:
      return net::LOW;


    case ResourceType::IMAGE:
    case ResourceType::FAVICON:
      return net::LOWEST;

    case ResourceType::PREFETCH:
    case ResourceType::PRERENDER:
      return net::IDLE;

    default:
      NOTREACHED();
      return net::LOW;
  }
}

go http2 依賴樹構建測試代碼

來自於 :https://github.com/bradfitz/http2/blob/master/priority_test.go

func TestPriority(t *testing.T) {
  // A -> B
  // move A's parent to B
  streams := make(map[uint32]*stream)
  a := &stream{
    parent: nil,
    weight: 16,
  }
  streams[1] = a
  b := &stream{
    parent: a,
    weight: 16,
  }
  streams[2] = b
  adjustStreamPriority(streams, 1, PriorityParam{
    Weight:    20,
    StreamDep: 2,
  })
  if a.parent != b {
    t.Errorf("Expected A's parent to be B")
  }
  if a.weight != 20 {
    t.Errorf("Expected A's weight to be 20; got %d", a.weight)
  }
  if b.parent != nil {
    t.Errorf("Expected B to have no parent")
  }
  if b.weight != 16 {
    t.Errorf("Expected B's weight to be 16; got %d", b.weight)
  }
}

go http2 專有標誌的演示

實現了final的協議,所以能夠看到依賴樹的測試代碼是比較完善的,很有演示概念的價值
來自於 :https://github.com/bradfitz/http2/blob/master/priority_test.go

func TestPriorityExclusiveZero(t *testing.T) {
  // A B and C are all children of the 0 stream.
  // Exclusive reprioritization to any of the streams
  // should bring the rest of the streams under the
  // reprioritized stream
  streams := make(map[uint32]*stream)
  a := &stream{
    parent: nil,
    weight: 16,
  }
  streams[1] = a
  b := &stream{
    parent: nil,
    weight: 16,
  }
  streams[2] = b
  c := &stream{
    parent: nil,
    weight: 16,
  }
  streams[3] = c
  adjustStreamPriority(streams, 3, PriorityParam{
    Weight:    20,
    StreamDep: 0,
    Exclusive: true,
  })
  if a.parent != c {
    t.Errorf("Expected A's parent to be C")
  }
  if a.weight != 16 {
    t.Errorf("Expected A's weight to be 16; got %d", a.weight)
  }
  if b.parent != c {
    t.Errorf("Expected B's parent to be C")
  }
  if b.weight != 16 {
    t.Errorf("Expected B's weight to be 16; got %d", b.weight)
  }
  if c.parent != nil {
    t.Errorf("Expected C to have no parent")
  }
  if c.weight != 20 {
    t.Errorf("Expected C's weight to be 20; got %d", b.weight)
  }
}

ref

[1]. Proposal for Stream Dependencies in SPDY
https://docs.google.com/document/d/1pNj2op5Y4r1AdnsG8bapS79b11iWDCStjC...

[2]. draft-ietf-httpbis-http2-15 - Hypertext Transfer Protocol version 2 - https://tools.ietf.org/html/draft-ietf-httpbis-http2-15#section-5.3

[3]. Prioritization Is Critical To SPDY - Insouciant - https://insouciant.org/tech/prioritization-is-critical-to-spdy/

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息