由于每秒上限而限制和排队 API 请求

Posted

技术标签:

【中文标题】由于每秒上限而限制和排队 API 请求【英文标题】:Throttle and queue up API requests due to per second cap 【发布时间】:2013-12-13 17:47:55 【问题描述】:

我使用mikeal/request 进行 API 调用。我最常使用的 API 之一(Shopify API)。最近发布了一个新的call limit,我看到如下错误:

Exceeded 6.0 calls per second for api client. Slow your requests or contact support for higher limits.

我已经升级了,但无论我获得多少带宽,我都必须考虑到这一点。 Shopify API 的大部分请求都在 async.map() 函数中,这些函数循环异步请求并收集正文。

我正在寻找任何帮助,也许是一个已经存在的库,它将包裹请求模块并实际阻止、睡眠、限制、分配、管理许多异步触发的同时请求并将它们限制为一次说6 请求。如果不存在这样的项目,我对从事这样的项目没有任何问题。我只是不知道如何处理这种情况,我希望有某种标准。

我用mikeal/request做了一张票。

【问题讨论】:

不开玩笑。我终于厌倦了 ElasticTranscoder UI 并构建代码以通过 JS SDK 使用 API 并立即达到这些限制。 2018 年有 rate-limiter-flexible 包可以完成这项工作 任何人都可以提供java解决方案 【参考方案1】:

我使用现代香草 JS 的解决方案:

function throttleAsync(fn, wait) 
  let lastRun = 0;

  async function throttled(...args) 
    const currentWait = lastRun + wait - Date.now();
    const shouldRun   = currentWait <= 0;

    if (shouldRun) 
      lastRun = Date.now();
      return await fn(...args);
     else 
      return await new Promise(function(resolve) 
        setTimeout(function() 
          resolve(throttled());
        , currentWait);
      );
    
  

  return throttled;

用法:

const throttledRun = throttleAsync(run, 1000);

【讨论】:

谢谢!但是,应该使用 ...args 调用对 throttled 的自调用调用【参考方案2】:

我使用async-sema 模块处理限制 HTTP 请求。这意味着它允许您发送具有速率限制的 HTTP 请求。

这是一个例子:

一个简单的 Node.js 服务器,在 API 中添加 express-rate-limit 中间件,使 API 具有限速功能。假设这是您的案例的 Shopify API。

server.ts:

import express from 'express';
import rateLimit from 'express-rate-limit';
import http from 'http';

const port = 3000;
const limiter = new rateLimit(
  windowMs: 1000,
  max: 3,
  message: 'Max RPS = 3',
);

async function createServer(): Promise<http.Server> 
  const app = express();

  app.get('/place', limiter, (req, res) => 
    res.end('Query place success.');
  );

  return app.listen(port, () => 
    console.log(`Server is listening on http://localhost:$port`);
  );


if (require.main === module) 
  createServer();


export  createServer ;

在客户端,我们希望发送 HTTP 请求,并发数 = 3,并且它们之间的每秒上限。我将客户端代码放在一个测试用例中。所以不要觉得奇怪。

server.test.ts:

import  RateLimit  from 'async-sema';
import rp from 'request-promise';
import  expect  from 'chai';
import  createServer  from './server';
import http from 'http';

describe('20253425', () => 
  let server: http.Server;
  beforeEach(async () => 
    server = await createServer();
  );
  afterEach((done) => 
    server.close(done);
  );
  it('should throttle http request per second', async () => 
    const url = 'http://localhost:3000/place';
    const n = 10;
    const lim = RateLimit(3,  timeUnit: 1000 );

    const resArr: string[] = [];
    for (let i = 0; i < n; i++) 
      await lim();
      const res = await rp(url);
      resArr.push(res);
      console.log(`[$new Date().toLocaleTimeString()] request $i + 1, response: $res`);
    

    expect(resArr).to.have.lengthOf(n);
    resArr.forEach((res) => 
      expect(res).to.be.eq('Query place success.');
    );
  );
);

