Slide 1

Slide 1 text

HTTP Retryの実装 AI事業本部 アプリ運用センター 岩見彰太

Slide 2

Slide 2 text

自己紹介 岩見 彰太 趣味:サックス・ドローン・カメラ AI事業本部 小売DX アプリ運用センター 22卒バックエンドエンジニア B_Sardine

Slide 3

Slide 3 text

事の発端 とあるAPIに対して特定のステータスコードの時に Exponential Backoffによるリトライを実装したい

Slide 4

Slide 4 text

最初こんな感じで実装してみた ということで…

Slide 5

Slide 5 text

client.Do(req)を実行する responseのステータスコードが429なら エラーを返す エラーだった時にリトライする cenkalti/backoff 用メソッド maxRetryNumberで設定 した回数だけリトライする

Slide 6

Slide 6 text

最初の1回目以降、Request Bodyが空だと怒られて Do がエラーになる 適当なサーバを立てて叩いて見ると…

Slide 7

Slide 7 text

こんな感じで、Bodyを入れ直すと通る Bodyの巻き戻し. 入れ直しが必要 先に結論

Slide 8

Slide 8 text

Requestの構造体を見てみる Bodyはio.ReadCloserというインターフェース型 今回入れた具象型はbytes.Buffer 複数回リトライする場合, これが複数回実行される bytes.Bufferの構造体を見てみる lastReadという読み切った場所を内部で保存してる なぜなくなる? リトライする場合は Request.Bodyを初期状態に戻す必要がある つまり…

Slide 9

Slide 9 text

http.Clientの中のTrasport.roundTripがリトライ処理し てる ネットワークエラーとかになった時にリトライしてる rewindBodyというメソッドを呼んでる Request.Bodyを閉じる body, err := req.GetBody()でRequst.Bodyのコピーを取 り出す 取り出したものをもう一度reqに入れる rewindBodyの流れ 1. 2. 3. 実はnet/httpもリトライしてる コネクションの確立とかコネクションプールの管理を行なってる Transport

Slide 10

Slide 10 text

*bytes.Buffer <----- 今回の場合 *bytes.Reader *strings.Reader Request.Bodyの具象型が以下の時に GetBodyがセットされる io.NopCloser は何もしない io.ReadCloser (Bodyで指定されているinterface型)を実装できる req.GetBody()は Request作成時に作られる

Slide 11

Slide 11 text

Transport.roundTripでも終了判定している リトライをする場合、呼び出し元のcontextを伝播す る必要がある 同じように判定する必要がある Contextの終了を確認する

Slide 12

Slide 12 text

レスポンス内容はio.ReadCloserの Response.Bodyから読み取れる 呼び出し側で最後まで読み取ってから Close しな いと keep-aliveの TCPコネクションが再利用さ れない レスポンスを返した後に接続を維持し、次のリク エストを送るときにその接続を再利用して送ること ができる Keep-AliveはResponseが読み切られてCloseした タイミングで再利用できるようになる > Bodyを閉じるのは呼び出し側の責任である。 > デフォルトの HTTP クライアントの Transport は、Body を最後まで読んで閉じな いと、 HTTP/1.x の "keep-alive" TCP コネクションを再利用しないかもしれません。 Keep-Alive http.Response.Bodyを 読み切ってから閉じる

Slide 13

Slide 13 text

Transport 内部では、connectMethodKey(接続先) 単位で idel 状態のTCPコネクションや 確立待ちのキューが管理されている これがデフォルトの場合はDefaultTransportが使用される 90s: IdleConnTimeout(リクエストが終わってもコネクションが維持される時間) 2: MaxIdleConnsPerHost(接続先ごとのKeep-Aliveの最大数) TransportでのTCPコネクションの管理

Slide 14

Slide 14 text

CloseIdleConnections()でコネクションプールを 一括で閉じることができる リトライ終了後に意図的に解放したい場合は使える (これを呼ばない場合、IdleConnTimeoutが過ぎると 自動的に解放される) コネクションプールを閉じる

Slide 15

Slide 15 text

リトライが必要となる場面(429 Too Many Requests)では、短期間で同じ宛先にリクエスト する場合 そうなると、 MaxIdleConnsPerHost だと少ない可能性が高い リトライしている期間中占有してしまい、解放まで時間がかかる可能性があるから 場合によっては専用の Transport を作成した方がいい この場合、使わないのに IdleConnTimeout で設定されている時間分 Keep-Alive が保持 されて、コネクションリソースを消費してしまうので、リトライが終了した後は CloseIdleConnections を読んでコネクションプールを開放するべき  (終了したらコネクションプールを閉じる) リトライ用のTransportを別で用意するべき?

Slide 16

Slide 16 text

ただリトライしたいだけなのに 考えること多くてめんどくさい… ここら辺まるっとやってくれる便利なライブラリないのか……

Slide 17

Slide 17 text

今まで説明してきたようなことが考慮されてる TransportはMaxIdleConnsPerHostを増やし て、リトライ後には閉じるようになっている net/httpと同じように使えて直感的 -> 結局これを使いました Hashicorp製 hashicorp/go-retryablehttp ありました

Slide 18

Slide 18 text

まとめ リクエストの内容(Request.Body)をリトライ前に巻き戻す Request.Context()の終了を確認する リトライ前にResponse.Bodyを全て読み切ってから閉じる デフォルトのTransportを使用してコネクションプール管理するか検討する hashicorp/go-retryablehttpの使用を検討する ちなみに、AWS SDK for GoとかでAPIコールする場合は、勝手にリトライしてくれるようになってる