web-dev-qa-db-ja.com

1秒あたりの上限によるAPIリクエストのスロットルとキューアップ

マイク/リクエスト を使用してAPI呼び出しを行います。私が最も頻繁に使用するAPIの1つ(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 でチケットを作成しました。

50
ThomasReggi

さまざまなAPIで同じ問題が発生しました。 AWSはスロットリングでも有名です。

いくつかのアプローチを使用できます。 async.map()関数について言及しました。 async.queue() を試しましたか?キュー方式では、しっかりとした制限(6など)を設定でき、その量を超えるものはすべてキューに配置されます。

もう1つの便利なツールは、 oibackoff です。そのライブラリを使用すると、サーバーからエラーが返された場合にリクエストをバックオフして、再試行できます。

2つのライブラリをラップして、両方のベースがカバーされていることを確認すると便利です。async.queueを使用して制限を超えないようにし、oibackoffを使用して、サーバーから指示があった場合にリクエストを取得する別のショットを取得しますエラーが発生しました。

14
Dan

別の解決策として、 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);
    });
};
33
Dmitry Chornyi

npmパッケージ simple-rate-limiter は、この問題の非常に良い解決策のようです。

さらに、node-rate-limiterおよびasync.queueよりも使いやすいです。

以下は、すべてのリクエストを1秒あたり10に制限する方法を示すスニペットです。

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

非同期モジュールでは、このリクエストされた機能は「修正不可」として閉じられます。

Leakybucketまたはトークンバケットモデルを使用したソリューションがあり、RateLimiterとして「リミッター」npmモジュールが実装されています。

RateLimiter、こちらの例を参照してください: https://github.com/caolan/async/issues/1314#issuecomment -26371555

別の方法は、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コールを明確に見ることができます。

ライブラリを使用するソリューションは次のとおりですrequest-promiseまたはaxiosで、このプロミスで呼び出しをラップします。

var Promise = require("bluebird")

// http://stackoverflow.com/questions/28459812/way-to-provide-this-to-the-global-scope#28459875
// http://stackoverflow.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));
  }
2
ThomasReggi

他の解決策は私の好みではありませんでした。さらに調査して、私は promise-ratelimit を見つけました。これは、単純に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
}

上記の例では、api.example.comへのクエリは最大で2000ミリ秒のみになります。つまり、最初のリクエストは2000ms待機しません

1
mindeavor

最新のバニラ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);
0
djanowski