Goのhttp.RoundTripperでレート制御とリトライの機能を追加する方法
はじめに
Goで外部へhttpリクエストを行う際には、多くの場合でnet/httpを利用すると思います。
net/httpには通常利用する分には必要な機能が備わっていますが、独自に拡張して使いたい場合はhttp.RoundTripperというインターフェースを利用できます。
利用方法の解説としては、こちらの「Go http.RoundTripper 実装ガイド」という記事に詳細に解説がありましたので参考にさせて頂き、
今回はより具体的な利用例にフォーカスして実装例を交えて紹介したいと思います。
やりたいこと
素のnet/httpではリクエストがエラーになった際のリトライや、秒間リクエストの最大値を制限するようなレート制御の機能はありません。
なので今回は、
- リトライ処理
- レート制御
この2つをhttp.RoundTripperを使って拡張実装したいと思います。
※今回のサンプルコードの完成品はこちらにupしています。
事前準備
今回実装するhttp.RoundTripperの枠を用意します。
http.RoundTripperをフィールドに持つ構造体(MyTransport)を定義し、
初期化用の関数を定義します。
さらに、MayTransportのレシーバー関数にRoundTrip(req *http.Request) (*http.Response, error)
を追加することで、暗黙的にhttp.RoundTripperインターフェースに準拠させます。
import (
"net/http"
)
type MyTransport struct {
wrapped http.RoundTripper
}
func NewMyTransport(transport http.RoundTripper) *MyTransport {
return &MyTransport{
wrapped: transport,
}
}
func (t *MyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.wrapped.RoundTrip(req)
}
RoundTripに処理を追加することで、httpリクエストの前後処理を追加することができます。
リトライ制御
RoundTrip(req *http.Request) (*http.Response, error)
にリトライ処理を追加していきましょう。
MyTransportのフィールドに、最大リトライ数とリトライした数を追加します。
type MyTransport struct {
wrapped http.RoundTripper
maxRetryCounts int // 最大リトライ数
retryCounts int // リトライした数
}
初期化時にmaxRetryCountsの値を渡せるようにします。
func NewLimitedTransport(transport http.RoundTripper, maxRetryCounts int) *MyTransport {
return &MyTransport{
wrapped: transport,
maxRetryCounts: maxRetryCounts,
}
}
次にRoundTrip内にリトライ処理を追加していきます。
レスポンスのステータスコードが50x系以上の場合はリトライするようにします。
また、maxRetryCountsで指定した上限までリトライし、Exponential BackOffのアルゴリズムで指数関数的リトライ間隔を増加させます。
func (t *MyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var res *http.Response
var err error
for {
// 実際のhttpリクエストはここ
res, err = lt.wrapped.RoundTrip(req)
// 50x系以上はリトライ
if res != nil && res.StatusCode < http.StatusInternalServerError {
break
}
// リトライ数の上限チェック
lt.retryCounts++
if lt.retryCounts > lt.maxRetryCounts {
break
}
// Exponential BackOff でウェイトを入れる
time.Sleep(time.Second * time.Duration(math.Pow(2, float64(lt.retryCounts))))
}
lt.retryCounts = 0
return res, err
}
レート制御
RoundTrip(req *http.Request) (*http.Response, error)にレート処理を追加していきましょう。
今回はFixed Window Counterというアルゴリズムでレート制御を実装してみます。
簡単にいうと、経過する時間を固定の期間で区切り、期間ごとに許可するリクエスト数を制限するアルゴリズムです。
固定期間を表す構造体(Window)を定義し、単位時間あたりのリクエスト数上限、単位時間(ms)、現在のwindowをMyTransportのフィールドに追加します。
type MyTransport struct {
... 省略
maxRequestCounts int // 単位時間あたりのリクエスト数上限
perMilliSecond int64 // 単位時間(ms)
window Window // 現在のwindow
}
// 固定期間(Window)の構造体
type Window struct {
key int64 // windowのキー
requestCounts int // window内のリクエスト数
}
MyTransportの初期化時ににこれらを初期化できるようにします。
func NewMyTransport(transport http.RoundTripper, maxRetryCounts int, maxRequestCounts int, perMilliSecond int64) *MyTransport {
return &MyTransport{
wrapped: transport,
maxRequestCounts: maxRequestCounts,
perMilliSecond: perMilliSecond,
maxRetryCounts: maxRetryCounts,
retryCounts: 0,
window: Window{
key: int64(0),
requestCounts: 0,
},
}
}
最後にレート制御の処理をRoundTripに追加します。
現在時刻をperMilliSecondで割って切り捨てた数をwindowのキーとします。
このkeyはperMilliSecond経過毎に新しい値になりますので、同じwindow内での一意なキーとなります。
func (t *MyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for {
now := time.Now().UnixMilli()
cKey := now / t.perMilliSecond
// 新しい固定期間(window)になったらwaitなしでリクエストする
if t.window.key != cKey {
t.window = Window{
key: cKey,
requestCounts: 0,
}
break
}
// 単位時間あたりのリクエスト数上限まではwaitなしでリクエストする
if t.window.requestCounts < t.maxRequestCounts {
break
}
// リクエスト数上限を超えていたら、waitする
wait := t.perMilliSecond - now%t.perMilliSecond
time.Sleep(time.Millisecond * time.Duration(wait))
}
t.window.requestCounts++
...省略、以下でリクエスト処理を行う
return res, err
}
テスト
それでは、完成したMyTransportを使ってテストをしてみます。
今回は以下のような設定でテストをしてみます
- 最大リトライ数は5
- 4.5秒毎に最大3リクエストまで
client := http.Client{
Transport: NewMyTransport(
http.DefaultTransport,
5, // 最大リトライ数
3, // 単位時間あたりのリクエスト数上限
4500, // 単位時間(ms)
),
}
この設定で500が返ってくるエンドポイントにリクエストを送ってみます
resp, _ := client.Get(url)
defer resp.Body.Close()
結果はこちら、リトライ間隔を徐々に開けつつ5回までりトライされていることがわかります
{"time":"2022-09-09T08:59:01.521892211Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
{"time":"2022-09-09T08:59:03.577460181Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
{"time":"2022-09-09T08:59:07.642723871Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
{"time":"2022-09-09T08:59:15.713717009Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
{"time":"2022-09-09T08:59:31.822121165Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
{"time":"2022-09-09T09:00:03.914825907Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"500"}
次に、200が返ってくるエンドポイントに20回連続してリクエストを送ってみます
for i := 0; i < 20; i++ {
resp, _ := client.Get(url)
defer resp.Body.Close()
}
結果はこちら。
少し見づらいですが、大体4.5秒毎に3リクエストのペースでリクエストされていることがわかります
{"time":"2022-09-09T09:00:04.006523686Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:04.103094697Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:04.189073231Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:04.552413101Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:04.647212881Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:04.727567042Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:09.051296264Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:09.149187824Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:09.239032885Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:13.549064137Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:13.636816718Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:13.724980151Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:18.054925814Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:18.15239561Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:18.231450035Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:22.566128516Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:22.663002133Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:22.750378144Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:27.06199479Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
{"time":"2022-09-09T09:00:27.156784826Z","level":"INFO","prefix":"-","file":"http.go","line":"69","message":"200"}
まとめ
今回はGoにおけるhttpリクエストのレート制御とリトライの機能を、
http.RoundTripperというインターフェースを拡張することで実現しました。
外部のAPIを利用するときに、「毎秒○回まで」のような利用制限が設けられている場合がよくありますが、そんな時に役立てれば幸いです。
Discussion