Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18694] ファイルの概要

このコミットは、Goのnet/httpパッケージにServer.SetKeepAlivesEnabledメソッドを追加するものです。これにより、HTTPサーバーのKeep-Alive接続の有効/無効をプログラムから制御できるようになります。これは、サーバーのグレースフルシャットダウン(優雅な終了)機能の一部として導入されました。

コミット

commit 916682ea367a290da9392ef38016af5b8fd6a3b7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Feb 28 07:40:25 2014 -0800

    net/http: add Server.SetKeepAlivesEnabled
    
    Part of graceful shutdown.
    
    Update #4674
    
    LGTM=adg, josharian
    R=adg, josharian, r
    CC=golang-codereviews
    https://golang.org/cl/69670043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/916682ea367a290da9392ef38016af5b8fd6a3b7

元コミット内容

net/http: add Server.SetKeepAlivesEnabled

このコミットは、net/httpパッケージのServer構造体にSetKeepAlivesEnabledメソッドを追加します。これはグレースフルシャットダウンの一部です。

関連するIssue: #4674

変更の背景

この変更の主な背景は、GoのHTTPサーバーにおける「グレースフルシャットダウン」の実現です。グレースフルシャットダウンとは、サーバーが停止する際に、現在処理中のリクエストを完了させ、新しいリクエストの受け入れを停止し、既存の接続を適切にクローズするプロセスを指します。

HTTP/1.1では、パフォーマンス向上のためにKeep-Alive接続がデフォルトで有効になっています。これにより、クライアントとサーバー間で複数のリクエスト/レスポンスを単一のTCP接続上でやり取りでき、接続の確立・切断にかかるオーバーヘッドを削減できます。しかし、サーバーをシャットダウンする際には、新しい接続を受け入れないだけでなく、既存のKeep-Alive接続に対しても新しいリクエストを受け付けないようにする必要があります。

SetKeepAlivesEnabledメソッドの追加は、このグレースフルシャットダウンのメカニズムの一部として、サーバーが新しいKeep-Aliveリクエストを受け付けないようにするための手段を提供します。これにより、サーバーは既存のKeep-Alive接続を使い切った後に、それらの接続を安全に閉じることが可能になります。

コミットメッセージにあるUpdate #4674は、GoのIssueトラッカーにおけるIssue 4674: net/http: graceful shutdownに関連しています。このIssueは、GoのHTTPサーバーにおけるグレースフルシャットダウン機能の必要性を議論し、その実装に向けた様々な提案がなされていました。SetKeepAlivesEnabledはその解決策の一部として導入されました。

前提知識の解説

HTTP Keep-Alive (持続的接続)

HTTP/1.0では、各リクエスト/レスポンスのペアごとに新しいTCP接続が確立され、完了後に切断されていました。これはオーバーヘッドが大きく、特に多数の小さなリソース(画像、CSS、JavaScriptなど)をロードするWebページでは非効率でした。

HTTP/1.1では、デフォルトで「持続的接続(Persistent Connections)」、一般に「Keep-Alive」と呼ばれる機能が導入されました。これにより、クライアントとサーバーは単一のTCP接続を再利用して複数のHTTPリクエスト/レスポンスを交換できます。これにより、以下の利点があります。

  • レイテンシの削減: TCP接続の確立(3ウェイハンドシェイク)と切断(4ウェイハンドシェイク)にかかる時間を削減します。
  • ネットワークリソースの効率化: 接続の確立・切断に必要なパケット数を減らし、ネットワークの混雑を緩和します。
  • サーバー負荷の軽減: サーバーが新しい接続を頻繁に確立・切断する必要がなくなるため、CPUやメモリの使用量を削減できます。

Keep-Alive接続は、HTTPヘッダーのConnection: keep-aliveによって示されます。サーバーは、Keep-Aliveを無効にする場合や、接続を閉じたい場合にConnection: closeヘッダーを送信します。

グレースフルシャットダウン (Graceful Shutdown)

