Subscribe to receive notifications of new posts:

Subscription confirmed. Thank you for subscribing!

「Go net/http タイムアウト」の完全ガイド

Loading...

GoでHTTPサーバーまたはクライアントを書くとき、タイムアウトは、最も間違えやすく、そして最も軽微な間違えです。選択する対象が数多くあり、間違えても、ネットワークの不具合やプロセスがハングアップするまで、長い間、何の影響もありません。

HTTPは複雑な多段階プロトコルであるため、タイムアウトには万能な解決策はありません。ストリーミングエンドポイントと JSON API とCometエンドポイントを考えてみてください。確かに、デフォルトは多くの場合、あなたが望むものではありません。

この記事では、お客様がタイムアウトを適用する必要があるかもしれないさまざまな段階を取り出し、サーバーとクライアント側の両方で、これを行う色々な方法を見ていきます。

SetDeadline

まず、タイムアウトを実装するためにGoが公開するネットワークプリミティブの「期限」について知る必要があります。

net.ConnのSet[Read|Write]Deadline(time.Time)メソッドで公開されているように、期限は絶対時間で、期限に達するとすべてのI/O操作がタイムアウトエラーで失敗します。

期限はタイムアウトではありません。一度セットされると、ずっと機能したままになります(または SetDeadlineに次の呼び出しがくるまで)、その間接続が使用されているか、どのように使用されているかは関係ありません。ですから、 SetDeadlineを使ってタイムアウトを構築するには、すべての読み/書き操作の前にそれを呼び出す必要があります。

お客様は、おそらく自らSetDeadlineを呼び出したいとは考えないはずです。代わりにnet/httpに、より高いレベルのタイムアウトを使って呼び出してもらうことになるでしょう。しかし、すべてのタイムアウトは期限付きで実装され、データが送受信されるたびにリセットわけではないことに注意してください。

サーバーのタイムアウト

ブログ記事「So you want to expose Go on the Internet」(Goをインターネットで公開する) では、サーバータイムアウトに関する情報を多く扱っており、特に、HTTP/2 とGo 1.7のバグについての詳細な説明をしています。

HTTP server phases

インターネットに公開されるHTTP サーバーが、クライアント接続でタイムアウトを実行することは重要です。タイムアウトが実行されなければ、クライアントが非常に遅かったり消えたりして、ファイル記述子の漏えいにつながる可能性があり、最終的に次のようなエラーが生じることになります。

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms

http.Serverでは、 ReadTimeoutWriteTimeoutの2つのタイムアウトが公開されています。この二つを設定するには、サーバーを明示的に使用して行います。

