Golang Gin API 接口限速

文章目录

    需求

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

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式