由于每秒上限而限制和排队 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-limiter
和async.queue
更容易使用。
这是一个 sn-p,它显示了如何将所有请求限制为每秒 10 个。
var limit = require("simple-rate-limiter");
var request = limit(require("request")).to(10).per(1000);
【讨论】:
伟大而简单的使用建议。谢谢!【参考方案7】:这是我的解决方案,使用库 request-promise
或 axios
并将调用包装在此承诺中。
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 请求的主要内容,如果未能解决你的问题,请参考以下文章
Google Maps API OVER QUERY LIMIT 每秒限制
如何将 Promise.all() 限制为每秒 5 个 Promise?