Slide 1

Slide 1 text

そのリトライ、 死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を 実プロダクト障害から学び直す 2026年09⽉11⽇(⾦) Go Conference 2026 松⽥ 祐佐 / REALITY株式会社

Slide 2

Slide 2 text

⾃⼰紹介 松⽥ 祐佐 (まつだ ゆうさ) REALITY株式会社 バックエンドエンジニア ● 2020年グリー株式会社(現グリーホールディングス株式会社)新卒⼊社 ● 2022年からグリーグループのREALITY株式会社で VTuberの配信プラットフォームアプリ「REALITY」の開発に従事 REALITY上でのアバターです! 2

Slide 3

Slide 3 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 3

Slide 4

Slide 4 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 4

Slide 5

Slide 5 text

あるネットワーク上の障害 背景 ● ● REALITYでは、親会社であるグリーグループで広く使われている課⾦‧認証基盤を利⽤ この課⾦‧認証基盤のAPIサーバに対して、REALITYのサーバからHTTP/2でAPIリクエストを 投げる形で利⽤ 起きた障害 ● ● ある⽇突然、課⾦認証基盤へのAPIリクエストが、context.Contextを⽤いてREALITY側で設定した タイムアウトにより⼤量に失敗する現象が発⽣ エラーメッセージは「context deadline exceeded」 5

Slide 6

Slide 6 text

前提: システムのアーキテクチャ REALITY 課⾦‧認証基盤 k8s Pod Pod Pod API サーバ HTTP/2 ロード バランサ API サーバ API サーバ 6

Slide 7

Slide 7 text

前提: システムのアーキテクチャ REALITY 課⾦‧認証基盤 k8s Pod API サーバ HTTP/2 ロード バランサ Pod API サーバ API サーバ Pod ここ起因で障害が起きた! 7

Slide 8

Slide 8 text

障害の特徴 今回の障害の特徴 ① 定常的ではなく、急にバースト的に発⽣して、しばらくすると⾃然回復する ② ⼀度発⽣したらほぼ全てのAPIリクエストについてタイムアウトする ③ 課⾦‧認証基盤側のロードバランサにそもそもリクエストが到達していない 早期に解決したいが、なかなか原因も対策もつかめず、 毎⽇補填対応をしながら調査を並⾏して進めるという期間が続きました... 8

Slide 9

Slide 9 text

問題が起きていたコード 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

Slide 10

Slide 10 text

問題が起きていたコード 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

Slide 11

Slide 11 text

問題が起きていたコード 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

Slide 12

Slide 12 text

問題が起きていたコード 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

Slide 13

Slide 13 text

問題が起きていたコード 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

Slide 14

Slide 14 text

問題が起きていたコード 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

Slide 15

Slide 15 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 15

Slide 16

Slide 16 text

対策:⼀時的なネットワーク起因の問題のようなので、リトライする ● 最初に思ったこと:⼀時的なネットワーク起因の問題なのでは? ○ ○ 障害時も課⾦‧認証基盤側のシステムに問題が起きていないことは確認をとれていた APIリクエストがタイムアウトする状態は⼀定時間経過すると⾃然回復する ● そもそも ○ 問題が起きていた箇所ではAPIリクエストを⼀度しか呼び出さないコードになっていた 仮説 ⼀時的なネットワーク起因の問題のように⾒えるので、リトライするように直せばOKなのでは? 16

Slide 17

Slide 17 text

対策:リトライを⼊れた擬似コード 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

Slide 18

Slide 18 text

対策:リトライを⼊れた擬似コード 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

Slide 19

Slide 19 text

対策:結果 解決しなかった! リトライしても成功せず、1回⽬と同じ 「context deadline exceeded」が発⽣ 19

Slide 20

Slide 20 text

Podのログを観察してみると... ● とりあえずで対策した案がうまくいかないのでPodのログを眺めてみる ● 以下のような現象が確認された ● ⼀度課⾦‧認証基盤へのAPIアクセスが「context deadline exceeded」で タイムアウトするようになると、そのpodからのAPIアクセスはリトライし てもほぼ成功しない状態が続く ● ただAPIリクエストの中に「http2: client connection lost」というエラー が出ると、そこからはAPIリクエストが成功するようになる 20

Slide 21

Slide 21 text

Podのログを観察してみると... ● コネクションが切断されたログ「http2: client connection lost」がでたら、 再度うまくいくようになった ● つまりコネクションが新規に接続され直したらうまくいくようになった 仮説 今のリトライ処理はネットワーク的に不調なコネクションを使いまわしているから うまくいかないのではないか? 21

Slide 22

Slide 22 text

解決につながりそうな仮説 ● 仮説:今のリトライ処理はネットワーク的に不調なコネクションを使いまわしている のではないか? ● ということは 使⽤しているコネクションがネットワーク不良の時、 「http2: client connection lost」が出る(つまり切断する)ようにしてから、 APIリクエストを送ればうまくいくのではないか HTTP/2とnet/httpのコネクションの死活管理の仕組みについて学んでいこう 22

Slide 23

Slide 23 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 23

Slide 24

Slide 24 text

HTTP/2の基礎:フレーム ● HTTP/1ではリクエストやレスポンスは基本的に⼀塊のデータとしてやりとりされる ○ ● transfer-encoding: chunkedなどの部分的な例外は存在 HTTP/2ではリクエストやレスポンスは「フレーム」という単位で細切れにされてやりとりされる HTTP/1 HTTP/2 24

Slide 25

Slide 25 text

HTTP/2の基礎:ストリーム ● HTTP/1では1本のTCPコネクションには同時に⼀つのリクエスト‧レスポンスのやりとりしかできない ● HTTP/2では1本のTCPコネクション上で同時に複数のリクエスト‧レスポンスのやりとりをできる ○ リクエスト/レスポンスのペアをストリームという単位で識別し、各ストリームごとのフレームを 混ぜて送っている HTTP/1 * HTTP/1に関して「パイプライン」もあるがここでは省略 HTTP/2 25

