Как справляться с резкими скачками трафика: алгоритмы Leaky Bucket и Token Bucket

API7.ai

January 5, 2023

OpenResty (NGINX + Lua)

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

OpenResty сейчас в основном используется как уровень доступа для веб-приложений, таких как WAF (Web Application Firewall) и API-шлюзы, которые должны справляться с нормальными и аномальными всплесками трафика. В конце концов, если вы не сможете обработать всплески трафика, серверные службы могут легко выйти из строя, и бизнес не сможет адекватно реагировать. Поэтому сегодня мы рассмотрим способы борьбы с всплесками трафика.

Управление трафиком

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

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

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

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

Возвращаясь к технологиям, предположим, что вышестоящая служба рассчитана на обработку 10 000 запросов в минуту. В пиковые периоды, если на входе нет управления трафиком и количество задач достигает 20 000 в минуту, производительность обработки этой вышестоящей службы может снизиться до, возможно, всего 5 000 запросов в минуту и продолжать ухудшаться, что в конечном итоге может привести к недоступности службы. Это не тот результат, который мы хотим видеть.

Общие алгоритмы управления трафиком, которые мы используем для борьбы с такими всплесками трафика, — это алгоритм "протекающего ведра" и алгоритм "токенового ведра".

Алгоритм "протекающего ведра"

Давайте начнем с рассмотрения алгоритма "протекающего ведра", который направлен на поддержание постоянной скорости запросов и сглаживание всплесков трафика. Но как это достигается? Сначала взгляните на следующую концептуальную абстракцию из введения в алгоритм "протекающего ведра" в Википедии.

алгоритм протекающего ведра

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

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

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

В-третьих, вода, превышающая объем ведра, не будет принята ведром, а утечет. Соответствующая метафора здесь заключается в том, что если слишком много запросов клиентов превышает длину очереди, то будет возвращено сообщение об ошибке клиенту. В этот момент серверная сторона не может обработать так много запросов, и очередь становится ненужной.

Так как же должен быть реализован этот алгоритм? Давайте возьмем библиотеку resty.limit.req, которая поставляется с OpenResty, в качестве примера. Это модуль ограничения скорости, реализованный с помощью алгоритма "протекающего ведра". Мы расскажем о нем больше в следующей статье. Сегодня мы начнем с краткого взгляда на следующие строки кода, которые являются ключевыми:

local elapsed = now - tonumber(rec.last) excess = max(tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000,0) if excess > self.burst then return nil, "rejected" end -- return the delay in seconds, as well as excess return excess / rate, excess / 1000

Давайте кратко прочитаем эти строки кода. Где elapsed — это количество миллисекунд между текущим и последним запросами, а rate — это скорость, которую мы установили в секунду. Поскольку наименьшая единица rate — это 0.001 с/р, код, реализованный выше, нужно умножить на 1000, чтобы рассчитать его.

excess указывает количество запросов, все еще находящихся в очереди, 0 означает, что ведро пусто, запросов в очереди нет, а burst относится к объему всего ведра. Если excess больше burst, это означает, что ведро заполнено, и входящий трафик будет просто отброшен; если excess больше 0 и меньше burst, он войдет в очередь для ожидания обработки, а возвращаемый здесь excess/rate — это время ожидания.

Таким образом, мы можем контролировать длину очереди всплесков трафика, регулируя размер burst, при этом пропускная способность серверной службы остается неизменной. Но, конечно, зависит от вашего бизнес-сценария, сообщите ли вы пользователю, что слишком много запросов и нужно повторить попытку позже, или позволите пользователю ждать дольше.

Алгоритм "токенового ведра"

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

Алгоритм "протекающего ведра" использует IP-адрес конечной точки для выполнения основ управления трафиком и ограничения скорости. Таким образом, скорость выхода каждого клиента из алгоритма "протекающего ведра" фиксирована. Однако это создает проблему:

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

Здесь на помощь приходит токеновое ведро.

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

Однако OpenResty не реализует токеновые ведра для ограничения трафика и скорости в своей библиотеке. Поэтому здесь приведен краткий пример модуля ограничения скорости на основе токенового ведра lua-resty-limit-rate, который был открыт UPYUN:

local limit_rate = require "resty.limit.rate" -- global 20r/s 6000r/5m local lim_global = limit_rate.new("my_limit_rate_store", 100, 6000, 2) -- single 2r/s 600r/5m local lim_single = limit_rate.new("my_limit_rate_store", 500, 600, 1) local t0, err = lim_global:take_available("__global__", 1) local t1, err = lim_single:take_available(ngx.var.arg_userid, 1) if t0 == 1 then return -- global bucket is not hungry else if t1 == 1 then return -- single bucket is not hungry else return ngx.exit(503) end end

В этом коде мы настроили два токеновых ведра: глобальное токеновое ведро и токеновое ведро с ngx.var.arg_userid в качестве key, разделенное по пользователям. Существует комбинация двух токеновых ведер, которая имеет следующие основные преимущества:

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

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

Модуль ограничения скорости в NGINX

После рассмотрения этих двух алгоритмов, давайте наконец посмотрим, как реализовать ограничение скорости в NGINX. В NGINX модуль limit_req является наиболее часто используемым модулем ограничения скорости, и вот простая конфигурация:

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; server { location /search/ { limit_req zone=one burst=5; } }

Этот код берет IP-адрес клиента в качестве key, запрашивает адресное пространство памяти 10M под названием one и ограничивает скорость до 1 запроса в секунду.

В location сервера также ссылается на правило ограничения скорости one, и brust установлен на 5. Если скорость превышает 1r/s, 5 запросов могут быть поставлены в очередь одновременно, что дает определенный буфер. Если brust не установлен, запросы, превышающие скорость, будут отклонены напрямую.

Этот модуль NGINX основан на протекающем ведре, поэтому он по сути такой же, как resty.limit.req в OpenResty, который мы описали выше.

Заключение

Самая большая проблема ограничения скорости в NGINX заключается в том, что они не могут быть изменены динамически. В конце концов, вам нужно перезапустить конфигурационный файл после его изменения, чтобы он вступил в силу, что неприемлемо в быстро меняющейся среде. Поэтому в следующей статье мы рассмотрим реализацию динамического управления трафиком и ограничения скорости в OpenResty.

Наконец, давайте рассмотрим вопрос. С точки зрения WAF и API-шлюзов, есть ли лучший способ определить, что является нормальными запросами пользователей, а что — злонамеренными? Потому что для всплесков трафика от нормальных пользователей мы можем быстро масштабировать серверные службы, чтобы увеличить пропускную способность службы, в то время как для злонамеренных запросов лучше отклонять их напрямую на уровне доступа.

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