Go rate limit http.client通过RoundTrip超过限制并产生致命的panic错误。

huangapple go评论125阅读模式
英文:

Go rate limit http.client via RoundTrip exceeds limit and produces fatal panic

问题

我的目标是设置每分钟600个请求的速率限制,下一分钟重置。我打算通过使用http.client设置RoundTriplimit.wait()来实现这一目标。这样我就可以为不同的http.clients()设置不同的限制,并通过roundtrip处理限制,而不是在代码的其他地方增加复杂性。

问题在于速率限制没有被遵守,我仍然超过了允许的请求数,并且设置超时会产生致命的恐慌net/http: request canceled (Client.Timeout exceeded while awaiting headers)

我创建了一个简化的main.go来复制这个问题。请注意,64000循环对我来说是一个现实的场景。

更新:设置ratelimiter: rate.NewLimiter(10, 10),仍然超过了600的速率限制,并产生了错误Context deadline exceeded与设置的超时。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"golang.org/x/time/rate"
)

var client http.Client

// ThrottledTransport Rate Limited HTTP Client
type ThrottledTransport struct {
	roundTripperWrap http.RoundTripper
	ratelimiter      *rate.Limiter
}

func (c *ThrottledTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	err := c.ratelimiter.Wait(r.Context()) // This is a blocking call. Honors the rate limit
	if err != nil {
		return nil, err
	}
	return c.roundTripperWrap.RoundTrip(r)
}

// NewRateLimitedTransport wraps transportWrap with a rate limitter
func NewRateLimitedTransport(transportWrap http.RoundTripper) http.RoundTripper {
	return &ThrottledTransport{
		roundTripperWrap: transportWrap,
		//ratelimiter:      rate.NewLimiter(rate.Every(limitPeriod), requestCount),
		ratelimiter: rate.NewLimiter(10, 10),
	}
}

func main() {
	concurrency := 20
	var ch = make(chan int, concurrency)
	var wg sync.WaitGroup

	wg.Add(concurrency)
	for i := 0; i < concurrency; i++ {
		go func() {
			for {
				a, ok := <-ch
				if !ok { // if there is nothing to do and the channel has been closed then end the goroutine
					wg.Done()
					return
				}
				resp, err := client.Get("https://api.guildwars2.com/v2/items/12452")
				if err != nil {
					fmt.Println(err)
				}
				body, err := ioutil.ReadAll(resp.Body)
				if err != nil {
					fmt.Println(err)
				}
				fmt.Println(a, ":", string(body[4:29]))
			}
		}()
	}
	client = http.Client{}
	client.Timeout = time.Second * 10

	// Rate limits 600 requests per 60 seconds via RoundTripper
	transport := NewRateLimitedTransport(http.DefaultTransport)
	client.Transport = transport

	for i := 0; i < 64000; i++ {
		ch <- i // add i to the queue
	}

	wg.Wait()
	fmt.Println("done")
}

英文:

My goal: is to set a rate limit of 600 requests per minute, which is reset at the next minute. My intend was to do this via the http.client setting a RoundTrip with a limit.wait(). So that I can set different limits for different http.clients() and have the limiting handled via roundtrip rather than adding complexity to my code elsewhere.

The issue is that the rate limit is not honoured, I still exceed the number of requests allowed and setting a timeout produces a fatal panic net/http: request canceled (Client.Timeout exceeded while awaiting headers)

I have created a barebones main.go that replicates the issue. Note that the 64000 loop is a realistic scenario for me.

Update: setting ratelimiter: rate.NewLimiter(10, 10), still exceeds the 600 rate limit somehow and produces errors Context deadline exceeded with the set timeout.

package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;sync&quot;
	&quot;time&quot;

	&quot;golang.org/x/time/rate&quot;
)

var client http.Client

// ThrottledTransport Rate Limited HTTP Client
type ThrottledTransport struct {
	roundTripperWrap http.RoundTripper
	ratelimiter      *rate.Limiter
}

