Эффективное использование API с ограниченной скоростью (Echo Nest) с распределенными клиентами

Задний план

У Echo Nest есть API с ограничением скорости. Данное приложение (идентифицированное в запросах с использованием ключа API) может выполнять до 120 вызовов REST в минуту. Ответ службы включает в себя оценку общего количества вызовов, сделанных за последнюю минуту; повторное злоупотребление API (превышение лимита) может привести к отзыву ключа API.

При использовании с одной машины (веб-сервера, предоставляющего услуги клиентам) легко контролировать доступ — сервер полностью знает историю запросов и может правильно себя регулировать.

Но я работаю над программой, в которой распределенные независимые клиенты делают запросы параллельно.

В таком случае гораздо менее ясно, каким будет оптимальное решение. И вообще проблема представляется неразрешимой - если более 120 клиентов, все без какой-либо истории, одновременно сделают первоначальный запрос, то скорость будет превышена.

Но так как это личный проект, и ожидается, что клиент будет пользоваться им спорадически (всплесками), а мои проекты никогда не были очень успешными, это не должно стать серьезной проблемой. Более вероятная проблема заключается в том, что бывают случаи, когда меньшее количество клиентов хочет сделать много запросов как можно быстрее (например, клиенту может потребоваться в виде исключения сделать несколько тысяч запросов при первом запуске - возможно два клиента запустятся примерно в одно и то же время, поэтому они должны сотрудничать, чтобы разделить доступную полосу пропускания).

Учитывая все вышеизложенное, какие алгоритмы подходят для клиентов, чтобы они правильно ограничивали скорость? Обратите внимание, что ограниченное сотрудничество возможно, поскольку API возвращает общее количество запросов в в последнюю минуту для всех клиентов.

Текущее решение

Мое текущее решение (когда вопрос был написан - в качестве ответа дается лучший подход) довольно простое. У каждого клиента есть запись о времени последнего звонка и количестве вызовов, сделанных за последнюю минуту, согласно отчету API для этого звонка.

Если количество вызовов меньше 60 (половина лимита), клиент не дросселирует. Это позволяет выполнять быстрые всплески небольшого количества запросов.

В противном случае (т. е. когда имеется больше предыдущих запросов) клиент вычисляет предельную скорость, с которой он должен работать (т. е. period = 60 / (120 - number of previous requests)), а затем ждет, пока разрыв между предыдущим вызовом и текущим временем не превысит этот период (в секундах; 60 секунд в в минуту; не более 120 запросов в минуту). Это эффективно снижает скорость, так что, если бы он действовал один, он не превышал бы предел.

Но у вышеперечисленного есть проблемы. Если вы внимательно все обдумаете, то увидите, что при большом количестве запросов один клиент колеблется и не достигает максимальной пропускной способности (отчасти это происходит из-за «начального всплеска», который внезапно «выпадает за пределы окна», а отчасти потому, что алгоритм не полностью использует свою историю). И несколько клиентов будут сотрудничать до некоторой степени, но я сомневаюсь, что это оптимально.

Лучшие решения

Я могу представить себе лучшее решение, которое использует полную локальную историю клиента и моделирует других клиентов, скажем, со скрытой марковской моделью. Таким образом, каждый клиент будет использовать отчет API для моделирования других (неизвестных) клиентов и соответствующей корректировки своей скорости.

Я также могу представить алгоритм для одного клиента, который постепенно переходит от неограниченного поведения для небольших всплесков к оптимальному, ограниченному поведению для многих запросов без возникновения колебаний.

Существуют ли такие подходы? Может ли кто-нибудь предоставить реализацию или ссылку? Может ли кто-нибудь придумать лучшую эвристику?

Я предполагаю, что это известная проблема где-то. В какой области? Теория очереди?

