V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mercurius
V2EX  ›  程序员

救救孩子,有个关于令牌桶算法限流的业务问题

  •  
  •   mercurius · 2023-06-30 12:25:45 +08:00 · 1701 次点击
    这是一个创建于 516 天前的主题,其中的信息可能已经有所发展或是发生改变。
    背景:
    1 、亚马逊提供的接口有 QPS 限制,并且他们是根据令牌桶算法对用户进行限制的,拿创建报表的接口举例,该接口每秒恢复 0.0167 个令牌,令牌最大容量为 15 个
    2 、为解决服务频繁触发亚马逊接口 QPS 的问题,这边也实现了个令牌桶算法,在调用亚马逊接口前,先内部去拿令牌,拿到了再去请求接口,并且这个令牌桶的令牌恢复速率、最大容量都跟亚马逊的保持一致


    令牌桶算法实现:
    参考 lua 脚本 https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#file-request_rate_limiter-lua
    每次拿令牌时,根据令牌恢复速率、上次剩余令牌数、上次请求时间,重新计算当前可用令牌数和当前时间,并存回 redis


    问题:
    由于亚马逊调用接口的耗时问题,导致两边令牌无法对上,即我这边刚恢复了 1 个令牌,但亚马逊那边令牌还没恢复到 1 ,这时调用接口会报 QPS 限制。


    举例:
    假设令牌 1 分钟恢复一个,现在我的可用令牌数刚好恢复到 1 个,于是服务拿到这个令牌去请求亚马逊接口,结果亚马逊接口请求了 1 分钟才返回给我结果(亚马逊扣除令牌的行为一定比我晚,具体晚多少则未知)。
    因为过了 1 分钟,我这边可用令牌又恢复了 1 个,于是继续调用亚马逊接口,但亚马逊此时的可用令牌数还没恢复到 1 ,所以给我报 QPS 限制(这时两边的令牌已经对不上了,因为我这边刚把令牌扣成 0 ,但亚马逊那边刚要恢复到 1 )


    已思考过的解决方案:
    核心想法:只要保证我的令牌恢复跟亚马逊相比是延迟的、滞后的,那我能拿到令牌就说明请求亚马逊接口大概率是没问题的。
    实现 1:在请求亚马逊接口后,若当前的可用令牌数大于 1 则正常扣掉 1 ,若小于 1 则不将令牌数重新计算、只将 redis 记录的上次请求时间重置为当前时间(保证该行为只会消耗小于或等于 1 个令牌)
    实现 2:放慢我这边的令牌恢复速率,例如亚马逊每秒恢复 0.0167 个令牌,那我就每秒只恢复 0.0117 个


    自己仅想出这两种解决方案,不知道大佬们有没其他思路可以救下孩子 orz
    totoro52
        1
    totoro52  
       2023-06-30 12:40:18 +08:00
    赶紧还不如直接无脑失败就在请求一次
    whoosy
        2
    whoosy  
       2023-06-30 12:42:11 +08:00
    有必要在应用侧再加个限流?
    hidemyself
        3
    hidemyself  
       2023-06-30 13:16:48 +08:00
    亚马逊那边 QPS 限制了就不给调用了吗?
    给调用就再重试就行了呗
    fkdtz
        4
    fkdtz  
       2023-06-30 13:19:39 +08:00
    看起来你追求的是跟对方完全一致的限流进度,想要维持最大并发量又不想触发 QPS 限流。
    这个有点类似分布式事务的场景,需要在网络环境中保持不同系统间状态完全一致,想要做到完全一致是比较难搞的,何况你这还是在三方系统中。

    但我理解实际上你没必要自己搞一个限流对标对方的限流,限流本身就是保护后端的,你只需要处理一下发生限流时做退避重试就好了,只要关注自己的业务并发量够用就行。
    tinyint00
        5
    tinyint00  
       2023-06-30 13:37:18 +08:00
    您这个属实是,简单问题复杂化。
    UN2758
        6
    UN2758  
       2023-06-30 13:43:10 +08:00
    你这样层层套娃没必要,退避重试就好了
    CodeGou
        7
    CodeGou  
       2023-06-30 14:03:55 +08:00
    新时代的刻舟求剑了。
    mercurius
        8
    mercurius  
    OP
       2023-06-30 14:11:02 +08:00
    好吧,看大家的意见都差不多,基本是没必要在这边也搞个限流,侧重点应该放在重试而不是限流。

    看样子是我自顾自陷入死胡同了,总想着待在坑里应该怎么优化,而没想过应该从坑里跳出来…… orz

    多谢大家的回复
    rockyliang
        9
    rockyliang  
       2023-06-30 14:13:14 +08:00
    简单问题复杂化了,请求失败就 sleep 1 秒或 2 秒,然后重试就可以了
    laozhoubuluo
        10
    laozhoubuluo  
       2023-06-30 14:31:55 +08:00
    如果每个请求时间恒定,可以考虑测量一下该场景下两边恢复时间的差值之后在发请求的时候补偿掉(例如预计 25 秒之后恢复,实际上根据经验还需要等一秒钟,那我 27 秒之后再发起请求)。
    z1829909
        11
    z1829909  
       2023-06-30 14:41:35 +08:00
    如果你的业务是离线的, 就只用一个进程去处理, 在处理的时候, sleep 特定的时间, 如果出现错误跳过, 在下一批再重试就行
    mercurius
        12
    mercurius  
    OP
       2023-06-30 14:47:26 +08:00
    @rockyliang #9
    @z1829909 #11
    实际业务是有高并发场景的,不能阻塞其他的请求任务,不然很容易堆积,同时还有性能问题,所以是做成了内部校验条件不满足就跳过去的情况
    MoYi123
        13
    MoYi123  
       2023-06-30 14:49:22 +08:00
    如果是公司的业务, 充钱可以让你变强.
    mercurius
        14
    mercurius  
    OP
       2023-06-30 14:50:25 +08:00
    @laozhoubuluo #10 很遗憾,因为实际请求耗时并不恒定,所以这个补偿的差值很难把控,若是能确定差值的话,直接调整令牌恢复速度就可以实现了
    FrankAdler
        15
    FrankAdler  
       2023-06-30 14:56:20 +08:00
    如果亚马逊接口返回后,你本地的令牌才能恢复,这样就不会有问题了吗,那直接再加一个发放桶就完事了
    darksword21
        16
    darksword21  
       2023-06-30 15:31:32 +08:00
    我也有过同样的问题,直接重试+sleep 一把梭
    z1829909
        17
    z1829909  
       2023-06-30 17:19:04 +08:00
    @mercurius 有时间要求的话,可以做成异步的. 因为你们水桶最短的那个板子已经确定了, 优化的空间不太大感觉
    huzhizhao
        18
    huzhizhao  
       2023-07-01 06:43:49 +08:00 via iPhone
    😂😂😂😂非常贴切,新时代刻舟求剑。
    套娃有啥作用呢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4070 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:19 · PVG 18:19 · LAX 02:19 · JFK 05:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.