The Go Blog

HTTP/2 サーバープッシュ

Jaana Burcu Dogan と Tom Bergan
2017年3月24日

はじめに

HTTP/2 は、HTTP/1.x の多くの欠点を解決するために設計されました。現代のウェブページは、HTML、スタイルシート、スクリプト、画像など、多くのリソースを使用します。HTTP/1.x では、これらのリソースはそれぞれ明示的にリクエストされなければなりませんでした。これは遅いプロセスになる可能性があります。ブラウザはまず HTML を取得し、ページを解析して評価するにつれて、段階的に他のリソースを学習します。サーバーはブラウザが各リクエストを行うのを待たなければならないため、ネットワークはしばしばアイドル状態になり、十分に活用されません。

レイテンシを改善するために、HTTP/2 はサーバーが明示的にリクエストされる前にリソースをブラウザにプッシュできるようにする、サーバープッシュを導入しました。サーバーは、ページが必要とする追加のリソースの多くを事前に知っていることが多く、初期リクエストに応答する際にそれらのリソースをプッシュし始めることができます。これにより、サーバーはアイドル状態のネットワークを完全に活用し、ページ読み込み時間を短縮できます。

プロトコルレベルでは、HTTP/2 サーバープッシュは PUSH_PROMISE フレームによって駆動されます。PUSH_PROMISE は、サーバーがブラウザが近い将来に行うと予測するリクエストを記述します。ブラウザが PUSH_PROMISE を受信するとすぐに、サーバーがそのリソースを配信することがわかります。ブラウザが後でこのリソースが必要であると判断した場合、新しいリクエストを送信するのではなく、プッシュが完了するのを待ちます。これにより、ブラウザがネットワークを待つ時間が短縮されます。

net/http におけるサーバープッシュ

Go 1.8 では、http.Server からの応答をプッシュするサポートが導入されました。この機能は、実行中のサーバーが HTTP/2 サーバーであり、受信接続が HTTP/2 を使用している場合に利用できます。任意の HTTP ハンドラで、http.ResponseWriter が新しい http.Pusher インターフェースを実装しているかどうかを確認することで、サーバープッシュをサポートしているかどうかをアサートできます。

たとえば、サーバーが app.js がページのレンダリングに必要であることを知っている場合、http.Pusher が利用可能であれば、ハンドラはプッシュを開始できます。

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if pusher, ok := w.(http.Pusher); ok {
            // Push is supported.
            if err := pusher.Push("/app.js", nil); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        // ...
    })

Push 呼び出しは、/app.js の合成リクエストを作成し、そのリクエストを PUSH_PROMISE フレームに合成し、その合成リクエストをサーバーのリクエストハンドラに転送します。これにより、プッシュされた応答が生成されます。Push の2番目の引数は、PUSH_PROMISE に含める追加のヘッダを指定します。たとえば、/app.js への応答が Accept-Encoding によって異なる場合、PUSH_PROMISE に Accept-Encoding の値を含める必要があります。

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if pusher, ok := w.(http.Pusher); ok {
            // Push is supported.
            options := &http.PushOptions{
                Header: http.Header{
                    "Accept-Encoding": r.Header["Accept-Encoding"],
                },
            }
            if err := pusher.Push("/app.js", options); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        // ...
    })

完全に機能する例はこちらで入手できます。

サーバーを実行し、https://:8080 を読み込むと、ブラウザの開発者ツールで app.jsstyle.css がサーバーによってプッシュされたことが表示されるはずです。

応答する前にプッシュを開始する

応答のバイトを送信する前に Push メソッドを呼び出すことをお勧めします。そうしないと、誤って重複した応答を生成してしまう可能性があります。たとえば、HTML 応答の一部を書き込んだとします。

<html>
<head>
    <link rel="stylesheet" href="a.css">...

次に、Push(“a.css”, nil) を呼び出します。ブラウザは PUSH_PROMISE を受信する前にこの HTML の断片を解析する可能性があり、その場合、ブラウザは PUSH_PROMISE を受信することに加えて、a.css のリクエストを送信します。これでサーバーは a.css に対して2つの応答を生成することになります。応答を書き込む前に Push を呼び出すことで、この可能性を完全に回避できます。

サーバープッシュを使用するタイミング

ネットワークリンクがアイドル状態のときはいつでもサーバープッシュの使用を検討してください。ウェブアプリの HTML の送信を終えたばかりですか?待機時間を無駄にせず、クライアントが必要とするリソースのプッシュを開始してください。レイテンシを減らすためにリソースを HTML ファイルにインライン化していますか?インライン化の代わりにプッシュを試してみてください。リダイレクトもプッシュを使用する良い機会です。なぜなら、クライアントがリダイレクトをたどる間、ほとんど常に無駄なラウンドトリップが発生するからです。プッシュを使用する可能性のあるシナリオはたくさんあります。まだ始まったばかりです。

いくつかの注意点に触れておかないと、説明不足になってしまいます。第一に、サーバーが権威を持つリソースのみをプッシュできます。つまり、サードパーティのサーバーや CDN でホストされているリソースをプッシュすることはできません。第二に、クライアントが本当に必要としていると確信できる場合を除き、リソースをプッシュしないでください。そうしないと、プッシュによって帯域幅を無駄にします。当然のことながら、クライアントがすでにキャッシュしている可能性が高いリソースのプッシュは避けるべきです。第三に、ページのすべてのリソースをプッシュするナイーブなアプローチは、パフォーマンスを悪化させることがよくあります。迷ったら、測定してください。

以下のリンクは、補足的な読み物として適しています。

まとめ

Go 1.8 では、標準ライブラリが HTTP/2 サーバープッシュをすぐにサポートし、ウェブアプリケーションを最適化するための柔軟性が向上します。

HTTP/2 サーバープッシュデモページで実際の動作をご覧ください。

次の記事: 開発者体験ワーキンググループの紹介
前の記事: Go 2016 調査結果
ブログインデックス