基于 IP 地址限制 Go 中的 HTTP 请求速率
如果您正在运行 HTTP 服务器,并且想要限制对端点的请求速率,则可以使用维护良好的工具,例如github.com/didip/tollbooth。但是,如果您正在构建一些非常简单的东西,那么自己实现它并不难。
已经有一个实验性的 Go 包x/time/rate
,我们可以使用它。
在本教程中,我们将创建一个简单的中间件,用于根据用户的 IP 地址进行速率限制。
纯 HTTP 服务器
我们先来构建一个简单的 HTTP 服务器,它有一个非常简单的端点。但它可能是一个负载很大的端点,所以我们想在那里添加速率限制。
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", okHandler)
if err := http.ListenAndServe(":8888", mux); err != nil {
log.Fatalf("unable to start server: %s", err.Error())
}
}
func okHandler(w http.ResponseWriter, r *http.Request) {
// Some very expensive database call
w.Write([]byte("alles gut"))
}
我们main.go
启动服务器:8888
并拥有一个端点/
。
golang.org/x/time/rate
我们将使用x/time/rate
Go 包,该包提供了一个令牌桶速率限制器算法。rate #Limiter控制允许事件发生的频率。它实现了一个大小为 的“令牌桶” b
,初始状态为满,并以每秒 的速率重新填充r
令牌。通俗地说,在任何足够长的时间间隔内,限制器将速率限制为每秒 r 个令牌,最大突发大小为 b 个事件。
由于我们想要为每个 IP 地址实现速率限制器,因此我们还需要维护一个限制器映射。
package main
import (
"sync"
"golang.org/x/time/rate"
)
// IPRateLimiter .
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}
// NewIPRateLimiter .
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
i := &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
return i
}
// AddIP creates a new rate limiter and adds it to the ips map,
// using the IP address as the key
func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter := rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
return limiter
}
// GetLimiter returns the rate limiter for the provided IP address if it exists.
// Otherwise calls AddIP to add IP address to the map
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
limiter, exists := i.ips[ip]
if !exists {
i.mu.Unlock()
return i.AddIP(ip)
}
i.mu.Unlock()
return limiter
}
NewIPRateLimiter
创建一个 IP 限制器实例,HTTP 服务器将必须调用GetLimiter
此实例来获取指定 IP 的限制器(从映射中或生成一个新的)。
中间件
让我们升级我们的 HTTP 服务器并将中间件添加到所有端点,因此如果 IP 已达到限制,它将响应 429 请求过多,否则,它将继续处理请求。
在limitMiddleware
函数中,每当中间件收到 HTTP 请求时,我们都会调用全局限制器的Allow()
方法。如果令牌桶中没有剩余令牌,Allow()
则返回 false,并向用户发送 429 Too Many Requests 响应。否则,调用该方法Allow()
只会消耗令牌桶中的一个令牌,并将控制权交给链中的下一个处理程序。
package main
import (
"log"
"net/http"
)
var limiter = NewIPRateLimiter(1, 5)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", okHandler)
if err := http.ListenAndServe(":8888", limitMiddleware(mux)); err != nil {
log.Fatalf("unable to start server: %s", err.Error())
}
}
func limitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
limiter := limiter.GetLimiter(r.RemoteAddr)
if !limiter.Allow() {
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func okHandler(w http.ResponseWriter, r *http.Request) {
// Some very expensive database call
w.Write([]byte("alles gut"))
}
构建并运行
go get golang.org/x/time/rate
go build -o server .
./server
测试
我喜欢使用一个非常好的工具来进行 HTTP 负载测试,称为vegeta(也是用 Go 编写的)。
brew install vegeta
我们需要创建一个简单的配置文件,说明我们想要产生什么请求。
GET http://localhost:8888/
然后以每单位时间 100 个请求的速度运行攻击 10 秒。
vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report
结果你会看到有些请求返回 200,但大多数返回 429。
文章来源:https://dev.to/der_gopher/rate-limiting-http-requests-in-go-based-on-ip-address-542g