需求
在做一个基于图片文字识别的题库管理系统,使用 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