Slide 26

Slide 26 text

HTTP/2の基礎:コネクションの特性 ● まとめるとHTTP/2では ○ フレームという単位でデータを細切れにしてやり取りしている ○ リクエスト‧レスポンスのやり取りを1本のTCPコネクション上で 複数同時に扱うことができる ● したがってHTTP/2のコネクションの特性として ○ 1本のTCPコネクションの利⽤される期間が⻑い ○ 1本のTCPコネクションが同時に複数のリクエスト‧レスポンスの処理で 利⽤される 26

Slide 27

Slide 27 text

HTTP/2のコネクション特性は今回の障害と合致している 今回の障害の特徴(再掲) ① 定常的ではなく、急にバースト的に発⽣して、しばらくすると⾃然回復する ② ⼀度発⽣したらほぼ全てのAPIリクエストについてタイムアウトする ③ 課⾦‧認証基盤側のロードバランサにそもそもリクエストが到達していない 説明がつく! HTTP/2は1本のコネクションを⻑期間同時に並⾏で使うし、 TCPコネクションを新規に張り直す機会が少ない → 1度コネクション不良になるとそれを使ったAPIリクエストが全て失敗するし、バースト的に⾒える 27

Slide 28

Slide 28 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 28

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

「http2: client connection lost」が出るタイミングはいつか ● net/httpはHTTP/2コネクションごとに対応する受信ループ⽤のgoroutineをもち、レ スポンスや制御⽤のフレームを読み続けている ● 受信ループごとにヘルスチェック処理を特定のタイミングで実⾏しており、ヘルス チェックに失敗すると対応するコネクションが死んだと判定される このとき処理中だったリクエストは「http2: client connection lost」で失敗する コネクションはプールから切断‧破棄され、次のリクエストは新しいコネクションを確⽴する 31

Slide 32

Slide 32 text

なぜこのヘルスチェック処理が必要なのか ● TCPの切断は必ずしも相⼿に通知されるとは限らない ○ 経路上の障害、ロードバランサの退役など ● 上記のような場合、⽚⽅だけが「切れた」と認識している状態が発⽣する →「half-open connections」 ○ ○ 送信側: ローカルのバッファに書き込めてしまい、エラーにならない 受信: 何も届かないだけなのでレスポンスに時間がかかっているだけの場合と区別がつかない ● したがってhalf-open connectionsの場合には能動的に検知をしなければ、気づきよ うがない このヘルスチェック処理はhttp.DefaultClientでは動かないような設定になっている 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

SendPingTimeoutとPingTimeoutを使えばやりたいことができそう 元々の仮説 使⽤しているコネクションがネットワーク不良であるならば、「http2 client connection lost」を出 るようにしてから、APIリクエストを投げればうまくいくのではないか 導出したアプローチ ● ● ● リクエストを投げてから終わるまでの間に、ヘルスチェックが終了することを保証すれば良い そうすると⼀回⽬のリクエストのタイムアウト前までに 「http2: client connection lost」エラーが出る(コネクションが死んでいる場合) その状態でリトライしたら、新しいコネクションを使ってくれるのでうまくいくようになるは ず! 43

Slide 44

Slide 44 text

SendPingTimeoutとPingTimeoutを使ったリトライの図 ⽬指すべきリトライの姿 44

Slide 45

Slide 45 text

SendPingTimeoutとPingTimeoutを使ったリトライの図 ⽬指すべきリトライの姿 SendPingTimeout + PingTimeout < Contextのタイムアウト となるようにすればよい!! 45

Slide 46

Slide 46 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 46

Slide 47

Slide 47 text

最終的なコード: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

Slide 48

Slide 48 text

最終的なコード: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)

Slide 49

Slide 49 text

結果 ● うまくいった! ○ ○ ⼀回⽬のリクエストで失敗する際にエラーが「context deadline exceeded」から「http2: client connection lost」に変わった ⼆回⽬のリクエスト(リトライ)が成功するように! ● 今回の障害に起因する補填作業の件数が0になった! ● ここまできてようやく最初の設定でうまくいかない理由もわかった 49

Slide 50

Slide 50 text

問題が起きていたコード(再掲) 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

Slide 51

Slide 51 text

問題が起きていたコード(再掲) 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

Slide 52

Slide 52 text

アジェンダ ● REALITYが遭遇したあるネットワーク上の障害 ● 効果がなかった対応策、そしてたてられた仮説 ● 障害解決のための知識:HTTP/2の基礎 ● 障害解決のための知識:net/httpのコネクション管理 ● 学んだ知識を元に導出したアプローチとその結果 ● まとめ 52

Slide 53

Slide 53 text

まとめ ● HTTP/2は仕組み上、コネクション1本あたりの⽣死が与える影響が⼤きいので、HTTP/1のときよりさら に慎重な死活管理が求められる ● コネクション不良を検知できない状態でリトライをしても死んだコネクションが使われ続けるリスクが ある ● HTTPクライアントの各種パラメータは適当な値ではなく、意思をもって根拠のある値を設定することが ⼤事 ○ そのためにはネットワークの基礎であったりnet/httpの実装を深掘りするのがなんだかんだ近道 今一度自分たちの HTTPClientのコードにどのようなパラメータ設定が されているか確認していただき、改めてそのパラメータにしている理由などに想 いを馳せるきっかけになれば幸いです。 53

Slide 54

Slide 54 text

ご清聴ありがとうございました 54

Slide 55

Slide 55 text

Appendix ● https://github.com/egonelbre/gophers 55