在我们的项目中,经常会碰到需要为我们的接口添加限速的功能。添加限速功能可能有很多种理由。我碰到的原因是为了保护服务器资源,防止因接口请求量过大导致服务宕机。


在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

点赞(0) 打赏
立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部