在我们的项目中,经常会碰到需要为我们的接口添加限速的功能。添加限速功能可能有很多种理由。我碰到的原因是为了保护服务器资源,防止因接口请求量过大导致服务宕机。
在Go应用中,我们可以使用limiter库,该库是Golang中一个非常简单的速率限制中间件。它拥有简单的API,支持Redis和内存作为后端存储,支持作为HTTP、FastHTTP和Gin的中间件。
该库使用非常简单,按照下面5个步骤来:
一、创建一个limiter.Rate实例,定义每周期内可访问多少个请求?
二、创建一个limiter.Store实例,可使用Redis或内存。
三、创建一个limiter.Limiter实例,用来绑定store和rate实例。
四、创建一个中间件实例。
五、将limiter实例绑定到中间件实例。
下面是对应的代码,看看代码会更清晰。
// Create a rate with the given limit (number of requests) for the given
// period (a time.Duration of your choice).
import "github.com/ulule/limiter/v3"
rate := limiter.Rate{
Period: 1 * time.Hour,
Limit: 1000,
}
// You can also use the simplified format "<limit>-<period>"", with the given
// periods:
//
// * "S": second
// * "M": minute
// * "H": hour
// * "D": day
//
// Examples:
//
// * 5 reqs/second: "5-S"
// * 10 reqs/minute: "10-M"
// * 1000 reqs/hour: "1000-H"
// * 2000 reqs/day: "2000-D"
//
rate, err := limiter.NewRateFromFormatted("1000-H")
if err != nil {
panic(err)
}
// Then, create a store. Here, we use the bundled Redis store. Any store
// compliant to limiter.Store interface will do the job. The defaults are
// "limiter" as Redis key prefix and a maximum of 3 retries for the key under
// race condition.
import "github.com/ulule/limiter/v3/drivers/store/redis"
store, err := redis.NewStore(client)
if err != nil {
panic(err)
}
// Alternatively, you can pass options to the store with the "WithOptions"
// function. For example, for Redis store:
import "github.com/ulule/limiter/v3/drivers/store/redis"
store, err := redis.NewStoreWithOptions(pool, limiter.StoreOptions{
Prefix: "your_own_prefix",
})
if err != nil {
panic(err)
}
// Or use a in-memory store with a goroutine which clears expired keys.
import "github.com/ulule/limiter/v3/drivers/store/memory"
store := memory.NewStore()
// Then, create the limiter instance which takes the store and the rate as arguments.
// Now, you can give this instance to any supported middleware.
instance := limiter.New(store, rate)
// Alternatively, you can pass options to the limiter instance with several options.
instance := limiter.New(store, rate, limiter.WithClientIPHeader("True-Client-IP"), limiter.WithIPv6Mask(mask))
// Finally, give the limiter instance to your middleware initializer.
import "github.com/ulule/limiter/v3/drivers/middleware/stdlib"
middleware := stdlib.NewMiddleware(instance)
它的工作原理是,将请求的IP地址作为key存储在store中。如果store中没有存在这个key,那么会在Store中设置一个具有过期时间的默认值。现在支持两个Store:一、Redis,依赖TTL,每个请求都会增加速率限制。二、在内存中,依靠一个go例程在默认的时间间隔去清除go-cache中已经过期的key。当请求达到限制时,那么会返回429的HTTP状态码。
我们看了工作原理后,经常会问到如果limiter在代理后怎么办?
如果你的limiter放在反向代理之后,则获取真实的客户端IP会比较困难。一些反向代理,例如AWS ALB,会允许所有未自行设置的标头值通过。例如,True-Client-IP和X-Real-IP。同样,X-Forwarded-For是一个逗号分割的IP列表,每个遍历的代理都会附加到该列表中。我们的想法是,第一个IP(由第一个代理添加)是真正的客户端IP,后续的IP都是路径上的代理。
攻击者可以伪造这些标头中的任何一个,这些标头可能会被报告为客户端IP。默认情况下,limiter不信任这些标头。你必须显示启用它们才能使用它们。如果启用它们,你必须始终意识到,由不受控制的任何代理添加的任何标头都是完全不可靠的。
除了在代理中更改标头,很多CDN和云提供商还支持自定义标头来传递客户端IP,你可以在limiter中使用ClientIPHeader。如果上述解决方案都不起作用,请在你的中间件使用自定义KeyGetter。
我们简单讲了一下使用,如果需要更详细的内容,请参考GitHub:
https://github.com/ulule/limiter