需求

在做一个基于图片文字识别的题库管理系统,使用 Golang 调用百度 OCR 文字识别接口, 但是百度 OCR 接口有调用频率限制:

  • 免费版的 QPS 为 2。即每秒最多调用两次
  • 付费版的 QPS 为 10

如果不限速,就会报错:

{"error_code":18,"error_msg":"Open api qps request limit reached"}

而前端在上传图片时,是支持多个图片批量上传的,且会有多人同时上传。 那么就需要一个限制接口速度的功能。

备选方案

  • 方案一:通过队列来处理,单个 worker 方便限速。
  • 方案二:通过 Gin Middleware 配合 time/rate 库来实现限速

由于是个小项目,不想做的太复杂,为了部署方便不考虑队列的方案。 虽然方案二存在超时的隐患(不丢弃请求的话),但是用于用户量有限,也就 30 人的规模,所以暂时可控。

time/rate

官方文档更简洁明了:

https://pkg.go.dev/golang.org/x/time/rate

不要急躁,过一遍文档,再看看源代码。

Wait 与 WaitN 的区别

没有 token 时,wait 会阻塞执行,直到有一个可用的 token。

  • Wait 是等待一个 token
  • WaitN 是等待 N 个 token

burst 怎么理解

构造一个限流器, 这里有两个参数:

limiter := NewLimiter(10, 1);
  • 第一个参数是 r Limit。代表每秒可以向 Token 桶中产生多少 token。即,限速的值。比如我要限速每秒两次,那么 Limit 就为 2.
  • 第二个参数是 b int。b 代表 Token 桶的容量大小。即, burst。

上面代码,其令牌桶大小为 1, 以每秒 10 个 Token 的速率向桶中放置 Token。

burst 突发的意思。可以理解为瞬间的突发值,其实也是令牌桶的最大容量。

例如,burst 为 2,那么 AllowN 3 就是不可能被允许的。

wait 的 context 参数

func (lim *Limiter) Wait(ctx context.Context) (err error)

设置 context 的 Deadline 或者 Timeout,可以决定 Wait 的最长时间。

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.

https://github.com/penril0326/shorturl/blob/main/controller/middleware/middleware.go

func PostRequestLimit(ctx *gin.Context) {
	postLimitHandler.Wait(ctx)

	ctx.Next()
}

这里直接使用了 gin.Context。

https://pkg.go.dev/context#Context

从这里可知 Context 实际上是个 interface,只要实现了 Deadline / Done / Err / Value 就可以。

Context is the most important part of gin. It allows us to pass variables between middleware, manage the flow, validate the JSON of a request and render a JSON response for example.

allow 与 wait 的区别

从使用场景上:

  • allow 判断是否有足够的 token。不够,可以直接丢弃请求,适合线上并发量大的情况。
  • wait 可以阻塞式等待,不丢弃请求。

code

package middleware

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

const (
	LIMIT_PER_SECOND = 1
	BUCKET_SIZE      = 1 // burst
)

var limiter *rate.Limiter

func init() {
	limiter = rate.NewLimiter(LIMIT_PER_SECOND, BUCKET_SIZE)
}

func OcrRequestLimit(ctx *gin.Context) {
	limiter.Wait(ctx)
	ctx.Next()
}

// 用于测试
func LimiterTest(c *gin.Context) {
	id := c.DefaultQuery("id", "-1")
	log.Println(id)
	c.JSON(http.StatusOK, gin.H{
		"err_code": 0,
		"err_msg":  "OK",
	})
}

使用

api.GET("/limiter", middleware.OcrRequestLimit, middleware.LimiterTest)
api.GET("/nolimiter", middleware.LimiterTest)

测试脚本

#!/bin/bash

set -e  # or use "set -o errexit" to quit on error.
set -x  # or use "set -o xtrace" to print the statement before you execute it.

for (( i = 0; i < 20; i++ )); do
        #curl -s -k 'GET' 'http://localhost:9018/api/nolimiter?id='$i
        curl -s -k 'GET' 'http://localhost:9018/api/limiter?id='$i
done

无限速测试:

