How is Go implemented based on IP's method of limiting HTTP access frequency
- 2020-09-28 08:54:42
- OfStack
If you run HTTP and want to limit the frequency of access to HTTP, you can use one of the more stable tools, such as github.com /didip/tollbooth. But if you're building a simple application, you can do it yourself.
We can use an existing Go package, x/time/rate.
In this course, we will create a simple middleware implementation that limits HTTP access frequency based on IP.
Simple HTTP service
Let's start by creating a simple HTTP service with a very simple terminal. However, because its access frequency can be very high, we'll add a frequency limit to it.
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 costly database requests
w.Write([]byte("alles gut"))
}
Via ES27en.go we start the service and listen on port 8888 so we have a simple terminal /.
golang.org/x/time/rate
We will use the Go package named x/time/rate, which provides a token bucket rate limiter algorithm. rate#Limiter controls how often allowed events occur. It implements a "token bucket" of b size, which is initially full and refills the token at r per second. In layman's terms, the limiter limits the rate to r tokens per second over any sufficiently large interval, with a maximum burst size of b events.
Since we want to implement a rate limiter for each IP address, we also need to maintain a limiter map.
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 To create the 1 A new rate limiter and add it to ips In the map ,
// use IP Address as 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 what is provided IP Rate limiter for address ( If it exists ).
// Otherwise the call AddIP will IP The address is added 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 creates one instance of the IP limiter, and the HTTP server must call GetLimiter of this instance to get the limiter specified for IP (from mapping or generating a new one).
The middleware
Let's upgrade the HTTP service and add the middleware to all the endpoints. If IP reaches the limit, it will respond to 429 Too Many Requests; otherwise, it will continue the request.
For each request through the middleware, we call the global method Allow() in the limitMiddleware function. If there is no token in the bucket, the method returns false, and the request receives a response from 429 Too Many Requests. Otherwise, the Allow() method consumes 1 token and passes the request to the next program.
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) {
// Very important data requests.
w.Write([]byte("alles gut"))
}
compile
&
perform
go get golang.org/x/time/rate
go build -o server .
./server
test
This is one of my favorite tools for HTTP load testing, and it's called vegeta (it's also written in Go).
brew install vegeta
We need to create a simple configuration file that shows the request we want to generate.
GET http://localhost:8888/
The attack is then run for 10 seconds with 100 requests per unit of time.
vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report
As a result, you will see that 1 of the requests returned 200, but most returned 429.