[インデックス 18882] ファイルの概要
このコミットは、Go言語の net/http/fcgi
パッケージにおけるFastCGIリクエストIDの再利用に関するバグ修正です。特に、nginxが fastcgi_keep_conn on
オプションを使用している場合に発生する、リクエストの重複処理とそれに伴うコネクション切断の問題を解決します。
コミット
commit a387f915538abbb6f5661cb39b8fccb606c5ad25
Author: Catalin Patulea <catalinp@google.com>
Date: Mon Mar 17 15:47:16 2014 -0700
net/http/fcgi: fix handling of request ID reuse
Request ID reuse is allowed by the FastCGI spec [1]. In particular nginx uses
the same request ID, 1, for all requests on a given connection. Because
serveRequest does not remove the request from conn.requests, this causes it to
treat the second request as a duplicate and drops the connection immediately
after beginRequest. This manifests with nginx option 'fastcgi_keep_conn on' as
the following message in nginx error log:
2014/03/17 01:39:13 [error] 730#0: *109 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: x.x.x.x, server: example.org, request: "GET / HTTP/1.1", upstream: "fastcgi://127.0.0.1:9001", host: "example.org"
Because handleRecord and serveRequest run in different goroutines, access to
conn.requests must now be synchronized.
[1] http://www.fastcgi.com/drupal/node/6?q=node/22#S3.3
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/76800043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a387f915538abbb6f5661cb39b8fccb606c5ad25
元コミット内容
net/http/fcgi: fix handling of request ID reuse
FastCGIの仕様ではリクエストIDの再利用が許可されています。特にnginxは、fastcgi_keep_conn on
オプションを使用すると、単一のコネクション上で全てのリクエストに対して同じリクエストID(通常は1)を使用します。serveRequest
関数が conn.requests
からリクエストを削除しないため、2番目のリクエストが重複として扱われ、beginRequest
の直後にコネクションが切断されていました。これはnginxのエラーログに「recv() failed (104: Connection reset by peer)
」というメッセージとして現れていました。
handleRecord
と serveRequest
が異なるゴルーチンで実行されるため、conn.requests
へのアクセスを同期する必要がありました。
変更の背景
この変更は、GoのFastCGI実装が、FastCGIプロトコルの重要な側面であるリクエストIDの再利用を正しく処理していなかったというバグを修正するために行われました。特に、nginxのような一般的なFastCGIクライアントが fastcgi_keep_conn on
の設定で動作する場合に問題が発生していました。
FastCGIは、単一のTCPコネクション上で複数のリクエストを多重化するためにリクエストIDを使用します。効率化のため、クライアントは同じコネクション上で以前使用したリクエストIDを再利用することがあります。しかし、GoのFastCGIハンドラは、一度処理したリクエストIDを内部のマップ conn.requests
から削除していませんでした。このため、同じリクエストIDが再利用されると、Goのハンドラはそれを新しいリクエストではなく、まだ処理中の古いリクエストの重複部分と誤認し、不正な状態としてコネクションを切断していました。
この問題は、特に高負荷な環境や、nginxとGoのFastCGIアプリケーションを組み合わせた場合に、クライアント側で「Connection reset by peer」エラーとして顕在化し、サービスの可用性に影響を与えていました。
前提知識の解説
FastCGI (Fast Common Gateway Interface)
FastCGIは、Webサーバーとアプリケーションプログラム(スクリプト)間のインタフェースを定義するプロトコルです。CGI(Common Gateway Interface)の進化版であり、CGIがリクエストごとに新しいプロセスを起動するのに対し、FastCGIは永続的なプロセスを維持し、複数のリクエストを処理することでオーバーヘッドを削減し、パフォーマンスを向上させます。
FastCGIの主要な特徴は以下の通りです。
- 永続的なプロセス: アプリケーションプロセスは起動後もメモリに常駐し、複数のリクエストを処理します。
- 多重化: 単一のTCPコネクション上で複数のリクエストを並行して処理できます。
- リクエストID: 各リクエストには一意のIDが割り当てられ、Webサーバーとアプリケーション間でリクエストとレスポンスを関連付けます。このIDは、コネクション内で再利用されることがあります。
- ロール: リクエストのタイプ(例:
FCGI_RESPONDER
は通常のWebリクエスト、FCGI_AUTHORIZER
は認証リクエストなど)を定義します。
Go言語の net/http/fcgi
パッケージ
Go言語の標準ライブラリ net/http/fcgi
は、FastCGIプロトコルを実装し、GoアプリケーションがFastCGIサーバーとして動作できるようにします。これにより、nginxやApacheなどのWebサーバーからFastCGIプロトコル経由でGoアプリケーションにリクエストを転送し、処理させることが可能になります。
このパッケージ内部では、FastCGIのレコード(ヘッダとボディからなるデータ単位)を解析し、Goの http.Request
オブジェクトに変換してアプリケーションのハンドラに渡します。また、アプリケーションからのレスポンスをFastCGIレコードに変換してWebサーバーに返します。
ゴルーチンと同期 (Goroutines and Synchronization)
Go言語のゴルーチンは、軽量な並行処理の単位です。OSのスレッドよりもはるかに少ないリソースで多数のゴルーチンを起動できます。しかし、複数のゴルーチンが共有データに同時にアクセスする場合、競合状態(Race Condition)が発生する可能性があります。これを防ぐために、Goでは sync
パッケージが提供されており、sync.Mutex
(ミューテックス)はその中でも最も基本的な同期プリミティブです。
sync.Mutex
: 相互排他ロックを提供します。Lock()
メソッドを呼び出すとロックを獲得し、Unlock()
メソッドを呼び出すとロックを解放します。ロックが獲得されている間は、他のゴルーチンはロックを獲得しようとするとブロックされ、ロックが解放されるまで待機します。これにより、共有データへのアクセスを一度に一つのゴルーチンに限定し、データの整合性を保ちます。
技術的詳細
このコミットの核心は、FastCGIリクエストIDのライフサイクル管理の改善と、それに伴う並行処理の安全性の確保です。
FastCGIプロトコルでは、リクエストは FCGI_BEGIN_REQUEST
レコードで開始され、FCGI_END_REQUEST
レコードで終了します。各リクエストには一意のIDが割り当てられますが、このIDはコネクション内で再利用されることが許されています(FastCGI仕様 3.3節)。
Goの net/http/fcgi
パッケージの child
構造体には、現在処理中のリクエストを map[uint16]*request
型の requests
フィールドで管理しています。このマップはリクエストIDをキーとしています。
問題は、serveRequest
ゴルーチンがリクエストの処理を完了した後も、この requests
マップから対応するエントリを削除していなかった点にありました。そのため、同じリクエストIDを持つ次のリクエストが到着すると、handleRecord
関数はマップ内に既存のエントリを見つけ、それを新しいリクエストではなく、まだ処理中のリクエストの続きと誤認していました。特に typeBeginRequest
の場合、既にマップにエントリが存在すると、GoのFastCGIハンドラはそれを不正な状態と判断し、コネクションを切断していました。
この修正では、以下の2つの主要な変更が行われました。
-
リクエストの削除:
serveRequest
関数がリクエストの処理を完了し、FCGI_END_REQUEST
を送信した後、c.requests
マップから該当するリクエストエントリを明示的に削除するように変更されました。これにより、リクエストIDが再利用された際に、新しいリクエストとして正しく扱われるようになります。同様に、typeAbortRequest
(リクエストの中止)の場合も、マップからリクエストが削除されるようになりました。 -
同期の導入:
handleRecord
とserveRequest
は異なるゴルーチンで実行されるため、c.requests
マップへのアクセスは競合状態を引き起こす可能性がありました。このため、child
構造体にsync.Mutex
型のmu
フィールドが追加されました。c.requests
マップへの読み書き(c.requests[rec.h.Id]
の参照、c.requests[rec.h.Id] = req
の代入、delete(c.requests, rec.h.Id)
)を行う際には、必ずc.mu.Lock()
でロックを獲得し、処理後にc.mu.Unlock()
でロックを解放するように変更されました。これにより、複数のゴルーチンが同時にマップを操作することによるデータ破損や不正な状態への遷移が防止されます。
これらの変更により、FastCGIリクエストIDの再利用が正しく処理され、nginxの fastcgi_keep_conn on
オプションを使用した場合でも、GoのFastCGIアプリケーションが安定して動作するようになりました。
コアとなるコードの変更箇所
変更は src/pkg/net/http/fcgi/child.go
ファイルに集中しています。
--- a/src/pkg/net/http/fcgi/child.go
+++ b/src/pkg/net/http/fcgi/child.go
@@ -16,6 +16,7 @@ import (
"net/http/cgi"
"os"
"strings"
+ "sync" // syncパッケージのインポートを追加
"time"
)
@@ -126,8 +127,10 @@ func (r *response) Close() error {
}
type child struct {
- conn *conn
- handler http.Handler
+ conn *conn
+ handler http.Handler
+
+ mu sync.Mutex // requestsマップを保護するためのミューテックスを追加
requests map[uint16]*request // keyed by request ID
}
@@ -157,7 +160,9 @@ var errCloseConn = errors.New("fcgi: connection should be closed")
var emptyBody = ioutil.NopCloser(strings.NewReader(""))
func (c *child) handleRecord(rec *record) error {
+\tc.mu.Lock() // マップアクセス前にロック
req, ok := c.requests[rec.h.Id]
+\tc.mu.Unlock() // マップアクセス後にアンロック
if !ok && rec.h.Type != typeBeginRequest && rec.h.Type != typeGetValues {
// The spec says to ignore unknown request IDs.
return nil
@@ -179,7 +184,10 @@ func (c *child) handleRecord(rec *record) error {
c.conn.writeEndRequest(rec.h.Id, 0, statusUnknownRole)
return nil
}
-\t\tc.requests[rec.h.Id] = newRequest(rec.h.Id, br.flags)
+\t\treq = newRequest(rec.h.Id, br.flags)
+\t\tc.mu.Lock() // マップアクセス前にロック
+\t\tc.requests[rec.h.Id] = req // 新しいリクエストをマップに追加
+\t\tc.mu.Unlock() // マップアクセス後にアンロック
return nil
case typeParams:
\t// NOTE(eds): Technically a key-value pair can straddle the boundary
@@ -220,7 +228,9 @@ func (c *child) handleRecord(rec *record) error {
\treturn nil
case typeAbortRequest:
\tprintln("abort")
+\t\tc.mu.Lock() // マップアクセス前にロック
\tdelete(c.requests, rec.h.Id) // リクエスト中止時にマップから削除
+\t\tc.mu.Unlock() // マップアクセス後にアンロック
\tc.conn.writeEndRequest(rec.h.Id, 0, statusRequestComplete)
\tif !req.keepConn {
\t\t// connection will close upon return
@@ -247,6 +257,9 @@ func (c *child) serveRequest(req *request, body io.ReadCloser) {
\tc.handler.ServeHTTP(r, httpReq)
}
r.Close()
+\tc.mu.Lock() // マップアクセス前にロック
+\tdelete(c.requests, req.reqId) // リクエスト処理完了後にマップから削除
+\tc.mu.Unlock() // マップアクセス後にアンロック
c.conn.writeEndRequest(req.reqId, 0, statusRequestComplete)
// Consume the entire body, so the host isn't still writing to
コアとなるコードの解説
-
import "sync"
の追加:child.go
の冒頭にsync
パッケージがインポートされました。これは、ミューテックス (sync.Mutex
) を使用して共有リソース (c.requests
マップ) へのアクセスを同期するために必要です。 -
child
構造体へのmu sync.Mutex
の追加:child
構造体にmu sync.Mutex
フィールドが追加されました。このミューテックスは、requests
マップへのアクセスを保護するために使用されます。コメント// protects requests:
がその目的を明確に示しています。 -
handleRecord
関数内の同期とリクエスト追加ロジックの変更:c.requests
マップからリクエストを読み取る際 (req, ok := c.requests[rec.h.Id]
)、その前後にc.mu.Lock()
とc.mu.Unlock()
が追加されました。typeBeginRequest
のケースで新しいリクエストを作成し、c.requests
マップに追加する際 (c.requests[rec.h.Id] = newRequest(...)
)、その処理の前後にc.mu.Lock()
とc.mu.Unlock()
が追加されました。これにより、複数のゴルーチンが同時にマップを更新しようとする競合状態が回避されます。
-
handleRecord
関数内のtypeAbortRequest
の同期とリクエスト削除:typeAbortRequest
のケースでリクエストが中止された際にdelete(c.requests, rec.h.Id)
を呼び出す際、その前後にc.mu.Lock()
とc.mu.Unlock()
が追加されました。これにより、中止されたリクエストがマップから安全に削除されます。
-
serveRequest
関数内の同期とリクエスト削除:serveRequest
関数は、HTTPリクエストの処理が完了し、r.Close()
が呼び出された後、c.requests
マップから該当するリクエストを削除するように変更されました。具体的には、delete(c.requests, req.reqId)
が追加されました。- この
delete
操作の前後にc.mu.Lock()
とc.mu.Unlock()
が追加され、serveRequest
ゴルーチンがマップを安全に操作できるようにしました。
これらの変更により、c.requests
マップへのすべてのアクセスがミューテックスによって保護され、リクエストの追加と削除が正しく行われるようになりました。特に、serveRequest
でのリクエスト削除は、FastCGIリクエストIDの再利用を可能にし、nginxのようなクライアントとの互換性を向上させる上で不可欠な修正です。
関連リンク
- Go言語の
net/http/fcgi
パッケージのドキュメント: https://pkg.go.dev/net/http/fcgi (コミット当時のバージョンとは異なる可能性があります) - Go言語の
sync
パッケージのドキュメント: https://pkg.go.dev/sync
参考にした情報源リンク
- FastCGI Specification (Section 3.3): http://www.fastcgi.com/drupal/node/6?q=node/22#S3.3
- Go CL 76800043: https://golang.org/cl/76800043 (このコミットに対応するGoのコードレビューページ)
- nginx fastcgi_keep_conn directive: http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_keep_conn (nginxのFastCGIモジュールのドキュメント)