Golang中使用gorilla mux实现限流器

Golang中使用gorilla mux实现限流器 我正在尝试实现一个HTTP请求限制器,以允许每秒10个请求。 以下是我参考rate-limit所实现的代码。

// Run a background goroutine to remove old entries from the visitors map.
func init() {
	fmt.Println("This will get called on main initialization")
	go cleanupVisitors()
}

func getVisitor(username string) *rate.Limiter {
	mu.Lock()
	defer mu.Unlock()
	v, exists := visitors[username]
	if !exists {
		//rt := rate.Every(1 * time.Minute)
		//limiter := rate.NewLimiter(rt, 1)
		limiter := rate.NewLimiter(5, 3)
		// Include the current time when creating a new visitor.
		visitors[username] = &visitor{limiter, time.Now()}
		return limiter
	}

	// Update the last seen time for the visitor.
	v.lastSeen = time.Now()
	return v.limiter
}

// Every minute check the map for visitors that haven't been seen for
// more than 3 minutes and delete the entries.
func cleanupVisitors() {
	for {
		time.Sleep(time.Minute)
		mu.Lock()
		for username, v := range visitors {
			if time.Since(v.lastSeen) > 1*time.Minute {
				delete(visitors, username)
			}
		}
		mu.Unlock()
	}
}

func limit(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mappedArray := hotelapi.SearchResponse{}
		mappedArray.StartTime = time.Now().Format("2006-02-01 15:04:05.000000")
		mappedArray.EndTime = time.Now().Format("2006-02-01 15:04:05.000000")
		userName := r.FormValue("username")
		limiter := getVisitor(userName)
		if !limiter.Allow() {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusTooManyRequests)
			mappedArray.MessageInfo = http.StatusText(http.StatusTooManyRequests)
			mappedArray.ErrorCode = strconv.Itoa(http.StatusTooManyRequests)
			json.NewEncoder(w).Encode(mappedArray)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func route() {
	r := mux.NewRouter()
	r.PathPrefix("/hello").HandlerFunc(api.ProcessSupplier).Methods("GET")
	ws := r.PathPrefix("/index.php").HandlerFunc(api.ProcessWebService).Methods("GET", "POST").Subrouter()
	r.Use(panicRecovery)
	ws.Use(limit)
	http.HandleFunc("/favicon.ico", faviconHandler)
	if config.HTTPSEnabled {
		err := http.ListenAndServeTLS(":"+config.Port, config.HTTPSCertificateFilePath, config.HTTPSKeyFilePath, handlers.CompressHandlerLevel(r, gzip.BestSpeed))
		if err != nil {
			fmt.Println(err)
			log.Println(err)
		}
	} else {
		err := http.ListenAndServe(":"+config.Port, limit(handlers.CompressHandler(r)))
		if err != nil {
			fmt.Println(err)
			log.Println(err)
		}
	}
}

我在这里有几个疑问。

  1. 我只想对 /index.php 路径进行限制,而不是 /hello。我使用了子路由来实现。这种方式正确吗?
  2. 限制中间件并没有按我预期的那样工作。它只允许1个成功的请求,其他所有请求都返回了“请求过多”的错误。

我在这里遗漏了什么吗?


更多关于Golang中使用gorilla mux实现限流器的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

是的。在前10个请求内,它返回了过多的请求。 让我尝试使用LimitByIP。 同时,我将分享路由器的设置。

更多关于Golang中使用gorilla mux实现限流器的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


但是你在哪里应用了为某些用户跳过限制器的逻辑?我没有看到。你应该在 WithKeyFuncs 中实现这个功能。

那么,你必须使用那个效果不佳的第一种方法,调试问题所在,或者最终向 go-chi/httprate 报告一个 issue。同时,检查那里的示例和测试。

你的意思是,即使在最初的10个请求内,它也返回了过多的请求吗?如果是这样,请提供路由器的完整配置。你也可以尝试一个简单的配置:

ws.Use(httprate.LimitByIP(3, 5*time.Second))

而不是上面那个。对于上面那个,尝试设置为每10秒100个请求,这可能会产生一些不同。

抱歉,忘记提了。我尝试使用了 LimitByIP,并据此进行了调试,修正了我在第一种方法中尝试的代码。现在第一种方法也能按预期工作了。目前我只是在努力实现基于URL中 username 字段的动态限制。我查看了示例,但没有找到任何相关的内容。我会继续尝试。如果还是找不到,我会向 go-chi/httprate 项目提出这个问题。

谢谢。

你好,谢谢。我已经做了修改并且它现在可以工作了。我正在做更多的测试。

只是有一个顾虑,我如何才能为每个用户动态设置限制和持续时间? 我尝试使用 r.FormValue("username") 来获取 username,但由于 httprate.Limit 没有 r *http.Request 参数,我获取到的用户名是空白的。

func main() {
    fmt.Println("hello world")
}

最好使用惯用的中间件。你不需要使用 chi,你可以直接使用 http 包或 Gorilla mux。

GitHub

GitHub - go-chi/httprate: net/http 速率限制器中间件

net/http 速率限制器中间件。通过在 GitHub 上创建帐户为 go-chi/httprate 的开发做出贡献。

你好 @acim

感谢你的回复。我确实尝试了这个方法。

ws.Use(httprate.Limit(
		10,            // requests
		1*time.Second, // per duration
		httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
			return r.FormValue("username"), nil
		}),
		httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
			http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
		}),
	))

然后我用 JMeter 以每秒 12 个请求的频率进行测试。所有的请求都收到了 请求过多 的错误。