Я также предполагаю (см. комментарии ранее), что оптимального решения не существует и что могут быть какие-то знания/традиции/принятые эвристики, которые хорошо работают на практике. Я хотел бы знать, что... В данный момент я изо всех сил пытаюсь определить подобную проблему в известных сетевых протоколах (я полагаю, что у Перлмана было бы какое-то красивое решение, если это так).

Я также заинтересован (в меньшей степени, для дальнейшего использования, если программа станет популярной) в решении, которое требует центрального сервера для облегчения совместной работы.

Отказ от ответственности

Этот вопрос вовсе не предназначен для критики Echo Nest; их обслуживание и условия использования великолепны. Но чем больше я думаю о том, как лучше всего это использовать, тем сложнее/интереснее это становится...

Кроме того, у каждого клиента есть локальный кеш, используемый для предотвращения повторных вызовов.

Обновления

Возможно актуальная статья.


person andrew cooke    schedule 28.08.2012    source источник
comment
Сообщает ли ответ также, сколько секунд осталось в текущей минуте, или это тоже нужно угадать? [Редактировать: на самом деле мой вопрос делает предположение об ограничении скорости сервера, которое может быть неверным. Является ли ограничение 120 запросов в течение 1 минуты, за которым следует новый период в 1 минуту, или ограничение составляет 120 запросов в любом 60-секундном окне?]   -  person Steve Jessop    schedule 28.08.2012
comment
Не совсем понятно, но я провел несколько простых тестов и думаю, что число ответов — это количество запросов за последние 60 секунд. Другими словами, оно выглядит как скользящее окно. Итак, текущей минуты нет. Но я предполагаю, что на самом деле это реализовано где-то посередине (например, возможно, они собирают запросы в 5-секундные интервалы и используют это для приближения к скользящему окну).   -  person andrew cooke    schedule 28.08.2012
comment
Хорошо, я предполагаю скользящее окно. Я не претендую на какую-либо конкретную теорию, стоящую за этим, но мне кажется, что после каждого запроса необходимо просмотреть текущее использование, о котором сообщает ответ API, и вычесть все запросы, сделанные этим клиентом за последние 60 секунд. Это говорит вам об общем использовании всеми другими клиентами, соответственно спланируйте использование этого клиента. Тогда у вас есть два варианта: (1) использовать всю доступную квоту, рискуя привести к голоданию других клиентов (особенно новых, которые приходят во время всплеска на этом клиенте) или (2) сохранить текущее использование менее 120 и оставить место для новых. клиентов для начала.   -  person Steve Jessop    schedule 28.08.2012
comment
И если у вас есть центральный сервер для совместной работы, вы можете использовать его для справедливого распределения квоты между несколькими клиентами. Базовая модель может заключаться в том, что клиент запрашивает разрешение на выполнение N запросов и получает разрешение на выполнение K <= N запросов в течение следующих t секунд. Затем, при желании, сервер может расставить приоритеты клиентов в зависимости от того, сколько запросов они хотят сделать и для чего.   -  person Steve Jessop    schedule 28.08.2012
comment
конечно, но как именно вы все это делаете? например, вы не хотите просто потреблять все оставшееся использование, поскольку это ничего не оставит новому клиенту. и как вы оцениваете, что используют другие клиенты? может быть, вам следует использовать среднее значение по времени, например? вы можете написать какой-нибудь сложный код, предполагающий очень специфическую модель, но я готов поспорить на фунты к копейкам, что он будет нестабильным, если другие клиенты не будут следовать этой модели. так что моя интуиция такова, что есть более надежная эвристика... (и, вероятно, уже известная кому-то). но большинство ограничений скорости основано на сервере.   -  person andrew cooke    schedule 28.08.2012


Ответы (3)


Вышеприведенное работало, но было очень шумно, а код был в беспорядке. Сейчас я использую более простой подход:

  • Позвонить
  • Из ответа обратите внимание на лимит и подсчитайте
  • Рассчитать

    barrier = now() + 60 / max(1, (limit - count))**greedy
    
  • При следующем звонке дождитесь barrier

