Началось всё с задачи: синхронизировать 10 000 сделок из Битрикс24 в нашу аналитическую базу. Звучит просто — вызвал crm.deal.list, получил данные, записал. Но на практике стандартный метод возвращает максимум 50 записей за раз. То есть для 10 000 сделок мне нужно 200 запросов. При среднем времени ответа Битрикс24 в 300–500 мс это 60–100 секунд. А если портал перегружен — можно и по таймауту поймать. Спойлер: мы их ловили регулярно.
Наивный подход и почему он не работает
Сначала я написал цикл с пагинацией через start-параметр. Классика:
$allDeals = [];
$step = 50;
$start = 0;
while (true) {
$response = $webhook->call('crm.deal.list', [
'select' => ['ID', 'TITLE', 'STAGE_ID', 'OPPORTUNITY', 'DATE_CREATE'],
'start' => $start,
]);
if (empty($response['result'])) {
break;
}
$allDeals = array_merge($allDeals, $response['result']);
$start += $step;
// Пауза, чтобы не словить лимит (50 req/sec для облака)
usleep(100000);
}
echo 'Всего сделок: ' . count($allDeals) . PHP_EOL;Этот код работает для сотен элементов. Но для 10 000 он падал у нас примерно в 30% случаев. Причины: таймаут cURL (по умолчанию 30 секунд для некоторых хостингов), временные ошибки 502/503 от Битрикс24, и главное — нет никакого механизма повторных попыток. Потерял один запрос на 201-й итерации — весь цикл сломался.
Batch API: что это и как работает
Batch-метод Битрикс24 позволяет объединить до 50 команд в один HTTP-запрос. Вместо 50 отдельных вызовов вы отправляете один POST на batch с набором команд. Это снижает количество HTTP-соединений в 50 раз и сильно уменьшает суммарное время.
class BitrixBatchClient
{
private string $webhookUrl;
private int $maxBatchSize = 50;
private int $maxRetries = 3;
private int $retryDelayMs = 500;
private int $rateLimitDelayMs = 1100;
public function __construct(string $webhookUrl)
{
$this->webhookUrl = rtrim($webhookUrl, '/') . '/batch';
}
public function fetchAllDeals(array $select = ['ID', 'TITLE', 'STAGE_ID']): array
{
$allDeals = [];
$batchIndex = 0;
$start = 0;
$step = 50;
$hasMore = true;
while ($hasMore) {
$commands = [];
for ($i = 0; $i < $this->maxBatchSize && $hasMore; $i++) {
$cmdName = 'deal_' . $batchIndex . '_' . $i;
$commands[$cmdName] = 'crm.deal.list?' . http_build_query([
'select' => $select,
'start' => $start + ($i * $step),
]);
}
$result = $this->callBatchWithRetry($commands);
foreach ($result as $cmdName => $deals) {
if (empty($deals)) {
$hasMore = false;
break;
}
$allDeals = array_merge($allDeals, $deals);
if (count($deals) < $step) {
$hasMore = false;
break;
}
}
$start += $this->maxBatchSize * $step;
$batchIndex++;
usleep($this->rateLimitDelayMs * 1000);
}
return $allDeals;
}
private function callBatchWithRetry(array $commands): array
{
for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
$ch = curl_init($this->webhookUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['cmd' => $commands]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
trigger_error("cURL ошибка (попытка $attempt): $error", E_USER_WARNING);
usleep($this->retryDelayMs * 1000 * $attempt);
continue;
}
$data = json_decode($response, true);
if ($httpCode !== 200 || isset($data['error'])) {
$errorMsg = $data['error_description'] ?? 'HTTP ' . $httpCode;
trigger_error("API ошибка (попытка $attempt): $errorMsg", E_USER_WARNING);
usleep($this->retryDelayMs * 1000 * $attempt);
continue;
}
return $data['result']['result'] ?? [];
}
throw new \RuntimeException('Batch-запрос не удался после ' . $this->maxRetries . ' попыток');
}
}Ключевые моменты в этом коде. Во-первых, callBatchWithRetry — обёртка с exponential backoff. Битрикс24 может временно вернуть 502 или 429, и мы должны это перехватить. Во-вторых, rateLimitDelayMs — пауза между batch-запросами. Облачный Битрикс24 ограничивает 50 запросами в секунду, и один batch считается за один запрос. Но на практике я ставлю 1.1 секунды — с запасом.
Подводные камни batch-запросов
Первый и самый больной — ограничение на размер тела запроса. У Битрикс24 нет официального лимита, но на практике batch больше 1 МБ начинает падать. Если вы запрашиваете много полей с длинными значениями, 50 команд могут не влезть. Я стабильно беру 25 команд на batch, когда запрашиваю описания сделок и пользовательские поля.
Второй подводный камень — частичные ошибки. Batch может выполниться наполовину: 30 команд прошли, 20 упали. В ответе будет result_error с номерами упавших команд. Нужно проверять это отдельно:
$data = json_decode($response, true);
$successfulResults = $data['result']['result'] ?? [];
$batchErrors = $data['result']['result_error'] ?? [];
if (!empty($batchErrors)) {
foreach ($batchErrors as $cmdName => $errorInfo) {
echo "Ошибка в команде $cmdName: {$errorInfo['error_description']}\n";
}
// Можно переотправить только упавшие команды
$failedCommands = array_intersect_key($commands, $batchErrors);
$retried = $this->callBatchWithRetry($failedCommands);
$successfulResults = array_merge($successfulResults, $retried);
}Третья проблема — отсутствие транзакционности. Если ваш скрипт упал на середине обработки 200-го batch, у вас в базе часть данных синхронизирована, часть — нет. Решение: завести таблицу логов синхронизации с sync_batch_id и при запуске проверять, до какого batch дошли в прошлый раз.
Итоги
После внедрения batch-клиента время синхронизации 10 000 сделок упало с 90–120 секунд до 12–15 секунд. Количество таймаутов — с 30% до нуля (за счёт retry). Главное правило: никогда не полагайтесь на стабильность API Битрикс24, всегда добавляйте retry-логику и логирование. И уменьшайте размер batch при работе с тяжёлыми пользовательскими полями — лучше сделать 8 запросов по 25 команд, чем 4 запроса, из которых 2 упадут.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье