分布式出站http速率限制器

时间:2019-06-30 20:09:16

标签: go distributed rate-limiting

我有一个微服务架构应用程序,其中有多个服务轮询外部API。外部API的速率限制为每分钟600个请求。如何使我的所有实例都保持在共享的600速率限制之下?

Google只给我带来了3种解决方案,最有希望的是:

  • myntra/golimit这三个中最有前途的,但是我真的不知道如何设置它。
  • wallstreetcn/rate似乎仅在达到限制时拒绝(我的应用需要等待,直到它可以发出请求为止),而Every函数中的rate.NewLimiter函数似乎是一种不同的导入/依赖关系,我无法弄清楚它是什么
  • manavo/go-rate-limiter有一个“软”限制,很明显,它可以使我超过该限制。如果我无法访问某些端点,我真的不介意,但是其他端点请求应该尽可能工作。

目前,我有一个业余解决方案。下面的代码允许我设置每分钟的限制,并且它在请求之间休眠,以将请求分散到每分钟。此客户端速率限制是每个实例的,因此我必须对600个请求进行硬编码除以实例数量。

var semaphore = make(chan struct{}, 5)
var rate = make(chan struct{}, 10)

func init(){
    // leaky bucket
    go func() {
        ticker := time.NewTicker(100 * time.Millisecond)
        defer ticker.Stop()
        for range ticker.C {
            _, ok := <-rate
            // if this isn't going to run indefinitely, signal
            // this to return by closing the rate channel.
            if !ok {
                return
            }
        }
}()

在发出HTTP API请求的函数中。

rate <- struct{}{}

    // check the concurrency semaphore
    semaphore <- struct{}{}
    defer func() {
        <-semaphore
}()

如何让我的所有实例都保持在共享的600速率限制之下?

首选项:  -基于密钥的限速计数器,因此可以设置多个计数器。  -将请求分散到设置的持续时间内,以便在前30秒内不会发送600个请求,而是在整分钟内发送该请求。

2 个答案:

答案 0 :(得分:0)

如果需要全局速率限制器,则需要一个位置来保持分布式状态,例如zookeeper。通常,我们不想支付开销。另外,您可以设置转发代理(https://golang.org/pkg/net/http/httputil/#ReverseProxy),并在其中进行速率限制。

答案 1 :(得分:0)

我无法与您找到的库对话,但是leaky bucket限速器非常简单。您需要某种共享的事务存储。每个存储桶(或速率限制器)都只是一个整数和一个时间值。整数是特定时间段中存储桶中的丢弃次数。每次您必须应用速率限制时,请减去自上次更新以来泄漏的墨滴数,然后添加一个,然后检查墨滴数是否在存储桶的容量之内。

我们正在将Redis用于此类事情。要在Redis中进行事务处理,需要一个脚本(请参见SCRIPT LOADEVALSHA)。例如,在SQL数据库中,SELECT FOR UPDATE后跟UPDATE语句将实现相同的目的。这是我们的Redis脚本:

-- replicate_commands allows us to use the TIME command. We depend on accurate
-- (and reasonably consistent) timestamps. Multiple clients may have
-- inacceptable clock drift.
redis.replicate_commands()

local rate = tonumber(ARGV[1]) -- how many drops leak away in one second
local cap = tonumber(ARGV[2]) -- how many drops fit in the bucket
local now, _ = unpack(redis.call('TIME'))

-- A bucket is represented by a hash with two keys, n and t. n is the number of
-- drops in the bucket at time t (seconds since epoch).
local xs = redis.call('HMGET', KEYS[1], 'n', 't')
local n = tonumber(xs[1])
local t = tonumber(xs[2])

if type(n) ~= "number" or type(t) ~= "number" then
    -- The bucket doesn't exist yet (n and t are false), or someone messed with
    -- our hash values. Either way, pretend the bucket is empty.
    n, t = 0, now
end

-- remove drops that leaked since t
n = n - (now-t)*rate
if n < 0 then
    n = 0
end

-- add one drop if it fits
if n < cap then
    n = n + 1
else
    n = cap
end

redis.call('HMSET', KEYS[1], 'n', n, 't', now)
redis.call('EXPIRE', KEYS[1], math.floor(n/rate) + 1)

return n

示例要求每秒10滴,容量为10滴:

EVALSHA <SHA_IN_HEX> 1 rate-limit:my-bucket 10 10 

该脚本返回存储桶中的删除数目。如果该数目等于容量,则可以根据需要进行短暂睡眠,然后重试,或者完全拒绝该请求。

请注意,脚本永远不会返回大于容量的值,因此在您的情况下,恢复时间不超过十分之一秒。这可能不是您所需要的,因为您正在尝试匹配第三方速率限制器。即您可能会因为存储桶溢出而没事,导致一连串的请求后恢复时间更长。