Идея довольно проста: вы должны подождать некоторое время, пропорциональное тому, как мало запросов осталось в эту минуту. Например, если count равен 39, а limit равен 40, вы ждете целую минуту. Но если count равен нулю, вы можете сделать запрос в ближайшее время. Параметр greedy является компромиссом: когда он больше 1, «первые» вызовы выполняются быстрее, но вы, скорее всего, достигнете предела и в конечном итоге будете ждать 60 секунд.

Производительность этого метода аналогична подходу, описанному выше, и он намного более надежен. Это особенно хорошо, когда клиенты «нестабильны», так как описанный выше подход приводит к путанице при попытке оценить линейные скорости, в то время как это позволит клиенту «украсть» несколько быстрых запросов, когда спрос низкий.

Код здесь.

person andrew cooke    schedule 09.02.2013
comment
Рассматривали ли вы Token Bucket? (en.wikipedia.org/wiki/Token_bucket). в вашем случае координация между несколькими клиентами усложняет реализацию. - person sw1nn; 26.08.2013

После некоторых экспериментов оказалось, что самое главное — получить как можно более точную оценку верхнего предела текущих скоростей соединения.

Каждый клиент может отслеживать собственную (локальную) скорость подключения, используя очередь меток времени. Временная метка добавляется в очередь при каждом подключении, а временные метки старше минуты отбрасываются. Затем по первой и последней меткам времени и количеству записей (минус один) определяется «долгосрочная» (более минуты) средняя скорость. «Кратковременную» (мгновенную) скорость можно найти по времени последних двух запросов. Верхний предел – это максимальное из этих двух значений.

Каждый клиент также может оценить скорость внешнего подключения (от других клиентов). «Долгосрочную» скорость можно определить по количеству «использованных» соединений за последнюю минуту, как сообщает сервер, с поправкой на количество локальных соединений (из упомянутой выше очереди). «Краткосрочную» скорость можно оценить по «использованному» номеру с момента предыдущего запроса (минус один для локального соединения), масштабированному по разнице во времени. Опять же, используется верхний предел (максимум из этих двух значений).

Каждый клиент вычисляет эти две скорости (локальную и внешнюю), а затем складывает их для оценки верхнего предела общей скорости соединений с сервером. Это значение сравнивается с целевым диапазоном скорости, который в настоящее время установлен между 80% и 90% от максимума (от 0.8 до 0.9 * 120 в минуту).

Исходя из разницы между расчетной и целевой скоростью, каждый клиент изменяет свою собственную скорость соединения. Это делается путем взятия предыдущей дельты (время между последним подключением и предыдущим) и ее масштабирования на 1.1 (если скорость превышает целевую) или 0.9 (если скорость ниже целевого). Затем клиент отказывается устанавливать новое соединение до тех пор, пока эта масштабируемая дельта не пройдет (переходя в спящий режим, если запрашивается новое соединение).

Наконец, ничто из вышеперечисленного не заставляет всех клиентов в равной степени делить полосу пропускания. Поэтому я добавляю дополнительные 10% к оценке местной ставки. Это приводит к предпочтительному завышению ставки для клиентов с высокими ставками, что повышает вероятность того, что они снизят свою ставку. Таким образом, «жадные» клиенты оказывают несколько более сильное давление на сокращение потребления, которое в долгосрочной перспективе оказывается достаточным для поддержания сбалансированного распределения ресурсов.

Важными выводами являются:

  • Принимая максимальные «долгосрочные» и «краткосрочные» оценки, система становится консервативной (и более стабильной) при запуске дополнительных клиентов.

  • Ни один клиент не знает общего числа клиентов (если только оно не равно нулю или одному), но все клиенты выполняют один и тот же код, поэтому могут «доверять» друг другу.

  • Учитывая вышесказанное, вы не можете сделать «точные» расчеты того, какой курс использовать, но вы можете сделать «постоянную» коррекцию (в данном случае +/- 10% фактор) в зависимости от глобального курса.

  • Подстройка частоты подключения клиента производится по дельте между двумя последними подключениями (подстройка по среднему значению за всю минуту слишком медленная и приводит к колебаниям).

  • Сбалансированное потребление может быть достигнуто за счет незначительного наказания жадных клиентов.