测试结果,注意请求的时间

  20253425
Server is listening on http://localhost:3000
[8:08:17 PM] request 1, response: Query place success.
[8:08:17 PM] request 2, response: Query place success.
[8:08:17 PM] request 3, response: Query place success.
[8:08:18 PM] request 4, response: Query place success.
[8:08:18 PM] request 5, response: Query place success.
[8:08:18 PM] request 6, response: Query place success.
[8:08:19 PM] request 7, response: Query place success.
[8:08:19 PM] request 8, response: Query place success.
[8:08:19 PM] request 9, response: Query place success.
[8:08:20 PM] request 10, response: Query place success.
    ✓ should throttle http request per second (3017ms)


  1 passing (3s)

【讨论】:

【参考方案3】:

在异步模块中,这个请求的功能被关闭为“不会修复”

2016 年给出的理由是“正确管理这种结构是 一个难题。”见右侧: https://github.com/caolan/async/issues/1314 2013 年给出的原因是“无法扩展到多个进程”请参阅: https://github.com/caolan/async/issues/37#issuecomment-14336237

有一个使用leakybucket或令牌桶模型的解决方案,它被实现为“限制器”npm模块作为RateLimiter。

RateLimiter,请参见此处的示例:https://github.com/caolan/async/issues/1314#issuecomment-263715550

另一种方法是使用 PromiseThrottle,我使用了这个,工作示例如下:

var PromiseThrottle = require('promise-throttle');
let RATE_PER_SECOND = 5; // 5 = 5 per second, 0.5 = 1 per every 2 seconds

var pto = new PromiseThrottle(
    requestsPerSecond: RATE_PER_SECOND, // up to 1 request per second
    promiseImplementation: Promise  // the Promise library you are using
);

let timeStart = Date.now();
var myPromiseFunction = function (arg) 
    return new Promise(function (resolve, reject) 
        console.log("myPromiseFunction: " + arg + ", " + (Date.now() - timeStart) / 1000);
        let response = arg;
        return resolve(response);
    );
;

let NUMBER_OF_REQUESTS = 15;
let promiseArray = [];
for (let i = 1; i <= NUMBER_OF_REQUESTS; i++) 
    promiseArray.push(
            pto
            .add(myPromiseFunction.bind(this, i)) // passing am argument using bind()
            );


Promise
        .all(promiseArray)
        .then(function (allResponsesArray)  // [1 .. 100]
            console.log("All results: " + allResponsesArray);
        );

输出:

myPromiseFunction: 1, 0.031
myPromiseFunction: 2, 0.201
myPromiseFunction: 3, 0.401
myPromiseFunction: 4, 0.602
myPromiseFunction: 5, 0.803
myPromiseFunction: 6, 1.003
myPromiseFunction: 7, 1.204
myPromiseFunction: 8, 1.404
myPromiseFunction: 9, 1.605
myPromiseFunction: 10, 1.806
myPromiseFunction: 11, 2.007
myPromiseFunction: 12, 2.208
myPromiseFunction: 13, 2.409
myPromiseFunction: 14, 2.61
myPromiseFunction: 15, 2.811
All results: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

我们可以清楚地看到输出的速率,即每秒 5 次调用。

【讨论】:

【参考方案4】:

其他解决方案不符合我的口味。进一步研究,我发现了promise-ratelimit,它为您提供了一个API,您可以简单地await

var rate = 2000 // in milliseconds
var throttle = require('promise-ratelimit')(rate)

async function queryExampleApi () 
  await throttle()
  var response = await get('https://api.example.com/stuff')
  return response.body.things

以上示例将确保您仅每 2000 毫秒最多api.example.com 进行一次查询。换句话说,第一个请求不会等待 2000 毫秒。

【讨论】:

【参考方案5】:

我在使用各种 API 时遇到了同样的问题。 AWS 也以节流而闻名。

