介绍
在 Go 中向外部发起 HTTP 请求时,大多数情况下会使用 net/http。
net/http 已具备常规所需的功能,但如果需要单独扩展功能,可以使用 http.RoundTripper 接口。
想要实现的功能
原生的 net/http 并没有提供请求失败时的重试功能,也没有限制每秒最大请求数的速率控制功能。
因此本次我们打算实现以下两种功能:
- 重试处理
- 速率控制
通过 http.RoundTripper 来扩展实现这两个功能。
准备工作
首先准备本次要实现的 http.RoundTripper 框架。
定义一个包含 http.RoundTripper 字段的结构体(MyTransport),并定义初始化函数。
此外,通过向 MayTransport 的接收器函数添加 RoundTrip(req *http.Request) (*http.Response, error) ,可以隐式地使其符合 http.RoundTripper 接口。
|
|
通过在 RoundTrip 中添加处理逻辑,可以在 HTTP 请求前后添加处理功能。
重试控制
在 MyTransport 字段中添加最大重试次数和已重试次数。
|
|
使初始化时能够传递 maxRetryCounts 的值。
|
|
接下来在 RoundTrip 内部添加重试处理。
当响应的状态码为 50x 及以上时进行重试。
另外,重试次数不超过 maxRetryCounts 指定的上限,并使用 Exponential BackOff 算法呈指数级增加重试间隔。
|
|
速率控制
本次我们使用 Fixed Window Counter 算法来实现速率控制。
简单来说,就是将经过的时间按固定期间进行分割,在每个期间内限制允许的请求数量的算法。
定义表示固定期间的结构体(Window),并将单位时间内的请求次数上限、单位时间(ms)、当前 window 作为 MyTransport 的字段添加进去。
|
|
在 MyTransport 的初始化时能够对这些进行初始化。
|
|
最后将速率控制处理添加到 RoundTrip 中。
将当前时间戳除以 perMilliSecond 并向下取整作为窗口的键值。
这个键值每隔 perMilliSecond 毫秒就会更新为新值,因此在同一窗口内是唯一的键。
|
|
测试
那么,我们来使用完成的 MyTransport 进行测试。
本次测试将使用以下设置:
- 最大重试次数为 5
- 每 4.5 秒最多 3 个请求
|
|
使用此配置向返回 500 的端点发送请求试试看
|
|
结果如图所示,可以看到重试间隔逐渐增大,并且最多重试了 5 次
|
|
接下来,向返回 200 的端点连续发送 20 次请求试试看
|
|
虽然看起来有些困难,但可以发现请求大约以每 4.5 秒 3 个请求的节奏发出。
|
|
总结
本次我们通过扩展 Go 中的 http.RoundTripper 接口,
实现了 HTTP 请求的速率控制和重试功能。
在调用外部 API 时经常会遇到「每秒最多调用 N 次」这样的使用限制,希望这个实现能在这种场景下发挥作用。
参考
本文翻译于 Goのhttp.RoundTripperでレート制御とリトライの機能を追加する方法
读后感
以"调用外部 API" 为例,如果并发调用频繁,会导致有许多阻塞,且顺序会被打乱,以上现有的代码适合单线程的场景,将上级调用也阻塞等待。
有个非常适合 http.RoundTripper 的用例,是 Digest 摘要鉴权,首次请求遇到 401,则带上鉴权信息第二次请求。
上面提到的是请求频率限流,还有 带宽/流量限速 ,可以通过包装 io.Reader 限制每秒读取字节数量,定义 LimieRead 结构体,替代 req.Body。