如果您正在运行HTTP服务器并希望对端点进行速率限制请求,则可以使用维护良好的工具,例如 github.com/didip/tollbooth。但是如果你正在构建一些非常简单的东西,那么自己实现它并不困难。
已经有一个很好用的的Go包x/time/rate,我们可以使用它。
在本教程中,我们将根据用户的IP地址创建一个简单的速率限制中间件。
纯HTTP服务器
让我们从构建一个简单的HTTP服务器开始,该服务器内容很简单。但是它可能面临着大量的请求,这就是我们想在那里添加速率限制的原因。
packagemain
import(
"log"
"net/http"
)
func main(){
mux:=http.NewServeMux()
mux.HandleFunc("/",okHandler)
iferr:=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/rateGo包,它提供令牌桶速率限制器算法。rate#Limiter控制允许事件发生的频率。它实现了一个有具体大小的“令牌桶” b,最初是满的,并以每秒的速率r令牌重新填充。非正式地,在任何足够大的时间间隔内,限制器将速率限制为每秒r个令牌,具有b个事件的最大突发大小。
由于我们希望为每个IP地址实施速率限制器,因此我们还需要维护一个限制器映射。
packagemain
import(
"sync"
"golang.org/x/time/rate"
)
// IPRateLimiter .
typeIPRateLimiterstruct{
ips map[string]*rate.Limiter
mu*sync.RWMutex
r rate.Limit
bint
}
// NewIPRateLimiter .
funcNewIPRateLimiter(r rate.Limit,bint)*IPRateLimiter{
i:=&IPRateLimiter{
ips:make(map[string]*rate.Limiter),
mu:&sync.RWMutex{},
r:r,
b:b,
}
returni
}
// AddIP creates a new rate limiter and adds it to the ips map,
// using the IP address as the key
func(i*IPRateLimiter)AddIP(ipstring)*rate.Limiter{
i.mu.Lock()
defer i.mu.Unlock()
limiter:=rate.NewLimiter(i.r,i.b)
i.ips[ip]=limiter
returnlimiter
}
// 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(ipstring)*rate.Limiter{
i.mu.Lock()
limiter,exists:=i.ips[ip]
if!exists{
i.mu.Unlock()
returni.AddIP(ip)
}
i.mu.Unlock()
returnlimiter
}
NewIPRateLimiter创建IP限制器的实例,HTTP服务器必须调用GetLimiter此实例以获取指定IP的限制器(从映射中生成或生成新的限制器)。
中间件
让我们升级我们的HTTP服务器并将中间件添加到所有端点,因此如果IP达到限制,它将响应429太多请求,否则,它将继续请求。
在limitMiddleware函数中,Allow()每次中间件收到HTTP请求时,我们都会调用全局限制器的方法。如果存储桶中没有剩余的令牌Allow()将返回false,我们会向用户发送429 Too Many Requests响应。否则,调用Allow()将从桶中只消耗一个令牌,并将控制传递给链中的下一个处理程序。
packagemain
import(
"log"
"net/http"
)
varlimiter=NewIPRateLimiter(1,5)
func main(){
mux:=http.NewServeMux()
mux.HandleFunc("/",okHandler)
iferr:=http.ListenAndServe(":8888",limitMiddleware(mux));err!=nil{
log.Fatalf("unable to start server: %s",err.Error())
}
}
func limitMiddleware(nexthttp.Handler)http.Handler{
returnhttp.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"))
}
建立和运行gogetgolang.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。