HTTP/2サーバーがRFC 9218のクライアント優先度シグナルを尊重するようになった
net/http
概要
HTTP/2サーバーが、RFC 9218で定義されたクライアントの優先度シグナル(priorityヘッダーやPRIORITY_UPDATEフレーム)を受け取り、優先度の高いストリームを優先して配信できるようになった。これにより、これまでのラウンドロビン方式(優先度を無視して全ストリームに均等に書き込みを行う方式)に代わり、クライアントが望む順序でレスポンスを送出できる。
この挙動を望まず、従来通り優先度を無視したラウンドロビン配信のままにしたい場合は、Server.DisableClientPriorityをtrueに設定すればよい。
導入経緯
もともとx/net/http2にはRFC 7540に基づく優先度書き込みスケジューラが存在したが、バグが多くCPU消費も大きい上に、そのプロトコル自体がRFC 9113で非推奨化されていた(golang/go#67817)。この古いスケジューラを廃止するには、実際にそれを使っているユーザーが新方式へ移行できる代替手段が必要だったため、後継となるRFC 9218優先度スキームのサポートを追加する提案として本issueが起票された。
議論のハイライト
- 当初案では新しい
priorityWriteSchedulerのほか、PriorityUpdateFrame・SettingNoRFC7540Prioritiesといった新規エクスポートAPIの追加、およびデフォルトスケジューラのRFC 9218方式への切り替えが提案された。 - レビューでは
PriorityUpdateFrame.Priorityの型について議論があり、最終的に構造化フィールド値(Structured Field Values)を表す文字列として扱うことで決着した。既存のPriorityParam(RFC 7540由来、weight/exclusiveを持つ)とは別概念であることが確認されている。 - 「デフォルトスケジューラをRFC 9218方式に切り替えると、
incrementalのデフォルトがfalseになるため、既存のラウンドロビン的な挙動から大きく変わってしまう」という懸念に対し、クライアントが一度もpriorityを送っていない接続ではurgency=3, incremental=true(実質ラウンドロビン相当)をデフォルトとし、クライアントがpriorityを送ってきた接続に限ってincremental=falseをデフォルトにする、という緩和策で合意した。 - プロキシ/仲介者経由の接続では、公平性のためクライアントが送った
priorityをurgency=3, incremental=trueに強制上書きする设計についても質疑があり、プロキシ自身がPRIORITY_UPDATEフレームを明示的に送ることで、この上書きを回避できることが確認された。 - 提案は2026年にlikely acceptを経てacceptedとなった。既存の公開
http2.WriteSchedulerインターフェースおよびRFC 7540実装自体の非推奨化は本提案の対象外とし、golang/go#67817で別途追跡することになった。 - リリース後、
cmuxのようにTCP接続をスニッフィングしてからhttp2.Serverに引き渡す仲介ライブラリで、SETTINGS_NO_RFC7540_PRIORITIESの値が2つのSETTINGSフレーム間で0から1に変化してしまい、クライアントがRFC 9218 §9.1違反としてコネクションを切断する事例が報告された。この件は本issueとは別のissueとして追跡するようメンテナから案内されている。 - GODEBUGでの挙動固定については、この変更はGODEBUGで保護されておらず、Go 1.27にアップグレードすると新しい挙動になる(
DisableClientPriorityで無効化は可能)とメンテナが回答している。クライアントが優先度シグナルを送らない限り、サーバーの実際の挙動はGo 1.26以前とほぼ変わらない。
使用例
Before
package main
import (
"log"
"net/http"
)
func main() {
srv := &http.Server{
Addr: ":8443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}),
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}
After
package main
import (
"log"
"net/http"
)
func main() {
srv := &http.Server{
Addr: ":8443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}),
// RFC 9218 の優先度シグナルを無視し、Go 1.26 以前と同じ
// ラウンドロビン配信に固定したい場合に true を設定する。
DisableClientPriority: true,
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}
移行時の注意
- クライアントが
priorityヘッダーやPRIORITY_UPDATEフレームを一切送らない場合、実際のストリーム配信順序はGo 1.26以前とほぼ変わらないため、大多数のユーザーへの影響は小さい。 - 独自の
http2.WriteScheduler実装をConfigureServer等で明示的に設定している場合、この変更は無効(no-op)であり、DisableClientPriorityの指定も効果を持たない。 cmuxなど、http2.Serverにコネクションを渡す前に独自のSETTINGSフレームを送る仲介・多重化ライブラリを利用している場合、SETTINGS_NO_RFC7540_PRIORITIESの値が途中で変化したとみなされ、対応クライアントからPROTOCOL_ERRORで切断される可能性がある。該当する場合はDisableClientPriority: trueを設定するか、仲介ライブラリ側の対応を確認する必要がある。
実装解説
net/http.Serverに追加されたDisableClientPriorityフィールドは、内部のhttp2ServerConfig(src/net/http/http2.go)経由で内蔵HTTP/2実装(net/http/internal/http2、旧x/net/http2がstdに取り込まれたもの)に伝播する。サーバーはコネクション確立時、この設定に応じて使用する書き込みスケジューラを切り替えている。
switch {
case sc.hs.DisableClientPriority():
sc.writeSched = newRoundRobinWriteScheduler()
default:
sc.writeSched = newPriorityWriteSchedulerRFC9218()
}
(internal/http2/server.go;l=303-307)
新設されたpriorityWriteSchedulerRFC9218(internal/http2/writesched_priority_rfc9218.go;l=17)は、urgency(0〜7)とincremental(bool)の組ごとにストリームのリング(循環リスト)を保持し、urgencyが低い(=優先度が高い)ストリームから先に書き込みを行う。
デフォルトの優先度はdefaultRFC9218Priority(internal/http2/frame.go;l=1220)が決定する。RFC 9218の規定どおりであればurgency=3, incremental=falseがデフォルトだが、それではラウンドロビンから大きく挙動が変わってしまうため、接続がまだRFC 9218を意識していない(クライアントが優先度シグナルを送ってきていない)間はincremental=trueをデフォルトとし、一度でも優先度シグナルを受け取った接続に限りincremental=falseに切り替えるという段階的な既定値になっている。この判定に使うpriorityAware/hasIntermediaryという2つのフラグはserverConn(server.go;l=483-484)が保持している。