В (ограниченных) экспериментах это работает довольно хорошо (даже в худшем случае, когда несколько клиентов запускаются одновременно). Основные недостатки: (1) он не допускает начального «взрыва» (что улучшит пропускную способность, если у сервера мало клиентов, а у клиента всего несколько запросов); (2) система все еще колеблется более ~ минуты (см. ниже); (3) обработка большего числа клиентов (в худшем случае, например, если все они запускаются одновременно) требует большего усиления (например, 20% коррекции вместо 10%), что делает систему менее стабильной.

сюжет

«Использованное» количество, сообщаемое (тестовым) сервером, в зависимости от времени (эпоха Unix). Это для четырех клиентов (выделено цветом), каждый из которых пытается потреблять как можно больше данных.

Колебания исходят от обычного источника - запаздывания сигнала коррекции. Они демпфируются (1) использованием верхнего предела ставок (прогнозирование долгосрочной скорости на основе мгновенного значения) и (2) использованием целевого диапазона. Вот почему ответ, сообщенный кем-то, кто разбирается в теории управления, будет оценен...

Мне не ясно, важно ли отдельно оценивать местные и внешние ставки (они могут помочь, если краткосрочная ставка для одного высока, а долгосрочная ставка для другого высока), но я сомневаюсь, что ее удаление улучшит ситуацию.

В заключение: все примерно так, как я и ожидал, для такого подхода. Это как бы работает, но поскольку это простой подход, основанный на обратной связи, он стабилен только в пределах ограниченного диапазона параметров. Я не знаю, какие альтернативы могут быть возможны.

person Community    schedule 28.08.2012

Поскольку вы используете Echonest API, почему бы вам не воспользоваться преимуществами заголовков ограничения скорости, которые возвращаются при каждом вызове API?

В целом вы получаете 120 запросов в минуту. Есть три заголовка, которые могут помочь вам самостоятельно регулировать потребление API:

X-Ratelimit-Used
X-Ratelimit-Remaining
X-Ratelimit-Limit

**(Обратите внимание на строчную букву «ell» в «Ratelimit» — документация заставляет вас думать, что она должна быть написана с большой буквы, но на практике это строчная буква.)

Эти подсчеты учитывают вызовы, сделанные другими процессами с использованием вашего ключа API.

Довольно аккуратно, да? Ну, боюсь, есть загвоздка...

Эти 120 запросов в минуту действительно являются верхней границей. Вы не можете рассчитывать на это. В документации указано, что значение может колебаться в зависимости от загрузки системы. Я видел, что в некоторых вызовах, которые я сделал, он опускался до 40, а в некоторых случаях видел, что он опускался ниже нуля (я действительно надеюсь, что это была ошибка в API echonest!)

Один из подходов, который вы можете использовать, — это замедлить работу, как только использование (используемое деление на предел) достигнет определенного порога. Имейте в виду, однако, что при следующем звонке ваш лимит загрузки может быть скорректирован настолько значительно, что «используется» больше, чем «лимит».

Это хорошо работает до определенного момента. Поскольку Echonest не регулирует лимит предсказуемым маннаром, на практике трудно избежать 400-х.

Вот несколько ссылок, которые я нашел полезными:

http://blog.echonest.com/post/15242456852/managing-your-api-rate-limit http://developer.ehonest.com/docs/v4/#rate-limits

person Gary Dusbabek    schedule 01.03.2013
comment
оба ответа, которые я дал, используют эту информацию. проблема заключается в том, как лучше всего использовать его, когда есть несколько клиентов, которые не обмениваются данными. потому что, если один клиент просто читает до тех пор, пока не исчерпает пособие, это может заставить других голодать. - person andrew cooke; 02.03.2013