srv := &http.Server{
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

ReadTimeoutは、接続が許可されてからリクエスト本文が完全に読まれるまでの時間をカバーします(本文を読む場合、読まない場合はヘッダーの終わりまで)。これは「許可」した直後に SetReadDedlineを呼び出すことで net/httpで実装されます。

WriteTimeoutは、通常、リクエストヘッダーの読み込みが完了した後から応答書き込みの最後(ServeHttpの有効期間)までの時間をカバーします。 readRequestの最後に SetWriteDeadlineを呼び出すことでこれを行います。

ただし、接続がHTTPSの場合、「許可」の直後に SetWriteDeadlineが呼び出されます。これは、TLSハンドシェイクの一部として書かれたパケットをカバーするためです。やっかいなことに、これは(この場合のみ) WriteTimeoutに最終的にヘッダーの読み込みと最初のバイトの待ち時間が含まれることを意味します。

信頼できないクライアントやネットワークを処理する場合は、両方のタイムアウトを設定する必要があります。これにより、書き込みや読み取りが遅くなることでクライアントの接続が断たれないようにするのです。

最後に、 http.TimeoutHandlerがあります。これは、サーバーパラメーターではなく、ハンドラーラッパーで、  ServeHTTPの呼び出しの最大時間を制限するものです。これは、応答をバッファリングし、期限を超えた場合は、504 Gateway Timeoutを送信します。1.6では壊れていましたが、1.6.2では修正されたことに注意してください。

http.ListenAndServeのやり方は間違っている

ちなみに、これは http.Serverhttp.ListenAndServe、  http.ListenAndServeTLS、  http.Serveのようにバイパスさせるパッケージレベルの利便性はパブリックインターネットサーバーには適さないという意味です。

これらの機能は、デフォルトでタイムアウト値をオフのままにし、これを有効にする方法もないため、これらの機能を使用すると、すぐに接続がリークし、ファイル記述子が不足します。私自身、少なくとも6回はこのミスを経験しました。

その代わりに、数段落前で示したように、 ReadTimeoutWriteTimeoutでhttp.Serverインスタンスを作って、その対応するメソッドを使用します。

ストリーミングについて

非常にやっかいなことに、 ServeHTTPから net.Connの基盤となるものにアクセスする方法がないため、応答をストリームしようとするサーバーが、 WriteTimeoutを取り消さざるを得なくなります(これが、デフォルトで0になっている理由とも関係しているかもしれません)。その理由は、 net.Connアクセスがなければ、各「Write(書き込み)」が、適切なアイドル(完全ではない)タイムアウトを実装する前に、 SetWriteDeadlineを呼び出す方法がないためです。

また、ブロックされた ResponseWriter.Writeをキャンセルする方法もありません。それは、 ResponseWriter.Close (インターフェイスのアップグレードを介してアクセス可能)が、同時書き込みを解除するように文書化されていないためです。したがって、 Timerを使って手動でタイムアウトを構築する方法もないのです。

残念ながら、これはストリーミングサーバーが低速なクライアントから身を守ることができないことを意味します。

私は問題とともにいくつかの提案をあげておりますので、そちらで、ぜひフィードバックをお願いします。

クライアントのタイムアウト

HTTP Client phases

クライアント側のタイムアウトは、使用するタイムアウトによって、より簡単になる場合もあれば、はるかに複雑になる場合もありますが、リソースの漏えいやフリーズを防止するためにも同様に重要です。

最も使いやすいのは、http.ClientTimeoutフィールドです。これはダイアルから(接続が再利用されていない場合)本文の読み込みまで、交換(exchange)全体をカバーするものです。

c := &http.Client{
    Timeout: 15 * time.Second,
}
resp, err := c.Get("https://blog.filippo.io/")

上記のサーバー側のケースと同様に、 http.Getのようなパッケージレベルの機能はクライアントをタイムアウトなしで使用するため、オープンなインターネットでの使用は危険です。

他にもきめ細かな制御があり、お客様側で設定できるタイムアウトは他にも多くあります。

  • net.Dialer.Timeoutは、TCP接続の確立に要する時間を制限します (新しい接続が必要な場合)。
  • http.Transport.TLSHandshakeTimeoutは、TLSハンドシェイクの実行に要する時間を制限します。
  • http.Transport.ResponseHeaderTimeoutは、レスポンスのヘッダーを読み取る時間を制限します。
  • HTTP.Transport.ExpectContinueTimeoutは、「Expect:100-continue」を含むリクエストヘッダーを送信してから本文を送信する許可を受けるまでの間、クライアントが待機する時間を制限します。1.6でこれを設定すると、HTTP/2が無効になること注意してください。(DefaultTransportは、1.6.2から特別なケースになっています)。
c := &http.Client{
    Transport: &http.Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

私が知る限り、リクエストを送信するのにかかる時間を具体的に制限する方法はありません。リクエスト本文の読み取りにかかる時間は、time.Timerで手動でコントロールすることができます。これは、クライアントメソッドが返された後の出来事だからです(リクエストをキャンセルする方法については、以下を参照してください)。

最後に、新しい1.7にはhttp.Transport.IdleConnTimeoutがあります。これは、クライアントリクエストのブロッキングフェーズを制御するものではありませんが、アイドル接続が、接続プールで待機する時間を制御します。

デフォルトでは、クライアントはリダイレクトに従うことに留意してください。  http.Client.Timeoutには、リダイレクト後に費やされるすべての時間が含まれますが、詳細なタイムアウトはリクエストごとに固有です。これは、 http.Transport がリダイレクトの概念をもたない下位レベルのシステムのためです。

キャンセルとコンテキスト

net/httpでは、クライアントリクエストを取り消す方法が2つあります。 Request.Cancelと、1.7の新機能、Contextです。

Request.Cancelはオプショナルなチャネルで、設定してから閉じると、まるで Request.Timeout が選択されたように、リクエストが停止されます。(これらは実際に同じメカニズムで実装されますが、この記事を書いている間に、1.7で バグを1つみつけました。そのバグでは、すべてのキャンセルがタイムアウトエラーとして返されます。)

より細かいタイムアウトの設定を行うには、 Request.Canceltime.Timerを使用することができます。これならストリーミングが許可され、本文からデータを読み込むたびに、期限が引き伸ばされます。

package main

import (
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func main() {
	c := make(chan struct{})
	timer := time.AfterFunc(5*time.Second, func() {
		close(c)
	})

        // Serve 256 bytes every second.
	req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Cancel = c

	log.Println("Sending request...")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	log.Println("Reading body...")
	for {
		timer.Reset(2 * time.Second)
                // Try instead: timer.Reset(50 * time.Millisecond)
		_, err = io.CopyN(ioutil.Discard, resp.Body, 256)
		if err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}
	}
}

上記の例では、5秒のタイムアウトをリクエストを実行するフェーズに設定していますが、8ラウンドで本文を読み取るのに少なくとも8秒間を費やし、そのたびに2秒のタイムアウトが必要になります。このようにしながら、途切れることなく、ストリーミングを永久に続けることができます。本文のデータの受信に2秒以上かかる場合は、io.CopyNが「 net/http: request canceled」を返します。

1.7では、contextパッケージはスタンダードライブラリに進化しています。そこでは コンテキストに関する情報がたくさんありますが、ここでコンテキストは、Request.Cancelに代わるもので、非推奨であることを理解してください。

コンテキストを使ってリクエストをキャンセルするには、 context.WithCancelで、新しいコンテキストと cancel()機能を入手して、 Request.WithContextと結び付けられているリクエストを作成します。リクエストをキャンセルしたい場合には、  cancel()を呼び出すことで、コンテキストをキャンセルします。 (キャンセルチャネルを閉じる代わりにこれを行います)。

ctx, cancel := context.WithCancel(context.TODO())
timer := time.AfterFunc(5*time.Second, func() {
	cancel()
})

req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
if err != nil {
	log.Fatal(err)
}
req = req.WithContext(ctx)

コンテキストには利点があり、親コンテキスト(我々がcontext.WithCancelに送信したもの)がキャンセルされると、我々の分もキャンセルされ、コマンドがパイプライン全体に伝達されます。

これでおしまいです。読者のみなさまのReadDeadlineを超過していないといいのですが!

もし、Goスタンダードライブラリに関する詳細情報などにご関心がおありでしたら、当社では新しい人材を、ロンドン、オースティン(TX)、シャンペーン(IL)、サンフランシスコ、シンガポールで募集中です。

Cloudflareは 企業のネットワーク全体 を保護し、お客様が インターネット規模のアプリケーションを効率的に 構築するためのお手伝いをします。また、すべての Webサイトまたはインターネットアプリケーション を迅速化し、 DDoS攻撃を軽減して、 ハッカーを封じ込めます 。さらに、 Zero Trustを始める、あるいは導入のあらゆるフェーズにいる お客様を支援します。

インターネットを高速化し、安全性を高めるには、ご使用のデバイスから 1.1.1.1 にアクセスすることで、Cloudflareの無料アプリをご利用いただけます。

より良いインターネットの構築を支援するというCloudflareの使命について詳しくは、 こちら をご覧ください。新たなキャリア形成をお考えの方は、 求人情報 にアクセスしてください。

日本語 信頼性 API (JP) JSON (JP) プログラミング

Follow on Twitter

Filippo Valsorda |@filosottile
Cloudflare |Cloudflare

Related Posts

July 06, 2022 1:55PM

2022年第2四半期におけるDDoS攻撃の傾向

2022年第2四半期DDoSレポートへようこそ。このレポートには、グローバルなCloudflareネットワークで観測されたDDoS脅威の状況についてのインサイトと動向が含まれています。また、本レポートのインタラクティブ版はRadarでご覧いただけます。...

June 20, 2022 2:28PM

Area 1の脅威指標をCloudflare Zero Trustで利用可能に

Area 1が持つフィッシング攻撃のTTP(戦術、テクニック、手順)に関する膨大なデータセット、シードインフラ、脅威モデルと、Cloudflareが持つ大規模ネットワーク、DNS、メール、Webトラフィックの送信元に関するグローバルな洞察を合わせて利用できるようになりました...

June 20, 2022 2:14PM

メールゲートウェイをCloudflare Area 1 に置き換えるには?

従来のSEGアーキテクチャは、昔のメール環境を念頭に構築されたものです。クラウドネイティブで先制的なメールセキュリティへ、シームレスに移行する方法を学びましょう...