许多服务不仅提供给普通用户通过完善和优化的图形界面与他们进行交互的能力,还提供了通过API通过其程序为外部开发人员提供与之交互的能力。同时,服务控制其基础架构上的负载也很重要。在普通用户的情况下,大多数负载问题不会由于服务开发人员控制向应用程序发送请求到服务的应用程序代码(用户试图在开发人员建议的接口框架和文档化功能之外的应用程序中执行某些操作)而发生。不考虑)。对于外部开发人员,仅在这些外部开发人员的想象力的作用下,限制在服务上创建负载的范围。为了稍微限制这个空间,在单位时间内对服务API引入请求数量限制的做法已经很普遍。
我们已经讨论了如何实现这些限制,如果您自己开发服务API,今天我们要讨论如何从“客户端”端开始,并方便地使用受请求数量限制的API。
输入项
我叫Yuri Gavrilov,我在ManyChat的Data Platfrom团队工作。我们公司有一个营销部门,除其他外,他喜欢通过对讲服务与客户沟通, 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 .