Содержание
Обработка проблем с регулированием или «429 — слишком много ошибок запросов» — Azure Logic Apps
- Статья
- 10 минут на чтение
В Azure Logic Apps ваше приложение логики возвращает ошибку «HTTP 429 Слишком много запросов» при регулировании, которое происходит, когда количество запросов превышает скорость, с которой пункт назначения может обрабатываться в течение определенного периода времени. Регулирование может создавать такие проблемы, как задержка обработки данных, снижение производительности и ошибки, такие как превышение указанной политики повторных попыток.
Вот некоторые распространенные типы регулирования, с которыми может столкнуться ваше приложение логики:
- Приложение логики
- Соединитель
- Служба назначения или система
Регулирование приложения логики
Служба Azure Logic Apps имеет собственные ограничения пропускной способности. Таким образом, если ваше приложение логики превышает эти ограничения, ресурсы вашего приложения логики регулируются, а не только конкретный экземпляр или выполнение.
Чтобы найти события регулирования на этом уровне, проверьте 9 приложений логики.0033 Панель показателей на портале Azure.
На портале Azure откройте приложение логики в конструкторе приложений логики.
В меню приложения логики в разделе Мониторинг выберите Метрики .
В разделе Заголовок диаграммы выберите Добавить метрику , чтобы добавить еще одну метрику к существующей.
В первой полосе метрик из списка METRIC выберите Action Throttled Events . На второй панели показателей из списка METRIC выберите Trigger Throttled Events .
Для управления регулированием на этом уровне доступны следующие параметры:
Ограничьте количество экземпляров приложения логики, которые могут выполняться одновременно.
По умолчанию, если условие триггера вашего приложения логики выполняется более одного раза одновременно, несколько экземпляров триггера для вашего приложения логики выполняются одновременно или параллельно . Это означает, что каждый экземпляр триггера срабатывает до того, как завершится выполнение предыдущего экземпляра рабочего процесса.
Хотя количество экземпляров триггера, которые могут одновременно выполняться по умолчанию, не ограничено, вы можете ограничить это число, включив параметр параллелизма триггера и, при необходимости, выбрав ограничение, отличное от значения по умолчанию.
Включить режим высокой пропускной способности.
Приложение логики имеет ограничение по умолчанию на количество действий, которые могут выполняться в течение 5-минутного скользящего интервала. Чтобы увеличить это ограничение до максимального количества действий, включите режим высокой пропускной способности в приложении логики.
Отключить поведение разделения массива («разбиение») в триггерах.
Если триггер возвращает массив для обработки оставшихся действий рабочего процесса, параметр триггера Split On разделяет элементы массива и запускает экземпляр рабочего процесса для каждого элемента массива, эффективно запуская несколько одновременных запусков до предела Split On . . Чтобы управлять регулированием, отключите поведение Split On и пусть ваше приложение логики будет обрабатывать весь массив одним вызовом, а не обрабатывать один элемент за вызов.
Рефакторинг действий в более мелкие приложения логики.
Как упоминалось ранее, приложение логики ограничено количеством действий по умолчанию, которые могут выполняться в течение 5-минутного периода. Хотя вы можете увеличить это ограничение, включив режим высокой пропускной способности, вы также можете подумать о том, хотите ли вы разбить действия вашего приложения логики на более мелкие приложения логики, чтобы количество действий, выполняемых в каждом приложении логики, оставалось ниже ограничения. Таким образом вы снижаете нагрузку на один ресурс приложения логики и распределяете нагрузку между несколькими приложениями логики. Это решение работает лучше для действий, которые обрабатывают большие наборы данных или запускают так много одновременно выполняемых действий, итераций цикла или действий внутри каждой итерации цикла, что они превышают предел выполнения действия.
Например, это приложение логики выполняет всю работу по получению таблиц из базы данных SQL Server и получает строки из каждой таблицы. Цикл For each одновременно выполняет итерацию по каждой таблице, так что действие Get rows возвращает строки для каждой таблицы. В зависимости от объемов данных в этих таблицах эти действия могут превысить ограничение на выполнение действий.
После рефакторинга приложение логики теперь является родительским и дочерним приложением логики. Родитель получает таблицы из SQL Server, а затем вызывает дочернее приложение логики для каждой таблицы, чтобы получить строки:
Вот дочернее приложение логики, вызываемое родительским приложением логики для получения строк для каждой таблицы:
Дросселирование соединителя
У каждого соединителя есть свои ограничения регулирования, которые можно найти на странице технической справки по соединителю. Например, у соединителя служебной шины Azure есть ограничение регулирования, которое допускает до 6000 вызовов в минуту, а у соединителя SQL Server ограничения регулирования зависят от типа операции.
Некоторые триггеры и действия, такие как HTTP, имеют «политику повторных попыток», которую можно настроить на основе ограничений политики повторных попыток для реализации обработки исключений. Эта политика определяет, будет ли и как часто триггер или действие повторяет запрос, когда исходный запрос завершается ошибкой или истекает время ожидания, и приводит к ответу 408, 429 или 5xx. Таким образом, когда регулирование начинается и возвращает ошибку 429, Logic Apps следует политике повторных попыток, если она поддерживается.
Чтобы узнать, поддерживает ли триггер или действие политики повторных попыток, проверьте настройки триггера или действия. Чтобы просмотреть попытки повторных попыток триггера или действия, перейдите к журналу запусков приложения логики, выберите запуск, который вы хотите просмотреть, и разверните этот триггер или действие, чтобы просмотреть сведения о входных и выходных данных и любых повторных попытках, например:
Хотя в журнале повторных попыток содержится информация об ошибках, у вас могут возникнуть проблемы с различением регулирования соединителя и регулирования назначения. В этом случае вам, возможно, придется просмотреть сведения об ответе или выполнить некоторые расчеты интервала регулирования, чтобы определить источник.
Для приложений логики в глобальной многопользовательской службе Azure Logic Apps регулирование происходит на уровне подключения . Так, например, для приложений логики, работающих в среде службы интеграции (ISE), регулирование по-прежнему происходит для подключений, не связанных с ISE, поскольку они выполняются в глобальной многопользовательской службе Logic Apps. Однако подключения ISE, созданные соединителями ISE, не регулируются, поскольку они выполняются в вашей ISE.
Для управления регулированием на этом уровне доступны следующие параметры:
Настройка нескольких подключений для одного действия, чтобы приложение логики разделяло данные для обработки.
Для этого варианта подумайте, можете ли вы распределить рабочую нагрузку, разделив запросы действия по нескольким подключениям к одному и тому же месту назначения с использованием одних и тех же учетных данных.
Например, предположим, что ваше приложение логики получает таблицы из базы данных SQL Server, а затем получает строки из каждой таблицы. В зависимости от количества строк, которые вам нужно обработать, вы можете использовать несколько соединений и несколько Для каждых циклов разделить общее количество строк на более мелкие наборы для обработки. В этом сценарии используются два цикла For each , чтобы разделить общее количество строк пополам. Первый Для каждого цикла используется выражение, которое получает первую половину. Другой Для каждого цикла используется другое выражение, которое получает вторую половину, например:
Выражение 1:
Функция take()
получает начало коллекции. Для получения дополнительной информации см.функция take()
.@take(имя-коллекции-или-массива, div(длина(имя-коллекции-или-массива), 2))
Выражение 2: Функция
skip()
удаляет начало коллекции и возвращает все остальные элементы. Для получения дополнительной информации см. функциюskip()
.@skip(имя-коллекции-или-массива, div(длина(имя-коллекции-или-массива), 2))
Вот наглядный пример, показывающий, как можно использовать эти выражения:
Настройте разные подключения для каждого действия.
Для этого варианта подумайте, сможете ли вы распределить рабочую нагрузку, распределяя запросы каждого действия по их собственному соединению, даже если действия подключаются к одной и той же службе или системе и используют одни и те же учетные данные.
Например, предположим, что ваше приложение логики получает таблицы из базы данных SQL Server и получает каждую строку в каждой таблице. Вы можете использовать отдельные соединения, чтобы для получения таблиц использовалось одно соединение, а для получения каждой строки использовалось другое соединение.
Измените параллелизм или параллелизм в цикле «Для каждого».
По умолчанию итерации цикла «Для каждого» выполняются одновременно до ограничения параллелизма. Если у вас есть соединитель, который регулируется внутри цикла «Для каждого», вы можете уменьшить количество итераций цикла, которые выполняются параллельно. Дополнительные сведения см. в следующих разделах:
.
Циклы «для каждого» — изменить параллелизм или запустить последовательно
Схема языка определения рабочего процесса — для каждого цикла
Схема языка определения рабочего процесса — изменение параллелизма цикла «Для каждого»
Схема языка определения рабочего процесса — последовательно запускать циклы «для каждого»
Регулирование целевой службы или системы
Хотя у соединителя есть собственные ограничения регулирования, целевая служба или система, вызываемые соединителем, также могут иметь ограничения регулирования. Например, некоторые API в Microsoft Exchange Server имеют более строгие ограничения регулирования, чем коннектор Office 365 Outlook.
По умолчанию экземпляры приложения логики и любые циклы или ветви внутри этих экземпляров выполняются параллельно . Такое поведение означает, что несколько экземпляров могут одновременно вызывать одну и ту же конечную точку. Каждый экземпляр не знает о существовании другого, поэтому попытки повторить неудачные действия могут создать условия гонки, когда несколько вызовов пытаются выполняться одновременно, но для успеха эти вызовы должны поступить в целевую службу или систему до того, как начнется регулирование. .
Например, предположим, что у вас есть массив из 100 элементов. Вы используете цикл «для каждого» для итерации по массиву и включаете управление параллелизмом цикла, чтобы вы могли ограничить количество параллельных итераций до 20 или текущим пределом по умолчанию. Внутри этого цикла действие вставляет элемент из массива в базу данных SQL Server, что допускает только 15 вызовов в секунду. Этот сценарий приводит к проблеме с регулированием, потому что количество повторных попыток накапливается и никогда не выполняется.
В этой таблице описывается временная шкала того, что происходит в цикле, когда интервал повторения действия составляет 1 секунду:
Момент времени | Количество выполняемых действий | Количество неудачных действий | Количество повторных попыток ожидания |
---|---|---|---|
Т + 0 секунд | 20 вставок | 5 сбой из-за ограничения SQL | 5 попыток |
Т + 0,5 секунды | 15 вставок из-за предыдущих 5 попыток ожидания | Все 15 не выполняются из-за того, что предыдущее ограничение SQL все еще действует еще 0,5 секунды | 20 попыток (предыдущие 5 + 15 новых) |
Т + 1 секунда | 20 вставок | 5 неудачных попыток плюс 20 предыдущих попыток из-за ограничения SQL | 25 попыток (предыдущие 20 + 5 новых) |
Для управления регулированием на этом уровне доступны следующие параметры:
Создайте приложения логики, чтобы каждое из них выполняло одну операцию.
Продолжая пример сценария SQL Server в этом разделе, вы можете создать приложение логики, которое помещает элементы массива в очередь, например очередь служебной шины Azure. Затем вы создаете другое приложение логики, которое выполняет только операцию вставки для каждого элемента в этой очереди. Таким образом, в любой момент времени запускается только один экземпляр приложения логики, который либо завершает операцию вставки и переходит к следующему элементу в очереди, либо экземпляр получает ошибку 429.ошибок, но не предпринимает непродуктивных повторных попыток.
Создайте родительское приложение логики, которое вызывает дочернее или вложенное приложение логики для каждого действия. Если родителю необходимо вызвать разные дочерние приложения на основе результата родителя, вы можете использовать действие условия или действие переключения, которое определяет, какое дочернее приложение вызывать. Этот шаблон может помочь вам сократить количество вызовов или операций.
Например, предположим, что у вас есть два приложения логики, каждое из которых имеет триггер опроса, который каждую минуту проверяет вашу учетную запись электронной почты на конкретную тему, например «Успех» или «Ошибка». Эта настройка приводит к 120 вызовам в час. Вместо этого, если вы создаете одно родительское приложение логики, которое опрашивает каждую минуту, но вызывает дочернее приложение логики, которое запускается в зависимости от того, является ли тема «Успехом» или «Отказом», вы сокращаете количество проверок опроса до половины или до 60 в этом случае. .
Настроить пакетную обработку.
Если служба назначения поддерживает пакетные операции, регулирование можно решить, обрабатывая элементы группами или пакетами, а не по отдельности. Чтобы реализовать решение пакетной обработки, вы создаете приложение логики «получатель пакета» и приложение логики «отправитель пакета». Пакетный отправитель собирает сообщения или элементы до тех пор, пока не будут выполнены указанные вами критерии, а затем отправляет эти сообщения или элементы в одной группе. Получатель пакетов принимает эту группу и обрабатывает эти сообщения или элементы. Дополнительные сведения см. в разделе Пакетная обработка сообщений в группах.
Используйте версии веб-перехватчиков для триггеров и действий, а не версии для опроса.
Почему? Триггер опроса продолжает проверять целевую службу или систему через определенные промежутки времени. Очень частый интервал, например, каждую секунду, может создать проблемы с регулированием. Однако триггер или действие веб-перехватчика, например веб-перехватчик HTTP, создает только один вызов к целевой службе или системе, который происходит во время подписки, и запрашивает, чтобы место назначения уведомляло триггер или действие только при возникновении события. Таким образом, триггеру или действию не нужно постоянно проверять место назначения.
Таким образом, если целевая служба или система поддерживает веб-перехватчики или предоставляет соединитель с версией веб-перехватчика, этот вариант лучше, чем использование версии с опросом. Чтобы идентифицировать триггеры и действия веб-перехватчиков, убедитесь, что они имеют тип
ApiConnectionWebhook
или что они не требуют указания повторения. Дополнительные сведения см. в разделе Триггер APIConnectionWebhook и действие APIConnectionWebhook.
Дальнейшие действия
- Дополнительные сведения об ограничениях и конфигурации Logic Apps
Внедрение 429 повторных попыток и регулирование для ограничений скорости API
Большинство API в дикой природе реализуют ограничения скорости. Они говорят: «Вы можете сделать только X запросов за Y секунд». Если вы превысите указанные ограничения скорости, их серверы будут отклонять ваши запросы в течение определенного периода времени, в основном говоря: «Извините, мы не обработали ваш запрос, попробуйте еще раз через 10 секунд».
Многие SDK и клиенты для конкретных языков, даже от основных поставщиков API, не имеют встроенной обработки ограничения скорости. Например, клиент узла Dropbox не реализует регулирование.
Некоторые компании предоставляют внешний модуль, такой как пакет GitHub для регулирования подключаемых модулей, для своих клиентов узлов. Но часто это зависит от вас, чтобы реализовать.
Эти ограничения скорости могут раздражать, особенно если вы работаете с ограничительной песочницей и пытаетесь что-то быстро запустить.
Эффективное обращение с ними сложнее, чем кажется. В этом посте мы рассмотрим несколько различных реализаций, плюсы и минусы каждой из них. Мы закончим примером сценария, который вы можете использовать для запуска тестов с API по вашему выбору. Все примеры будут на ванильном JavaScript.
Быстро и грязно ⏱️
Может быть, вы просто хотите, чтобы что-то работало быстро и без ошибок. Самый простой способ обойти ограничение скорости — отложить запросы, чтобы они соответствовали указанному окну.
Например, если API разрешил 6 запросов в течение 3 секунд, API будет разрешать запрос каждые 500 мс и не будет завершаться ошибкой ( 3000 / 6 = 500
).
for (постоянный элемент элементов) { ожидание вызоваTheAPI(элемент) await sleep(500) // ВЗЛОМ! }
Где спать
:
функция сна (миллисекунды) { вернуть новое обещание ((разрешить) => setTimeout (разрешить, миллисекунды)) }
Это плохая практика! Это все еще может привести к ошибке, если вы находитесь на границе временного окна, и оно не может обрабатывать допустимые пакеты. Что делать, если вам нужно сделать только 6 запросов? Код выше займет 3 секунды, но API позволяет делать все 6 параллельно, что будет значительно быстрее.
Подход со спящим режимом подходит для хобби-проектов, быстрых сценариев и т. д. Я признаю, что использовал его в ситуациях с локальными сценариями. Но вы, вероятно, захотите убрать это из своего производственного кода.
Есть лучшие способы!
Мечта
Идеальное решение скрывает от разработчика детали ограничений API. Я не хочу думать о том, сколько запросов я могу сделать, просто делайте все запросы эффективно и сообщайте мне результаты.
Мой идеал в JavaScript:
const responses = await Promise.all(items.map((item) => ( callTheAPI(элемент) )))
Как потребитель API, я также хочу, чтобы все мои запросы выполнялись как можно быстрее в пределах ограничений скорости.
Предполагая 10 запросов в предыдущем примере ограничения 6 запросов за 3 секунд, каков теоретический предел? Давайте также предположим, что API может выполнять все 6 запросов параллельно, и один запрос занимает 200 мс
- Первые 6 запросов должны выполняться за 200 мс, но это должно занять 3 секунды из-за ограничения скорости API
- Последние 4 запроса должны начинаться с отметки 3 секунды и занимать всего 200 мс
- Теоретическая сумма: 3200 мс или 3,2 секунды
.
Ладно, посмотрим, как близко мы сможем подобраться.
Обработка ответа об ошибке
Первое, что нам нужно понять, это то, как обрабатывать ответы об ошибке при превышении ограничений API.
Если вы превысите ограничение скорости поставщика API, его сервер должен ответить кодом состояния 429
( Too Many Requests
) и заголовком Retry-After
.
429 Повторить после: 5
Заголовок Retry-After
может быть либо в секундах ожидания, либо в дате при снятии ограничения скорости.
Формат даты в заголовке не соответствует дате ISO 8601, а является форматом «даты HTTP»:
<название дня>, <день> <месяц> <год> <час>:<минута>:<секунда> GMT
Пример:
Пн, 29 марта 2021 г. 04:58:00 по Гринвичу
К счастью, если вы являетесь пользователем JavaScript/Node, этот формат можно разобрать, передав его в Дата
конструктор.
Вот функция, которая анализирует оба формата в JavaScript:
function getMillisToSleep (retryHeaderString) { пусть millisToSleep = Math.round (parseFloat (retryHeaderString) * 1000) если (isNaN(millisToSleep)) { millisToSleep = Math. max(0, новая дата(retryHeaderString) - новая дата()) } вернуть миллистосон } getMillisToSleep('4') // => 4000 getMillisToSleep('Mon, 29 Mar 2021 04:58:00 GMT') // => 4000
Теперь мы можем создать функцию, которая использует Retry-After
заголовок для повторной попытки при обнаружении 429
кода состояния HTTP:
async function fetchAndRetryIfNecessary (callAPIFn) { постоянный ответ = ожидание callAPIFn() если (ответ.статус === 429) { const retryAfter = response.headers.get('retry-after') constmillisToSleep = getMillisToSleep(retryAfter) ждать сна (millisToSleep) вернуть fetchAndRetryIfNecessary (callAPIFn) } вернуть ответ }
Эта функция будет повторять попытки до тех пор, пока не перестанет получать 429
код состояния.
// Использование константный ответ = ожидание fetchAndRetryIfNecessary (async () => ( ожидание выборки (apiURL, requestOptions) )) console.log(response.status) // => 200
Теперь мы готовы делать запросы!
Настройка
Я работаю с локальным API и выполняю запросы 10 и 20 с теми же примерными ограничениями, что и выше: 6 запросов за 3 секунд.
Наилучшая теоретическая производительность, которую мы можем ожидать с этими параметрами:
- 10 запросов: 3,2 секунды
- 20 запросов: 9,2 секунды
Посмотрим, как близко мы сможем подобраться!
Базовый уровень: сон между запросами
Помните «быстрый и грязный» метод запроса, о котором мы говорили в начале? Мы будем использовать его поведение и время в качестве основы для улучшения.
Напоминание:
const items = [...10 items...] for (const item of items) { ожидание вызоваTheAPI(элемент) жду сна(3000/6) }
Итак, как это работает?
- При 10 запросах: около 7 секунд
- При 20 запросах: около 14 секунд
Наше теоретическое время для последовательных запросов составляет 5 секунд при 10 запросах и 10 секунд при 20 запросах, но для каждого запроса есть некоторые накладные расходы, поэтому реальное время немного выше.
Вот проход из 10 запросов:
⏱️ Выполнение теста Спящий режим между запросами, без повторных попыток Начало запроса: 0 попытка:0 2021-03-29T00:53:09. 629Z Конец запроса: 0 попытка: 0 200 344 мс Начало запроса: 1 попытка:0 2021-03-29Т00:53:10.479З Конец запроса: 1 попытка: 0 200 252 мс Начало запроса: 2 попытка:0 2021-03-29T00:53:11.236Z Конец запроса: 2 попытка: 0 200 170 мс Начало запроса: 3 попытка:0 2021-03-29T00:53:11.910Z Конец запроса: 3 попытка: 0 200 174 мс Начало запроса: 4 попытка:0 2021-03-29T00:53:12.585Z Конец запроса: 4 попытка: 0 200 189 мс Начало запроса: 5 попытка:0 2021-03-29T00:53:13.275Z Конец запроса: 5 попытка: 0 200 226 мс Начало запроса: 6 попытка:0 2021-03-29T00:53:14.005Z Конец запроса: 6 попытка: 0 200 168 мс Старт запроса: 7 попытка:0 29.03.2021Т00:53:14.675З Конец запроса: 7 попытка: 0 200 195 мс Начало запроса: 8 попытка:0 2021-03-29T00:53:15.375Z Конец запроса: 8 попытка: 0 200 218 мс Начало запроса: 9 попытка:0 2021-03-29T00:53:16.096Z Конец запроса: 9 попытка: 0 200 168 мс ✅ Total Sleep между запросами, без повторных попыток: 7136 мс
Подход 1: последовательный без сна
Теперь у нас есть функция для обработки ошибки и повторной попытки, давайте попробуем удалить вызов sleep из базового уровня.
константных элементов = [...10 элементов...] for (const item of items) { await fetchAndRetryIfNecessary(() => callTheAPI(item)) }
Похоже, около 4,7 секунды, определенно улучшение, но не совсем на теоретическом уровне 3,2 секунды.
⏱️ Запуск серийного теста без ограничений Начало запроса: 0 попытка:0 2021-03-29T00:59:01.118Z Конец запроса: 0 попытка: 0 200 327 мс Начало запроса: 1 попытка:0 2021-03-29T00:59:01.445Z Конец запроса: 1 попытка: 0 200 189 мс Начало запроса: 2 попытка:0 2021-03-29T00:59:01.634Z Конец запроса: 2 попытка: 0 200 194 мс Начало запроса: 3 попытка:0 2021-03-29T00:59:01.828Z Конец запроса: 3 попытка: 0 200 177 мс Старт запроса: 4 попытка:0 2021-03-29Т00:59:02.005З Конец запроса: 4 попытка: 0 200 179 мс Начало запроса: 5 попытка:0 2021-03-29T00:59:02.185Z Конец запроса: 5 попытка: 0 200 196 мс Начало запроса: 6 попытка:0 2021-03-29T00:59:02.381Z Конец запроса: 6 попытка: 0 429 10 мс ❗ Повторные попытки: 6 попыток: 1 в понедельник, 29 марта 2021 г. , 00:59:05 по Гринвичу Спящий режим на 2609 мс Начало запроса: 6 попытка:1 2021-03-29T00:59:05.156Z Конец запроса: 6 попытка: 1 200 167 мс Начало запроса: 7 попытка:0 2021-03-29T00:59:05.323Z Конец запроса: 7 попытка: 0 200 176 мс Старт запроса: 8 попытка:0 2021-03-29Т00:59:05.499З Конец запроса: 8 попытка: 0 200 208 мс Начало запроса: 9 попытка:0 2021-03-29T00:59:05.707Z Конец запроса: 9 попытка: 0 200 157 мс ✅ Общее количество последовательных запросов без ограничений: 4746 мс
Подход 2: параллельный без регулирования
Давайте попробуем пройти все запросы параллельно, просто чтобы посмотреть, что произойдет.
константных элементов = [...10 элементов...] константные ответы = ожидание Promise.all(items.map((item) => ( fetchAndRetryIfNecessary(() => callTheAPI(item)) )))
Этот прогон занял примерно 4,3 секунды. Небольшое улучшение по сравнению с предыдущим последовательным подходом, но повторная попытка замедляет работу. Вы можете увидеть последние 4 запроса, которые нужно было повторить.
⏱️ Параллельный запуск тестов без ограничений Начало запроса: 0 попытка:0 2021-03-29T00:55:01.463Z Начало запроса: 1 попытка:0 2021-03-29T00:55:01.469Z Начало запроса: 2 попытка:0 2021-03-29T00:55:01.470Z Начало запроса: 3 попытка:0 2021-03-29T00:55:01.471Z Начало запроса: 4 попытка:0 2021-03-29T00:55:01.471Z Начало запроса: 5 попытка:0 2021-03-29T00:55:01.472Z Начало запроса: 6 попытка:0 2021-03-29T00:55:01.472Z Начало запроса: 7 попытка:0 2021-03-29T00:55:01.472Z Старт запроса: 8 попытка:0 2021-03-29Т00:55:01.472З Начало запроса: 9 попытка:0 2021-03-29T00:55:01.473Z Конец запроса: 5 попытка: 0 429 250 мс ❗ Повторные попытки: 5 попыток: 1 в понедельник, 29 марта 2021 г., 00:55:05 по Гринвичу Спящий режим на 3278 мс Конец запроса: 6 попытка: 0 429 261 мс ❗ Повторные попытки: 6 попыток: 1 в понедельник, 29 марта 2021 г., 00:55:05 по Гринвичу Спящий режим на 3267 мс Конец запроса: 8 попытка: 0 429 261 мс ❗ Повторные попытки: 8 попыток: 1 в понедельник, 29 марта 2021 г., 00:55:05 по Гринвичу Спящий режим на 3267 мс Конец запроса: 2 попытка: 0 429 264 мс ❗ Повторные попытки: 2 попытки: 1 в понедельник, 29 марта 2021 г. , 00:55:05 по Гринвичу Спящий режим на 3266 мс Конец запроса: 1 попытка: 0 200 512 мс Конец запроса: 3 попытка: 0 200 752 мс Конец запроса: 0 попытка: 0 200 766 мс Конец запроса: 4 попытка: 0 200 884 мс Конец запроса: 7 попытка:0 200 1039РС Конец запроса: 9 попытка: 0 200 1158 мс Начало запроса: 5 попытка:1 2021-03-29T00:55:05.155Z Начало запроса: 6 попытка:1 2021-03-29T00:55:05.156Z Начало запроса: 8 попытка:1 2021-03-29T00:55:05.157Z Начало запроса: 2 попытка:1 2021-03-29T00:55:05.157Z Конец запроса: 2 попытка: 1 200 233 мс Конец запроса: 6 попытка: 1 200 392 мс Конец запроса: 8 попытка: 1 200 513 мс Конец запроса: 5 попытка: 1 200 637 мс ✅ Total Parallel без ограничений: 4329 мс
Это выглядит довольно разумно, всего 4 попытки, но этот подход не масштабируется . Повторные попытки в этом сценарии ухудшаются только при увеличении количества запросов. Если бы у нас было, скажем, 20 запросов, некоторые из них нужно было бы повторить более одного раза — нам потребовалось бы 4 отдельных 3-секундных окна, чтобы выполнить все 20 запросов, поэтому некоторые запросы нужно было бы повторить , в лучшем случае 3 раза.
Кроме того, реализация ограничителя скорости, которую использует мой пример сервера, будет сдвигать временную метку Retry-After
для последующих запросов, когда клиент уже достиг предела — он возвращает Retry-After
временная метка на основе 6-й самой старой временной метки запроса + 3 секунды.
Это означает, что если вы делаете больше запросов, когда вы уже достигли лимита, старые метки времени удаляются, а метка времени Retry-After
сдвигается на более позднее время. В результате временные метки Retry-After
для некоторых запросов, ожидающих повторной попытки, становятся устаревшими. Они повторяют попытку, но терпят неудачу, потому что их метки времени устарели. Сбой запускает еще одну повторную попытку , и вызывают повторную попытку после
временная метка должна быть выдвинута еще дальше . Все это закручивается в порочный круг, состоящий в основном из повторных попыток. Очень плохо.
Вот сокращенный лог попытки сделать 20 запросов. Некоторые запросы необходимо было повторить 35 раз (❗) из-за смещения окна и устаревших заголовков Retry-After
. В конце концов это закончилось, но заняло целую минуту. Плохая реализация, не используйте.
⏱️ Параллельный запуск тестов без ограничений ...много очень грязных запросов... Конец запроса: 11 попытка: 32 200 260 мс Конец запроса: 5 попытка: 34 200 367 мс Конец запроса: 6 попытка: 34 200 487 мс ✅ Всего параллелей без ограничений: 57964 мс
Подход 3: параллельно с async.mapLimit
Кажется, что простым решением проблемы, описанной выше, будет выполнение только n
параллельных запросов одновременно. Например, наш демонстрационный API допускает 6 запросов во временном окне, поэтому просто разрешите 6 параллельных запросов, верно? Давайте попробуем.
Существует пакет узла под названием async, реализующий это поведение (среди прочего) в функции с именем mapLimit
.
импортировать mapLimit из 'async/mapLimit' импортировать асинхронность из 'async/asyncify' const элементы = [...10 элементов...] константные ответы = ожидание mapLimit(items, 6, asyncify((item) => ( fetchAndRetryIfNecessary(() => callTheAPI(item)) )))
После многих запусков с 10 запросами 5,5 секунды были в лучшем случае, медленнее, чем даже последовательные запуски.
⏱️ Запуск тестов параллельно с `async.mapLimit` Начало запроса: 0 попытка:0 2021-03-29T17:20:42.144Z Начало запроса: 1 попытка:0 2021-03-29T17:20:42.151Z Начало запроса: 2 попытка:0 2021-03-29T17:20:42.151Z Начало запроса: 3 попытка:0 2021-03-29T17:20:42.152Z Начало запроса: 4 попытка:0 2021-03-29T17:20:42.152Z Начало запроса: 5 попытка:0 2021-03-29T17:20:42.153Z Конец запроса: 1 попытка: 0 200 454 мс Старт запроса: 6 попытка:0 2021-03-29Т17:20:42.605З Конец запроса: 6 попытка: 0 429 11 мс ❗ Повторные попытки: 6 попыток: 1 в понедельник, 29 марта 2021 г., 17:20:47 по Гринвичу переход в спящий режим на 4384 мс Конец запроса: 5 попытка: 0 200 571 мс Начало запроса: 7 попытка:0 2021-03-29T17:20:42. 723Z Конец запроса: 7 попытка: 0 429 15 мс ❗ Повторные попытки: 7 попыток: 1 в понедельник, 29 марта 2021 г., 17:20:47 по Гринвичу переход в спящий режим на 4262 мс Конец запроса: 2 попытка: 0 200 728 мс Начало запроса: 8 попытка:0 2021-03-29T17:20:42.879Z Конец запроса: 8 попытка: 0 429 12 мс ❗ Повторные попытки: 8 попыток: 1 в понедельник, 29 марта 2021 г., 17:20:47 по Гринвичу, сон для 4109РС Конец запроса: 4 попытка: 0 200 891 мс Начало запроса: 9 попытка:0 2021-03-29T17:20:43.044Z Конец запроса: 9 попытка: 0 429 12 мс ❗ Повторные попытки: 9 попыток: 1 в понедельник, 29 марта 2021 г., 17:20:47 по Гринвичу Спящий режим на 3944 мс Конец запроса: 3 попытка: 0 200 1039 мс Конец запроса: 0 попытка: 0 200 1163 мс Старт запроса: 6 попытка:1 2021-03-29T17:20:47.005Z Старт запроса: 7 попытка:1 2021-03-29T17:20:47.006Z Начало запроса: 8 попытка:1 2021-03-29T17:20:47.007Z Старт запроса: 9 попытка:1 2021-03-29T17:20:47.007Z Окончание запроса: 8 попытка:1 200 249РС Конец запроса: 9 попытка: 1 200 394 мс Конец запроса: 6 попытка: 1 200 544 мс Конец запроса: 7 попытка: 1 200 671 мс ✅ Total Parallel с `async. mapLimit`: 5534 мс
При 20 запросах он завершился примерно за 16 секунд. Положительным моментом является то, что он не страдает от смертельной спирали повторных попыток, которую мы видели в предыдущей параллельной реализации! Но все равно медленно. Продолжаем копать.
⏱️ Запуск тестов параллельно с `async.mapLimit` Начало запроса: 0 попытка:0 2021-03-29T17:25:21.166Z Начало запроса: 1 попытка:0 2021-03-29Т17:25:21.173З Начало запроса: 2 попытка:0 2021-03-29T17:25:21.173Z Начало запроса: 3 попытка:0 2021-03-29T17:25:21.174Z Начало запроса: 4 попытка:0 2021-03-29T17:25:21.174Z Начало запроса: 5 попытка:0 2021-03-29T17:25:21.174Z Конец запроса: 0 попытка: 0 200 429 мс Начало запроса: 6 попытка:0 2021-03-29T17:25:21.596Z Конец запроса: 6 попытка: 0 429 19 мс ❗ Повторные попытки: 6 попыток: 1 в понедельник, 29 марта 2021 г., 17:25:27 по Гринвичу Спящий режим на 5385 мс Конец запроса: 5 попытка: 0 200 539 мс Начало запроса: 7 попытка:0 2021-03-29T17:25:21.714Z Конец запроса: 7 попытка:0 42913 мс ❗ Повторные попытки: 7 попыток: 1 в понедельник, 29 марта 2021 г. , 17:25:27 по Гринвичу Спящий режим на 5273 мс Конец запроса: 2 попытка: 0 200 664 мс Начало запроса: 8 попытка:0 2021-03-29T17:25:21.837Z Конец запроса: 8 попытка: 0 429 10 мс ❗ Повторные попытки: 8 попыток: 1 в понедельник, 29 марта 2021 г., 17:25:27 по Гринвичу Спящий режим на 5152 мс Конец запроса: 1 попытка: 0 200 1068 мс Начало запроса: 9 попытка:0 2021-03-29T17:25:22.241Z .... больше строк .... ❗ Повторные попытки: 17 попыток: 2 в понедельник, 29 марта 2021 г., 17:25:37 по Гринвичу Спящий режим на 3987 мс Старт запроса: 19 попытка:1 29.03.2021Т17:25:37.001З Начало запроса: 17 попытка:2 2021-03-29T17:25:37.002Z Конец запроса: 19 попытка: 1 200 182 мс Конец запроса: 17 попытка: 2 200 318 мс ✅ Total Parallel с `async.mapLimit`: 16154 мс
Подход 4: выигрыш с ведром токенов
До сих пор ни один из подходов не был оптимальным. Все они были медленными, вызывали много повторных попыток или и то, и другое.
Идеальным сценарием, который приблизит нас к нашему теоретическому минимальному времени в 3,2 секунды для 10 запросов, будет попытка только 6 запросов для каждого 3-секундного временного окна. например
- Пакет 6 запросов параллельно
- Дождитесь сброса кадра
-
ПЕРЕЙТИ
1
Обработка ошибок 429
хороша, и мы сохраним ее, но мы должны рассматривать ее как исключительный случай, поскольку это ненужная работа. Цель здесь состоит в том, чтобы сделать все запросы, не вызывая повторных попыток при обычных обстоятельствах.
Введите алгоритм корзины маркеров. Наше желаемое поведение — это его предполагаемая цель: у вас есть n
жетонов, которые можно потратить в течение некоторого временного окна — в нашем случае 6 токенов за 3 секунды. После того, как все жетоны будут потрачены, вам нужно дождаться окончания окна, чтобы получить новый набор жетонов.
Вот простая реализация корзины токенов для нашей конкретной цели. Он будет считать до тех пор, пока не достигнет maxRequests
, любые запросы сверх этого будут ждать maxRequestWindowMS
, а затем снова попытаются получить токен.
класс TokenBucketRateLimiter { конструктор ({ maxRequests, maxRequestWindowMS }) { this.maxRequests = максимальное количество запросов this.maxRequestWindowMS = maxRequestWindowMS это.сброс() } перезагрузить () { это количество = 0 this.resetTimeout = ноль } расписаниеСброс () { // Только первый токен в наборе запускает resetTimeout если (!this.resetTimeout) { this.resetTimeout = setTimeout(() => ( это.сброс() ), это.maxRequestWindowMS) } } асинхронное приобретениеToken (fn) { это.scheduleReset() если (this.count === this.maxRequests) { ожидание сна (this.maxRequestWindowMS) вернуть this.acquireToken(fn) } это.счет += 1 ждать следующего тика () вернуть фн() } }
Давайте попробуем!
константных элементов = [...10 элементов...] const tokenBucket = новый TokenBucketRateLimiter({ максимальное количество запросов: 6, максрекуествиндовмс: 3000 }) константные обещания = items. map((item) => ( fetchAndRetryIfNecessary(() => ( tokenBucket.acquireToken(() => callTheAPI(item)) )) )) const responses = await Promise.all(обещания)
С 10 запросами это около 4 секунд. Лучшее на данный момент, и без повторных попыток!
⏱️ Параллельный запуск сравнительного анализа с сегментом токенов Старт запроса: 0 попытка:0 2021-03-29Т01:14:17.700З Начало запроса: 1 попытка:0 2021-03-29T01:14:17.707Z Начало запроса: 2 попытка:0 2021-03-29T01:14:17.708Z Начало запроса: 3 попытка:0 2021-03-29T01:14:17.709Z Начало запроса: 4 попытка:0 2021-03-29T01:14:17.709Z Начало запроса: 5 попытка:0 2021-03-29T01:14:17.710Z Конец запроса: 2 попытка: 0 200 301 мс Конец запроса: 4 попытка: 0 200 411 мс Конец запроса: 5 попытка: 0 200 568 мс Конец запроса: 3 попытка: 0 200 832 мс Конец запроса: 0 попытка: 0 200 844 мс Конец запроса: 1 попытка: 0 200 985 мс Старт запроса: 6 попытка:0 2021-03-29Т01:14:20.916З Начало запроса: 7 попытка:0 2021-03-29T01:14:20.917Z Начало запроса: 8 попытка:0 2021-03-29T01:14:20. 918Z Начало запроса: 9 попытка:0 2021-03-29T01:14:20.918Z Конец запроса: 8 попытка: 0 200 223 мс Конец запроса: 6 попытка: 0 200 380 мс Конец запроса: 9 попытка: 0 200 522 мс Конец запроса: 7 попытка: 0 200 661 мс ✅ Всего параллелится с ведром токенов: 3992 мс
И 20 запросов? Всего это занимает около 10 секунд. Весь прогон очень чистый, без повторных попыток. Это именно то поведение, которое мы ищем!
⏱️ Параллельный запуск тестов с сегментом токенов Начало запроса: 0 попытка:0 2021-03-29T22:30:51.321Z Начало запроса: 1 попытка:0 2021-03-29T22:30:51.329Z Начало запроса: 2 попытка:0 2021-03-29T22:30:51.329Z Начало запроса: 3 попытка:0 2021-03-29T22:30:51.330Z Начало запроса: 4 попытка:0 2021-03-29T22:30:51.330Z Начало запроса: 5 попытка:0 2021-03-29T22:30:51.331Z Конец запроса: 5 попытка: 0 200 354 мс Конец запроса: 2 попытка: 0 200 507 мс Конец запроса: 3 попытка: 0 200 624 мс Конец запроса: 4 попытка: 0 200 969 мс Конец запроса: 0 попытка: 0 200 980 мс Конец запроса: 1 попытка: 0 200 973 мс Начало запроса: 6 попытка:0 2021-03-29T22:30:54. 538Z Начало запроса: 7 попытка:0 2021-03-29T22:30:54.539Z Начало запроса: 8 попытка:0 2021-03-29T22:30:54.540Z Начало запроса: 9 попытка:0 2021-03-29T22:30:54.541Z Начало запроса: 10 попытка:0 2021-03-29T22:30:54.541Z Начало запроса: 11 попытка:0 2021-03-29T22:30:54.542Z Конец запроса: 8 попытка: 0 200 270 мс Конец запроса: 10 попытка: 0 200 396 мс Конец запроса: 6 попытка: 0 200 525 мс Конец запроса: 7 попытка: 0 200 761 мс Конец запроса: 11 попытка: 0 200 762 мс Окончание запроса: 9попытка:0 200 870 мс Начало запроса: 12 попытка:0 2021-03-29T22:30:57.746Z Начало запроса: 13 попытка:0 2021-03-29T22:30:57.746Z Начало запроса: 14 попытка:0 2021-03-29T22:30:57.747Z Начало запроса: 15 попытка:0 2021-03-29T22:30:57.748Z Начало запроса: 16 попытка:0 2021-03-29T22:30:57.748Z Начало запроса: 17 попытка:0 2021-03-29T22:30:57.749Z Конец запроса: 15 попытка: 0 200 340 мс Конец запроса: 13 попытка: 0 200 461 мс Конец запроса: 17 попытка: 0 200 581 мс Конец запроса: 16 попытка: 0 200 816 мс Конец запроса: 12 попытка: 0 200 823 мс Конец запроса: 14 попытка: 0 200 962 мс Начало запроса: 18 попытка:0 2021-03-29T22:31:00. 954Z Начало запроса: 19 попытка:0 2021-03-29T22:31:00.955Z Конец запроса: 19 попытка: 0 200 169 мс Конец запроса: 18 попытка: 0 200 294 мс ✅ Total Parallel с ведром токенов: 10047 мс
Подход 4.1: использование чужого ведра токенов
Вышеприведенная реализация ведра токенов была предназначена для демонстрационных целей. В производственной среде вы можете не захотеть поддерживать собственное ведро токенов, если можете помочь.
Если вы используете узел, существует модуль узла, называемый ограничителем, который реализует поведение корзины маркеров. Библиотека более общая, чем наша TokenBucketRateLimiter
класс выше, но мы можем использовать его для достижения точно такого же поведения:
import { RateLimiter } from 'limiter' класс LimiterLibraryRateLimiter { конструктор ({ maxRequests, maxRequestWindowMS }) { this.maxRequests = максимальное количество запросов this.maxRequestWindowMS = maxRequestWindowMS this.limiter = новый ограничитель скорости (this. maxRequests, this.maxRequestWindowMS, false) } асинхронное приобретениеToken (fn) { если (this.limiter.tryRemoveTokens(1)) { ждать следующего тика () вернуть фн() } еще { ожидание сна (this.maxRequestWindowMS) вернуть this.acquireToken(fn) } } }
Использование точно такое же, как и в предыдущем примере, просто замените LimiterLibraryRateLimiter
на TokenBucketRateLimiter
:
const items = [...10 items...] const rateLimiter = новый LimiterLibraryRateLimiter({ максимальное количество запросов: 6, максрекуествиндовмс: 3000 }) константные обещания = items.map((item) => ( fetchAndRetryIfNecessary(() => ( rateLimiter.acquireToken(() => callTheAPI(item)) )) )) константные ответы = await Promise.all(обещания)
Прочие соображения
С помощью корзины токенов в двух описанных выше подходах у нас есть работающее решение для использования API с ограничениями скорости в производственной среде. В зависимости от вашей архитектуры могут быть некоторые другие соображения.
API с ограничениями скорости часто возвращают заголовки ограничения скорости при успешном запросе. например
HTTP: 200 X-Ratelimit-Limit: 40 # Всего запросов в окне X-Ratelimit-Remaining: 30 # Количество оставшихся запросов в окне X-Ratelimit-Reset: 1617054237 # Секунды с начала эпохи до сброса окна
Имена заголовков являются условными на момент написания, но многие API используют указанные выше заголовки.
Вы можете запустить свою корзину токенов со значением из этих заголовков, а не сохранять состояние в своем клиенте API.
Регулирование в распределенной системе
Если у вас есть несколько узлов, отправляющих запросы к API с ограниченной скоростью, локальное сохранение состояния корзины маркеров на одном узле не будет работать. Несколько вариантов минимизации количества повторных попыток:
- Заголовки X-Ratelimit : Использование описанных выше заголовков
- Общее состояние : вы можете сохранить состояние корзины токенов в чем-то доступном для всех узлов, например
redis
Вердикт: используйте ведро токенов
Надеюсь, понятно, что использование ведра токенов — лучший способ реализации регулирования API.