Upgrade to Pro — share decks privately, control downloads, hide ads and more …

そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHT...

Avatar for Yusa Matsuda Yusa Matsuda
September 14, 2026

そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を実プロダクト障害から学び直す

Go Conference 2026の登壇で使ったスライドです。
https://gocon.jp/2026/timetable/1264230/

Avatar for Yusa Matsuda

Yusa Matsuda

September 14, 2026

Other Decks in Programming

Transcript

  1. ⾃⼰紹介 松⽥ 祐佐 (まつだ ゆうさ) REALITY株式会社 バックエンドエンジニア • 2020年グリー株式会社(現グリーホールディングス株式会社)新卒⼊社 •

    2022年からグリーグループのREALITY株式会社で VTuberの配信プラットフォームアプリ「REALITY」の開発に従事 REALITY上でのアバターです! 2
  2. あるネットワーク上の障害 背景 • • REALITYでは、親会社であるグリーグループで広く使われている課⾦‧認証基盤を利⽤ この課⾦‧認証基盤のAPIサーバに対して、REALITYのサーバからHTTP/2でAPIリクエストを 投げる形で利⽤ 起きた障害 • •

    ある⽇突然、課⾦認証基盤へのAPIリクエストが、context.Contextを⽤いてREALITY側で設定した タイムアウトにより⼤量に失敗する現象が発⽣ エラーメッセージは「context deadline exceeded」 5
  3. 前提: システムのアーキテクチャ REALITY 課⾦‧認証基盤 k8s Pod API サーバ HTTP/2 ロード

    バランサ Pod API サーバ API サーバ Pod ここ起因で障害が起きた! 7
  4. 問題が起きていたコード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... req, err := http.NewRequestWithContext( ctx, "POST", "/api/v1/coin/issue", bytes.NewReader(data) ) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { return err } } 9
  5. 問題が起きていたコード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... req, err := http.NewRequestWithContext( ctx, "POST", "/api/v1/coin/issue", bytes.NewReader(data) ) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { return err } } 10
  6. 問題が起きていたコード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... req, err := http.NewRequestWithContext( ctx, "POST", "/api/v1/coin/issue", bytes.NewReader(data) ) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { return err } } 11
  7. 問題が起きていたコード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... req, err := http.NewRequestWithContext( ctx, "POST", "/api/v1/coin/issue", bytes.NewReader(data) ) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { return err } } 12
  8. 問題が起きていたコード func createHTTPClient() *http.Client { transport := http.DefaultTransport.(*http.Transport).Clone() … (DialContextだったり

    IdleConnTimeoutだったりその他パラメータも調整 ) transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: 10 * time.Second, PingTimeout: 10 * time.Second, } return &http.Client{ Transport: transport, } } var client = createHTTPClient() 13
  9. 問題が起きていたコード func createHTTPClient() *http.Client { transport := http.DefaultTransport.(*http.Transport).Clone() … (DialContextだったり

    IdleConnTimeoutだったりその他パラメータも調整 ) transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: 10 * time.Second, PingTimeout: 10 * time.Second, } return &http.Client{ Transport: transport, } } AIに推奨されたパラメータに推奨値をそのまま⼊れていた var client = createHTTPClient() 14
  10. 対策:リトライを⼊れた擬似コード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { for i := range 2 { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { ... continue } } return errors.New("リトライしても失敗 ") } 17
  11. 対策:リトライを⼊れた擬似コード var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { for i := range 2 { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { ... continue } } return errors.New("リトライしても失敗 ") } 18
  12. 解決につながりそうな仮説 • 仮説:今のリトライ処理はネットワーク的に不調なコネクションを使いまわしている のではないか? • ということは 使⽤しているコネクションがネットワーク不良の時、 「http2: client connection

    lost」が出る(つまり切断する)ようにしてから、 APIリクエストを送ればうまくいくのではないか HTTP/2とnet/httpのコネクションの死活管理の仕組みについて学んでいこう 22
  13. HTTP/2におけるnet/httpのclient.Do概観 コネクションプール map[string][]*ClientConn 送信⽤ goroutine A client.Do A “example.com:443” コネクション1

    送信⽤ goroutine B Aのフレーム Bのフレーム Aのフレーム Aのフレーム リクエスト Bのフレーム レスポンス ⋮ client.Do B 受信ループ⽤ goroutine The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). ⋮ 29
  14. HTTP/2におけるnet/httpのclient.Do概観 コネクションプール map[string][]*ClientConn 送信⽤ goroutine A client.Do A “example.com:443” コネクション1

    送信⽤ goroutine B Aのフレーム Bのフレーム Aのフレーム リクエスト Bのフレーム レスポンス ⋮ http2: client connection lost client.Do B のキーマン 受信ループ⽤ goroutine The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). Aのフレーム ⋮ 30
  15. なぜこのヘルスチェック処理が必要なのか • TCPの切断は必ずしも相⼿に通知されるとは限らない ◦ 経路上の障害、ロードバランサの退役など • 上記のような場合、⽚⽅だけが「切れた」と認識している状態が発⽣する →「half-open connections」 ◦

    ◦ 送信側: ローカルのバッファに書き込めてしまい、エラーにならない 受信: 何も届かないだけなのでレスポンスに時間がかかっているだけの場合と区別がつかない • したがってhalf-open connectionsの場合には能動的に検知をしなければ、気づきよ うがない このヘルスチェック処理はhttp.DefaultClientでは動かないような設定になっている 32
  16. net/httpが⽤意する能動的なコネクション死活管理の仕組み • ヘルスチェック処理は以下のようなステップで⾏われる ◦ 受信ループのgoroutineで⼀定時間フレームを読み込んでいない場合にヘルスチェック処理を発⽕ ◦ 受信ループが対応しているコネクション上でPingフレームという制御⽤のフレームを送る ◦ Pingフレームに対し、⼀定時間以内に ▪

    ACKが返された場合はコネクションを「Healthy」とする ▪ ACKが返されなかった場合はコネクションを「Unhealthy」とする ◦ 「Unhealthy」となったコネクションはコネクションプールから除外され、そのコネクション上のス トリームを使っていた処理は「http2: client connection lost」エラーで中断される 33
  17. net/httpのヘルスチェック処理 送信⽤ goroutine A client.Do A コネクション1 Aのフレーム Aのフレーム The

    Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). Aのフレーム リクエスト レスポンス 受信ループ⽤ goroutine 34
  18. net/httpのヘルスチェック処理 受信ループのgoroutineで⼀定時間フレームを読み込んでいない場合にヘルスチェック処理を発⽕ 送信⽤ goroutine A コネクション1 Aのフレーム client.Do A リクエスト

    レスポンス 最近フレーム読み込んでないな... The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). 受信ループ⽤ goroutine 35
  19. net/httpのヘルスチェック処理 受信ループのgoroutineで⼀定時間フレームを読み込んでいない場合にヘルスチェック処理を発⽕ 送信⽤ goroutine A コネクション1 Aのフレーム client.Do A リクエスト

    レスポンス ヘルスチェック⽤ goroutine 起動 The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). 念の為ヘルスチェックするか! 受信ループ⽤ goroutine 36
  20. net/httpのヘルスチェック処理 受信ループが対応しているコネクション上でPingフレームという制御⽤のフレームを送る 送信⽤ goroutine A client.Do A コネクション1 Pingフレーム Aのフレーム

    リクエスト レスポンス ヘルスチェック⽤ goroutine The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). 受信ループ⽤ goroutine 37
  21. net/httpのヘルスチェック処理 ⼀定時間内にACKが返された場合はコネクションを「Healthy」とする 送信⽤ goroutine A コネクション1 client.Do A Pingフレーム Ping

    ACK Aのフレーム リクエスト レスポンス ヘルスチェック⽤ goroutine 通知 ⼀定時間内にPing ACK 返ってきました! The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). よし!コネクション⼤丈夫だな! 受信ループ⽤ goroutine 38
  22. net/httpのヘルスチェック処理 ⼀定時間内にACKが返されなかった場合はコネクションを「Unhealthy」とする 送信⽤ goroutine A コネクション1 client.Do A Pingフレーム Aのフレーム

    リクエスト レスポンス ヘルスチェック⽤ goroutine 通知 ⼀定時間内にPing ACK 返ってきませんでした... The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). え、このコネクションダメだ... 受信ループ⽤ goroutine 39
  23. net/httpのヘルスチェック処理 「Unhealthy」となったコネクションはコネクションプールから除外され、 そのコネクション上のストリームを使っていた処理は「http2: client connection lost」エラーで中断される client.Do A http2: client

    connection lost 除外だ! このコネクション使っているやつに連絡! The Go gopher was designed by Renée French. Gopher illustrations by Egon Elbre (CC0). 受信ループ⽤ goroutine 40
  24. net/httpが⽤意する能動的なコネクション死活管理の仕組み(再掲) • ヘルスチェック処理は以下のようなステップで⾏われる ◦ 受信ループのgoroutineで⼀定時間フレームを読み込んでいない場合にヘルスチェック処理を発⽕ ◦ 受信ループが対応しているコネクション上でPingフレームという制御⽤のフレームを送る ◦ Pingフレームに対し、⼀定時間以内に ▪

    ACKが返された場合はコネクションを「Healthy」とする ▪ ACKが返されなかった場合はコネクションを「Unhealthy」とする ◦ 「Unhealthy」となったコネクションはコネクションプールから除外され、そのコネクション上のス トリームを使っていた処理は「http2: client connection lost」エラーで中断される 41
  25. net/httpが⽤意する能動的なコネクション死活管理の仕組み(再掲) • ヘルスチェック処理は以下のようなステップで⾏われる ◦ 受信ループのgoroutineで⼀定時間フレームを読み込んでいない場合にヘルスチェック処理を発⽕ ◦ 受信ループが対応しているコネクション上でPingフレームという制御⽤のフレームを送る http.HTTP2Config.SendPingTimeout ◦ Pingフレームに対し、⼀定時間以内に

    ▪ ACKが返された場合はコネクションを「Healthy」とする ▪ ACKが返されなかった場合はコネクションを「Unhealthy」とする ◦ 「Unhealthy」となったコネクションはコネクションプールから除外され、そのコネクション上のス トリームを使っていた処理は「http2: client connection lost」エラーで中断される http.HTTP2Config.PingTimeout 42
  26. SendPingTimeoutとPingTimeoutを使えばやりたいことができそう 元々の仮説 使⽤しているコネクションがネットワーク不良であるならば、「http2 client connection lost」を出 るようにしてから、APIリクエストを投げればうまくいくのではないか 導出したアプローチ • •

    • リクエストを投げてから終わるまでの間に、ヘルスチェックが終了することを保証すれば良い そうすると⼀回⽬のリクエストのタイムアウト前までに 「http2: client connection lost」エラーが出る(コネクションが死んでいる場合) その状態でリトライしたら、新しいコネクションを使ってくれるのでうまくいくようになるは ず! 43
  27. 最終的なコード:SendPingTimeoutとPingTimeoutの設定値 func createHTTPClient() *http.Client { transport := http.DefaultTransport.(*http.Transport).Clone() … (DialContextだったり

    IdleConnTimeoutだったりその他パラメータも調整 ) transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: 5 * time.Second, PingTimeout: 3 * time.Second, } return &http.Client{ SendPingTimeout: 10秒から5秒に PingTimeout: 10秒から3秒に Transport: transport, } } var client = createHTTPClient() 47
  28. 最終的なコード:API呼び出し部分 var client = createHTTPClient() // デフォルトからカスタマイズした http.Client // コインを付与する処理

    func issueCoin(coinAmount uint) error { for i := range 2 { ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) ... res, err := client.Do(req) // コインを付与する APIを叩く if err != nil { ... continue } } return errors.New("リトライしても失敗 ") SendPingTimeout(5s) + PingTimeout(3s) <Contextのタイムアウト(10s) が成⽴ マージン⽤に2s確保されている } 48
  29. 結果 • うまくいった! ◦ ◦ ⼀回⽬のリクエストで失敗する際にエラーが「context deadline exceeded」から「http2: client connection

    lost」に変わった ⼆回⽬のリクエスト(リトライ)が成功するように! • 今回の障害に起因する補填作業の件数が0になった! • ここまできてようやく最初の設定でうまくいかない理由もわかった 49
  30. 問題が起きていたコード(再掲) func createHTTPClient() *http.Client { transport := http.DefaultTransport.(*http.Transport).Clone() … (DialContextだったり

    IdleConnTimeoutだったりその他パラメータも調整 ) transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: 10 * time.Second, PingTimeout: 10 * time.Second, } return &http.Client{ Transport: transport, } } var client = createHTTPClient() 50
  31. 問題が起きていたコード(再掲) func createHTTPClient() *http.Client { transport := http.DefaultTransport.(*http.Transport).Clone() … (DialContextだったり

    IdleConnTimeoutだったりその他パラメータも調整 ) transport.HTTP2 = &http.HTTP2Config{ SendPingTimeout: 10 * time.Second, PingTimeout: 10 * time.Second, } return &http.Client{ Transport: transport, } } SendPingTimeoutが PingTimeoutがともに10秒 つまりContextのタイムアウト時間より⼤ var client = createHTTPClient() 51
  32. まとめ • HTTP/2は仕組み上、コネクション1本あたりの⽣死が与える影響が⼤きいので、HTTP/1のときよりさら に慎重な死活管理が求められる • コネクション不良を検知できない状態でリトライをしても死んだコネクションが使われ続けるリスクが ある • HTTPクライアントの各種パラメータは適当な値ではなく、意思をもって根拠のある値を設定することが ⼤事

    ◦ そのためにはネットワークの基礎であったりnet/httpの実装を深掘りするのがなんだかんだ近道 今一度自分たちの HTTPClientのコードにどのようなパラメータ設定が されているか確認していただき、改めてそのパラメータにしている理由などに想 いを馳せるきっかけになれば幸いです。 53