func (c *ThrottledTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	err := c.ratelimiter.Wait(r.Context()) // This is a blocking call. Honors the rate limit
	if err != nil {
		return nil, err
	}
	return c.roundTripperWrap.RoundTrip(r)
}

// NewRateLimitedTransport wraps transportWrap with a rate limitter
func NewRateLimitedTransport(transportWrap http.RoundTripper) http.RoundTripper {
	return &amp;ThrottledTransport{
		roundTripperWrap: transportWrap,
		//ratelimiter:      rate.NewLimiter(rate.Every(limitPeriod), requestCount),
		ratelimiter: rate.NewLimiter(10, 10),
	}
}

func main() {
	concurrency := 20
	var ch = make(chan int, concurrency)
	var wg sync.WaitGroup

	wg.Add(concurrency)
	for i := 0; i &lt; concurrency; i++ {
		go func() {
			for {
				a, ok := &lt;-ch
				if !ok { // if there is nothing to do and the channel has been closed then end the goroutine
					wg.Done()
					return
				}
				resp, err := client.Get(&quot;https://api.guildwars2.com/v2/items/12452&quot;)
				if err != nil {
					fmt.Println(err)
				}
				body, err := ioutil.ReadAll(resp.Body)
				if err != nil {
					fmt.Println(err)
				}
				fmt.Println(a, &quot;:&quot;, string(body[4:29]))
			}
		}()
	}
	client = http.Client{}
	client.Timeout = time.Second * 10

	// Rate limits 600 requests per 60 seconds via RoundTripper
	transport := NewRateLimitedTransport(http.DefaultTransport)
	client.Transport = transport

	for i := 0; i &lt; 64000; i++ {
		ch &lt;- i // add i to the queue
	}

	wg.Wait()
	fmt.Println(&quot;done&quot;)
}

答案1

得分: 2

rate.NewLimiter(rate.Every(60*time.Second), 600) 不是你想要的。

根据 https://pkg.go.dev/golang.org/x/time/rate#Limiter

> Limiter 控制事件发生的频率。它实现了一个大小为 b 的“令牌桶”,初始时桶是满的,并以每秒 r 个令牌的速率进行补充。简单来说,在任何足够长的时间间隔内,Limiter 限制速率为每秒 r 个令牌,最大突发大小为 b 个事件。
>
> ---
> func NewLimiter(r Limit, b int) *Limiter
>
> NewLimiter 返回一个新的 Limiter,允许最多 r 的速率和最多 b 个令牌的突发。
>
> ---
> func Every(interval time.Duration) Limit
>
> Every 将事件之间的最小时间间隔转换为 Limit。

rate.Every(60*time.Second) 意味着它将每60秒填充1个令牌。也就是说,速率是每秒 1/60 个令牌。

大多数情况下,每分钟600个请求 意味着在开始时允许 600 个请求,并在下一分钟立即重置为 600。在我看来,golang.org/x/time/rate 不太适合这种情况。也许 rate.NewLimiter(10, 10) 是一个安全的选择。

英文:

rate.NewLimiter(rate.Every(60*time.Second), 600) is not what you want.

According to https://pkg.go.dev/golang.org/x/time/rate#Limiter:

> A Limiter controls how frequently events are allowed to happen. It implements a "token bucket" of size b, initially full and refilled at rate r tokens per second. Informally, in any large enough time interval, the Limiter limits the rate to r tokens per second, with a maximum burst size of b events.
>
> ---
> func NewLimiter(r Limit, b int) *Limiter
>
> NewLimiter returns a new Limiter that allows events up to rate r and permits bursts of at most b tokens.
>
> ---
> func Every(interval time.Duration) Limit
>
> Every converts a minimum time interval between events to a Limit.

rate.Every(60*time.Second) means that it will fill the bucket with 1 token every 60s. Namely, the rate is 1/60 tokens per second.

Most of the time, 600 requests per minute means that 600 requests are allowed at the beginning, and will be reset to 600 at the next minute at once. In my opinion, golang.org/x/time/rate does not fit this use case very well. Maybe rate.NewLimiter(10, 10) is a safe choice.

答案2

得分: 0

这是一个示例代码,其中roundTripper模拟了来自guildwars API的响应:

https://go.dev/play/p/FTw6IGo_moP

对你的代码来说,唯一有意义的修复是:

  • 在出现错误时不要尝试读取resp.Body(也许这是你的panic的原因之一?)
  • 在64k次迭代循环后关闭通道

简而言之:在这种设置下(没有网络问题,不依赖于实际API服务器的行为),它可以工作:

  • 速率限制器按预期工作,
  • 请求不会超时
# 输出的摘录:
...
235 : "name": "Omnomberry Bar"
236 : "name": "Omnomberry Bar"
237 : "name": "Omnomberry Bar"
238 : "name": "Omnomberry Bar"
239 : "name": "Omnomberry Bar"
--- 60 reqs/sec
240 : "name": "Omnomberry Bar"
241 : "name": "Omnomberry Bar"
242 : "name": "Omnomberry Bar"
...

也许你的问题来自于与实际服务器的联系。

如果它开始在没有警告的情况下断开连接,或者给出越来越长的延迟响应,这可能解释了你的超时问题。

尝试测量RoundTripratelimiter.Wait()上停留的实际时间,以及与服务器进行请求/响应所花费的实际时间。


我用较短的突发运行了我的示例,如果你的程序运行时间足够长(64k个请求以10个请求/秒的速度仍然需要6400秒,接近2小时...),你可能会遇到运行时问题:

由于传输在设置单个请求的超时之后才检查速率限制,如果运行时选择(出于某种不好的原因)在10秒后调度等待rate.Wait(...)的20个工作线程之一,那么你将遇到上下文截止时间错误。

(注意:我没有事实来支持这个说法,只是在这里假设)

最简单的解决方法是:

  • 将速率限制器移到传输之外,
  • client.Get(...)之前检查ratelimiter.Wait(...)

另一个测试选项:

  • 不要设置client.Timeout
  • 在传输通过ratelimiter.Wait(...)保护之后,在请求上设置超时。
英文:

Here is a playground example, where the roundTripper mocks the response from the guildwars API :

https://go.dev/play/p/FTw6IGo_moP

the only meaningful fixes to your code were:

  • don't try to read resp.Body in case of errors (perhaps that is the cause of your panics ?)
  • close the channel after the 64k iterations loop

The short answer is : with this setup (no network issues, no depending on the behavior of the actual api server), it works :

  • the rate limiter works as expected,
  • the requests do not time out
# excerpt from the output:
...
235 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
236 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
237 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
238 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
239 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
--- 60 reqs/sec
240 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
241 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
242 : &quot;name&quot;: &quot;Omnomberry Bar&quot;
...

Perhaps your issue comes from contacting the actual server.

If it starts dropping connections without warning you, or giving responses with longer and longer delays, this could explain your timeout issues.

Try measuring the actual time a RoundTrip remains stuck on ratelimiter.Wait(), and the actual time taken for a request/response with the server.


I ran my examples with shorter bursts, if your program runs long enough (64k requests at 10 req/s is still 6400s, which is close to 2h ...), you may experience runtime issues :

since the transport checks the rate limit after the timeout on an individual request is set, if the runtime chooses (for some bad reason) to schedule one of the 20 workers waiting on rate.Wait(...) after 10 seconds, then you would hit your context.Deadline error.

(note: I have no fact to back this claim, just hypothesizing here)

The simplest workaround for that would be :

  • move the rate limiter outside of the transport,
  • check ratelimiter.Wait(...) right before client.Get(...).

Another option to test :

  • don't set client.Timeout,
  • have your transport set a timeout on the request after it has passed the ratelimiter.Wait(...) guard.

huangapple
  • 本文由 发表于 2022年10月29日 09:11:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/74242183.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定