サーバーアプリケーションを停止する際、単にプロセスを強制終了するのではなく、現在処理中の作業を中断せずに完了させ、新しい作業の受け入れを停止し、リソースを適切に解放してから終了する手法をグレースフルシャットダウンと呼びます。これにより、以下の利点があります。

  • データの一貫性: 処理中のリクエストが中断されないため、データの破損や不整合を防ぎます。
  • ユーザーエクスペリエンスの向上: ユーザーはエラーページを見ることなく、リクエストが正常に完了するのを待つことができます。
  • リソースの適切な解放: データベース接続、ファイルハンドル、ネットワークソケットなどのリソースが適切に閉じられ、リークを防ぎます。
  • ダウンタイムの最小化: サービス停止時間を最小限に抑え、高可用性を維持するのに役立ちます。

HTTPサーバーのグレースフルシャットダウンでは、通常以下のステップが含まれます。

  1. 新しい接続の受け入れ停止: リスニングソケットを閉じ、新しいTCP接続の受け入れを停止します。
  2. 新しいリクエストの受け入れ停止: 既存のKeep-Alive接続に対して、新しいHTTPリクエストの受け入れを停止します。
  3. 既存リクエストの完了待機: 現在処理中のHTTPリクエストが完了するのを待ちます。
  4. 既存接続のクローズ: すべてのリクエストが完了した後、残りのアイドル状態の接続をクローズします。
  5. サーバープロセスの終了: すべてのクリーンアップが完了したら、サーバープロセスを終了します。

このコミットは、上記のステップ2、すなわち「既存のKeep-Alive接続に対して、新しいHTTPリクエストの受け入れを停止する」機能に直接貢献します。

sync/atomicパッケージ

Goのsync/atomicパッケージは、低レベルのアトミックな(不可分な)操作を提供します。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、競合状態(Race Condition)が発生しないことを保証します。これは、共有変数の読み書きを安全に行うために重要です。

このコミットでは、disableKeepAlivesというフラグをint32型で定義し、atomic.LoadInt32atomic.StoreInt32を使用してその値を読み書きしています。これにより、複数のゴルーチン(例えば、異なる接続を処理するゴルーチン)が同時にこのフラグにアクセスしても、値の整合性が保たれます。

技術的詳細

