[インデックス 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サーバーのグレースフルシャットダウンでは、通常以下のステップが含まれます。
- 新しい接続の受け入れ停止: リスニングソケットを閉じ、新しいTCP接続の受け入れを停止します。
- 新しいリクエストの受け入れ停止: 既存のKeep-Alive接続に対して、新しいHTTPリクエストの受け入れを停止します。
- 既存リクエストの完了待機: 現在処理中のHTTPリクエストが完了するのを待ちます。
- 既存接続のクローズ: すべてのリクエストが完了した後、残りのアイドル状態の接続をクローズします。
- サーバープロセスの終了: すべてのクリーンアップが完了したら、サーバープロセスを終了します。
このコミットは、上記のステップ2、すなわち「既存のKeep-Alive接続に対して、新しいHTTPリクエストの受け入れを停止する」機能に直接貢献します。
sync/atomic
パッケージ
Goのsync/atomic
パッケージは、低レベルのアトミックな(不可分な)操作を提供します。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、競合状態(Race Condition)が発生しないことを保証します。これは、共有変数の読み書きを安全に行うために重要です。
このコミットでは、disableKeepAlives
というフラグをint32
型で定義し、atomic.LoadInt32
とatomic.StoreInt32
を使用してその値を読み書きしています。これにより、複数のゴルーチン(例えば、異なる接続を処理するゴルーチン)が同時にこのフラグにアクセスしても、値の整合性が保たれます。
技術的詳細
このコミットは、net/http
パッケージのServer
構造体にSetKeepAlivesEnabled
メソッドを追加し、その状態を内部的に管理するためのdisableKeepAlives
フィールドを導入します。
-
Server
構造体へのフィールド追加:Server
構造体にdisableKeepAlives int32
フィールドが追加されました。このフィールドは、Keep-Alive接続が有効かどうかを示すフラグとして機能します。int32
型が使用され、sync/atomic
パッケージの関数でアトミックにアクセスされることがコメントで明記されています。 -
SetKeepAlivesEnabled
メソッドの追加:func (s *Server) SetKeepAlivesEnabled(v bool)
という新しいメソッドが追加されました。v
がtrue
の場合、s.disableKeepAlives
を0
に設定し、Keep-Aliveを有効にします。v
がfalse
の場合、s.disableKeepAlives
を1
に設定し、Keep-Aliveを無効にします。- これらの操作は
atomic.StoreInt32
を使用して行われ、複数のゴルーチンからの同時アクセスに対して安全です。 - ドキュメントコメントには、「デフォルトではKeep-Aliveは常に有効です。非常にリソース制約のある環境や、シャットダウン中のサーバーのみがこれらを無効にすべきです。」と記載されています。
-
doKeepAlives
ヘルパーメソッドの追加:func (s *Server) doKeepAlives() bool
という内部ヘルパーメソッドが追加されました。- このメソッドは
atomic.LoadInt32(&s.disableKeepAlives) == 0
を返します。つまり、disableKeepAlives
が0
(Keep-Alive有効)であればtrue
を、1
(Keep-Alive無効)であればfalse
を返します。 - これにより、Keep-Aliveの状態を安全かつ効率的に取得できます。
- このメソッドは
-
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")) {
- まず、
-
テストケースの追加:
src/pkg/net/http/serve_test.go
にTestServerKeepAlivesEnabled
という新しいテストケースが追加されました。- このテストは
httptest.NewUnstartedServer
を使用してテストサーバーを作成します。 ts.Config.SetKeepAlivesEnabled(false)
を呼び出して、サーバーのKeep-Aliveを無効にします。- サーバーを起動し、HTTPリクエストを送信します。
- レスポンスの
res.Close
フィールドがtrue
であることをアサートします。res.Close
がtrue
であることは、サーバーがレスポンス後に接続を閉じることを意図していることを示します。これは、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接続を制御する新しいメカニズムです。
-
Server.disableKeepAlives
: これはServer
構造体に追加されたint32
型のフィールドで、Keep-Aliveが有効か無効かをアトミックに管理するためのフラグです。0
は有効、1
は無効を示します。sync/atomic
パッケージを使用することで、複数のゴルーチンが同時にこのフラグを読み書きしても、データ競合が発生しないことが保証されます。 -
Server.SetKeepAlivesEnabled(v bool)
: このパブリックメソッドは、外部からサーバーのKeep-Alive設定を変更するためのAPIです。v
がtrue
であればKeep-Aliveを有効にし、false
であれば無効にします。内部的にはatomic.StoreInt32
を使ってdisableKeepAlives
の値を設定します。これにより、サーバーのシャットダウンプロセス中に、新しいKeep-Alive接続の受け入れを停止するトリガーとして機能します。 -
Server.doKeepAlives()
: これは内部的なヘルパーメソッドで、現在のdisableKeepAlives
の状態をアトミックに読み取り、bool
値として返します。このメソッドは、HTTPレスポンスヘッダーを構築する際に、Keep-Aliveを継続すべきかどうかを判断するために使用されます。 -
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
ヘッダーを強制的に含めるか、またはレスポンス後に接続を閉じるように動作します。これにより、クライアントは現在のレスポンスを受け取った後、その接続を再利用しようとせず、新しい接続を確立する必要があることを認識します。
- HTTP/1.0のKeep-Alive: HTTP/1.0では、Keep-Aliveはデフォルトで無効であり、
これらの変更は、GoのHTTPサーバーがグレースフルシャットダウンの際に、新しいKeep-Aliveリクエストを停止し、既存の接続を適切に終了させるための基盤を提供します。これにより、サーバーは稼働中のリクエストを完了させつつ、徐々に接続数を減らし、最終的に安全に停止できるようになります。