https://raw.githubusercontent.com/go-chi/httprate/master/_example/main.go

		r.Use(httprate.Limit(
			10,
			10*time.Second,
			httprate.WithKeyFuncs(httprate.KeyByIP, func(r *http.Request) (string, error) {
				token := r.Context().Value("userID").(string)
				return token, nil
			}),
			httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
				// We can send custom responses for the rate limited requests, e.g. a JSON message
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests)
				w.Write([]byte(`{"error": "Too many requests"}`))
			}),
		))

你需要修改 httprate.WithKeyFuncs 来使用你自己的方式获取用户ID(例如从请求头、Cookie、查询参数或其他地方)。

我这样做了。出于测试目的,我传递了一个随机文本作为令牌(不是实际的用户名),但奇怪的是,限流器仍然对其生效。 我的实际用户名是:ganesh,位于 r.FormValue(“username”) 中。

ws.Use(httprate.Limit(
		5,
		1*time.Second,
		httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
			return "test", nil
		}),
		httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests)
				w.Write([]byte(`{"error": "Too many requests"}`))
		}),
	))
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 1
X-Ratelimit-Limit: 5
X-Ratelimit-Remaining: 0
X-Ratelimit-Reset: 1634128918
Date: Wed, 13 Oct 2021 12:41:57 GMT
Content-Length: 271

你的实现有几个关键问题需要解决。让我分析并提供修正方案:

1. 路由配置问题

你的子路由配置是正确的,但中间件应用方式需要调整:

func route() {
    r := mux.NewRouter()
    
    // /hello 路径 - 无限制
    r.PathPrefix("/hello").HandlerFunc(api.ProcessSupplier).Methods("GET")
    
    // /index.php 路径 - 应用限流中间件
    ws := r.PathPrefix("/index.php").Subrouter()
    ws.Use(limit)  // 只在子路由应用限流
    ws.HandleFunc("", api.ProcessWebService).Methods("GET", "POST")
    
    r.Use(panicRecovery)
    
    http.HandleFunc("/favicon.ico", faviconHandler)
    
    // 注意:不要在全局应用 limit 中间件
    if config.HTTPSEnabled {
        err := http.ListenAndServeTLS(":"+config.Port, 
            config.HTTPSCertificateFilePath, 
            config.HTTPSKeyFilePath, 
            handlers.CompressHandlerLevel(r, gzip.BestSpeed))
        if err != nil {
            log.Println(err)
        }
    } else {
        err := http.ListenAndServe(":"+config.Port, 
            handlers.CompressHandler(r))  // 移除全局的 limit()
        if err != nil {
            log.Println(err)
        }
    }
}

2. 限流器配置问题

你的主要问题是 rate.NewLimiter(5, 3) 配置。这创建了一个令牌桶:

  • 速率:每秒5个令牌
  • 桶容量:3个令牌

要实现每秒10个请求,应该使用:

func getVisitor(username string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    
    v, exists := visitors[username]
    if !exists {
        // 创建每秒10个请求的限流器
        limiter := rate.NewLimiter(10, 10)  // 每秒10个,桶容量10
        visitors[username] = &visitor{
            limiter:  limiter,
            lastSeen: time.Now(),
        }
        return limiter
    }
    
    v.lastSeen = time.Now()
    return v.limiter
}

3. 完整修正示例

package main

import (
    "net/http"
    "sync"
    "time"
    
    "golang.org/x/time/rate"
    "github.com/gorilla/mux"
)

var (
    visitors = make(map[string]*visitor)
    mu       sync.Mutex
)

type visitor struct {
    limiter  *rate.Limiter
    lastSeen time.Time
}

func getVisitor(username string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    
    v, exists := visitors[username]
    if !exists {
        // 每秒10个请求,桶容量10
        limiter := rate.NewLimiter(10, 10)
        visitors[username] = &visitor{
            limiter:  limiter,
            lastSeen: time.Now(),
        }
        return limiter
    }
    
    v.lastSeen = time.Now()
    return v.limiter
}

func cleanupVisitors() {
    for {
        time.Sleep(time.Minute)
        mu.Lock()
        for username, v := range visitors {
            if time.Since(v.lastSeen) > 3*time.Minute {
                delete(visitors, username)
            }
        }
        mu.Unlock()
    }
}

func limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 从请求中获取用户名
        userName := r.FormValue("username")
        if userName == "" {
            // 如果没有用户名,使用IP地址
            userName = r.RemoteAddr
        }
        
        limiter := getVisitor(userName)
        if !limiter.Allow() {
            http.Error(w, http.StatusText(http.StatusTooManyRequests), 
                http.StatusTooManyRequests)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

func main() {
    go cleanupVisitors()
    
    r := mux.NewRouter()
    
    // 不受限的路由
    r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, no rate limit here!"))
    })
    
    // 受限的子路由
    ws := r.PathPrefix("/index.php").Subrouter()
    ws.Use(limit)
    ws.HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("This endpoint is rate limited"))
    }).Methods("GET", "POST")
    
    http.ListenAndServe(":8080", r)
}

关键修正点:

  1. 路由配置:使用 Subrouter().Use(limit) 只在特定路径应用限流
  2. 限流器参数rate.NewLimiter(10, 10) 实现每秒10个请求
  3. 中间件位置:确保不在全局路由应用限流中间件
  4. 清理逻辑:修正了清理函数中的时间判断(3分钟清理)

这样配置后,/index.php 路径会按用户名/IP进行限流(每秒10个请求),而 /hello 路径不受限制。

回到顶部