このコミットは、net/httpパッケージのServer構造体にSetKeepAlivesEnabledメソッドを追加し、その状態を内部的に管理するためのdisableKeepAlivesフィールドを導入します。

  1. Server構造体へのフィールド追加: Server構造体にdisableKeepAlives int32フィールドが追加されました。このフィールドは、Keep-Alive接続が有効かどうかを示すフラグとして機能します。int32型が使用され、sync/atomicパッケージの関数でアトミックにアクセスされることがコメントで明記されています。

  2. SetKeepAlivesEnabledメソッドの追加: func (s *Server) SetKeepAlivesEnabled(v bool)という新しいメソッドが追加されました。

    • vtrueの場合、s.disableKeepAlives0に設定し、Keep-Aliveを有効にします。
    • vfalseの場合、s.disableKeepAlives1に設定し、Keep-Aliveを無効にします。
    • これらの操作はatomic.StoreInt32を使用して行われ、複数のゴルーチンからの同時アクセスに対して安全です。
    • ドキュメントコメントには、「デフォルトではKeep-Aliveは常に有効です。非常にリソース制約のある環境や、シャットダウン中のサーバーのみがこれらを無効にすべきです。」と記載されています。
  3. doKeepAlivesヘルパーメソッドの追加: func (s *Server) doKeepAlives() boolという内部ヘルパーメソッドが追加されました。

    • このメソッドはatomic.LoadInt32(&s.disableKeepAlives) == 0を返します。つまり、disableKeepAlives0(Keep-Alive有効)であればtrueを、1(Keep-Alive無効)であればfalseを返します。
    • これにより、Keep-Aliveの状態を安全かつ効率的に取得できます。
  4. chunkWriter.writeHeaderの変更: HTTPレスポンスヘッダーを書き込むchunkWriter.writeHeaderメソッドが変更されました。

    • まず、keepAlivesEnabled := w.conn.server.doKeepAlives()を呼び出して、現在のサーバーのKeep-Alive設定を取得します。
    • HTTP/1.0 Keep-Aliveの条件変更: HTTP/1.0のKeep-Alive接続を処理するロジックに&& keepAlivesEnabledが追加されました。これにより、サーバーがKeep-Aliveを無効にしている場合、HTTP/1.0のKeep-Alive接続であってもConnection: keep-aliveヘッダーが送信されず、接続が閉じられるようになります。 変更前: if w.req.wantsHttp10KeepAlive() { 変更後: if w.req.wantsHttp10KeepAlive() && keepAlivesEnabled {
    • Connection: closeヘッダーの条件変更: レスポンスにConnection: closeヘッダーを追加するか、接続を閉じるべきかを判断するロジックに|| !keepAlivesEnabledが追加されました。これにより、サーバーがKeep-Aliveを無効にしている場合、明示的にConnection: closeヘッダーがなくても接続が閉じられるようになります。 変更前: if header.get("Connection") == "close" { 変更後: if header.get("Connection") == "close" || !keepAlivesEnabled {
    • w.closeAfterReplyの条件変更: w.closeAfterReply(レスポンス後に接続を閉じるべきかを示すフラグ)を設定するロジックにも(!keepAlivesEnabled || !hasToken(cw.header.get("Connection"), "close"))が追加されました。これは、Keep-Aliveが無効になっている場合、またはConnection: closeヘッダーがない場合に、接続を閉じることを保証します。 変更前: if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") { 変更後: if w.closeAfterReply && (!keepAlivesEnabled || !hasToken(cw.header.get("Connection"), "close")) {
  5. テストケースの追加: src/pkg/net/http/serve_test.goTestServerKeepAlivesEnabledという新しいテストケースが追加されました。

    • このテストはhttptest.NewUnstartedServerを使用してテストサーバーを作成します。
    • ts.Config.SetKeepAlivesEnabled(false)を呼び出して、サーバーのKeep-Aliveを無効にします。
    • サーバーを起動し、HTTPリクエストを送信します。
    • レスポンスのres.Closeフィールドがtrueであることをアサートします。res.Closetrueであることは、サーバーがレスポンス後に接続を閉じることを意図していることを示します。これは、Keep-Aliveが無効になっている場合に期待される動作です。

これらの変更により、GoのHTTPサーバーは、SetKeepAlivesEnabled(false)が呼び出された際に、新しいKeep-Aliveリクエストを受け付けなくなり、既存の接続もレスポンス後に適切に閉じられるようになります。これは、サーバーのグレースフルシャットダウンを実現するための重要なステップです。

コアとなるコードの変更箇所

src/pkg/net/http/server.go

// Server構造体へのdisableKeepAlivesフィールドの追加
type Server struct {
	// ... 既存のフィールド ...
	disableKeepAlives int32 // accessed atomically.
}

// doKeepAlivesヘルパーメソッドの追加
func (s *Server) doKeepAlives() bool {
	return atomic.LoadInt32(&s.disableKeepAlives) == 0
}

// SetKeepAlivesEnabledメソッドの追加
func (s *Server) SetKeepAlivesEnabled(v bool) {
	if v {
		atomic.StoreInt32(&s.disableKeepAlives, 0)
	} else {
		atomic.StoreInt32(&s.disableKeepAlives, 1)
	}
}

// chunkWriter.writeHeaderメソッド内の変更
func (cw *chunkWriter) writeHeader(p []byte) {
	// ... 既存のコード ...
	keepAlivesEnabled := w.conn.server.doKeepAlives() // 追加

	// ... 既存のコード ...

	// HTTP/1.0 Keep-Aliveの条件変更
	// 変更前: if w.req.wantsHttp10KeepAlive() {
	if w.req.wantsHttp10KeepAlive() && keepAlivesEnabled { // 変更
		// ...
	}

	// Connection: closeヘッダーの条件変更
	// 変更前: if header.get("Connection") == "close" {
	if header.get("Connection") == "close" || !keepAlivesEnabled { // 変更
		w.closeAfterReply = true
	}

	// w.closeAfterReplyの条件変更
	// 変更前: if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") {
	if w.closeAfterReply && (!keepAlivesEnabled || !hasToken(cw.header.get("Connection"), "close")) { // 変更
		// ...
	}
	// ... 既存のコード ...
}

src/pkg/net/http/serve_test.go

// TestServerKeepAlivesEnabledテストケースの追加
func TestServerKeepAlivesEnabled(t *testing.T) {
	defer afterTest(t)
	ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) {}))
	ts.Config.SetKeepAlivesEnabled(false) // Keep-Aliveを無効化
	ts.Start()
	defer ts.Close()
	res, err := Get(ts.URL)
	if err != nil {
		t.Fatal(err)
	}
	defer res.Body.Close()
	if !res.Close { // res.Closeがtrueであることを期待
		t.Errorf("Body.Close == false; want true")
	}
}

コアとなるコードの解説

このコミットの核となるのは、net/http.ServerがKeep-Alive接続を制御する新しいメカニズムです。

  1. Server.disableKeepAlives: これはServer構造体に追加されたint32型のフィールドで、Keep-Aliveが有効か無効かをアトミックに管理するためのフラグです。0は有効、1は無効を示します。sync/atomicパッケージを使用することで、複数のゴルーチンが同時にこのフラグを読み書きしても、データ競合が発生しないことが保証されます。

  2. Server.SetKeepAlivesEnabled(v bool): このパブリックメソッドは、外部からサーバーのKeep-Alive設定を変更するためのAPIです。vtrueであればKeep-Aliveを有効にし、falseであれば無効にします。内部的にはatomic.StoreInt32を使ってdisableKeepAlivesの値を設定します。これにより、サーバーのシャットダウンプロセス中に、新しいKeep-Alive接続の受け入れを停止するトリガーとして機能します。

  3. Server.doKeepAlives(): これは内部的なヘルパーメソッドで、現在のdisableKeepAlivesの状態をアトミックに読み取り、bool値として返します。このメソッドは、HTTPレスポンスヘッダーを構築する際に、Keep-Aliveを継続すべきかどうかを判断するために使用されます。

  4. chunkWriter.writeHeaderの変更: このメソッドは、HTTPレスポンスのヘッダーを実際にクライアントに書き込む役割を担っています。ここでの変更は、Server.doKeepAlives()の戻り値に基づいて、Connectionヘッダーの振る舞いを調整することです。

    • HTTP/1.0のKeep-Alive: HTTP/1.0では、Keep-Aliveはデフォルトで無効であり、Connection: keep-aliveヘッダーが明示的に存在する場合にのみ有効になります。この変更により、Server.SetKeepAlivesEnabled(false)が呼び出されている場合、たとえクライアントがHTTP/1.0でKeep-Aliveを要求しても、サーバーはそれを拒否し、接続を閉じます。
    • Connection: closeの強制: Server.SetKeepAlivesEnabled(false)が呼び出されている場合、サーバーはレスポンスにConnection: closeヘッダーを強制的に含めるか、またはレスポンス後に接続を閉じるように動作します。これにより、クライアントは現在のレスポンスを受け取った後、その接続を再利用しようとせず、新しい接続を確立する必要があることを認識します。

これらの変更は、GoのHTTPサーバーがグレースフルシャットダウンの際に、新しいKeep-Aliveリクエストを停止し、既存の接続を適切に終了させるための基盤を提供します。これにより、サーバーは稼働中のリクエストを完了させつつ、徐々に接続数を減らし、最終的に安全に停止できるようになります。

関連リンク

参考にした情報源リンク