可以使用几种方法。您提到了 async.map() 函数。你试过async.queue()吗? queue 方法应该允许你设置一个固定的限制(比如 6 个),超过这个数量的任何东西都将被放入队列中。

另一个有用的工具是oibackoff。如果您从服务器收到错误并重试,该库将允许您回退您的请求。

包装这两个库以确保您的两个库都被覆盖会很有用:async.queue 可确保您不会超出限制,oibackoff 可确保您在收到请求时再次获得机会服务器告诉你有一个错误。

【讨论】:

我将深入研究这两个建议。我唯一的问题是我的async.maps 分散并相互嵌套。所以我不能只用async.queue 替换它们,因为我仍然不能保证对 API 的请求一次是 6 个。它们将是 6 * 每个 async.queue。但我认为球在滚动? caolan.github.io/async/docs.html#queue 不会节流(每秒/分钟)。这只是异步操作的数量。【参考方案6】:

npm 包simple-rate-limiter 似乎是解决这个问题的一个很好的解决方案。

而且,它比node-rate-limiterasync.queue更容易使用。

这是一个 sn-p,它显示了如何将所有请求限制为每秒 10 个。

var limit = require("simple-rate-limiter");
var request = limit(require("request")).to(10).per(1000);

【讨论】:

伟大而简单的使用建议。谢谢!【参考方案7】:

这是我的解决方案,使用库 request-promiseaxios 并将调用包装在此承诺中。

var Promise = require("bluebird")

// http://***.com/questions/28459812/way-to-provide-this-to-the-global-scope#28459875
// http://***.com/questions/27561158/timed-promise-queue-throttle

module.exports = promiseDebounce

function promiseDebounce(fn, delay, count) 
  var working = 0, queue = [];
  function work() 
    if ((queue.length === 0) || (working === count)) return;
    working++;
    Promise.delay(delay).tap(function ()  working--; ).then(work);
    var next = queue.shift();
    next[2](fn.apply(next[0], next[1]));
  
  return function debounced() 
    var args = arguments;
    return new Promise(function(resolve)
      queue.push([this, args, resolve]);
      if (working < count) work();
    .bind(this));
  

【讨论】:

【参考方案8】:

对于另一种解决方案,我使用node-rate-limiter 来包装请求函数,如下所示:

var request = require('request');
var RateLimiter = require('limiter').RateLimiter;

var limiter = new RateLimiter(1, 100); // at most 1 request every 100 ms
var throttledRequest = function() 
    var requestArgs = arguments;
    limiter.removeTokens(1, function() 
        request.apply(this, requestArgs);
    );
;

【讨论】:

我要调查一下!非常感谢! node-rate-limiter 的作者在这里。这个库可能更适合上述问题,因为 async.queue() 只限制并发并且没有时间概念。 API 速率限制通常是基于时间的(即每秒最多 6 次调用),可以表示为 var limiter = new RateLimiter(6, 'second'); 它是对 oibackoff 等解决方案的补充,它会在达到速率限制后改变行为。 我可以为所有请求整体做还是需要单独做?我的意思是我可以把它放在我的中间件中吗?如果是,它将如何应用于所有端点或每个端点? 这只是限制调用还是作为排队机制起作用。意思是如果我超过限制,它将排队请求并在刷新限制后再次开始调用? 是的,它会排队。我认为一旦令牌可用,node-rate-limiter 就会给你回电

以上是关于由于每秒上限而限制和排队 API 请求的主要内容,如果未能解决你的问题,请参考以下文章

如何限制每秒 Web 请求以避免垃圾邮件和拒绝服务

Google Maps API OVER QUERY LIMIT 每秒限制

Adwords API 每秒查询次数限制

如何将 Promise.all() 限制为每秒 5 个 Promise?

如何在 microsoft face api 中增加每秒事务限制?

了解Google Search Console URL测试工具API限制