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.


Related articles: