リクエスト数の外部API制限に対応する方法

多くのサービスは、洗練され最適化されたグラフィカルインターフェイスを介して通常のユーザーだけでなく、APIを介してプログラムから外部の開発者にも対話する機能を提供します。同時に、サービスがインフラストラクチャの負荷を制御することも重要です。通常のユーザーの状況では、サービス開発者(開発者によって提案されたインターフェイスと文書化された機能のフレームワークの外でアプリケーションで何かをしようとしているユーザー、私たちはこの記事にいます)によってサービスに要求を送信するアプリケーションコードの制御により、ほとんどの負荷の問題は発生しません考慮されません)。外部の開発者の場合、サービスに負荷をかける範囲は、これらの非常に外部の開発者の想像力によってのみ制限されます。このスペースを少し制限するには、単位時間あたりの要求数に制限をサービスAPIに導入する慣行が広まっています。 

我々はすでにしている話を、あなた自身が、私たちは「クライアント」側から住んでいると便利な要求の数に制限APIを使用する方法についてお話したいと思い、今日のサービスのAPIを開発する場合は、これらの制限を実装する方法について。

入力

私の名前はYuriGavrilovです。ManyChatのDataPlatfromチームで働いています。当社には、とりわけインターコムサービスを通じてクライアントとコミュニケーションをとることが大好きなマーケティング部門があります。, In-App -. , Intercom (, , ..). Intercom - -, . , , ( ), , -. , ML- . , Intercom.

: , , API 1000 . , Intercom, .

, API , . «» «» -, .

- , « » API, API, API. , , API .

« »

, ManyChat Redis — . « » - , API . , API, «», , - . , , «» , - Intercom, , «» .

Redis, List .

, API, consumer API. rate-limit, , , .

— «» - ( BackendQueue), «» (AnalyticsQueue). , , consumer, , .

(JSON):

{
    "method_name": "users_update", //  ,   
    "parameters": {"user_id": 123} // ,       
}

MVP consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    
    ...
    
    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        while (true) {
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }
}

, , — .

:

Backend (PHP):

...
$payload = [
    'method_name' => 'users_update',
    'parameters' => ['user_id' => 123, 'registration_date' => '2020-10-01'],
];
$this->getRedis()->rawCommand('RPUSH', 'BackendQueue', json_encode($payload));
...

(Python):

...
payload = {
    'method_name': 'users_update',
    'parameters': {'user_id': 123, 'advanced_metric': 42},
}
redis_client.rpush('AnalyticsQueue', json.dumps(payload))
...

— , Intercom, . — - , API «» , rate-limit, customer'a rate-limit', , - . Redis ( ) consumer'. , , consumer', , . , , , , .

, , consumer' , . consumer' , API .

consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    private const INTERCOM_RATE_LIMIT = 150;
    private const INTERCOM_API_WORKERS = 5;

    ...

    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        $currentTimeframe = $this->getCurrentTimeframe();
        $currentRequestCount = 0;
        
        while (true) {
            if ($currentTimeframe !== $this->getCurrentTimeframe()) {
                $currentTimeframe = $this->getCurrentTimeframe();
                $currentRequestCount = 0;
            } elseif ($currentRequestCount > $this->getProcessRateLimit()) {
                usleep(100 * 1000);
                continue;
            }
            
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }

    private function getProcessRateLimit(): int
    {
        return (int) floor(self::INTERCOM_RATE_LIMIT / self::INTERCOM_API_WORKERS);
    }

    private function getCurrentTimeframe(): int
    {
        return (int) ceil(time() / self::RATE_LIMIT_TIMEFRAME);
    }
}

API

- API, . API . — . , , callback'e, consumer' . callback', , .

, , , , .

, , //?

, API , rate-limit, . , . , , , , , .

, ,   .

API, , , API , API .

, - . , API .




All Articles