[GIN] 2022/12/25 - 09:29:28 | 200 |        37.7µs |       127.0.0.1 | GET      "/api/nolimiter?id=9"
2022/12/25 09:30:01 0
[GIN] 2022/12/25 - 09:30:01 | 200 |        48.6µs |       127.0.0.1 | GET      "/api/nolimiter?id=0"
2022/12/25 09:30:01 1
[GIN] 2022/12/25 - 09:30:01 | 200 |        38.9µs |       127.0.0.1 | GET      "/api/nolimiter?id=1"
2022/12/25 09:30:01 2
[GIN] 2022/12/25 - 09:30:01 | 200 |        52.5µs |       127.0.0.1 | GET      "/api/nolimiter?id=2"
2022/12/25 09:30:01 3
[GIN] 2022/12/25 - 09:30:01 | 200 |        52.6µs |       127.0.0.1 | GET      "/api/nolimiter?id=3"
2022/12/25 09:30:01 4
[GIN] 2022/12/25 - 09:30:01 | 200 |        85.7µs |       127.0.0.1 | GET      "/api/nolimiter?id=4"
2022/12/25 09:30:01 5
[GIN] 2022/12/25 - 09:30:01 | 200 |          54µs |       127.0.0.1 | GET      "/api/nolimiter?id=5"
2022/12/25 09:30:01 6
[GIN] 2022/12/25 - 09:30:01 | 200 |        53.8µs |       127.0.0.1 | GET      "/api/nolimiter?id=6"
2022/12/25 09:30:01 7
[GIN] 2022/12/25 - 09:30:01 | 200 |        45.8µs |       127.0.0.1 | GET      "/api/nolimiter?id=7"
2022/12/25 09:30:01 8
[GIN] 2022/12/25 - 09:30:01 | 200 |        46.2µs |       127.0.0.1 | GET      "/api/nolimiter?id=8"
2022/12/25 09:30:02 9
[GIN] 2022/12/25 - 09:30:02 | 200 |        64.4µs |       127.0.0.1 | GET      "/api/nolimiter?id=9"
2022/12/25 09:30:02 10
[GIN] 2022/12/25 - 09:30:02 | 200 |        37.7µs |       127.0.0.1 | GET      "/api/nolimiter?id=10"
2022/12/25 09:30:02 11
[GIN] 2022/12/25 - 09:30:02 | 200 |        57.7µs |       127.0.0.1 | GET      "/api/nolimiter?id=11"
2022/12/25 09:30:02 12
[GIN] 2022/12/25 - 09:30:02 | 200 |        47.2µs |       127.0.0.1 | GET      "/api/nolimiter?id=12"
2022/12/25 09:30:02 13
[GIN] 2022/12/25 - 09:30:02 | 200 |        37.2µs |       127.0.0.1 | GET      "/api/nolimiter?id=13"
2022/12/25 09:30:02 14
[GIN] 2022/12/25 - 09:30:02 | 200 |        43.2µs |       127.0.0.1 | GET      "/api/nolimiter?id=14"
2022/12/25 09:30:02 15
[GIN] 2022/12/25 - 09:30:02 | 200 |       130.6µs |       127.0.0.1 | GET      "/api/nolimiter?id=15"
2022/12/25 09:30:02 16
[GIN] 2022/12/25 - 09:30:02 | 200 |          42µs |       127.0.0.1 | GET      "/api/nolimiter?id=16"
2022/12/25 09:30:02 17
[GIN] 2022/12/25 - 09:30:02 | 200 |        38.2µs |       127.0.0.1 | GET      "/api/nolimiter?id=17"
2022/12/25 09:30:02 18
[GIN] 2022/12/25 - 09:30:02 | 200 |        38.7µs |       127.0.0.1 | GET      "/api/nolimiter?id=18"
2022/12/25 09:30:02 19
[GIN] 2022/12/25 - 09:30:02 | 200 |          48µs |       127.0.0.1 | GET      "/api/nolimiter?id=19"

限速测试:

2022/12/25 09:31:34 0
[GIN] 2022/12/25 - 09:31:34 | 200 |          67µs |       127.0.0.1 | GET      "/api/limiter?id=0"
2022/12/25 09:31:35 1
[GIN] 2022/12/25 - 09:31:35 | 200 |    450.4608ms |       127.0.0.1 | GET      "/api/limiter?id=1"
2022/12/25 09:31:35 2
[GIN] 2022/12/25 - 09:31:35 | 200 |    453.2647ms |       127.0.0.1 | GET      "/api/limiter?id=2"
2022/12/25 09:31:36 3
[GIN] 2022/12/25 - 09:31:36 | 200 |      431.77ms |       127.0.0.1 | GET      "/api/limiter?id=3"
2022/12/25 09:31:36 4
[GIN] 2022/12/25 - 09:31:36 | 200 |    431.9334ms |       127.0.0.1 | GET      "/api/limiter?id=4"
2022/12/25 09:31:37 5
[GIN] 2022/12/25 - 09:31:37 | 200 |    453.9694ms |       127.0.0.1 | GET      "/api/limiter?id=5"
2022/12/25 09:31:37 6
[GIN] 2022/12/25 - 09:31:37 | 200 |    444.9289ms |       127.0.0.1 | GET      "/api/limiter?id=6"
2022/12/25 09:31:38 7
[GIN] 2022/12/25 - 09:31:38 | 200 |    450.2494ms |       127.0.0.1 | GET      "/api/limiter?id=7"
2022/12/25 09:31:38 8
[GIN] 2022/12/25 - 09:31:38 | 200 |    445.8131ms |       127.0.0.1 | GET      "/api/limiter?id=8"
2022/12/25 09:31:39 9
[GIN] 2022/12/25 - 09:31:39 | 200 |    446.8746ms |       127.0.0.1 | GET      "/api/limiter?id=9"
2022/12/25 09:31:39 10
[GIN] 2022/12/25 - 09:31:39 | 200 |    436.7093ms |       127.0.0.1 | GET      "/api/limiter?id=10"
2022/12/25 09:31:40 11
[GIN] 2022/12/25 - 09:31:40 | 200 |    448.8258ms |       127.0.0.1 | GET      "/api/limiter?id=11"
2022/12/25 09:31:45 12
[GIN 2022/12/25 - 09:31:45 | 200 |       154.4µs |       127.0.0.1 | GET      "/api/limiter?id=12"
2022/12/25 09:31:45 13
[GIN] 2022/12/25 - 09:31:45 | 200 |     446.706ms |       127.0.0.1 | GET      "/api/limiter?id=13"
2022/12/25 09:31:46 14
[GIN] 2022/12/25 - 09:31:46 | 200 |    450.3291ms |       127.0.0.1 | GET      "/api/limiter?id=14"
2022/12/25 09:31:46 15
[GIN] 2022/12/25 - 09:31:46 | 200 |    447.9787ms |       127.0.0.1 | GET      "/api/limiter?id=15"
2022/12/25 09:31:51 16
[GIN] 2022/12/25 - 09:31:51 | 200 |        39.9µs |       127.0.0.1 | GET      "/api/limiter?id=16"
2022/12/25 09:31:52 17
[GIN] 2022/12/25 - 09:31:52 | 200 |    451.9662ms |       127.0.0.1 | GET      "/api/limiter?id=17"
2022/12/25 09:31:52 18
[GIN] 2022/12/25 - 09:31:52 | 200 |    450.0781ms |       127.0.0.1 | GET      "/api/limiter?id=18"
2022/12/25 09:31:53 19
[GIN] 2022/12/25 - 09:31:53 | 200 |    448.0615ms |       127.0.0.1 | GET      "/api/limiter?id=19"]

确实有效。

但是奇怪的不知为何第 12 号请求卡了 5 秒。

超时时间

  • Nginx 的默认超时时间是 60 秒
  • Golang Gin 没有找到默认的超时时间

gopls 报错

[gopls] cannot use ctx (variable of type *gin.Context) as context.Context value in argument to limiter.Wait: wrong type for method Value [Error]

但是编译能通过。

参考

  • https://www.sunzhongwei.com/golang-bluetooth-signs-monitoring-equipment-data-reporting-and-storage-of-frequency-control?from=bottom
  • https://www.cyhone.com/articles/usage-of-golang-rate/
  • https://stackoverflow.com/questions/54900121/rate-limit-function-40-second-with-golang-org-x-time-rate
  • https://github.com/takeshiyu/gin-throttle
  • https://pkg.go.dev/context#WithTimeout
  • https://stackoverflow.com/questions/72891546/golang-org-x-time-rate-and-context
  • 重点 https://github.com/penril0326/shorturl/blob/main/controller/middleware/middleware.go
  • 这个项目作为 gin 的项目结构组织也非常不错 https://github.com/penril0326/shorturl